transceiver-db/packages/api/src/routes/price-comparison.ts
root 443c1f08c6 security: exclude flexoptix-marketplace data from all public API endpoints
Public price-comparison and hot-topics routes previously had no filter
for the flexoptix/flexoptix-selfconfigure/flexoptix-msrp marketplaces.
Flexoptix API data is customer-confidential and must not leave the system.

Adds marketplace NOT LIKE flexoptix% filter to:
- /api/price-comparison (top-50 CTE)
- /api/price-comparison/summary (both CTEs)
- /api/price-comparison/:sku (per-vendor subquery)
- /api/hot-topics price_drops query (vendor name filter)
2026-07-07 14:44:57 +00:00

274 lines
9.8 KiB
TypeScript

/**
* 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<string, number | null> = {
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" });
}
});