/** * WS4: Competitor Intelligence — Alerts & Price Changes */ import { Router } from "express"; import { pool } from "../db/client"; export const competitorRouter = Router(); /** * GET /api/competitor-alerts?vendor=&type=&severity=&days=&limit=&offset= */ competitorRouter.get("/", async (req, res) => { try { const { vendor, type, severity, days = "7", acknowledged, limit = "50", offset = "0" } = req.query; let sql = ` SELECT ca.*, v.name AS vendor_name, v.slug AS vendor_slug FROM competitor_alerts ca LEFT JOIN vendors v ON ca.vendor_id = v.id WHERE ca.created_at > NOW() - INTERVAL '1 day' * $1 `; const params: any[] = [parseInt(days as string)]; let idx = 2; if (vendor) { sql += ` AND v.slug = $${idx}`; params.push(vendor); idx++; } if (type) { sql += ` AND ca.alert_type = $${idx}`; params.push(type); idx++; } if (severity) { sql += ` AND ca.severity = $${idx}`; params.push(severity); idx++; } if (acknowledged === 'false') { sql += ` AND ca.acknowledged = false`; } sql += ` ORDER BY ca.created_at DESC LIMIT $${idx} OFFSET $${idx + 1}`; params.push(parseInt(limit as string), parseInt(offset as string)); const result = await pool.query(sql, params); // Summary stats const stats = await pool.query(` SELECT alert_type, COUNT(*) AS count, COUNT(*) FILTER (WHERE acknowledged = false) AS unread FROM competitor_alerts WHERE created_at > NOW() - INTERVAL '1 day' * $1 GROUP BY alert_type ORDER BY count DESC `, [parseInt(days as string)]); res.json({ alerts: result.rows, total: result.rowCount, stats: stats.rows, period_days: parseInt(days as string), }); } catch (err) { console.error("Competitor alerts error:", err); res.status(500).json({ error: "Internal server error" }); } }); /** * GET /api/competitor-alerts/price-changes?vendor=&speed=&days= */ competitorRouter.get("/price-changes", async (req, res) => { try { const { vendor, speed, days = "30", limit = "50" } = req.query; let sql = ` SELECT pc.*, v.name AS vendor_name, t.slug, t.form_factor, t.speed_gbps, t.reach_label FROM price_changes pc JOIN vendors v ON pc.vendor_id = v.id JOIN transceivers t ON pc.transceiver_id = t.id WHERE pc.detected_at > NOW() - INTERVAL '1 day' * $1 `; const params: any[] = [parseInt(days as string)]; let idx = 2; if (vendor) { sql += ` AND v.slug = $${idx}`; params.push(vendor); idx++; } if (speed) { sql += ` AND t.speed_gbps = $${idx}`; params.push(parseFloat(speed as string)); idx++; } sql += ` ORDER BY ABS(pc.delta_pct) DESC LIMIT $${idx}`; params.push(parseInt(limit as string)); const result = await pool.query(sql, params); res.json({ price_changes: result.rows, total: result.rowCount }); } catch (err) { console.error("Price changes error:", err); res.status(500).json({ error: "Internal server error" }); } }); /** * PUT /api/competitor-alerts/:id/acknowledge */ competitorRouter.put("/:id/acknowledge", async (req, res) => { try { const { notes } = req.body || {}; await pool.query( `UPDATE competitor_alerts SET acknowledged = true, notes = COALESCE($2, notes) WHERE id = $1`, [req.params.id, notes] ); res.json({ success: true }); } catch (err) { res.status(500).json({ error: "Internal server error" }); } }); /** * GET /api/competitor-alerts/summary * * High-level competitor intelligence overview */ competitorRouter.get("/summary", async (req, res) => { try { const [alertsByVendor, recentDrops, newProducts, coverage] = await Promise.all([ pool.query(` SELECT v.name, v.slug, COUNT(*) AS alert_count, COUNT(*) FILTER (WHERE ca.alert_type = 'price_drop') AS drops, COUNT(*) FILTER (WHERE ca.alert_type = 'price_increase') AS increases, COUNT(*) FILTER (WHERE ca.alert_type = 'new_product') AS new_products FROM competitor_alerts ca JOIN vendors v ON ca.vendor_id = v.id WHERE ca.created_at > NOW() - INTERVAL '7 days' GROUP BY v.name, v.slug ORDER BY alert_count DESC LIMIT 20 `), pool.query(` SELECT pc.*, v.name AS vendor_name, t.form_factor, t.speed_gbps, t.reach_label FROM price_changes pc JOIN vendors v ON pc.vendor_id = v.id JOIN transceivers t ON pc.transceiver_id = t.id WHERE pc.delta_pct < -5 AND pc.detected_at > NOW() - INTERVAL '7 days' ORDER BY pc.delta_pct ASC LIMIT 10 `), pool.query(` SELECT ca.*, v.name AS vendor_name FROM competitor_alerts ca JOIN vendors v ON ca.vendor_id = v.id WHERE ca.alert_type = 'new_product' AND ca.created_at > NOW() - INTERVAL '30 days' ORDER BY ca.created_at DESC LIMIT 20 `), pool.query(`SELECT * FROM v_price_coverage WHERE has_recent_price = false LIMIT 20`), ]); res.json({ period: "7 days", by_vendor: alertsByVendor.rows, biggest_price_drops: recentDrops.rows, new_competitor_products: newProducts.rows, products_missing_prices: coverage.rows, }); } catch (err) { console.error("Summary error:", err); res.status(500).json({ error: "Internal server error" }); } });