feat(flexoptix): populate flexoptix_product_map from API compatibility data

Parse Flexoptix API compat entries (compatible_to_vendor + original_part_number)
and batch-upsert into flexoptix_product_map during each catalog sync.

- Fix extractCompatibility() to recognise compatibleToVendor key from API
- Add batchUpsertProductMap(): 500-row batch INSERTs with ON CONFLICT update
- Add writeProductMapEntries() per product, called after price/stock writes
- Track mapWrites in FlexoptixSyncResult and log in completion message

First sync: 9254 upserts → 2903 unique rows, 95 vendors, 343 Flexoptix SKUs.
Example: CWDM-SFP10G-1330 (Cisco) → P.1696.10.D.I (EUR 150.77, SFP+ 10G).
This commit is contained in:
root 2026-07-07 14:25:21 +00:00
parent 764f16d68f
commit eb2888cb0e

View File

@ -81,6 +81,7 @@ export interface FlexoptixSyncResult {
skipped: number;
priceWrites: number;
stockWrites: number;
mapWrites: number;
}
// ── Generic helpers ────────────────────────────────────────────────────────
@ -239,7 +240,7 @@ function extractCompatibility(row: JsonRecord): CatalogProduct["compatibility"]
const rows = Array.isArray(rawCompat) ? rawCompat.filter(isRecord) : [];
return rows.flatMap(entry => {
const flat = flatLookup(entry);
const vendor = asString(pick(flat, ["vendor", "manufacturer", "brand", "systemVendor"]));
const vendor = asString(pick(flat, ["vendor", "manufacturer", "brand", "systemVendor", "compatibleToVendor"]));
if (!vendor) return [];
return [{
vendor,
@ -362,7 +363,7 @@ async function importProduct(
speedGbps: product.optics.speedGbps ?? undefined,
speed: speedLabel(product.optics.speedGbps),
reachMeters: product.optics.reachM ?? undefined,
reachLabel: reachLabel(product.optics.reachM),
reachLabel: reachLabel(product.optics.reachM) ?? undefined,
fiberType: product.optics.fiberType ?? undefined,
wavelengths: product.optics.wavelengthNm === null ? undefined : `${product.optics.wavelengthNm}nm`,
category: categoryFor(product),
@ -456,6 +457,22 @@ async function importProduct(
return { priceWritten, stockWritten };
}
async function writeProductMapEntries(product: CatalogProduct): Promise<number> {
const entries = product.compatibility
.filter(c => c.coding !== null && c.vendor !== null)
.map(c => ({
oemPartNumber: c.coding!,
oemVendor: c.vendor,
flexoptixSku: product.sku,
priceEur: product.price.amount,
formFactor: product.optics.formFactor,
speedGbps: product.optics.speedGbps,
reachLabel: reachLabel(product.optics.reachM) ?? null,
fiberType: product.optics.fiberType,
}));
return batchUpsertProductMap(entries);
}
// ── API client ─────────────────────────────────────────────────────────────
function validateEnv(): { baseUrl: string; username: string | null; password: string | null; token: string | null } {
@ -585,6 +602,54 @@ async function fetchSelfConfigurePrices(
return priceMap;
}
// ── Product map batch upsert ─────────────────────────────────────────────────
// Writes OEM vendor coding entries into flexoptix_product_map.
// One row per (oem_part_number, oem_vendor) pair — ON CONFLICT updates specs.
async function batchUpsertProductMap(entries: Array<{
oemPartNumber: string;
oemVendor: string;
flexoptixSku: string;
priceEur: number | null;
formFactor: string | null;
speedGbps: number | null;
reachLabel: string | null;
fiberType: string | null;
}>): Promise<number> {
if (entries.length === 0) return 0;
const BATCH_SIZE = 500;
let written = 0;
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
const chunk = entries.slice(i, i + BATCH_SIZE);
const placeholders: string[] = [];
const params: unknown[] = [];
let n = 1;
for (const e of chunk) {
placeholders.push(`($${n},$${n+1},$${n+2},$${n+3},$${n+4},$${n+5},$${n+6},$${n+7},'exact',NOW())`);
params.push(e.oemPartNumber, e.oemVendor, e.flexoptixSku, e.priceEur, e.formFactor, e.speedGbps, e.reachLabel, e.fiberType);
n += 8;
}
await pool.query(`
INSERT INTO flexoptix_product_map
(oem_part_number, oem_vendor, flexoptix_sku, flexoptix_price_eur,
form_factor, speed_gbps, reach_label, fiber_type, match_type, last_verified)
VALUES ${placeholders.join(',')}
ON CONFLICT (oem_part_number, oem_vendor) DO UPDATE SET
flexoptix_sku = EXCLUDED.flexoptix_sku,
flexoptix_price_eur = EXCLUDED.flexoptix_price_eur,
form_factor = EXCLUDED.form_factor,
speed_gbps = EXCLUDED.speed_gbps,
reach_label = EXCLUDED.reach_label,
fiber_type = EXCLUDED.fiber_type,
last_verified = EXCLUDED.last_verified,
updated_at = NOW()
`, params);
written += chunk.length;
}
return written;
}
export async function syncFlexoptixCatalog(): Promise<FlexoptixSyncResult> {
const { baseUrl, username, password, token } = validateEnv();
const timeoutMs = parseInt(process.env["FLEXOPTIX_API_TIMEOUT_MS"]?.trim() ?? "30000", 10);
@ -634,19 +699,21 @@ export async function syncFlexoptixCatalog(): Promise<FlexoptixSyncResult> {
let priceWrites = 0;
let stockWrites = 0;
let mapWrites = 0;
for (const product of importable) {
try {
const result = await importProduct(product, vendorId);
if (result.priceWritten) priceWrites++;
if (result.stockWritten) stockWrites++;
mapWrites += await writeProductMapEntries(product);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
console.warn(`[${new Date().toISOString()}] Flexoptix import error (${product.sku}): ${message.slice(0, 100)}`);
}
}
console.log(`[${new Date().toISOString()}] Flexoptix API sync complete: ${importable.length} products, ${priceWrites} price writes, ${stockWrites} stock writes`);
console.log(`[${new Date().toISOString()}] Flexoptix API sync complete: ${importable.length} products, ${priceWrites} price writes, ${stockWrites} stock writes, ${mapWrites} product-map upserts`);
return {
fetched: rawRows.length,
@ -654,5 +721,6 @@ export async function syncFlexoptixCatalog(): Promise<FlexoptixSyncResult> {
skipped,
priceWrites,
stockWrites,
mapWrites,
};
}