/** * WS0c: Procurement Intelligence API * * Endpoints: * GET /api/procurement/overview — Dashboard summary * GET /api/procurement/signals — Active reorder signals * GET /api/procurement/signals/:id — Signal for a specific transceiver * GET /api/procurement/abc — ABC classification list * GET /api/procurement/market-intel — Market intelligence events * GET /api/procurement/stock-trends/:id — Stock history for a transceiver * GET /api/procurement/lifecycle — Lifecycle events (EOL, standards) * GET /api/procurement/ai-clusters — AI datacenter announcements with transceiver demand * GET /api/procurement/internal-demand — Flexoptix internal demand velocity (fast/slow/dead) */ import { Router, Request, Response } from "express"; import { pool } from "../db/client"; import { sendCSV } from "../utils/csv"; export const procurementRouter = Router(); // ───────────────────────────────────────────────────────────────────────────── // GET /api/procurement/overview // ───────────────────────────────────────────────────────────────────────────── procurementRouter.get("/overview", async (_req: Request, res: Response) => { try { const [signals, abc, intel, lifecycle] = await Promise.all([ pool.query(` WITH latest AS ( SELECT DISTINCT ON (transceiver_id) signal FROM reorder_signals WHERE expires_at > NOW() ORDER BY transceiver_id, computed_at DESC ) SELECT signal, COUNT(*) AS count FROM latest GROUP BY signal `), pool.query(` SELECT abc_class, COUNT(*) AS count FROM abc_classification GROUP BY abc_class ORDER BY abc_class `), pool.query(` SELECT intel_type, buy_signal_implication, COUNT(*) AS count FROM market_intelligence WHERE created_at > NOW() - INTERVAL '90 days' GROUP BY intel_type, buy_signal_implication ORDER BY count DESC LIMIT 10 `), pool.query(` SELECT event_type, impact_level, COUNT(*) AS count FROM product_lifecycle_events WHERE created_at > NOW() - INTERVAL '180 days' GROUP BY event_type, impact_level ORDER BY count DESC `), ]); res.json({ signals_summary: signals.rows, abc_summary: abc.rows, market_intel_summary: intel.rows, lifecycle_summary: lifecycle.rows, }); } catch (err) { console.error("Procurement overview error:", err); res.status(500).json({ error: "Internal server error" }); } }); // ───────────────────────────────────────────────────────────────────────────── // GET /api/procurement/signals?signal=buy_now&abc_class=A&limit=50&offset=0 // ───────────────────────────────────────────────────────────────────────────── procurementRouter.get("/signals", async (req: Request, res: Response) => { try { const { signal, abc_class, form_factor, speed_gbps, limit = "50", offset = "0" } = req.query; // Use DISTINCT ON with the existing idx_reorder_transceiver index instead of // a correlated subquery that would run once per active row (108k+ scans). const params: any[] = []; let idx = 1; const signalFilter = signal ? ` AND rs.signal = $${idx++}` : ""; if (signal) params.push(signal); const abcFilter = abc_class ? ` AND ac.abc_class = $${idx++}` : ""; if (abc_class) params.push(abc_class); const ffFilter = form_factor ? ` AND t.form_factor = $${idx++}` : ""; if (form_factor) params.push(form_factor); const speedFilter = speed_gbps ? ` AND t.speed_gbps = $${idx++}` : ""; if (speed_gbps) params.push(parseFloat(speed_gbps as string)); params.push(parseInt(limit as string), parseInt(offset as string)); const limitIdx = idx; idx++; const offsetIdx = idx; const sql = ` WITH latest AS ( SELECT DISTINCT ON (transceiver_id) id, transceiver_id, signal, signal_strength, reasons, stock_trend, price_trend, lead_time_weeks, hype_phase, computed_at, expires_at, is_demo_data FROM reorder_signals WHERE expires_at > NOW() ORDER BY transceiver_id, computed_at DESC ) SELECT rs.*, t.part_number, t.standard_name, t.form_factor, t.speed_gbps, t.reach_label, t.image_url, t.image_r2_key, ac.abc_class, ac.demand_score, ac.supply_risk, v.name AS vendor_name FROM latest rs JOIN transceivers t ON rs.transceiver_id = t.id LEFT JOIN abc_classification ac ON ac.transceiver_id = t.id LEFT JOIN vendors v ON t.vendor_id = v.id WHERE 1=1${signalFilter}${abcFilter}${ffFilter}${speedFilter} ORDER BY rs.signal_strength DESC LIMIT $${limitIdx} OFFSET $${offsetIdx} `; const result = await pool.query(sql, params); res.json({ data: result.rows, total: result.rowCount }); } catch (err) { console.error("Signals error:", err); res.status(500).json({ error: "Internal server error" }); } }); // ───────────────────────────────────────────────────────────────────────────── // GET /api/procurement/signals/:transceiver_id // ───────────────────────────────────────────────────────────────────────────── procurementRouter.get("/signals/:id", async (req: Request, res: Response) => { try { const { id } = req.params; const [signal, stockHistory, priceHistory, lifecycle] = await Promise.all([ pool.query(` SELECT rs.*, ac.abc_class, ac.demand_score, ac.supply_risk FROM reorder_signals rs LEFT JOIN abc_classification ac ON ac.transceiver_id = rs.transceiver_id WHERE rs.transceiver_id::text = $1 ORDER BY rs.computed_at DESC LIMIT 1 `, [id]), pool.query(` SELECT ss.stock_level, ss.stock_quantity, ss.incoming_quantity, ss.incoming_eta, ss.lead_time_days, ss.moq, ss.price_breaks, ss.scraped_at, ss.crawler_confidence, v.name AS vendor_name FROM stock_snapshots ss JOIN vendors v ON ss.vendor_id = v.id WHERE ss.transceiver_id::text = $1 ORDER BY ss.scraped_at DESC LIMIT 50 `, [id]), pool.query(` SELECT po.price, po.currency, po.time, v.name AS vendor_name FROM price_observations po JOIN vendors v ON po.source_vendor_id = v.id WHERE po.transceiver_id::text = $1 ORDER BY po.time DESC LIMIT 30 `, [id]), pool.query(` SELECT * FROM product_lifecycle_events WHERE transceiver_id::text = $1 ORDER BY effective_date ASC NULLS LAST, created_at DESC `, [id]), ]); res.json({ signal: signal.rows[0] || null, stock_history: stockHistory.rows, price_history: priceHistory.rows, lifecycle_events: lifecycle.rows, }); } catch (err) { console.error("Signal detail error:", err); res.status(500).json({ error: "Internal server error" }); } }); // ───────────────────────────────────────────────────────────────────────────── // GET /api/procurement/abc?class=A&form_factor=QSFP28 // ───────────────────────────────────────────────────────────────────────────── procurementRouter.get("/abc", async (req: Request, res: Response) => { try { const { class: cls, form_factor, speed_gbps, limit = "100", offset = "0" } = req.query; let sql = ` SELECT ac.*, t.part_number, t.standard_name, t.form_factor, t.speed_gbps, t.reach_label, t.image_url, v.name AS vendor_name, rs.signal, rs.signal_strength FROM abc_classification ac JOIN transceivers t ON ac.transceiver_id = t.id LEFT JOIN vendors v ON t.vendor_id = v.id LEFT JOIN LATERAL ( SELECT signal, signal_strength FROM reorder_signals WHERE transceiver_id = ac.transceiver_id AND expires_at > NOW() ORDER BY computed_at DESC LIMIT 1 ) rs ON true WHERE 1=1 `; const params: any[] = []; let idx = 1; if (cls) { sql += ` AND ac.abc_class = $${idx}`; params.push(cls); idx++; } if (form_factor) { sql += ` AND t.form_factor = $${idx}`; params.push(form_factor); idx++; } if (speed_gbps) { sql += ` AND t.speed_gbps = $${idx}`; params.push(parseFloat(speed_gbps as string)); idx++; } sql += ` ORDER BY ac.abc_class, ac.demand_score DESC LIMIT $${idx} OFFSET $${idx + 1}`; params.push(parseInt(limit as string), parseInt(offset as string)); const result = await pool.query(sql, params); if ((req.query.format as string) === "csv") { return sendCSV(res, result.rows, `tip-abc-classification-${new Date().toISOString().slice(0,10)}.csv`); } res.json({ data: result.rows, total: result.rowCount }); } catch (err) { console.error("ABC error:", err); res.status(500).json({ error: "Internal server error" }); } }); // ───────────────────────────────────────────────────────────────────────────── // GET /api/procurement/market-intel?type=&days=90&signal=buy_now // ───────────────────────────────────────────────────────────────────────────── procurementRouter.get("/market-intel", async (req: Request, res: Response) => { try { const { type, days = "90", signal, technology, limit = "50", offset = "0" } = req.query; let sql = ` SELECT * FROM market_intelligence WHERE created_at > NOW() - INTERVAL '1 day' * $1 `; const params: any[] = [parseInt(days as string)]; let idx = 2; if (type) { sql += ` AND intel_type = $${idx}`; params.push(type); idx++; } if (signal) { sql += ` AND buy_signal_implication = $${idx}`; params.push(signal); idx++; } if (technology) { sql += ` AND $${idx} = ANY(technologies)`; params.push(technology); idx++; } sql += ` ORDER BY relevance_score DESC, created_at DESC LIMIT $${idx} OFFSET $${idx + 1}`; params.push(parseInt(limit as string), parseInt(offset as string)); const result = await pool.query(sql, params); res.json({ data: result.rows, total: result.rowCount }); } catch (err) { console.error("Market intel error:", err); res.status(500).json({ error: "Internal server error" }); } }); // ───────────────────────────────────────────────────────────────────────────── // GET /api/procurement/stock-trends/:transceiver_id // ───────────────────────────────────────────────────────────────────────────── procurementRouter.get("/stock-trends/:id", async (req: Request, res: Response) => { try { const result = await pool.query(` SELECT DISTINCT ON (ss.vendor_id, date_trunc('day', ss.scraped_at)) ss.stock_level, ss.stock_quantity, ss.incoming_quantity, ss.incoming_eta, ss.lead_time_days, ss.scraped_at, v.name AS vendor_name FROM stock_snapshots ss JOIN vendors v ON ss.vendor_id = v.id WHERE ss.transceiver_id::text = $1 ORDER BY ss.vendor_id, date_trunc('day', ss.scraped_at) DESC, ss.scraped_at DESC LIMIT 200 `, [req.params.id]); res.json({ data: result.rows }); } catch (err) { console.error("Stock trends error:", err); res.status(500).json({ error: "Internal server error" }); } }); // ───────────────────────────────────────────────────────────────────────────── // GET /api/procurement/lifecycle?type=eol_announced&impact=high&days=180 // ───────────────────────────────────────────────────────────────────────────── procurementRouter.get("/lifecycle", async (req: Request, res: Response) => { try { const { type, impact, technology, signal, days = "180", limit = "50" } = req.query; let sql = ` SELECT ple.*, t.part_number, t.standard_name, t.form_factor, t.speed_gbps FROM product_lifecycle_events ple LEFT JOIN transceivers t ON ple.transceiver_id = t.id WHERE ple.created_at > NOW() - INTERVAL '1 day' * $1 `; const params: any[] = [parseInt(days as string)]; let idx = 2; if (type) { sql += ` AND ple.event_type = $${idx}`; params.push(type); idx++; } if (impact) { sql += ` AND ple.impact_level = $${idx}`; params.push(impact); idx++; } if (technology) { sql += ` AND ple.technology ILIKE $${idx}`; params.push(`%${technology}%`); idx++; } if (signal) { sql += ` AND ple.buy_signal = $${idx}`; params.push(signal); idx++; } sql += ` ORDER BY ple.impact_level DESC, ple.effective_date ASC NULLS LAST, ple.created_at DESC LIMIT $${idx}`; params.push(parseInt(limit as string)); const result = await pool.query(sql, params); res.json({ data: result.rows }); } catch (err) { console.error("Lifecycle error:", err); res.status(500).json({ error: "Internal server error" }); } }); // ───────────────────────────────────────────────────────────────────────────── // GET /api/procurement/ai-clusters?days=90&limit=50&min_transceivers=0 // Returns AI datacenter announcements with transceiver demand estimates // ───────────────────────────────────────────────────────────────────────────── procurementRouter.get("/ai-clusters", async (req: Request, res: Response) => { try { const { days = "90", limit = "50", min_transceivers = "0", } = req.query; const daysN = Math.min(Math.max(parseInt(days as string) || 90, 1), 730); const limitN = Math.min(parseInt(limit as string) || 50, 200); const minTx = parseInt(min_transceivers as string) || 0; const result = await pool.query( `SELECT id, company, title, summary, announced_date, scale_mw, scale_servers, network_speed, estimated_transceivers, deployment_date, location, source_url, source_name, created_at FROM ai_cluster_announcements WHERE (announced_date IS NULL OR announced_date >= NOW() - INTERVAL '1 day' * $1) AND ($2 = 0 OR estimated_transceivers >= $2) ORDER BY announced_date DESC NULLS LAST, created_at DESC LIMIT $3`, [daysN, minTx, limitN] ); // Aggregate stats const statsResult = await pool.query( `SELECT COUNT(*) AS total, COUNT(*) FILTER (WHERE estimated_transceivers > 0) AS with_estimates, SUM(estimated_transceivers) FILTER (WHERE estimated_transceivers > 0) AS total_estimated_transceivers, SUM(scale_mw) FILTER (WHERE scale_mw IS NOT NULL) AS total_mw, COUNT(DISTINCT company) FILTER (WHERE company != 'Unknown') AS distinct_companies FROM ai_cluster_announcements WHERE announced_date >= NOW() - INTERVAL '1 day' * $1`, [daysN] ); res.json({ data: result.rows, stats: statsResult.rows[0], period_days: daysN, }); } catch (err) { console.error("AI clusters error:", err); res.status(500).json({ error: "Internal server error" }); } }); // ───────────────────────────────────────────────────────────────────────────── // GET /api/procurement/internal-demand?velocity_class=fast_mover&limit=100 // Returns Flexoptix internal demand data — real SKU velocity from internal data // ───────────────────────────────────────────────────────────────────────────── procurementRouter.get("/internal-demand", async (req: Request, res: Response) => { try { const { velocity_class, limit = "100", offset = "0", sort = "demand_12m", } = req.query; const allowedSorts: Record = { demand_12m: "fid.demand_12m DESC", demand_3m: "fid.demand_3m DESC", trend: "fid.demand_trend_pct DESC NULLS LAST", sku: "fid.sku ASC", }; const orderBy = allowedSorts[sort as string] ?? allowedSorts["demand_12m"]; const params: unknown[] = []; const conditions: string[] = ["fid.is_internal = true"]; let idx = 1; if (velocity_class) { conditions.push(`fid.velocity_class = $${idx}`); params.push(velocity_class); idx++; } params.push(Math.min(parseInt(limit as string) || 100, 500)); params.push(Math.max(parseInt(offset as string) || 0, 0)); const result = await pool.query( `SELECT fid.id, fid.sku, fid.description, fid.demand_12m, fid.demand_3m, fid.demand_trend_pct, fid.velocity_class, fid.imported_at, t.part_number, t.standard_name, t.form_factor, t.speed_gbps, t.reach_label, t.image_url, v.name AS vendor_name FROM flexoptix_internal_demand fid LEFT JOIN transceivers t ON t.id = fid.transceiver_id LEFT JOIN vendors v ON v.id = t.vendor_id WHERE ${conditions.join(" AND ")} ORDER BY ${orderBy} LIMIT $${idx} OFFSET $${idx + 1}`, params ); // Velocity summary const summaryResult = await pool.query( `SELECT velocity_class, COUNT(*) AS cnt, SUM(demand_12m)::numeric(12,0) AS total_demand_12m, AVG(demand_trend_pct)::numeric(8,1) AS avg_trend_pct FROM flexoptix_internal_demand WHERE is_internal = true GROUP BY velocity_class ORDER BY total_demand_12m DESC NULLS LAST` ); res.json({ data: result.rows, summary: summaryResult.rows, total: result.rowCount, }); } catch (err) { console.error("Internal demand error:", err); res.status(500).json({ error: "Internal server error" }); } }); // ───────────────────────────────────────────────────────────────────────────── // GET /api/procurement/hyperscaler-capex // Hyperscaler quarterly CapEx from SEC filings — demand context for transceivers // ───────────────────────────────────────────────────────────────────────────── procurementRouter.get("/hyperscaler-capex", async (_req: Request, res: Response) => { try { const result = await pool.query(` SELECT company, period_label, period_end, capex_usd_millions, dc_capex_est_millions, yoy_growth_pct, filing_type, source_url FROM hyperscaler_capex ORDER BY period_end DESC, capex_usd_millions DESC `); const summaryResult = await pool.query(` SELECT company, MAX(period_end) AS latest_period_end, MAX(period_label) AS latest_period, (ARRAY_AGG(capex_usd_millions ORDER BY period_end DESC))[1] AS latest_capex, (ARRAY_AGG(dc_capex_est_millions ORDER BY period_end DESC))[1] AS latest_dc_capex, (ARRAY_AGG(yoy_growth_pct ORDER BY period_end DESC))[1] AS latest_yoy_growth, AVG(yoy_growth_pct) FILTER (WHERE period_end >= NOW() - INTERVAL '365 days') AS avg_yoy_12m FROM hyperscaler_capex GROUP BY company ORDER BY latest_capex DESC NULLS LAST `); res.json({ data: result.rows, summary: summaryResult.rows, }); } catch (err) { console.error("Hyperscaler capex error:", err); res.status(500).json({ error: "Internal server error" }); } }); // ───────────────────────────────────────────────────────────────────────────── // GET /api/procurement/marketplace-velocity // Secondary market (eBay) sell-through as demand signal // ───────────────────────────────────────────────────────────────────────────── procurementRouter.get("/marketplace-velocity", async (_req: Request, res: Response) => { try { const result = await pool.query(` SELECT DISTINCT ON (form_factor, speed_label) marketplace, keyword, form_factor, speed_label, sold_count_30d, active_listings, avg_sold_price, min_price, max_price, currency, scraped_at FROM marketplace_velocity ORDER BY form_factor, speed_label, scraped_at DESC `); const hotResult = await pool.query(` SELECT DISTINCT ON (form_factor, speed_label) marketplace, form_factor, speed_label, sold_count_30d, active_listings, avg_sold_price FROM marketplace_velocity WHERE sold_count_30d > 0 ORDER BY form_factor, speed_label, scraped_at DESC `); res.json({ data: result.rows, hot: hotResult.rows.sort( (a: { sold_count_30d: string }, b: { sold_count_30d: string }) => parseInt(b.sold_count_30d) - parseInt(a.sold_count_30d) ), }); } catch (err) { console.error("Marketplace velocity error:", err); res.status(500).json({ error: "Internal server error" }); } }); // ─── E: GET /api/procurement/reorder-top ───────────────────────────────────── // Top buy_now reorder signals with full reasons — 211k precomputed signals procurementRouter.get("/reorder-top", async (req: Request, res: Response) => { const limit = Math.min(parseInt(req.query.limit as string) || 50, 200); const formFactor = (req.query.form_factor as string) || ""; const minStrength = parseFloat(req.query.min_strength as string) || 0; try { const result = await pool.query(` SELECT DISTINCT ON (t.id) t.id, t.part_number, t.speed_gbps, t.form_factor, t.reach_label, v.name AS vendor_name, rs.signal, rs.signal_strength, rs.price_trend, rs.stock_trend, rs.hype_phase, rs.reasons, rs.computed_at FROM reorder_signals rs JOIN transceivers t ON t.id = rs.transceiver_id JOIN vendors v ON v.id = t.vendor_id WHERE rs.signal = 'buy_now' AND rs.is_demo_data = false AND rs.signal_strength >= $1 AND ($2 = '' OR t.form_factor ILIKE $2) ORDER BY t.id, rs.signal_strength DESC, rs.computed_at DESC `, [minStrength, formFactor]); // After DISTINCT ON, re-sort by signal_strength const rows = result.rows.sort( (a: { signal_strength: string }, b: { signal_strength: string }) => parseFloat(b.signal_strength) - parseFloat(a.signal_strength) ); const summary = await pool.query(` SELECT COUNT(*) FILTER (WHERE signal = 'buy_now' AND is_demo_data = false)::int AS buy_now, COUNT(*) FILTER (WHERE signal = 'wait' AND is_demo_data = false)::int AS wait, COUNT(*) FILTER (WHERE signal = 'hold' AND is_demo_data = false)::int AS hold, COUNT(*) FILTER (WHERE signal = 'monitor' AND is_demo_data = false)::int AS monitor, ROUND(AVG(signal_strength) FILTER (WHERE signal = 'buy_now' AND is_demo_data = false)::numeric,3) AS avg_buy_strength FROM reorder_signals `); res.json({ success: true, data: rows.slice(0, limit), summary: summary.rows[0] }); } catch (err) { res.status(500).json({ error: String(err) }); } }); // ─── B: GET /api/procurement/switch-compat ─────────────────────────────────── // Switch ↔ transceiver compatibility matrix procurementRouter.get("/switch-compat", async (req: Request, res: Response) => { const search = (req.query.search as string) || ""; const limitNum = Math.min(parseInt(req.query.limit as string) || 30, 100); try { if (search.length >= 2) { // Search for switches matching query, return their compatible transceivers const switches = await pool.query(` SELECT DISTINCT ON (sw.id) sw.id, v.name AS sw_vendor, sw.model AS sw_model, sw.series AS sw_series, COUNT(c.transceiver_id) OVER (PARTITION BY sw.id)::int AS compat_count FROM switches sw JOIN compatibility c ON c.switch_id = sw.id LEFT JOIN vendors v ON v.id = sw.vendor_id WHERE sw.model ILIKE $1 OR COALESCE(v.name,'') ILIKE $1 OR sw.series ILIKE $1 ORDER BY sw.id, compat_count DESC LIMIT $2 `, [`%${search}%`, limitNum]); // For each matched switch, get top compatible transceivers with prices const switchIds = switches.rows.map((s: { id: string }) => s.id); if (switchIds.length === 0) { return res.json({ success: true, switches: [], transceivers: [] }); } const transceivers = await pool.query(` SELECT c.switch_id, t.id AS tx_id, t.part_number, t.speed_gbps, t.form_factor, t.reach_label, v.name AS vendor_name, c.verification_method, c.status, (SELECT ROUND(MIN(po.price)::numeric,2) FROM price_observations po WHERE po.transceiver_id = t.id AND po.price > 0) AS min_price, (SELECT po.currency FROM price_observations po WHERE po.transceiver_id = t.id AND po.price > 0 ORDER BY po.time DESC LIMIT 1) AS currency FROM compatibility c JOIN transceivers t ON t.id = c.transceiver_id JOIN vendors v ON v.id = t.vendor_id WHERE c.switch_id = ANY($1) AND c.status = 'compatible' ORDER BY t.speed_gbps DESC, t.form_factor LIMIT 200 `, [switchIds]); return res.json({ success: true, switches: switches.rows, transceivers: transceivers.rows, }); } // No search — return top switches by compat count const top = await pool.query(` SELECT v.name AS vendor, sw.model, sw.series, COUNT(c.transceiver_id)::int AS compat_count FROM switches sw JOIN compatibility c ON c.switch_id = sw.id LEFT JOIN vendors v ON v.id = sw.vendor_id WHERE c.status = 'compatible' GROUP BY sw.id, v.name, sw.model, sw.series ORDER BY compat_count DESC LIMIT $1 `, [limitNum]); const stats = await pool.query(` SELECT COUNT(DISTINCT sw.id)::int AS total_switches, COUNT(DISTINCT c.transceiver_id)::int AS total_transceivers, COUNT(*)::int AS total_compat_rows FROM switches sw JOIN compatibility c ON c.switch_id = sw.id WHERE c.status = 'compatible' `); return res.json({ success: true, topSwitches: top.rows, stats: stats.rows[0] }); } catch (err) { res.status(500).json({ error: String(err) }); } }); // ─── A: GET /api/procurement/arbitrage ─────────────────────────────────────── // OEM vs Flexoptix price gaps via transceiver_equivalences procurementRouter.get("/arbitrage", async (_req: Request, res: Response) => { // FX rates for normalization — approximate const FX: Record = { USD: 1.0, EUR: 1.08, GBP: 1.27 }; try { const result = await pool.query(` SELECT te.confidence, fx.part_number AS fx_part, vfx.name AS fx_vendor, fx.speed_gbps, fx.form_factor, fx.reach_label, comp.part_number AS comp_part, vcomp.name AS comp_vendor, (SELECT price FROM price_observations WHERE transceiver_id = te.flexoptix_id AND price > 2 ORDER BY time DESC LIMIT 1) AS fx_price, (SELECT currency FROM price_observations WHERE transceiver_id = te.flexoptix_id AND price > 2 ORDER BY time DESC LIMIT 1) AS fx_curr, (SELECT price FROM price_observations WHERE transceiver_id = te.competitor_id AND price > 2 ORDER BY time DESC LIMIT 1) AS comp_price, (SELECT currency FROM price_observations WHERE transceiver_id = te.competitor_id AND price > 2 ORDER BY time DESC LIMIT 1) AS comp_curr FROM transceiver_equivalences te JOIN transceivers fx ON fx.id = te.flexoptix_id JOIN transceivers comp ON comp.id = te.competitor_id JOIN vendors vfx ON vfx.id = fx.vendor_id JOIN vendors vcomp ON vcomp.id = comp.vendor_id WHERE te.status IN ('approved','auto_approved') AND EXISTS(SELECT 1 FROM price_observations WHERE transceiver_id = te.flexoptix_id AND price > 2) AND EXISTS(SELECT 1 FROM price_observations WHERE transceiver_id = te.competitor_id AND price > 2) ORDER BY te.confidence DESC LIMIT 2000 `); const pairs = result.rows .map((r: { fx_price: string; fx_curr: string; comp_price: string; comp_curr: string; confidence: string; fx_part: string; fx_vendor: string; comp_part: string; comp_vendor: string; speed_gbps: string; form_factor: string; reach_label: string; }) => { const fxUSD = parseFloat(r.fx_price) * (FX[r.fx_curr] || 1.0); const compUSD = parseFloat(r.comp_price) * (FX[r.comp_curr] || 1.0); if (!fxUSD || !compUSD) return null; const savings = compUSD - fxUSD; const savingsPct = Math.round((savings / compUSD) * 100); return { ...r, fxUSD: Math.round(fxUSD), compUSD: Math.round(compUSD), savings: Math.round(savings), savingsPct }; }) .filter((r): r is NonNullable => r !== null && r.savings > 0) .sort((a, b) => b.savingsPct - a.savingsPct) .slice(0, 100); // Stats const totalPairs = result.rows.length; const fxCheaper = pairs.length; const avgSavings = pairs.length ? Math.round(pairs.reduce((s, r) => s + r.savingsPct, 0) / pairs.length) : 0; res.json({ success: true, pairs, stats: { totalPairs, fxCheaper, avgSavingsPct: avgSavings } }); } catch (err) { res.status(500).json({ error: String(err) }); } }); // ─── D: GET /api/procurement/dead-stock-revival ────────────────────────────── // Dead-stock SKUs whose equivalents are in rising hype phases procurementRouter.get("/dead-stock-revival", async (_req: Request, res: Response) => { try { const [deadStock, hypeMap] = await Promise.all([ pool.query(` SELECT fid.transceiver_id, fid.sku AS part_number, fid.velocity_class, fid.demand_12m, fid.demand_trend_pct, t.speed_gbps, t.form_factor, t.reach_label, v.name AS vendor_name FROM flexoptix_internal_demand fid JOIN transceivers t ON t.id = fid.transceiver_id JOIN vendors v ON v.id = t.vendor_id WHERE fid.velocity_class = 'dead_stock' AND fid.is_internal = true LIMIT 7500 `), pool.query(` SELECT DISTINCT ON (technology) technology, hype_phase, hype_score, computed_at FROM hype_cycle_analysis ORDER BY technology, computed_at DESC `), ]); // Build speed → hype phase map type HypeRow = { technology: string; hype_phase: string; hype_score: string }; const ASCENDING = new Set(["innovation_trigger","peak_inflated_expectations","slope_enlightenment","plateau_productivity"]); const speedToHype = new Map(); for (const h of hypeMap.rows as HypeRow[]) { const speedMatch = h.technology.match(/^(\d+(?:\.\d+)?)G/); if (speedMatch) speedToHype.set(parseFloat(speedMatch[1]), h); } type DeadRow = { transceiver_id: string; part_number: string; speed_gbps: string; form_factor: string; reach_label: string; vendor_name: string; demand_12m: string; demand_trend_pct: string; velocity_class: string; }; const revivals = (deadStock.rows as DeadRow[]) .map((r) => { const speed = parseFloat(r.speed_gbps); const hype = speedToHype.get(speed); if (!hype) return null; const ascending = ASCENDING.has(hype.hype_phase); const score = parseFloat(hype.hype_score); return { ...r, hype_phase: hype.hype_phase, hype_score: score, ascending }; }) .filter((r): r is NonNullable => r !== null && r.ascending && r.hype_score > 30) .sort((a, b) => b.hype_score - a.hype_score) .slice(0, 100); const totalDead = deadStock.rows.length; res.json({ success: true, revivals, totalDeadStock: totalDead, revivalCount: revivals.length }); } catch (err) { res.status(500).json({ error: String(err) }); } }); // ─── C: GET /api/procurement/supply-squeeze ────────────────────────────────── // Multi-signal supply constraint detector procurementRouter.get("/supply-squeeze", async (_req: Request, res: Response) => { try { const [priceSignals, aiDemand, hypeData, stockData] = await Promise.all([ // Price momentum: 30d vs 60d avg by speed/form_factor pool.query(` SELECT t.speed_gbps, t.form_factor, ROUND(AVG(po.price) FILTER (WHERE po.time >= NOW() - INTERVAL '30 days')::numeric,2) AS avg_30d, ROUND(AVG(po.price) FILTER (WHERE po.time >= NOW() - INTERVAL '60 days' AND po.time < NOW() - INTERVAL '30 days')::numeric,2) AS avg_prior_30d, COUNT(*) FILTER (WHERE po.time >= NOW() - INTERVAL '30 days') AS obs_30d FROM price_observations po JOIN transceivers t ON t.id = po.transceiver_id WHERE po.price > 5 AND po.currency = 'USD' GROUP BY t.speed_gbps, t.form_factor HAVING COUNT(*) FILTER (WHERE po.time >= NOW() - INTERVAL '30 days') >= 3 `), // AI cluster demand by speed tier pool.query(` SELECT CASE WHEN COALESCE(network_speed, title, summary, '') ILIKE '%800G%' THEN 800 WHEN COALESCE(network_speed, title, summary, '') ILIKE '%400G%' THEN 400 WHEN COALESCE(network_speed, title, summary, '') ILIKE '%100G%' THEN 100 ELSE 0 END AS speed_tier, COALESCE(SUM(estimated_transceivers),0)::int AS total_tx, COUNT(*)::int AS cluster_count FROM ai_cluster_announcements WHERE announced_date >= NOW() - INTERVAL '90 days' GROUP BY 1 HAVING COALESCE(SUM(estimated_transceivers),0) > 0 `), // Hype phase per technology pool.query(` SELECT DISTINCT ON (technology) technology, hype_phase, hype_score FROM hype_cycle_analysis ORDER BY technology, computed_at DESC `), // Stock level distribution (in_stock vs out_of_stock) pool.query(` SELECT t.speed_gbps, t.form_factor, COUNT(*) FILTER (WHERE so.stock_level = 'out_of_stock')::int AS out_of_stock, COUNT(*) FILTER (WHERE so.stock_level = 'in_stock')::int AS in_stock, COUNT(*)::int AS total_obs FROM stock_observations so JOIN transceivers t ON t.id = so.transceiver_id WHERE so.observed_at >= NOW() - INTERVAL '14 days' GROUP BY t.speed_gbps, t.form_factor HAVING COUNT(*) >= 3 `).catch(() => ({ rows: [] })), ]); type PriceRow = { speed_gbps: string; form_factor: string; avg_30d: string; avg_prior_30d: string; obs_30d: string }; type HypeRow = { technology: string; hype_phase: string; hype_score: string }; type AiRow = { speed_tier: string; total_tx: string; cluster_count: string }; type StockRow = { speed_gbps: string; form_factor: string; out_of_stock: string; in_stock: string; total_obs: string }; const speedToHype = new Map(); for (const h of hypeData.rows as HypeRow[]) { const m = h.technology.match(/^(\d+(?:\.\d+)?)G/); if (m) speedToHype.set(parseFloat(m[1]), h); } const aiBySpeed = new Map(); for (const a of aiDemand.rows as AiRow[]) { aiBySpeed.set(parseFloat(a.speed_tier), a); } const stockByKey = new Map(); for (const s of stockData.rows as StockRow[]) { stockByKey.set(`${s.speed_gbps}:${s.form_factor}`, s); } const RISKY_PHASES = new Set(["peak_inflated_expectations","slope_enlightenment","plateau_productivity"]); const signals = (priceSignals.rows as PriceRow[]) .map((r) => { const speed = parseFloat(r.speed_gbps); const priceUp = r.avg_30d && r.avg_prior_30d ? ((parseFloat(r.avg_30d) - parseFloat(r.avg_prior_30d)) / parseFloat(r.avg_prior_30d)) * 100 : 0; const hype = speedToHype.get(speed); const ai = aiBySpeed.get(speed); const stock = stockByKey.get(`${r.speed_gbps}:${r.form_factor}`); let activeSignals = 0; const reasons: string[] = []; if (priceUp > 5) { activeSignals++; reasons.push(`Price +${Math.round(priceUp)}% (30d)`); } if (hype && RISKY_PHASES.has(hype.hype_phase)) { activeSignals++; reasons.push(`Hype: ${hype.hype_phase.replace(/_/g,' ')}`); } if (ai && parseInt(ai.total_tx) > 50000) { activeSignals++; reasons.push(`AI demand: ${parseInt(ai.total_tx).toLocaleString()} tx in 90d`); } if (stock && parseInt(stock.out_of_stock) > parseInt(stock.in_stock)) { activeSignals++; reasons.push(`Stock pressure: ${stock.out_of_stock}/${stock.total_obs} vendors OOS`); } const severity = activeSignals >= 3 ? "critical" : activeSignals === 2 ? "warning" : activeSignals === 1 ? "watch" : "ok"; return { speed_gbps: r.speed_gbps, form_factor: r.form_factor, avg_30d: r.avg_30d, avg_prior_30d: r.avg_prior_30d, price_momentum_pct: Math.round(priceUp), hype_phase: hype?.hype_phase || null, hype_score: hype ? parseFloat(hype.hype_score) : null, ai_demand_tx: ai ? parseInt(ai.total_tx) : 0, activeSignals, severity, reasons, }; }) .filter((r) => r.activeSignals >= 1) .sort((a, b) => b.activeSignals - a.activeSignals || b.price_momentum_pct - a.price_momentum_pct); res.json({ success: true, signals, criticalCount: signals.filter(s => s.severity === "critical").length }); } catch (err) { res.status(500).json({ error: String(err) }); } }); // ───────────────────────────────────────────────────────────────────────────── // GET /api/procurement/lead-times — Rolling lead-time trends per vendor/speed // Query params: form_factor, speed_gbps, days (default 90), limit (default 20) // ───────────────────────────────────────────────────────────────────────────── procurementRouter.get("/lead-times", async (req: Request, res: Response) => { const days = Math.min(parseInt(req.query.days as string) || 90, 365); const limit = Math.min(parseInt(req.query.limit as string) || 20, 50); const ff = req.query.form_factor as string | undefined; const spd = req.query.speed_gbps as string | undefined; try { const [weekly, summary, concentration] = await Promise.all([ // Weekly avg lead time per vendor × speed tier pool.query(` SELECT v.name AS vendor_name, v.id::text AS vendor_id, t.speed_gbps::text, t.form_factor, DATE_TRUNC('week', ss.scraped_at)::date AS week, ROUND(AVG(ss.lead_time_days)::numeric, 1) AS avg_lead_days, ROUND(MIN(ss.lead_time_days)::numeric, 1) AS min_lead_days, ROUND(MAX(ss.lead_time_days)::numeric, 1) AS max_lead_days, COUNT(*)::int AS observations FROM stock_snapshots ss JOIN transceivers t ON t.id = ss.transceiver_id JOIN vendors v ON v.id = ss.source_vendor_id WHERE ss.scraped_at >= NOW() - INTERVAL '${days} days' AND ss.lead_time_days IS NOT NULL AND ss.lead_time_days > 0 ${ff ? `AND t.form_factor = '${ff.replace(/'/g,"''")}'` : ""} ${spd ? `AND t.speed_gbps = ${parseFloat(spd)}` : ""} GROUP BY v.name, v.id, t.speed_gbps, t.form_factor, DATE_TRUNC('week', ss.scraped_at) ORDER BY week DESC, avg_lead_days DESC LIMIT 500 `), // Overall summary: current vs prior-period avg per vendor pool.query(` WITH cur AS ( SELECT v.name AS vendor_name, v.id::text AS vendor_id, ROUND(AVG(ss.lead_time_days)::numeric, 1) AS avg_days, COUNT(*)::int AS obs FROM stock_snapshots ss JOIN vendors v ON v.id = ss.source_vendor_id JOIN transceivers t ON t.id = ss.transceiver_id WHERE ss.scraped_at >= NOW() - INTERVAL '30 days' AND ss.lead_time_days > 0 ${ff ? `AND t.form_factor = '${ff.replace(/'/g,"''")}'` : ""} ${spd ? `AND t.speed_gbps = ${parseFloat(spd)}` : ""} GROUP BY v.name, v.id ), prior AS ( SELECT v.id::text AS vendor_id, ROUND(AVG(ss.lead_time_days)::numeric, 1) AS avg_days FROM stock_snapshots ss JOIN vendors v ON v.id = ss.source_vendor_id JOIN transceivers t ON t.id = ss.transceiver_id WHERE ss.scraped_at >= NOW() - INTERVAL '60 days' AND ss.scraped_at < NOW() - INTERVAL '30 days' AND ss.lead_time_days > 0 ${ff ? `AND t.form_factor = '${ff.replace(/'/g,"''")}'` : ""} ${spd ? `AND t.speed_gbps = ${parseFloat(spd)}` : ""} GROUP BY v.id ) SELECT c.vendor_name, c.vendor_id, c.avg_days AS current_30d_avg, p.avg_days AS prior_30d_avg, ROUND((c.avg_days - COALESCE(p.avg_days, c.avg_days))::numeric, 1) AS delta_days, c.obs FROM cur c LEFT JOIN prior p ON p.vendor_id = c.vendor_id ORDER BY c.avg_days DESC LIMIT ${limit} `), // Speed-tier breakdown — which form factors have longest lead times right now pool.query(` SELECT t.speed_gbps::text, t.form_factor, ROUND(AVG(ss.lead_time_days)::numeric, 1) AS avg_lead_days, COUNT(DISTINCT ss.source_vendor_id)::int AS vendors_reporting, COUNT(*)::int AS total_obs FROM stock_snapshots ss JOIN transceivers t ON t.id = ss.transceiver_id WHERE ss.scraped_at >= NOW() - INTERVAL '30 days' AND ss.lead_time_days > 0 GROUP BY t.speed_gbps, t.form_factor HAVING COUNT(*) >= 3 ORDER BY avg_lead_days DESC LIMIT 20 `), ]); res.json({ success: true, filters: { days, form_factor: ff || null, speed_gbps: spd || null }, weekly_trend: weekly.rows, vendor_summary: summary.rows, speed_tier_breakdown: concentration.rows, }); } catch (err) { res.status(500).json({ error: String(err) }); } }); // ───────────────────────────────────────────────────────────────────────────── // GET /api/procurement/supply-concentration — Single-vendor dependency risk // Flags SKUs where >70% of price observations come from one vendor (30d) // ───────────────────────────────────────────────────────────────────────────── procurementRouter.get("/supply-concentration", async (_req: Request, res: Response) => { try { const result = await pool.query(` WITH obs_30d AS ( SELECT po.transceiver_id, po.source_vendor_id, COUNT(*) AS vendor_obs FROM price_observations po WHERE po.time >= NOW() - INTERVAL '30 days' AND po.price > 0 AND COALESCE(po.is_anomalous, false) = false GROUP BY po.transceiver_id, po.source_vendor_id ), totals AS ( SELECT transceiver_id, SUM(vendor_obs) AS total_obs FROM obs_30d GROUP BY transceiver_id ), ranked AS ( SELECT o.transceiver_id, o.source_vendor_id, o.vendor_obs, t.total_obs, ROUND((o.vendor_obs::numeric / NULLIF(t.total_obs,0)) * 100, 1) AS share_pct, ROW_NUMBER() OVER (PARTITION BY o.transceiver_id ORDER BY o.vendor_obs DESC) AS rnk FROM obs_30d o JOIN totals t ON t.transceiver_id = o.transceiver_id ) SELECT tx.id::text, tx.part_number, tx.form_factor, tx.speed_gbps::text, tx.standard_name, v.name AS dominant_vendor, r.share_pct, r.total_obs::int, r.vendor_obs::int AS dominant_obs, CASE WHEN r.share_pct >= 90 THEN 'critical' WHEN r.share_pct >= 75 THEN 'high' ELSE 'medium' END AS risk_level FROM ranked r JOIN transceivers tx ON tx.id = r.transceiver_id JOIN vendors v ON v.id = r.source_vendor_id WHERE r.rnk = 1 AND r.share_pct >= 70 AND r.total_obs >= 5 ORDER BY r.share_pct DESC LIMIT 50 `); const rows = result.rows; res.json({ success: true, concentrated: rows, stats: { total_at_risk: rows.length, critical: rows.filter(r => r.risk_level === "critical").length, high: rows.filter(r => r.risk_level === "high").length, medium: rows.filter(r => r.risk_level === "medium").length, }, }); } catch (err) { res.status(500).json({ error: String(err) }); } }); // GET /api/procurement/price-movers — SKUs with biggest price delta vs prior period procurementRouter.get("/price-movers", async (req: Request, res: Response) => { const days = Math.min(parseInt(req.query.days as string) || 7, 90); const limit = Math.min(parseInt(req.query.limit as string) || 20, 50); try { const result = await pool.query(` WITH cur AS ( SELECT transceiver_id, source_vendor_id, currency, AVG(price) AS avg_price, COUNT(*) AS obs FROM price_observations WHERE time >= NOW() - INTERVAL '${days} days' AND price > 0 AND COALESCE(is_anomalous, false) = false GROUP BY transceiver_id, source_vendor_id, currency ), prior AS ( SELECT transceiver_id, source_vendor_id, AVG(price) AS avg_price FROM price_observations WHERE time >= NOW() - INTERVAL '${days * 2} days' AND time < NOW() - INTERVAL '${days} days' AND price > 0 AND COALESCE(is_anomalous, false) = false GROUP BY transceiver_id, source_vendor_id ) SELECT t.id, t.part_number, t.form_factor, t.speed_gbps::text AS speed_gbps, t.standard_name, sv.name AS vendor_name, ROUND(c.avg_price::numeric, 2) AS current_avg, ROUND(p.avg_price::numeric, 2) AS prior_avg, ROUND(((c.avg_price - p.avg_price) / NULLIF(p.avg_price, 0) * 100)::numeric, 1) AS delta_pct, c.currency, c.obs::int AS observations FROM cur c JOIN prior p ON p.transceiver_id = c.transceiver_id AND p.source_vendor_id = c.source_vendor_id JOIN transceivers t ON t.id = c.transceiver_id JOIN vendors sv ON sv.id = c.source_vendor_id WHERE ABS((c.avg_price - p.avg_price) / NULLIF(p.avg_price, 0) * 100) >= 2 AND c.obs::int >= 2 ORDER BY ABS((c.avg_price - p.avg_price) / NULLIF(p.avg_price, 0) * 100) DESC LIMIT ${limit * 2} `); const rows = result.rows; const gainers = rows.filter((r) => parseFloat(r.delta_pct) > 0).slice(0, limit); const losers = rows.filter((r) => parseFloat(r.delta_pct) < 0).slice(0, limit); const avgOf = (arr: typeof gainers, key: string) => arr.length ? Math.round(arr.reduce((s, r) => s + parseFloat(r[key]), 0) / arr.length * 10) / 10 : 0; res.json({ success: true, days, gainers, losers, stats: { totalMovers: rows.length, gainersCount: gainers.length, losersCount: losers.length, avgGainPct: avgOf(gainers, "delta_pct"), avgLossPct: avgOf(losers, "delta_pct"), }, }); } catch (err) { res.status(500).json({ error: String(err) }); } });