import { Pool } from "pg"; import { config } from "dotenv"; import { join } from "path"; import { contentHash } from "./hash"; config({ path: join(__dirname, "..", "..", "..", "..", ".env") }); export const pool = new Pool({ host: process.env.POSTGRES_HOST || "localhost", port: parseInt(process.env.POSTGRES_PORT || "5433"), database: process.env.POSTGRES_DB || "transceiver_db", user: process.env.POSTGRES_USER || "tip", password: process.env.POSTGRES_PASSWORD || "tip_dev_2026", max: 5, idleTimeoutMillis: 10000, connectionTimeoutMillis: 5000, }); // Alias — some scrapers import { db } instead of { pool } export const db = pool; export async function recordVerificationEvidence(params: { transceiverId: string; verificationType: "price" | "image" | "details" | "competitor_match" | "competitor_no_match" | "competitor_ambiguous" | "artifact_quarantine"; sourceUrl?: string; sourceVendorId?: string; evidenceValue?: Record; robotName: string; confidence?: number; }): Promise { const evidenceValue = params.evidenceValue || {}; const evidenceHash = contentHash({ type: params.verificationType, sourceUrl: params.sourceUrl || "", sourceVendorId: params.sourceVendorId || "", evidenceValue, }); await pool.query( `INSERT INTO transceiver_verification_evidence ( transceiver_id, verification_type, source_url, source_vendor_id, evidence_value, evidence_hash, robot_name, confidence ) VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7, $8) ON CONFLICT DO NOTHING`, [ params.transceiverId, params.verificationType, params.sourceUrl || null, params.sourceVendorId || null, JSON.stringify(evidenceValue), evidenceHash, params.robotName, params.confidence ?? null, ] ); } /** * After any verified flag is set, check if all 4 criteria are met and promote * the transceiver to fully_verified. Call this wherever price/image/details/ * competitor_verified are written so the counter stays consistent. */ export async function checkAndSetFullyVerified(transceiverId: string): Promise { const result = await pool.query( `UPDATE transceivers SET fully_verified = true, fully_verified_at = COALESCE(fully_verified_at, NOW()) WHERE id = $1 AND price_verified = true AND image_verified = true AND details_verified = true AND competitor_verified = true AND (fully_verified IS NULL OR fully_verified = false) RETURNING id`, [transceiverId] ); return (result.rowCount ?? 0) > 0; } export async function markImageVerified( transceiverId: string, imageUrl: string ): Promise { const result = await pool.query( `UPDATE transceivers SET image_url = CASE WHEN image_url IS NULL OR image_url = '' OR image_url ~* '(placeholder|no-image|no_image|missing|default)' THEN $2::text ELSE image_url END, has_image = true, image_verified = true, image_verified_at = NOW(), image_verified_url = $2::text, image_scraped_at = NOW(), updated_at = NOW() WHERE id = $1 AND $2::text IS NOT NULL AND $2::text != '' RETURNING id`, [transceiverId, imageUrl] ); await checkAndSetFullyVerified(transceiverId); await recordVerificationEvidence({ transceiverId, verificationType: "image", sourceUrl: imageUrl, evidenceValue: { imageUrl }, robotName: "markImageVerified", confidence: 1, }); return (result.rowCount ?? 0) > 0; } export async function markDetailsVerified(params: { transceiverId: string; sourceUrl?: string; }): Promise { const result = await pool.query( `UPDATE transceivers SET product_page_url = COALESCE(NULLIF(product_page_url, ''), NULLIF($2::text, '')), details_verified = true, details_verified_at = COALESCE(details_verified_at, NOW()), details_source_url = COALESCE(NULLIF(details_source_url, ''), NULLIF($2::text, ''), product_page_url), data_confidence = CASE WHEN data_confidence IS NULL OR data_confidence IN ('unknown', 'enriched_estimated') THEN 'scraped_unverified' ELSE data_confidence END, updated_at = NOW() WHERE id = $1 AND form_factor IS NOT NULL AND speed_gbps IS NOT NULL AND part_number IS NOT NULL AND part_number != '' AND reach_label IS NOT NULL AND reach_label != '' AND fiber_type IS NOT NULL AND fiber_type != '' AND COALESCE(data_confidence, 'unknown') != 'garbage' RETURNING id`, [params.transceiverId, params.sourceUrl || null] ); await checkAndSetFullyVerified(params.transceiverId); if ((result.rowCount ?? 0) > 0) { await recordVerificationEvidence({ transceiverId: params.transceiverId, verificationType: "details", sourceUrl: params.sourceUrl, evidenceValue: { sourceUrl: params.sourceUrl || null }, robotName: "markDetailsVerified", confidence: 1, }); } return (result.rowCount ?? 0) > 0; } // Per-form-factor price bounds [min, max] in USD equivalent const PRICE_BOUNDS: Record = { "SFP": [2, 3000], "SFP+": [4, 5000], "SFP28": [10, 8000], "SFP56": [20, 10000], "SFP-DD": [30, 12000], "QSFP+": [15, 6000], "QSFP28": [20, 10000], "QSFP56": [50, 15000], "QSFP-DD": [60, 20000], "QSFP112": [80, 25000], "OSFP": [100, 35000], "OSFP112": [150, 40000], "OSFP224": [200, 60000], "CFP": [100, 30000], "CFP2": [100, 30000], "XFP": [10, 5000], "GBIC": [2, 2000], }; async function isPriceAnomalous(transceiverId: string, priceUsd: number): Promise { const row = await pool.query( `SELECT form_factor FROM transceivers WHERE id = $1`, [transceiverId] ); const formFactor = row.rows[0]?.form_factor as string | undefined; if (!formFactor) return false; const bounds = PRICE_BOUNDS[formFactor]; if (!bounds) return false; return priceUsd < bounds[0] || priceUsd > bounds[1]; } export async function upsertPriceObservation(params: { transceiverId: string; sourceVendorId: string; price: number; currency: string; stockLevel: string; quantityAvailable?: number; leadTimeDays?: number; url?: string; contentHash: string; }): Promise { // Normalize price to USD for sanity check (rough conversion) const priceUsd = params.currency === "EUR" ? params.price * 1.09 : params.currency === "GBP" ? params.price * 1.27 : params.price; // Hard floor: no transceiver of any type can cost less than $1.50 — catches accessories/cables // misidentified as transceivers (e.g. FS-XXXXX DAC cables scraped as OSFP/QSFP28) if (priceUsd < 1.5) { return false; } const anomalous = await isPriceAnomalous(params.transceiverId, priceUsd); if (anomalous) { return false; // Reject price outside form-factor bounds } // Check if price changed via content hash — also check observation age const existing = await pool.query( `SELECT content_hash, time FROM price_observations WHERE transceiver_id = $1 AND source_vendor_id = $2 ORDER BY time DESC LIMIT 1`, [params.transceiverId, params.sourceVendorId] ); // Check if vendor is a competitor (non-Flexoptix) for competitor_verified flag const vendorRow = await pool.query( `SELECT is_competitor FROM vendors WHERE id = $1`, [params.sourceVendorId] ); const isCompetitor = vendorRow.rows[0]?.is_competitor === true; // Price unchanged AND observation is fresh (< 7 days old) → skip insertion const REFRESH_DAYS = 7; const isStale = !existing.rows.length || (Date.now() - new Date(existing.rows[0].time).getTime()) > REFRESH_DAYS * 24 * 60 * 60 * 1000; if (existing.rows.length > 0 && existing.rows[0].content_hash === params.contentHash && !isStale) { // Price unchanged and recent — still ensure verified flags are current await pool.query( `UPDATE price_observations SET is_verified = true, verified_at = NOW() WHERE transceiver_id = $1 AND source_vendor_id = $2 AND content_hash = $3 AND time > NOW() - INTERVAL '${REFRESH_DAYS} days'`, [params.transceiverId, params.sourceVendorId, params.contentHash] ); await pool.query( `UPDATE transceivers SET price_verified = true, price_verified_at = NOW() ${isCompetitor ? ", competitor_verified = true, competitor_verified_at = NOW(), competitor_status = 'matched', competitor_status_updated_at = NOW()" : ""} WHERE id = $1`, [params.transceiverId] ); await checkAndSetFullyVerified(params.transceiverId); await recordVerificationEvidence({ transceiverId: params.transceiverId, verificationType: "price", sourceUrl: params.url, sourceVendorId: params.sourceVendorId, evidenceValue: { price: params.price, currency: params.currency, stockLevel: params.stockLevel }, robotName: "upsertPriceObservation", confidence: 1, }); return false; // No change } await pool.query( `INSERT INTO price_observations ( time, transceiver_id, source_vendor_id, price, currency, stock_level, quantity_available, lead_time_days, url, content_hash, is_verified, verified_at ) VALUES (NOW(), $1, $2, $3, $4, $5, $6, $7, $8, $9, true, NOW())`, [ params.transceiverId, params.sourceVendorId, params.price, params.currency, params.stockLevel, params.quantityAvailable || null, params.leadTimeDays || null, params.url || null, params.contentHash, ] ); // Mark price_verified always; competitor_verified only for non-Flexoptix vendors if (isCompetitor) { await pool.query( `UPDATE transceivers SET price_verified = true, price_verified_at = NOW(), competitor_verified = true, competitor_verified_at = NOW(), competitor_status = 'matched', competitor_status_updated_at = NOW() WHERE id = $1`, [params.transceiverId] ); } else { await pool.query( `UPDATE transceivers SET price_verified = true, price_verified_at = NOW() WHERE id = $1`, [params.transceiverId] ); } await checkAndSetFullyVerified(params.transceiverId); await recordVerificationEvidence({ transceiverId: params.transceiverId, verificationType: "price", sourceUrl: params.url, sourceVendorId: params.sourceVendorId, evidenceValue: { price: params.price, currency: params.currency, stockLevel: params.stockLevel }, robotName: "upsertPriceObservation", confidence: 1, }); return true; // New observation written } /** * Upsert a stock observation with full warehouse breakdown (FS.com v2+). * Writes to stock_observations including DE-Lager, Global-Lager, Nachlieferung, * units_sold, compatible_brands, price_net, product_url, plus quality metadata: * stock_confidence, price_currency, price_includes_tax, stock_vendor_ts. * * stock_confidence: * 1 = boolean only (in_stock true/false, no unit count) * 2 = aggregated global count (single number, e.g. QSFPTEK "5507 in real-time stock") * 3 = per-warehouse breakdown (e.g. FS.com DE-Lager + Global-Lager split) * * Returns true only when the data has changed since the last observation. */ export async function upsertStockObservation(params: { transceiverId: string; sourceVendorId: string; stockLevel: string; quantityAvailable?: number; warehouseDeQty?: number; warehouseDeDeliveryDate?: string | null; warehouseGlobalQty?: number; warehouseGlobalDeliveryDate?: string | null; backorderQty?: number; backorderEstimatedDate?: string | null; unitsSold?: number; compatibleBrands?: string[]; priceNet?: number; productUrl?: string; // Quality metadata (migration 038) stockConfidence?: 1 | 2 | 3; priceCurrency?: string; priceIncludesTax?: boolean; stockVendorTs?: Date | null; }): Promise { // Skip if there is genuinely no warehouse data at all // (includes backorderQty so products available only on backorder are recorded) if ( params.warehouseDeQty === undefined && params.warehouseGlobalQty === undefined && params.quantityAvailable === undefined && params.backorderQty === undefined ) { return false; } // Compare against the last observation to avoid duplicate writes const lastObs = await pool.query( `SELECT warehouse_de_qty, warehouse_global_qty, backorder_qty, units_sold, quantity_available FROM stock_observations WHERE transceiver_id = $1 AND source_vendor_id = $2 ORDER BY time DESC LIMIT 1`, [params.transceiverId, params.sourceVendorId] ); if (lastObs.rows.length > 0) { const r = lastObs.rows[0]; const unchanged = (r.warehouse_de_qty ?? null) === (params.warehouseDeQty ?? null) && (r.warehouse_global_qty ?? null) === (params.warehouseGlobalQty ?? null) && (r.backorder_qty ?? null) === (params.backorderQty ?? null) && (r.units_sold ?? null) === (params.unitsSold ?? null) && (r.quantity_available ?? null) === (params.quantityAvailable ?? null); if (unchanged) return false; } const inStock = ((params.warehouseDeQty ?? 0) + (params.warehouseGlobalQty ?? 0) + (params.quantityAvailable ?? 0)) > 0; await pool.query( `INSERT INTO stock_observations ( time, transceiver_id, source_vendor_id, in_stock, quantity_available, warehouse_de_qty, warehouse_de_delivery_date, warehouse_global_qty, warehouse_global_delivery_date, backorder_qty, backorder_estimated_date, units_sold, compatible_brands, price_net, product_url, stock_confidence, price_currency, price_includes_tax, stock_vendor_ts ) VALUES ( NOW(), $1, $2, $3, $4, $5, $6::date, $7, $8::date, $9, $10::date, $11, $12, $13, $14, $15, $16, $17, $18 )`, [ params.transceiverId, params.sourceVendorId, inStock, params.quantityAvailable ?? null, params.warehouseDeQty ?? null, params.warehouseDeDeliveryDate ?? null, params.warehouseGlobalQty ?? null, params.warehouseGlobalDeliveryDate ?? null, params.backorderQty ?? null, params.backorderEstimatedDate ?? null, params.unitsSold ?? null, params.compatibleBrands?.length ? params.compatibleBrands : null, params.priceNet ?? null, params.productUrl ?? null, params.stockConfidence ?? 1, params.priceCurrency ?? null, params.priceIncludesTax ?? null, params.stockVendorTs ?? null, ] ); return true; } export async function findOrCreateScrapedTransceiver(params: { partNumber: string; vendorId: string; productUrl?: string; formFactor?: string; speedGbps?: number; speed?: string; reachMeters?: number; reachLabel?: string; fiberType?: string; wavelengths?: string; category?: string; imageUrl?: string; }): Promise { // Try to match existing transceiver by part number + vendor const existing = await pool.query( `SELECT id, image_url FROM transceivers WHERE part_number = $1 AND vendor_id = $2`, [params.partNumber, params.vendorId] ); if (existing.rows.length > 0) { await pool.query( `UPDATE transceivers SET product_page_url = COALESCE(NULLIF(product_page_url, ''), NULLIF($2, '')), form_factor = COALESCE(NULLIF(form_factor, ''), $3), speed_gbps = CASE WHEN speed_gbps IS NULL OR speed_gbps = 0 THEN COALESCE($4, speed_gbps) ELSE speed_gbps END, speed = COALESCE(NULLIF(speed, ''), $5), reach_meters = CASE WHEN reach_meters IS NULL OR reach_meters = 0 THEN COALESCE($6, reach_meters) ELSE reach_meters END, reach_label = COALESCE(NULLIF(reach_label, ''), $7), fiber_type = COALESCE(NULLIF(fiber_type, ''), $8), wavelengths = COALESCE(NULLIF(wavelengths, ''), $9), category = COALESCE(NULLIF(category, ''), $10), updated_at = NOW() WHERE id = $1`, [ existing.rows[0].id, params.productUrl || null, params.formFactor || null, params.speedGbps || null, params.speed || null, params.reachMeters || null, params.reachLabel || null, params.fiberType || null, params.wavelengths || null, params.category || null, ] ); // Re-validate image metadata whenever the scraper sees a current product image. if (params.imageUrl) { await markImageVerified(existing.rows[0].id, params.imageUrl); } if (params.productUrl) { await markDetailsVerified({ transceiverId: existing.rows[0].id, sourceUrl: params.productUrl, }); } return existing.rows[0].id; } // Create new transceiver entry const slug = `scraped-${params.partNumber.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`; const result = await pool.query( `INSERT INTO transceivers ( slug, part_number, vendor_id, product_page_url, form_factor, speed_gbps, speed, reach_meters, reach_label, fiber_type, wavelengths, category, market_status, data_confidence, image_url, has_image, image_verified, image_verified_at, image_verified_url, details_verified, details_verified_at, details_source_url ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'Mainstream', 'scraped_unverified', $13, $14, $14, CASE WHEN $14 THEN NOW() ELSE NULL END, $13, $15, CASE WHEN $15 THEN NOW() ELSE NULL END, $4 ) ON CONFLICT (slug) DO UPDATE SET product_page_url = COALESCE(transceivers.product_page_url, EXCLUDED.product_page_url), image_url = COALESCE(transceivers.image_url, EXCLUDED.image_url), has_image = COALESCE(transceivers.has_image, false) OR COALESCE(EXCLUDED.has_image, false), image_verified = COALESCE(transceivers.image_verified, false) OR COALESCE(EXCLUDED.image_verified, false), image_verified_at = COALESCE(transceivers.image_verified_at, EXCLUDED.image_verified_at), image_verified_url = COALESCE(transceivers.image_verified_url, EXCLUDED.image_verified_url), details_verified = COALESCE(transceivers.details_verified, false) OR COALESCE(EXCLUDED.details_verified, false), details_verified_at = COALESCE(transceivers.details_verified_at, EXCLUDED.details_verified_at), details_source_url = COALESCE(transceivers.details_source_url, EXCLUDED.details_source_url), data_confidence = CASE WHEN transceivers.data_confidence IS NULL OR transceivers.data_confidence IN ('unknown', 'enriched_estimated') THEN EXCLUDED.data_confidence ELSE transceivers.data_confidence END, updated_at = NOW() RETURNING id`, [ slug, params.partNumber, params.vendorId, params.productUrl || null, params.formFactor || "SFP", params.speedGbps || 0, params.speed || "Unknown", params.reachMeters || 0, params.reachLabel || "", params.fiberType || "", params.wavelengths || "", params.category || "DataCenter", params.imageUrl || null, Boolean(params.imageUrl), Boolean(params.productUrl && params.reachLabel && params.fiberType), ] ); const id = result.rows[0].id; await checkAndSetFullyVerified(id); return id; } export interface SwitchParams { model: string; vendorId: string; series?: string; category?: string; layer?: string; portsConfig?: Record; totalPorts?: number; uplinkSpeedGbps?: number; maxSpeedGbps?: number; switchingCapacityTbps?: number; forwardingRateMpps?: number; asicVendor?: string; asicModel?: string; asicSeries?: string; asicGeneration?: string; rackUnits?: number; maxPowerW?: number; typicalPowerW?: number; poeSupport?: string; stackingSupport?: boolean; vxlanSupport?: boolean; evpnSupport?: boolean; bgpSupport?: boolean; mplsSupport?: boolean; openconfigSupport?: boolean; sonicCompatible?: boolean; macsecSupport?: boolean; lifecycleStatus?: string; releaseDate?: string; eolDate?: string; msrpUsd?: number; tags?: string[]; // Whitebox-specific fields isWhitebox?: boolean; isOcpAccepted?: boolean; ocpStatus?: string; supportedNos?: string[]; onlCompatible?: boolean; dentCompatible?: boolean; cumulusCompatible?: boolean; fbossCompatible?: boolean; cpu?: string; cpuCores?: number; ramGb?: number; storageGb?: number; storageType?: string; transceiverFormFactors?: string[]; catalogUrl?: string; sonicHwsku?: string; onieSupport?: boolean; scrapeSource?: string; } export async function findOrCreateSwitch(params: SwitchParams): Promise { const existing = await pool.query( `SELECT id FROM switches WHERE model = $1 AND vendor_id = $2`, [params.model, params.vendorId] ); if (existing.rows.length > 0) { await pool.query( `UPDATE switches SET series = COALESCE($2, series), category = COALESCE($3, category), ports_config = COALESCE($4, ports_config), total_ports = COALESCE($5, total_ports), max_speed_gbps = COALESCE($6, max_speed_gbps), switching_capacity_tbps = COALESCE($7, switching_capacity_tbps), is_whitebox = COALESCE($8, is_whitebox), supported_nos = COALESCE($9, supported_nos), sonic_compatible = COALESCE($10, sonic_compatible), sonic_hwsku = COALESCE($11, sonic_hwsku), cpu = COALESCE($12, cpu), ram_gb = COALESCE($13, ram_gb), storage_gb = COALESCE($14, storage_gb), transceiver_form_factors = COALESCE($15, transceiver_form_factors), catalog_url = COALESCE($16, catalog_url), is_ocp_accepted = COALESCE($17, is_ocp_accepted), ocp_status = COALESCE($18, ocp_status), onie_support = COALESCE($19, onie_support), asic_series = COALESCE($20, asic_series), last_scraped = CASE WHEN $21::text IS NOT NULL THEN NOW() ELSE last_scraped END, scrape_source = COALESCE($21, scrape_source), updated_at = NOW() WHERE id = $1`, [ existing.rows[0].id, params.series || null, params.category || null, params.portsConfig ? JSON.stringify(params.portsConfig) : null, params.totalPorts || null, params.maxSpeedGbps || null, params.switchingCapacityTbps || null, params.isWhitebox ?? null, params.supportedNos?.length ? params.supportedNos : null, params.sonicCompatible ?? null, params.sonicHwsku || null, params.cpu || null, params.ramGb || null, params.storageGb || null, params.transceiverFormFactors?.length ? params.transceiverFormFactors : null, params.catalogUrl || null, params.isOcpAccepted ?? null, params.ocpStatus || null, params.onieSupport ?? null, params.asicSeries || null, params.scrapeSource || null, ] ); return existing.rows[0].id; } const result = await pool.query( `INSERT INTO switches ( model, vendor_id, series, category, layer, ports_config, total_ports, uplink_speed_gbps, max_speed_gbps, switching_capacity_tbps, forwarding_rate_mpps, asic_vendor, asic_model, asic_series, asic_generation, rack_units, max_power_w, typical_power_w, poe_support, stacking_support, vxlan_support, evpn_support, bgp_support, mpls_support, openconfig_support, sonic_compatible, macsec_support, lifecycle_status, release_date, eol_date, msrp_usd, tags, is_whitebox, is_ocp_accepted, ocp_status, supported_nos, onl_compatible, dent_compatible, cumulus_compatible, fboss_compatible, cpu, cpu_cores, ram_gb, storage_gb, storage_type, transceiver_form_factors, catalog_url, sonic_hwsku, onie_support, last_scraped, scrape_source ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51 ) ON CONFLICT (vendor_id, model) DO UPDATE SET updated_at = NOW() RETURNING id`, [ params.model, params.vendorId, params.series || null, params.category || 'DataCenter', params.layer || 'L3', JSON.stringify(params.portsConfig || {}), params.totalPorts || null, params.uplinkSpeedGbps || null, params.maxSpeedGbps || null, params.switchingCapacityTbps || null, params.forwardingRateMpps || null, params.asicVendor || null, params.asicModel || null, params.asicSeries || null, params.asicGeneration || null, params.rackUnits || null, params.maxPowerW || null, params.typicalPowerW || null, params.poeSupport || 'None', params.stackingSupport || false, params.vxlanSupport || false, params.evpnSupport || false, params.bgpSupport || false, params.mplsSupport || false, params.openconfigSupport || false, params.sonicCompatible || false, params.macsecSupport || false, params.lifecycleStatus || 'Active', params.releaseDate || null, params.eolDate || null, params.msrpUsd || null, params.tags || [], params.isWhitebox || false, params.isOcpAccepted || false, params.ocpStatus || 'None', params.supportedNos || [], params.onlCompatible || false, params.dentCompatible || false, params.cumulusCompatible || false, params.fbossCompatible || false, params.cpu || null, params.cpuCores || null, params.ramGb || null, params.storageGb || null, params.storageType || null, params.transceiverFormFactors || [], params.catalogUrl || null, params.sonicHwsku || null, params.onieSupport || false, params.scrapeSource ? new Date() : null, params.scrapeSource || null, ] ); return result.rows[0].id; } export async function ensureWhiteboxVendor( name: string, website?: string, options?: { isOdm?: boolean; ocpMember?: boolean; sonicContributor?: boolean } ): Promise { const existing = await pool.query(`SELECT id FROM vendors WHERE name ILIKE $1`, [name]); if (existing.rows.length > 0) { if (options) { await pool.query( `UPDATE vendors SET is_whitebox_vendor = TRUE, is_odm = COALESCE($2, is_odm), ocp_member = COALESCE($3, ocp_member), sonic_contributor = COALESCE($4, sonic_contributor), updated_at = NOW() WHERE id = $1`, [existing.rows[0].id, options.isOdm ?? null, options.ocpMember ?? null, options.sonicContributor ?? null] ); } return existing.rows[0].id; } const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-"); const result = await pool.query( `INSERT INTO vendors (name, slug, type, website, is_whitebox_vendor, is_odm, ocp_member, sonic_contributor) VALUES ($1, $2, 'manufacturer', $3, TRUE, $4, $5, $6) RETURNING id`, [ name, slug, website || null, options?.isOdm ?? true, options?.ocpMember ?? false, options?.sonicContributor ?? false, ] ); return result.rows[0].id; } export async function getVendorId(name: string): Promise { const result = await pool.query(`SELECT id FROM vendors WHERE name = $1`, [name]); return result.rows[0]?.id || null; } export async function ensureVendor( name: string, type: string, website?: string, shopUrl?: string ): Promise { // Try to find existing vendor first const existing = await pool.query(`SELECT id FROM vendors WHERE name ILIKE $1`, [name]); if (existing.rows.length > 0) return existing.rows[0].id; const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-"); try { const result = await pool.query( `INSERT INTO vendors (name, slug, type, website, shop_url, is_competitor) VALUES ($1, $2, $3, $4, $5, true) RETURNING id`, [name, slug, type, website || null, shopUrl || null] ); return result.rows[0].id; } catch (err: unknown) { // Handle race condition — re-query if insert fails on unique constraint const existing2 = await pool.query(`SELECT id FROM vendors WHERE name ILIKE $1 OR slug = $2`, [name, slug]); if (existing2.rows.length > 0) return existing2.rows[0].id; throw err; } }