157 lines
4.8 KiB
TypeScript
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) });
|
|
}
|
|
});
|