diff --git a/packages/api/src/routes/procurement.ts b/packages/api/src/routes/procurement.ts index 0f58743..bc02418 100644 --- a/packages/api/src/routes/procurement.ts +++ b/packages/api/src/routes/procurement.ts @@ -781,6 +781,100 @@ procurementRouter.get("/dead-stock-revival", async (_req: Request, res: Response }); // ─── C: GET /api/procurement/supply-squeeze ────────────────────────────────── + +// ─── GET /api/procurement/availability ─────────────────────────────────────── +// Data-grounded supply-availability per speed tier. Derived from real supplier +// diversity (distinct source vendors offering it) + stock coverage. Surfaces the +// 400G/800G/1.6T supply tightening that exists in the data but was not shown. +procurementRouter.get("/availability", async (_req: Request, res: Response) => { + const client = await pool.connect(); + try { + // Disable parallel workers for this aggregate — Docker /dev/shm (64MB) is too + // small for parallel hash and the query is fast enough single-threaded. + await client.query('BEGIN'); + await client.query('SET LOCAL max_parallel_workers_per_gather = 0'); + const result = await client.query(` + WITH sup AS ( + SELECT t.speed_gbps, + COUNT(DISTINCT po.source_vendor_id) FILTER (WHERE po.time > NOW() - INTERVAL '45 days') AS suppliers, + COUNT(DISTINCT po.source_vendor_id) FILTER (WHERE po.time <= NOW() - INTERVAL '45 days') AS suppliers_prior + FROM price_observations po + JOIN transceivers t ON t.id = po.transceiver_id + WHERE t.speed_gbps IN (100,200,400,800,1600) AND po.time > NOW() - INTERVAL '90 days' + GROUP BY t.speed_gbps + ), + sk AS ( + SELECT speed_gbps, COUNT(*) AS sku_count + FROM transceivers WHERE speed_gbps IN (100,200,400,800,1600) + GROUP BY speed_gbps HAVING COUNT(*) >= 3 + ), + st AS ( + SELECT t.speed_gbps, + COUNT(*) FILTER (WHERE s.in_stock IS TRUE) AS skus_in_stock, + COUNT(*) AS skus_with_stock + FROM transceivers t + JOIN LATERAL ( + SELECT in_stock FROM stock_observations so + WHERE so.transceiver_id = t.id ORDER BY so.time DESC LIMIT 1 + ) s ON true + WHERE t.speed_gbps IN (100,200,400,800,1600) + GROUP BY t.speed_gbps + ) + SELECT + sk.speed_gbps, + sk.sku_count::int, + COALESCE(sup.suppliers,0)::int AS suppliers, + COALESCE(sup.suppliers_prior,0)::int AS suppliers_prior, + COALESCE(st.skus_in_stock,0)::int AS skus_in_stock, + COALESCE(st.skus_with_stock,0)::int AS skus_with_stock, + CASE WHEN COALESCE(st.skus_with_stock,0) > 0 + THEN ROUND(100.0 * st.skus_in_stock / st.skus_with_stock)::int ELSE NULL END AS in_stock_pct + FROM sk + LEFT JOIN sup ON sup.speed_gbps = sk.speed_gbps + LEFT JOIN st ON st.speed_gbps = sk.speed_gbps + ORDER BY sk.speed_gbps DESC + `); + + type Row = { speed_gbps: string; sku_count: number; suppliers: number; suppliers_prior: number; skus_in_stock: number; skus_with_stock: number; in_stock_pct: number | null }; + const tiers = (result.rows as Row[]).map((r) => { + const sup = r.suppliers ?? 0; + const inStockPct = r.in_stock_pct ?? null; + // availability class from supplier diversity + stock coverage + let availability: "scarce" | "constrained" | "moderate" | "abundant"; + if (sup <= 3 || (inStockPct !== null && inStockPct < 20)) availability = "scarce"; + else if (sup <= 6 || (inStockPct !== null && inStockPct < 45)) availability = "constrained"; + else if (sup <= 9) availability = "moderate"; + else availability = "abundant"; + // tightening trend: supplier diversity dropped vs prior window + const trend = r.suppliers_prior > 0 + ? (sup < r.suppliers_prior ? "tightening" : sup > r.suppliers_prior ? "loosening" : "stable") + : "stable"; + const drivers: string[] = [`${sup} active suppliers`]; + if (inStockPct !== null) drivers.push(`${inStockPct}% of tracked SKUs in stock`); + drivers.push(`${r.sku_count} SKUs`); + if (trend === "tightening") drivers.push(`supplier base shrinking (${r.suppliers_prior}→${sup})`); + return { + speed_gbps: parseFloat(r.speed_gbps), + sku_count: r.sku_count, + suppliers: sup, + in_stock_pct: inStockPct, + availability, + trend, + drivers, + }; + }); + + res.json({ success: true, tiers }); + } catch (err) { + try { await client.query('ROLLBACK'); } catch { /* ignore */ } + console.error("availability error:", err); + res.status(500).json({ success: false, error: String(err) }); + } finally { + client.release(); + } +}); + + // Multi-signal supply constraint detector procurementRouter.get("/supply-squeeze", async (_req: Request, res: Response) => { try {