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)
274 lines
9.8 KiB
TypeScript
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" });
|
|
}
|
|
});
|