187 lines
6.5 KiB
TypeScript

import { Router, Request, Response } from "express";
import { pool } from "../db/client";
export const equivalencesRouter = Router();
// GET /api/equivalences?q=<part_number>&vendor=<vendor>&limit=50&offset=0
// Search equivalences by competitor or Flexoptix part number
equivalencesRouter.get("/", async (req: Request, res: Response) => {
const { q, vendor, limit: lim, offset: off } = req.query as Record<string, string>;
const limit = Math.min(parseInt(lim || "50"), 200);
const offset = parseInt(off || "0");
const conditions: string[] = ["e.status IN ('approved', 'auto_approved')"];
const values: unknown[] = [];
let idx = 1;
if (q) {
conditions.push(
`(fx.part_number ILIKE $${idx} OR fx.standard_name ILIKE $${idx} OR cx.part_number ILIKE $${idx} OR cx.standard_name ILIKE $${idx})`
);
values.push(`%${q}%`);
idx++;
}
if (vendor) {
conditions.push(`(cv.name ILIKE $${idx} OR fv.name ILIKE $${idx})`);
values.push(`%${vendor}%`);
idx++;
}
const where = `WHERE ${conditions.join(" AND ")}`;
const query = `
SELECT
e.id,
e.confidence,
e.match_basis,
e.status,
e.created_at,
-- Flexoptix side
fx.id AS flexoptix_id,
fx.part_number AS flexoptix_pn,
fx.standard_name AS flexoptix_std,
fx.form_factor AS flexoptix_form_factor,
fx.speed AS flexoptix_speed,
fx.speed_gbps AS flexoptix_speed_gbps,
fx.reach_label AS flexoptix_reach,
fx.product_page_url AS flexoptix_url,
fx.price_verified_eur AS flexoptix_price_eur,
fx.market_status AS flexoptix_market_status,
-- Competitor side
cx.id AS competitor_id,
cx.part_number AS competitor_pn,
cx.standard_name AS competitor_std,
cx.form_factor AS competitor_form_factor,
cx.speed AS competitor_speed,
cx.reach_label AS competitor_reach,
cx.product_page_url AS competitor_url,
cx.price_verified_eur AS competitor_price_eur,
cx.market_status AS competitor_market_status,
cv.name AS competitor_vendor,
cv.website AS competitor_vendor_website
FROM transceiver_equivalences e
JOIN transceivers fx ON fx.id = e.flexoptix_id
JOIN vendors fv ON fv.id = fx.vendor_id
JOIN transceivers cx ON cx.id = e.competitor_id
JOIN vendors cv ON cv.id = cx.vendor_id
${where}
ORDER BY e.confidence DESC, e.status DESC
LIMIT ${limit} OFFSET ${offset}
`;
const countQuery = `
SELECT COUNT(*) FROM transceiver_equivalences e
JOIN transceivers fx ON fx.id = e.flexoptix_id
JOIN vendors fv ON fv.id = fx.vendor_id
JOIN transceivers cx ON cx.id = e.competitor_id
JOIN vendors cv ON cv.id = cx.vendor_id
${where}
`;
try {
const [data, count] = await Promise.all([
pool.query(query, values),
pool.query(countQuery, values),
]);
res.json({
success: true,
data: data.rows,
total: parseInt(count.rows[0].count),
limit,
offset,
});
} catch (err) {
res.status(500).json({ success: false, error: (err as Error).message });
}
});
// GET /api/equivalences/transceiver/:id — all equivalences for a specific transceiver (both sides)
equivalencesRouter.get("/transceiver/:id", async (req: Request, res: Response) => {
const { id } = req.params;
try {
const result = await pool.query(
`SELECT
e.id,
e.confidence,
e.match_basis,
e.status,
-- Flexoptix side
fx.id AS flexoptix_id,
fx.part_number AS flexoptix_pn,
fx.standard_name AS flexoptix_std,
fx.form_factor AS flexoptix_form_factor,
fx.speed AS flexoptix_speed,
fx.reach_label AS flexoptix_reach,
fx.product_page_url AS flexoptix_url,
fx.price_verified_eur AS flexoptix_price_eur,
-- Competitor side
cx.id AS competitor_id,
cx.part_number AS competitor_pn,
cx.standard_name AS competitor_std,
cx.form_factor AS competitor_form_factor,
cx.speed AS competitor_speed,
cx.reach_label AS competitor_reach,
cx.product_page_url AS competitor_url,
cx.price_verified_eur AS competitor_price_eur,
cv.name AS competitor_vendor,
cv.website AS competitor_vendor_website
FROM transceiver_equivalences e
JOIN transceivers fx ON fx.id = e.flexoptix_id
JOIN transceivers cx ON cx.id = e.competitor_id
JOIN vendors cv ON cv.id = cx.vendor_id
WHERE (e.flexoptix_id = $1::uuid OR e.competitor_id = $1::uuid)
AND e.status IN ('approved', 'auto_approved')
ORDER BY e.confidence DESC`,
[id]
);
res.json({ success: true, data: result.rows, total: result.rows.length });
} catch (err) {
res.status(500).json({ success: false, error: (err as Error).message });
}
});
// GET /api/equivalences/stats — overview numbers
equivalencesRouter.get("/stats", async (_req: Request, res: Response) => {
try {
const result = await pool.query(`
SELECT
COUNT(*) FILTER (WHERE status IN ('approved','auto_approved')) AS active,
COUNT(DISTINCT competitor_id) FILTER (WHERE status IN ('approved','auto_approved')) AS unique_competitor_products,
COUNT(DISTINCT flexoptix_id) FILTER (WHERE status IN ('approved','auto_approved')) AS unique_flexoptix_products,
COUNT(DISTINCT cv.name) AS unique_competitor_vendors,
AVG(confidence) FILTER (WHERE status IN ('approved','auto_approved'))::numeric(4,3) AS avg_confidence
FROM transceiver_equivalences e
JOIN transceivers cx ON cx.id = e.competitor_id
JOIN vendors cv ON cv.id = cx.vendor_id
`);
res.json({ success: true, stats: result.rows[0] });
} catch (err) {
res.status(500).json({ success: false, error: (err as Error).message });
}
});
// GET /api/equivalences/top-vendors — which competitor vendors have most equivalences
equivalencesRouter.get("/top-vendors", async (_req: Request, res: Response) => {
try {
const result = await pool.query(`
SELECT
cv.name AS vendor,
cv.website,
COUNT(*) AS equiv_count,
COUNT(DISTINCT e.competitor_id) AS products_covered,
AVG(e.confidence)::numeric(4,3) AS avg_confidence
FROM transceiver_equivalences e
JOIN transceivers cx ON cx.id = e.competitor_id
JOIN vendors cv ON cv.id = cx.vendor_id
WHERE e.status IN ('approved','auto_approved')
GROUP BY cv.name, cv.website
ORDER BY equiv_count DESC
LIMIT 20
`);
res.json({ success: true, data: result.rows });
} catch (err) {
res.status(500).json({ success: false, error: (err as Error).message });
}
});