import { Router } from "express"; import { pool } from "../db.js"; import { requireAdmin } from "../middleware/admin-auth.js"; import { v4 as uuidv4 } from "uuid"; const router = Router(); // Helper: generate a REF-XXXXX agent code function generateAgentCode(): string { const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // no I/O/1/0 to avoid confusion let code = "REF-"; for (let i = 0; i < 5; i++) code += chars[Math.floor(Math.random() * chars.length)]; return code; } // Helper: create commission record when an order uses an agent's code // This is exported so other route files can call it export async function createCommission(params: { agentCode: string; orderType: string; orderId: number; orderNumber: string; serviceSlug?: string; customerName: string; customerEmail: string; orderAmountCents: number; discountCents: number; }): Promise { // Look up the agent const agentResult = await pool.query( "SELECT id, agent_code, commission_default_cents, commission_pct, commission_overrides, commission_type FROM sales_agents WHERE agent_code = $1 AND active = TRUE", [params.agentCode], ); if (agentResult.rows.length === 0) return; const agent = agentResult.rows[0]; // Calculate commission amount let commissionCents = agent.commission_default_cents || 30000; // $300 default const overrides = agent.commission_overrides || {}; // Precedence: // 1. Explicit per-service override (always wins, flat cents) // 2. Percent-based agents earn commission_pct of the order on EVERY order type // 3. Otherwise fall back to per-type flat defaults if (params.serviceSlug && overrides[params.serviceSlug]) { commissionCents = overrides[params.serviceSlug]; } else if (agent.commission_type === "percent") { // Percent agents (e.g. referral partners on a flat % deal) get the same // percentage regardless of order type. order_amount_cents is the total paid. commissionCents = Math.round((params.orderAmountCents * (agent.commission_pct || 10)) / 100); } else if (params.orderType === "canada_crtc") { commissionCents = overrides["canada-crtc"] || 30000; } else if (params.orderType === "formation") { commissionCents = overrides["formation"] || 5000; } else if (params.orderType === "bundle") { commissionCents = overrides["bundle"] || 10000; } await pool.query( `INSERT INTO commission_ledger (agent_id, agent_code, order_type, order_id, order_number, service_slug, customer_name, customer_email, order_amount_cents, discount_cents, commission_cents, status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'pending')`, [agent.id, agent.agent_code, params.orderType, params.orderId, params.orderNumber, params.serviceSlug || null, params.customerName, params.customerEmail, params.orderAmountCents, params.discountCents, commissionCents], ); // Update agent stats await pool.query( "UPDATE sales_agents SET total_referrals = total_referrals + 1, total_pending_cents = total_pending_cents + $1, updated_at = now() WHERE id = $2", [commissionCents, agent.id], ); } // ===================================================================== // Admin: Create a new sales agent // ===================================================================== router.post("/api/v1/admin/agents", requireAdmin, async (req, res) => { try { const { name, email, phone, company, commission_type, commission_default_cents, commission_pct, commission_overrides, client_discount_value, notes, } = req.body ?? {}; if (!name || !email) { res.status(400).json({ error: "Name and email are required." }); return; } // A single agent (person/company) may hold MULTIPLE referral codes, each with // its own client discount + commission split. We therefore allow repeat emails; // each call creates a new code. We surface any existing codes for the same email // in the response so the caller knows it's an additional code, not a mistake. const existing = await pool.query( "SELECT agent_code, commission_pct FROM sales_agents WHERE email = $1 ORDER BY created_at", [email.toLowerCase().trim()], ); const existingCodes = existing.rows.map((r) => r.agent_code); // Per-code config (defaults preserve prior behaviour: 5% client discount, 10% commission) const clientDiscount = client_discount_value !== undefined ? client_discount_value : 5; const commType = commission_type === "flat" ? "flat" : "percent"; const commPct = commission_pct !== undefined ? commission_pct : 10; // Generate unique agent code let agentCode = generateAgentCode(); let attempts = 0; while (attempts < 10) { const dup = await pool.query("SELECT id FROM sales_agents WHERE agent_code = $1", [agentCode]); if (dup.rows.length === 0) break; agentCode = generateAgentCode(); attempts++; } // Create the linked discount code (client discount off service fees; // referral_pct mirrors the agent commission for reporting). const dcResult = await pool.query( `INSERT INTO discount_codes (code, description, discount_type, discount_value, referral_partner, referral_email, referral_pct, active) VALUES ($1, $2, 'percent', $3, $4, $5, $6, TRUE) RETURNING id`, [agentCode, `Sales agent: ${name}`, clientDiscount, name, email.toLowerCase().trim(), commType === "percent" ? commPct : 0], ); const discountCodeId = dcResult.rows[0].id; // Create the agent (one row per referral code) const result = await pool.query( `INSERT INTO sales_agents (agent_code, discount_code_id, name, email, phone, company, commission_type, commission_default_cents, commission_pct, commission_overrides, notes, onboarded_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now()) RETURNING id, agent_code`, [agentCode, discountCodeId, name, email.toLowerCase().trim(), phone || null, company || null, commType, commission_default_cents || 30000, commPct, JSON.stringify(commission_overrides || {}), notes || null], ); const agent = result.rows[0]; res.status(201).json({ success: true, agent_id: agent.id, agent_code: agent.agent_code, client_discount_value: clientDiscount, commission_type: commType, commission_pct: commType === "percent" ? commPct : undefined, existing_codes_for_email: existingCodes, referral_url: `https://performancewest.net/order/canada-crtc?code=${agent.agent_code}`, message: existingCodes.length > 0 ? `Additional referral code created for ${email}. New code: ${agent.agent_code} (${clientDiscount}% client discount${commType === "percent" ? `, ${commPct}% commission` : ""}). This agent now has ${existingCodes.length + 1} codes.` : `Agent created. Referral code: ${agent.agent_code}. Client gets ${clientDiscount}% off, agent earns commission per sale.`, }); } catch (err) { console.error("[agents] Create error:", err); res.status(500).json({ error: "Could not create agent." }); } }); // ===================================================================== // Admin: List all agents // ===================================================================== router.get("/api/v1/admin/agents", requireAdmin, async (req, res) => { try { const active = req.query.active !== "false"; const result = await pool.query( `SELECT id, agent_code, name, email, phone, company, active, commission_default_cents, commission_pct, total_referrals, total_earned_cents, total_paid_cents, total_pending_cents, onboarded_at, created_at FROM sales_agents WHERE ($1::boolean IS NULL OR active = $1) ORDER BY created_at DESC`, [active], ); res.json({ agents: result.rows }); } catch (err) { console.error("[agents] List error:", err); res.status(500).json({ error: "Could not load agents." }); } }); // ===================================================================== // Admin: Get single agent with commission history // ===================================================================== router.get("/api/v1/admin/agents/:id", requireAdmin, async (req, res) => { try { const id = parseInt(req.params.id, 10); const agent = await pool.query("SELECT * FROM sales_agents WHERE id = $1", [id]); if (agent.rows.length === 0) { res.status(404).json({ error: "Agent not found." }); return; } const commissions = await pool.query( "SELECT * FROM commission_ledger WHERE agent_id = $1 ORDER BY created_at DESC LIMIT 50", [id], ); res.json({ agent: agent.rows[0], commissions: commissions.rows }); } catch (err) { console.error("[agents] Get error:", err); res.status(500).json({ error: "Could not load agent." }); } }); // ===================================================================== // Admin: Update agent // ===================================================================== router.patch("/api/v1/admin/agents/:id", requireAdmin, async (req, res) => { try { const id = parseInt(req.params.id, 10); const { name, phone, company, commission_default_cents, commission_pct, commission_overrides, active, notes } = req.body ?? {}; const updates: string[] = []; const params: any[] = []; let idx = 1; if (name !== undefined) { updates.push(`name = $${idx++}`); params.push(name); } if (phone !== undefined) { updates.push(`phone = $${idx++}`); params.push(phone); } if (company !== undefined) { updates.push(`company = $${idx++}`); params.push(company); } if (commission_default_cents !== undefined) { updates.push(`commission_default_cents = $${idx++}`); params.push(commission_default_cents); } if (commission_pct !== undefined) { updates.push(`commission_pct = $${idx++}`); params.push(commission_pct); } if (commission_overrides !== undefined) { updates.push(`commission_overrides = $${idx++}`); params.push(JSON.stringify(commission_overrides)); } if (active !== undefined) { updates.push(`active = $${idx++}`); params.push(active); } if (notes !== undefined) { updates.push(`notes = $${idx++}`); params.push(notes); } if (updates.length > 0) { updates.push("updated_at = now()"); params.push(id); await pool.query(`UPDATE sales_agents SET ${updates.join(", ")} WHERE id = $${idx}`, params); } res.json({ success: true, message: "Agent updated." }); } catch (err) { console.error("[agents] Update error:", err); res.status(500).json({ error: "Could not update agent." }); } }); // ===================================================================== // Admin: List commissions (with filters) // ===================================================================== router.get("/api/v1/admin/commissions", requireAdmin, async (req, res) => { try { const status = req.query.status as string || ""; const agentId = req.query.agent_id as string || ""; const limit = Math.min(parseInt(req.query.limit as string, 10) || 50, 200); const offset = parseInt(req.query.offset as string, 10) || 0; let where = "WHERE 1=1"; const params: any[] = []; let idx = 1; if (status) { where += ` AND c.status = $${idx++}`; params.push(status); } if (agentId) { where += ` AND c.agent_id = $${idx++}`; params.push(parseInt(agentId, 10)); } const countResult = await pool.query(`SELECT COUNT(*) as total FROM commission_ledger c ${where}`, params); params.push(limit, offset); const result = await pool.query( `SELECT c.*, a.name as agent_name, a.email as agent_email FROM commission_ledger c JOIN sales_agents a ON c.agent_id = a.id ${where} ORDER BY c.created_at DESC LIMIT $${idx++} OFFSET $${idx++}`, params, ); res.json({ commissions: result.rows, total: parseInt(countResult.rows[0].total, 10), limit, offset, }); } catch (err) { console.error("[commissions] List error:", err); res.status(500).json({ error: "Could not load commissions." }); } }); // ===================================================================== // Admin: Update commission status (approve, pay, cancel) // ===================================================================== router.patch("/api/v1/admin/commissions/:id", requireAdmin, async (req, res) => { try { const id = parseInt(req.params.id, 10); const { status, payment_method, payment_reference, notes } = req.body ?? {}; const current = await pool.query("SELECT * FROM commission_ledger WHERE id = $1", [id]); if (current.rows.length === 0) { res.status(404).json({ error: "Commission not found." }); return; } const comm = current.rows[0]; const updates: string[] = []; const params: any[] = []; let idx = 1; if (status) { updates.push(`status = $${idx++}`); params.push(status); if (status === "approved") { updates.push(`approved_by = $${idx++}`); params.push(req.admin!.id); updates.push("approved_at = now()"); } if (status === "paid") { updates.push("paid_at = now()"); if (payment_method) { updates.push(`payment_method = $${idx++}`); params.push(payment_method); } if (payment_reference) { updates.push(`payment_reference = $${idx++}`); params.push(payment_reference); } // Update agent totals await pool.query( "UPDATE sales_agents SET total_paid_cents = total_paid_cents + $1, total_pending_cents = total_pending_cents - $1, total_earned_cents = total_earned_cents + $1, updated_at = now() WHERE id = $2", [comm.commission_cents, comm.agent_id], ); } if (status === "cancelled") { await pool.query( "UPDATE sales_agents SET total_pending_cents = total_pending_cents - $1, updated_at = now() WHERE id = $2", [comm.commission_cents, comm.agent_id], ); } } if (notes !== undefined) { updates.push(`notes = $${idx++}`); params.push(notes); } if (updates.length > 0) { updates.push("updated_at = now()"); params.push(id); await pool.query(`UPDATE commission_ledger SET ${updates.join(", ")} WHERE id = $${idx}`, params); } res.json({ success: true, message: `Commission status updated to: ${status || "updated"}` }); } catch (err) { console.error("[commissions] Update error:", err); res.status(500).json({ error: "Could not update commission." }); } }); // ===================================================================== // Admin: Commission stats dashboard // ===================================================================== router.get("/api/v1/admin/commissions/stats", requireAdmin, async (req, res) => { try { const result = await pool.query(` SELECT COUNT(*) FILTER (WHERE status = 'pending') as pending_count, COUNT(*) FILTER (WHERE status = 'eligible') as eligible_count, COUNT(*) FILTER (WHERE status = 'approved') as approved_count, COUNT(*) FILTER (WHERE status = 'paid') as paid_count, COUNT(*) FILTER (WHERE status = 'cancelled') as cancelled_count, COALESCE(SUM(commission_cents) FILTER (WHERE status = 'pending'), 0) as pending_cents, COALESCE(SUM(commission_cents) FILTER (WHERE status = 'eligible'), 0) as eligible_cents, COALESCE(SUM(commission_cents) FILTER (WHERE status IN ('approved','processing')), 0) as approved_cents, COALESCE(SUM(commission_cents) FILTER (WHERE status = 'paid'), 0) as paid_cents, COUNT(DISTINCT agent_id) as active_agents FROM commission_ledger `); res.json({ stats: result.rows[0] }); } catch (err) { res.status(500).json({ error: "Could not load commission stats." }); } }); export default router;