/** * 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"; 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(` SELECT signal, COUNT(*) AS count FROM reorder_signals WHERE expires_at > NOW() AND computed_at = (SELECT MAX(r2.computed_at) FROM reorder_signals r2 WHERE r2.transceiver_id = reorder_signals.transceiver_id) 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; let sql = ` 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 reorder_signals 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 rs.expires_at > NOW() AND rs.computed_at = ( SELECT MAX(r2.computed_at) FROM reorder_signals r2 WHERE r2.transceiver_id = rs.transceiver_id ) `; const params: any[] = []; let idx = 1; if (signal) { sql += ` AND rs.signal = $${idx}`; params.push(signal); idx++; } if (abc_class) { sql += ` AND ac.abc_class = $${idx}`; params.push(abc_class); 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 rs.signal_strength 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("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); 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" }); } });