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:
parent
764f16d68f
commit
eb2888cb0e
@ -81,6 +81,7 @@ export interface FlexoptixSyncResult {
|
|||||||
skipped: number;
|
skipped: number;
|
||||||
priceWrites: number;
|
priceWrites: number;
|
||||||
stockWrites: number;
|
stockWrites: number;
|
||||||
|
mapWrites: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Generic helpers ────────────────────────────────────────────────────────
|
// ── Generic helpers ────────────────────────────────────────────────────────
|
||||||
@ -239,7 +240,7 @@ function extractCompatibility(row: JsonRecord): CatalogProduct["compatibility"]
|
|||||||
const rows = Array.isArray(rawCompat) ? rawCompat.filter(isRecord) : [];
|
const rows = Array.isArray(rawCompat) ? rawCompat.filter(isRecord) : [];
|
||||||
return rows.flatMap(entry => {
|
return rows.flatMap(entry => {
|
||||||
const flat = flatLookup(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 [];
|
if (!vendor) return [];
|
||||||
return [{
|
return [{
|
||||||
vendor,
|
vendor,
|
||||||
@ -362,7 +363,7 @@ async function importProduct(
|
|||||||
speedGbps: product.optics.speedGbps ?? undefined,
|
speedGbps: product.optics.speedGbps ?? undefined,
|
||||||
speed: speedLabel(product.optics.speedGbps),
|
speed: speedLabel(product.optics.speedGbps),
|
||||||
reachMeters: product.optics.reachM ?? undefined,
|
reachMeters: product.optics.reachM ?? undefined,
|
||||||
reachLabel: reachLabel(product.optics.reachM),
|
reachLabel: reachLabel(product.optics.reachM) ?? undefined,
|
||||||
fiberType: product.optics.fiberType ?? undefined,
|
fiberType: product.optics.fiberType ?? undefined,
|
||||||
wavelengths: product.optics.wavelengthNm === null ? undefined : `${product.optics.wavelengthNm}nm`,
|
wavelengths: product.optics.wavelengthNm === null ? undefined : `${product.optics.wavelengthNm}nm`,
|
||||||
category: categoryFor(product),
|
category: categoryFor(product),
|
||||||
@ -456,6 +457,22 @@ async function importProduct(
|
|||||||
return { priceWritten, stockWritten };
|
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 ─────────────────────────────────────────────────────────────
|
// ── API client ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function validateEnv(): { baseUrl: string; username: string | null; password: string | null; token: string | null } {
|
function validateEnv(): { baseUrl: string; username: string | null; password: string | null; token: string | null } {
|
||||||
@ -585,6 +602,54 @@ async function fetchSelfConfigurePrices(
|
|||||||
return priceMap;
|
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> {
|
export async function syncFlexoptixCatalog(): Promise<FlexoptixSyncResult> {
|
||||||
const { baseUrl, username, password, token } = validateEnv();
|
const { baseUrl, username, password, token } = validateEnv();
|
||||||
const timeoutMs = parseInt(process.env["FLEXOPTIX_API_TIMEOUT_MS"]?.trim() ?? "30000", 10);
|
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 priceWrites = 0;
|
||||||
let stockWrites = 0;
|
let stockWrites = 0;
|
||||||
|
let mapWrites = 0;
|
||||||
|
|
||||||
for (const product of importable) {
|
for (const product of importable) {
|
||||||
try {
|
try {
|
||||||
const result = await importProduct(product, vendorId);
|
const result = await importProduct(product, vendorId);
|
||||||
if (result.priceWritten) priceWrites++;
|
if (result.priceWritten) priceWrites++;
|
||||||
if (result.stockWritten) stockWrites++;
|
if (result.stockWritten) stockWrites++;
|
||||||
|
mapWrites += await writeProductMapEntries(product);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
console.warn(`[${new Date().toISOString()}] Flexoptix import error (${product.sku}): ${message.slice(0, 100)}`);
|
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 {
|
return {
|
||||||
fetched: rawRows.length,
|
fetched: rawRows.length,
|
||||||
@ -654,5 +721,6 @@ export async function syncFlexoptixCatalog(): Promise<FlexoptixSyncResult> {
|
|||||||
skipped,
|
skipped,
|
||||||
priceWrites,
|
priceWrites,
|
||||||
stockWrites,
|
stockWrites,
|
||||||
|
mapWrites,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user