1148 lines
50 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<string, string> = {
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<string, number> = { 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<typeof r> => 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<number, HypeRow>();
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<typeof r> => 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<number, HypeRow>();
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<number, AiRow>();
for (const a of aiDemand.rows as AiRow[]) {
aiBySpeed.set(parseFloat(a.speed_tier), a);
}
const stockByKey = new Map<string, StockRow>();
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) });
}
});