import { Router, Request, Response } from "express"; import { pool } from "../db/client"; import { listVendors } from "../db/queries"; export const vendorRouter = Router(); // GET /api/vendors — List all vendors vendorRouter.get("/", async (req: Request, res: Response) => { try { const vendors = await listVendors(req.query.type ? String(req.query.type) : undefined); res.json({ success: true, data: vendors, total: vendors.length }); } catch (err) { console.error("List vendors error:", err); res.status(500).json({ success: false, error: "Internal server error" }); } }); // POST /api/vendors — Create a new vendor + queue auto-crawl vendorRouter.post("/", async (req: Request, res: Response) => { try { const { name, type, website, shop_url, headquarters, country, founded_year, revenue_usd, employee_count, market_position, specialties, is_competitor, } = req.body as Record; if (!name || typeof name !== "string" || !name.trim()) { return res.status(400).json({ success: false, error: "name is required" }); } const validTypes = ["manufacturer", "distributor", "oem", "reseller", "compatible"]; const resolvedType = validTypes.includes(type) ? type : "compatible"; // Generate slug from name const slug = name .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, ""); // Build specialties array const specialtiesArr: string[] = Array.isArray(specialties) ? specialties : typeof specialties === "string" && specialties.trim() ? specialties.split(",").map((s: string) => s.trim()).filter(Boolean) : []; // Insert vendor const insertResult = await pool.query( `INSERT INTO vendors (name, slug, type, website, shop_url, headquarters, country, founded_year, revenue_usd, employee_count, market_position, specialties, is_competitor, scrape_config) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *`, [ name.trim(), slug, resolvedType, website || null, shop_url || website || null, headquarters || null, country || null, founded_year ? Number(founded_year) : null, revenue_usd ? Number(revenue_usd) : null, employee_count ? Number(employee_count) : null, market_position || null, specialtiesArr, is_competitor === true || is_competitor === "true", website ? JSON.stringify({ url: website, enabled: true, auto_queued: true }) : "{}", ] ); const vendor = insertResult.rows[0]; // Queue crawl job: insert into crawler_llm_log as a pending task signal // The scraper fleet polls for new vendors with scrape_config.enabled=true if (website) { await pool .query( `INSERT INTO crawler_llm_log (vendor_id, url, action, model, tokens_used, created_at) VALUES ($1, $2, 'vendor_created_auto_crawl', 'system', 0, NOW())`, [vendor.id, website] ) .catch(() => null); // Non-fatal — vendor is created regardless } return res.status(201).json({ success: true, vendor, crawl_queued: !!website, message: website ? `Vendor "${name}" created. Auto-crawl queued for ${website}.` : `Vendor "${name}" created. Add a website URL to enable auto-crawl.`, }); } catch (err: any) { if (err.code === "23505") { // Unique constraint violation (name or slug) return res.status(409).json({ success: false, error: "A vendor with this name already exists." }); } console.error("Create vendor error:", err); return res.status(500).json({ success: false, error: "Internal server error" }); } }); // GET /api/vendors/:id — Get single vendor with full stats vendorRouter.get("/:id", async (req: Request, res: Response) => { try { const { id } = req.params; const result = await pool.query( `SELECT v.*, (SELECT COUNT(*) FROM transceivers t WHERE t.vendor_id = v.id)::int AS transceiver_count, (SELECT COUNT(*) FROM switches s WHERE s.vendor_id = v.id)::int AS switch_count, (SELECT COUNT(*) FROM price_observations po JOIN transceivers t ON po.transceiver_id = t.id WHERE t.vendor_id = v.id)::int AS price_obs_count FROM vendors v WHERE v.id::text = $1 OR v.slug = $1`, [id] ); const vendor = result.rows[0]; if (!vendor) return res.status(404).json({ success: false, error: "Vendor not found" }); return res.json({ success: true, vendor }); } catch (err) { console.error("Get vendor error:", err); return res.status(500).json({ success: false, error: "Internal server error" }); } });