transceiver-db/packages/api/src/routes/stock-competitor.ts
Rene Fichtmueller 9bf7da3fda feat(stock): FS.COM competitor stock by technology in Sales Velocity table
Adds /api/stock/competitor-by-tech endpoint aggregating warehouse_de_qty +
warehouse_global_qty from stock_observations for public competitors (FS.COM
etc.) per technology class. Dashboard velocity table gets two new columns
FS.COM DE + FS.COM Global with traffic-light coloring vs. monthly demand.
2026-06-05 23:06:13 +00:00

59 lines
2.6 KiB
TypeScript

import { Router, Request, Response } from "express";
import { pool } from "../db/client";
export const stockCompetitorRouter = Router();
// GET /api/stock/competitor-by-tech
// Returns publicly-available competitor stock levels aggregated by technology (form_factor+speed_gbps).
// Used by Warehouse Stock Intelligence to benchmark Flexoptix demand against competitor availability.
stockCompetitorRouter.get("/competitor-by-tech", async (_req: Request, res: Response) => {
try {
const result = await pool.query(`
SELECT
t.form_factor,
t.speed_gbps::numeric AS speed_gbps,
sv.name AS vendor_name,
COUNT(DISTINCT so.transceiver_id)::int AS skus,
COALESCE(SUM(so.warehouse_de_qty),0)::int AS de_stock,
COALESCE(SUM(so.warehouse_global_qty),0)::int AS global_stock,
COALESCE(SUM(so.quantity_available),0)::int AS qty_available,
MAX(so.time)::date AS last_seen
FROM stock_observations so
JOIN transceivers t ON t.id = so.transceiver_id
JOIN vendors sv ON sv.id = so.source_vendor_id
WHERE so.time > NOW() - INTERVAL '14 days'
AND sv.name IN ('FS.COM','ATGBICS','FiberMall','NADDOD','Prolabs','Blue Optics','Skylane Optics')
GROUP BY t.form_factor, t.speed_gbps, sv.name
ORDER BY sv.name, global_stock DESC NULLS LAST
`);
// Pivot: { "1G SFP": { "FS.COM": { de:..., global:... }, ... }, ... }
const byTech: Record<string, Record<string, { de: number; global: number; skus: number; last_seen: string }>> = {};
const vendors = new Set<string>();
for (const row of result.rows) {
const n = parseFloat(row.speed_gbps);
const spd = n >= 1000
? ((n / 1000 * 10) % 10 === 0 ? Math.round(n / 1000).toString() : (n / 1000).toFixed(1)) + "T"
: ((n * 10) % 10 === 0 ? Math.round(n).toString() : String(n)) + "G";
const key = spd + " " + (row.form_factor || "?");
if (!byTech[key]) byTech[key] = {};
byTech[key][row.vendor_name] = {
de: parseInt(row.de_stock) || 0,
global: parseInt(row.global_stock) || parseInt(row.qty_available) || 0,
skus: parseInt(row.skus) || 0,
last_seen: row.last_seen,
};
vendors.add(row.vendor_name);
}
res.json({
success: true,
vendors: [...vendors].sort(),
by_tech: byTech,
});
} catch (err) {
res.status(500).json({ success: false, error: String(err) });
}
});