diff --git a/packages/api/src/routes/vendors.ts b/packages/api/src/routes/vendors.ts index 2168ed5..88615ee 100644 --- a/packages/api/src/routes/vendors.ts +++ b/packages/api/src/routes/vendors.ts @@ -114,6 +114,39 @@ vendorRouter.post("/", async (req: Request, res: Response) => { }); // GET /api/vendors/:id — Get single vendor with full stats +// GET /api/vendors/reliability — per-vendor data-reliability scores (freshness/frequency/coverage) +vendorRouter.get("/reliability", async (_req: Request, res: Response) => { + try { + const result = await pool.query(` + SELECT v.id::text AS vendor_id, v.name AS vendor_name, + COUNT(DISTINCT t.id)::int AS sku_count, + COUNT(po.transceiver_id)::int AS obs_count, + MAX(po.time) AS last_obs + FROM vendors v + JOIN transceivers t ON t.vendor_id = v.id + LEFT JOIN price_observations po ON po.transceiver_id = t.id + GROUP BY v.id, v.name + HAVING COUNT(DISTINCT t.id) > 0`); + const rows = result.rows as Array<{ vendor_id: string; vendor_name: string; sku_count: number; obs_count: number; last_obs: string | null }>; + const maxSku = Math.max(1, ...rows.map((r) => r.sku_count)); + const maxObs = Math.max(1, ...rows.map((r) => r.obs_count)); + const now = Date.now(); + const vendors = rows.map((r) => { + const daysStale = r.last_obs ? Math.max(0, (now - new Date(r.last_obs).getTime()) / 86400000) : 999; + const freshness_score = Math.max(0, Math.min(100, Math.round(100 - daysStale * 3))); + const frequency_score = Math.round(100 * Math.min(1, Math.log10(r.obs_count + 1) / Math.log10(maxObs + 1))); + const coverage_score = Math.round(100 * Math.min(1, r.sku_count / maxSku)); + const reliability_score = Math.round(0.4 * freshness_score + 0.3 * frequency_score + 0.3 * coverage_score); + return { vendor_id: r.vendor_id, vendor_name: r.vendor_name, sku_count: r.sku_count, obs_count: r.obs_count, last_obs: r.last_obs, freshness_score, frequency_score, coverage_score, reliability_score }; + }); + vendors.sort((a, b) => b.reliability_score - a.reliability_score); + res.json({ success: true, count: vendors.length, vendors }); + } catch (err) { + console.error("vendor reliability error:", err); + res.status(500).json({ success: false, error: "Failed to compute vendor reliability" }); + } +}); + vendorRouter.get("/:id", async (req: Request, res: Response, next: NextFunction) => { if (req.params.id === "market-share" || req.params.id === "intelligence") return next(); try {