/** * Price Comparison Dashboard — Public API * * Public-facing endpoints powering the "Octopart for optical transceivers" * price comparison page. No authentication required. * * Routes: * GET /api/price-comparison — Top 50 SKUs by vendor coverage * GET /api/price-comparison/summary — Market-level aggregate stats * GET /api/price-comparison/:sku — Per-SKU price breakdown */ import { Router, Request, Response } from "express"; import { pool } from "../db/client"; import { sendCSV } from "../utils/csv"; export const priceComparisonRouter = Router(); // ─── GET /api/price-comparison/summary ─────────────────────────────────────── // MUST be registered before /:sku to avoid route conflict /** * Market summary: * - Total unique SKUs tracked * - Total price observations * - Number of active vendors * - Average prices broken down by form_factor */ priceComparisonRouter.get("/summary", async (_req: Request, res: Response) => { try { const [overview, byFormFactor] = await Promise.all([ // Overall market counts pool.query(` WITH latest AS ( SELECT DISTINCT ON (po.transceiver_id, po.source_vendor_id) po.transceiver_id, po.source_vendor_id, po.price, po.currency FROM price_observations po WHERE po.marketplace NOT LIKE 'flexoptix%' ORDER BY po.transceiver_id, po.source_vendor_id, po.time DESC ) SELECT COUNT(DISTINCT l.transceiver_id) AS total_skus_tracked, (SELECT COUNT(*) FROM price_observations)::bigint AS total_observations, COUNT(DISTINCT l.source_vendor_id) AS active_vendor_count, ROUND(AVG(l.price)::numeric, 2) AS overall_avg_price FROM latest l `), // Avg price per form_factor (using latest price per vendor per transceiver) pool.query(` WITH latest AS ( SELECT DISTINCT ON (po.transceiver_id, po.source_vendor_id) po.transceiver_id, po.source_vendor_id, po.price, po.currency FROM price_observations po WHERE po.marketplace NOT LIKE 'flexoptix%' ORDER BY po.transceiver_id, po.source_vendor_id, po.time DESC ) SELECT t.form_factor, COUNT(DISTINCT l.transceiver_id) AS sku_count, COUNT(DISTINCT l.source_vendor_id) AS vendor_count, ROUND(MIN(l.price)::numeric, 2) AS min_price, ROUND(MAX(l.price)::numeric, 2) AS max_price, ROUND(AVG(l.price)::numeric, 2) AS avg_price, -- Most common currency for this form factor ( SELECT currency FROM price_observations po2 JOIN transceivers t2 ON t2.id = po2.transceiver_id WHERE t2.form_factor = t.form_factor GROUP BY currency ORDER BY COUNT(*) DESC LIMIT 1 ) AS currency FROM latest l JOIN transceivers t ON t.id = l.transceiver_id GROUP BY t.form_factor ORDER BY sku_count DESC `), ]); res.json({ success: true, data: { ...overview.rows[0], by_form_factor: byFormFactor.rows, }, }); } catch (err) { console.error("GET /api/price-comparison/summary error:", err); res.status(500).json({ success: false, error: "Internal server error" }); } }); // ─── GET /api/price-comparison ─────────────────────────────────────────────── /** * Top 50 transceivers ranked by number of vendors tracking them. * Add ?format=csv to download as CSV. */ priceComparisonRouter.get("/", async (req: Request, res: Response) => { const fmt = req.query.format as string | undefined; try { const result = await pool.query(` WITH latest AS ( SELECT DISTINCT ON (po.transceiver_id, po.source_vendor_id) po.transceiver_id, po.source_vendor_id, po.price, po.currency FROM price_observations po WHERE po.marketplace NOT LIKE 'flexoptix%' ORDER BY po.transceiver_id, po.source_vendor_id, po.time DESC ) SELECT t.form_factor, t.speed, t.standard_name, COUNT(DISTINCT l.source_vendor_id) AS vendor_count, ROUND(MIN(l.price)::numeric, 2) AS min_price, ROUND(MAX(l.price)::numeric, 2) AS max_price, ROUND(AVG(l.price)::numeric, 2) AS avg_price, -- Dominant currency (most common for this SKU) ( SELECT currency FROM latest l2 WHERE l2.transceiver_id = t.id GROUP BY currency ORDER BY COUNT(*) DESC LIMIT 1 ) AS currency, ROUND( ((MAX(l.price) - MIN(l.price)) / NULLIF(MIN(l.price), 0) * 100)::numeric, 1 ) AS spread_pct FROM latest l JOIN transceivers t ON t.id = l.transceiver_id GROUP BY t.id, t.form_factor, t.speed, t.standard_name ORDER BY vendor_count DESC, avg_price ASC LIMIT 50 `); if (fmt === "csv") { return sendCSV(res, result.rows, `tip-price-comparison-${new Date().toISOString().slice(0,10)}.csv`); } res.json({ success: true, data: result.rows, }); } catch (err) { console.error("GET /api/price-comparison error:", err); res.status(500).json({ success: false, error: "Internal server error" }); } }); // ─── GET /api/price-comparison/:sku ───────────────────────────────────────── /** * Full price breakdown for a single SKU. * :sku matches against standard_name OR part_number (case-insensitive ILIKE). * Returns per-vendor prices, stock status, and aggregate stats. */ priceComparisonRouter.get("/:sku", async (req: Request, res: Response) => { try { const sku = String(req.params.sku).trim(); // Resolve transceiver const transceiverResult = await pool.query( `SELECT id, standard_name, form_factor, speed, reach_label, fiber_type, part_number FROM transceivers WHERE standard_name ILIKE $1 OR part_number ILIKE $1 LIMIT 1`, [sku] ); if (transceiverResult.rows.length === 0) { res.status(404).json({ success: false, error: "Transceiver not found" }); return; } const transceiver = transceiverResult.rows[0]; // Latest price per vendor + stock level from stock_observations (if available) const pricesResult = await pool.query(` SELECT v.name AS vendor, po.price, po.currency, po.stock_level, -- Prefer stock_observations for latest stock info (in_stock is boolean) COALESCE( ( SELECT CASE WHEN so.in_stock THEN 'in_stock' ELSE 'out_of_stock' END FROM stock_observations so WHERE so.transceiver_id = po.transceiver_id AND so.source_vendor_id = po.source_vendor_id ORDER BY so.time DESC LIMIT 1 ), po.stock_level ) AS stock_level, -- Use direct product URL from price observation, fall back to vendor shop/website COALESCE( po.url, v.shop_url, v.website ) AS url, po.time AS observed_at FROM ( SELECT DISTINCT ON (po.transceiver_id, po.source_vendor_id) po.transceiver_id, po.source_vendor_id, po.price, po.currency, po.stock_level, po.url, po.time FROM price_observations po WHERE po.transceiver_id = $1 AND po.marketplace NOT LIKE 'flexoptix%' ORDER BY po.transceiver_id, po.source_vendor_id, po.time DESC ) po JOIN vendors v ON v.id = po.source_vendor_id ORDER BY po.price ASC `, [transceiver.id]); const prices = pricesResult.rows; // Compute aggregate stats const priceValues = prices.map((r) => parseFloat(r.price)).filter((v) => Number.isFinite(v)); let stats: Record = { vendor_count: prices.length, min: null, max: null, avg: null, spread_pct: null, }; if (priceValues.length > 0) { const min = Math.min(...priceValues); const max = Math.max(...priceValues); const avg = priceValues.reduce((a, b) => a + b, 0) / priceValues.length; const spreadPct = min > 0 ? Math.round(((max - min) / min) * 1000) / 10 : null; stats = { vendor_count: prices.length, min: Math.round(min * 100) / 100, max: Math.round(max * 100) / 100, avg: Math.round(avg * 100) / 100, spread_pct: spreadPct, }; } res.json({ success: true, transceiver: { standard_name: transceiver.standard_name, form_factor: transceiver.form_factor, speed: transceiver.speed, reach_label: transceiver.reach_label, fiber_type: transceiver.fiber_type, }, prices, stats, }); } catch (err) { console.error("GET /api/price-comparison/:sku error:", err); res.status(500).json({ success: false, error: "Internal server error" }); } });