/** * 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(); 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) }); } });