diff --git a/packages/scraper/src/robots/flexoptix-api-sync.ts b/packages/scraper/src/robots/flexoptix-api-sync.ts index 4ca7a2a..28129f1 100644 --- a/packages/scraper/src/robots/flexoptix-api-sync.ts +++ b/packages/scraper/src/robots/flexoptix-api-sync.ts @@ -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 { + 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 { + 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 { 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 { 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 { skipped, priceWrites, stockWrites, + mapWrites, }; }