feat(procurement): data-grounded supply-availability signal per speed tier (1.6T scarce/800G moderate/400G abundant from real supplier+stock data)
This commit is contained in:
parent
de40c65b01
commit
5d69f0160c
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user