/** * Portal Setup — client selects mailbox unit + Canadian DID after payment. * * GET /api/v1/portal/setup-info — order details + AMB location + flags * GET /api/v1/portal/setup-units — scrape available unit numbers from AMB * GET /api/v1/portal/setup-dids — search available Canadian DIDs from Flowroute * POST /api/v1/portal/setup-confirm — client confirms → dispatches purchase job */ import { Router, type Request, type Response } from "express"; import { pool } from "../db.js"; import { requirePortalAuth } from "../middleware/portalAuth.js"; const router = Router(); const FLOWROUTE_KEY = process.env.FLOWROUTE_ACCESS_KEY || ""; const FLOWROUTE_SECRET = process.env.FLOWROUTE_SECRET_KEY || ""; const FLOWROUTE_BASE = "https://api.flowroute.com"; const WORKER_URL = process.env.WORKER_URL || "http://workers:8090"; // ── GET /api/v1/portal/setup-info ────────────────────────────────────────── router.get("/api/v1/portal/setup-info", requirePortalAuth, async (req: Request, res: Response) => { const orderId = (req as any).portalAuth?.order_id || req.query.order_id; if (!orderId) { res.status(400).json({ error: "order_id required" }); return; } try { const { rows } = await pool.query( `SELECT o.order_number, o.customer_name, o.customer_email, o.company_type, o.director_name, o.entity_name, o.has_own_ca_address, o.amb_location_slug, o.amb_annual_price_cents, o.client_selected_unit, o.client_selected_did, o.funds_available, o.payment_status, a.name AS amb_name, a.full_address AS amb_address, a.city AS amb_city, a.provider_url AS amb_url FROM canada_crtc_orders o LEFT JOIN amb_locations a ON a.slug = o.amb_location_slug WHERE o.order_number = $1`, [orderId], ); if (!rows.length) { res.status(404).json({ error: "Order not found" }); return; } const order = rows[0] as Record; res.json({ order_number: order.order_number, customer_name: order.customer_name, has_own_ca_address: order.has_own_ca_address, company_type: order.company_type, funds_available: order.funds_available, payment_status: order.payment_status, amb_location: order.amb_location_slug ? { slug: order.amb_location_slug, name: order.amb_name, address: order.amb_address, city: order.amb_city, url: order.amb_url, annual_price_cents: order.amb_annual_price_cents, } : null, selected_unit: order.client_selected_unit || null, selected_did: order.client_selected_did || null, }); } catch (err: any) { console.error("[portal-setup] setup-info error:", err); res.status(500).json({ error: "Internal error" }); } }); // ── GET /api/v1/portal/setup-units ───────────────────────────────────────── // Dispatches a scrape job to the workers container and returns available units. // For now, returns a placeholder — real scraping will be async via worker. router.get("/api/v1/portal/setup-units", requirePortalAuth, async (req: Request, res: Response) => { const orderId = (req as any).portalAuth?.order_id || req.query.order_id; if (!orderId) { res.status(400).json({ error: "order_id required" }); return; } try { // Get the AMB location URL for this order const { rows } = await pool.query( `SELECT a.provider_url FROM canada_crtc_orders o JOIN amb_locations a ON a.slug = o.amb_location_slug WHERE o.order_number = $1 AND o.has_own_ca_address = FALSE`, [orderId], ); if (!rows.length) { res.status(404).json({ error: "No mailbox location selected for this order" }); return; } const locationUrl = rows[0].provider_url as string; // Call the worker to scrape available units try { const workerRes = await fetch(`${WORKER_URL}/jobs`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "scrape_amb_units", data: { location_url: locationUrl, order_id: orderId }, }), }); const workerData = await workerRes.json() as { units?: string[]; error?: string }; if (workerData.units) { res.json({ units: workerData.units, location_url: locationUrl }); } else { res.json({ units: [], error: workerData.error || "No units found" }); } } catch (workerErr) { console.error("[portal-setup] Worker scrape_amb_units failed:", workerErr); res.status(502).json({ error: "Could not fetch available units. Please try again." }); } } catch (err: any) { console.error("[portal-setup] setup-units error:", err); res.status(500).json({ error: "Internal error" }); } }); // ── GET /api/v1/portal/setup-dids ────────────────────────────────────────── // Returns available Canadian DIDs from Flowroute (up to 10 per area code). // Area codes are selected based on the order's incorporation province. router.get("/api/v1/portal/setup-dids", requirePortalAuth, async (_req: Request, res: Response) => { if (!FLOWROUTE_KEY || !FLOWROUTE_SECRET) { res.status(503).json({ error: "DID provider not configured" }); return; } const CA_AREA_CODES: Record = { BC: ["604", "778", "236", "250"], ON: ["416", "437", "647", "905", "289", "365", "519", "226", "548", "613", "343", "705", "249", "807"], }; // Determine province from the order (default BC for backward compat) const province = ((_req as any).portalAuth?.province || "BC").toUpperCase(); const areaCodes = CA_AREA_CODES[province] || CA_AREA_CODES.BC; const auth = Buffer.from(`${FLOWROUTE_KEY}:${FLOWROUTE_SECRET}`).toString("base64"); try { const results: Record> = {}; for (const areaCode of areaCodes) { try { const r = await fetch( `${FLOWROUTE_BASE}/v2/numbers/available?starts_with=1${areaCode}&limit=10&contains=&ends_with=&rate_center=&state=`, { headers: { Authorization: `Basic ${auth}` } }, ); if (!r.ok) continue; const data = await r.json() as { data?: Array<{ id: string; attributes?: { value?: string; monthly_cost?: string } }> }; results[areaCode] = (data.data || []).map(d => ({ number: d.id || d.attributes?.value || "", formatted: formatDID(d.id || d.attributes?.value || ""), monthly_cost: d.attributes?.monthly_cost || "1.00", })); } catch { results[areaCode] = []; } } res.json({ area_codes: areaCodes, dids: results }); } catch (err: any) { console.error("[portal-setup] setup-dids error:", err); res.status(500).json({ error: "Failed to search available numbers" }); } }); function formatDID(raw: string): string { const digits = raw.replace(/\D/g, ""); if (digits.length === 11 && digits.startsWith("1")) { return `+1 (${digits.slice(1, 4)}) ${digits.slice(4, 7)}-${digits.slice(7)}`; } if (digits.length === 10) { return `+1 (${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`; } return raw; } // ── POST /api/v1/portal/setup-confirm ────────────────────────────────────── // Client confirms mailbox unit + DID selection → dispatch purchase job. router.post("/api/v1/portal/setup-confirm", requirePortalAuth, async (req: Request, res: Response) => { const orderId = (req as any).portalAuth?.order_id || req.body?.order_id; const { selected_unit, selected_did } = req.body || {}; if (!orderId) { res.status(400).json({ error: "order_id required" }); return; } try { // Validate order exists and is in Client Selection state const { rows } = await pool.query( `SELECT order_number, has_own_ca_address, funds_available, client_selected_unit, client_selected_did FROM canada_crtc_orders WHERE order_number = $1`, [orderId], ); if (!rows.length) { res.status(404).json({ error: "Order not found" }); return; } const order = rows[0] as Record; if (!order.funds_available) { res.status(409).json({ error: "Funds not yet available for this order" }); return; } // Validate: DID is always required if (!selected_did) { res.status(400).json({ error: "Please select a Canadian phone number" }); return; } // Validate: unit required unless own address if (!order.has_own_ca_address && !selected_unit) { res.status(400).json({ error: "Please select a mailbox unit number" }); return; } // Store selections await pool.query( `UPDATE canada_crtc_orders SET client_selected_unit = $1, client_selected_did = $2 WHERE order_number = $3`, [selected_unit || null, selected_did, orderId], ); // Dispatch purchase job to workers try { await fetch(`${WORKER_URL}/jobs`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "purchase_client_selections", data: { order_id: orderId, selected_unit: selected_unit || null, selected_did: selected_did, has_own_ca_address: order.has_own_ca_address, }, }), }); } catch (workerErr) { console.error("[portal-setup] Worker dispatch failed:", workerErr); // Non-blocking — the selections are saved, admin can trigger manually } res.json({ success: true, message: "Selections confirmed. We're now provisioning your mailbox and phone number.", order_number: orderId, selected_unit: selected_unit || null, selected_did: selected_did, }); } catch (err: any) { console.error("[portal-setup] setup-confirm error:", err); res.status(500).json({ error: "Internal error" }); } }); export default router;