157 lines
4.8 KiB
TypeScript

/**
* Bulk Price Lookup
*
* Routes:
* POST /api/bulk-price — Get current prices for multiple part numbers at once
*/
import { Router, Request, Response } from "express";
import { pool } from "../db/client";
export const bulkPriceRouter = Router();
const MAX_PART_NUMBERS = 100;
// ─── POST /api/bulk-price ─────────────────────────────────────────────────────
bulkPriceRouter.post("/", async (req: Request, res: Response) => {
try {
const { part_numbers, limit } = req.body as {
part_numbers?: unknown;
limit?: unknown;
};
if (!Array.isArray(part_numbers) || part_numbers.length === 0) {
res.status(400).json({ success: false, error: "part_numbers must be a non-empty array" });
return;
}
const safe = part_numbers
.filter((p): p is string => typeof p === "string" && p.trim().length > 0)
.slice(0, MAX_PART_NUMBERS)
.map((p) => p.trim());
if (safe.length === 0) {
res.status(400).json({ success: false, error: "No valid part numbers provided" });
return;
}
const perVendorLimit = typeof limit === "number" && limit > 0 ? Math.min(limit, 50) : 10;
// Build $1,$2,... placeholders for the IN clause
const placeholders = safe.map((_, i) => `$${i + 1}`).join(", ");
const result = await pool.query<{
part_number: string;
transceiver_id: number;
model_name: string;
form_factor: string;
speed_gbps: number;
vendor_id: number;
vendor_name: string;
price: string;
currency: string;
observed_at: Date;
}>(
`WITH matched AS (
SELECT id, part_number, COALESCE(standard_name, part_number, '') AS model_name, form_factor, speed_gbps
FROM transceivers
WHERE part_number ILIKE ANY (ARRAY[${placeholders}])
),
recent_prices AS (
SELECT DISTINCT ON (po.transceiver_id, po.source_vendor_id)
po.transceiver_id,
po.source_vendor_id,
po.price,
po.currency,
po.time AS observed_at
FROM price_observations po
JOIN matched m ON m.id = po.transceiver_id
WHERE po.time > NOW() - INTERVAL '30 days'
ORDER BY po.transceiver_id, po.source_vendor_id, po.time DESC
)
SELECT
m.part_number,
m.id AS transceiver_id,
m.model_name,
m.form_factor,
m.speed_gbps,
v.id AS vendor_id,
v.name AS vendor_name,
rp.price,
rp.currency,
rp.observed_at
FROM matched m
LEFT JOIN recent_prices rp ON rp.transceiver_id = m.id
LEFT JOIN vendors v ON v.id = rp.source_vendor_id
ORDER BY m.part_number, rp.price ASC NULLS LAST
LIMIT $${safe.length + 1}`,
[...safe, safe.length * perVendorLimit]
);
// Group rows by part_number
type PriceEntry = {
vendor_id: number;
vendor_name: string;
price_usd: number; // normalised name for API output
currency: string;
observed_at: string;
};
type ResultEntry = {
part_number: string;
transceiver_id: number;
model_name: string;
form_factor: string;
speed_gbps: number;
prices: PriceEntry[];
best_price_usd: number | null;
price_count: number;
};
const map = new Map<string, ResultEntry>();
for (const row of result.rows) {
if (!map.has(row.part_number)) {
map.set(row.part_number, {
part_number: row.part_number,
transceiver_id: row.transceiver_id,
model_name: row.model_name,
form_factor: row.form_factor,
speed_gbps: row.speed_gbps,
prices: [],
best_price_usd: null,
price_count: 0,
});
}
const entry = map.get(row.part_number)!;
if (row.vendor_id !== null && row.price !== null) {
const priceNum = parseFloat(row.price);
entry.prices.push({
vendor_id: row.vendor_id,
vendor_name: row.vendor_name,
price_usd: priceNum,
currency: row.currency,
observed_at: row.observed_at.toISOString(),
});
if (entry.best_price_usd === null || priceNum < entry.best_price_usd) {
entry.best_price_usd = priceNum;
}
entry.price_count += 1;
}
}
const foundKeys = new Set(map.keys());
const notFound = safe.filter(
(pn) => !Array.from(foundKeys).some((k) => k.toLowerCase() === pn.toLowerCase())
);
res.json({
success: true,
results: Array.from(map.values()),
total_found: map.size,
not_found: notFound,
});
} catch (err) {
console.error("POST /api/bulk-price error:", err);
res.status(500).json({ success: false, error: String(err) });
}
});