diff --git a/api/src/routes/agents.ts b/api/src/routes/agents.ts index bfabb6a..d18641b 100644 --- a/api/src/routes/agents.ts +++ b/api/src/routes/agents.ts @@ -37,19 +37,23 @@ export async function createCommission(params: { // Calculate commission amount let commissionCents = agent.commission_default_cents || 30000; // $300 default const overrides = agent.commission_overrides || {}; - - // Check for service-specific override + + // 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; - } else if (agent.commission_type === "percent") { - // For compliance services, use percentage - commissionCents = Math.round((params.orderAmountCents * (agent.commission_pct || 10)) / 100); } await pool.query( diff --git a/api/src/routes/fcc-carrier-registration.ts b/api/src/routes/fcc-carrier-registration.ts index 79dd605..1b94b8d 100644 --- a/api/src/routes/fcc-carrier-registration.ts +++ b/api/src/routes/fcc-carrier-registration.ts @@ -51,6 +51,7 @@ router.post("/api/v1/fcc-carrier-registration", async (req: Request, res: Respon address_street, address_city, address_state, address_zip, service_wizard, services, engagement_accepted, + discount_code, } = req.body ?? {}; // Validate required fields @@ -104,6 +105,53 @@ router.post("/api/v1/fcc-carrier-registration", async (req: Request, res: Respon if (includeOcn) addonFeeCents += OCN_FEE_CENTS; const pucFeeCents = statePucStates.length * STATE_PUC_FEE_CENTS; + // ── Discount / referral code ────────────────────────────────────────────── + // Discounts apply to the Performance West service fee only. State filing fees + // (passed through at cost) are never discountable, matching the CRTC flow. + let discountCents = 0; + let normalizedDiscountCode: string | null = null; + if (discount_code && typeof discount_code === "string" && discount_code.trim().length >= 2) { + const code = discount_code.toUpperCase().trim(); + try { + const dcResult = await pool.query( + "SELECT * FROM discount_codes WHERE code = $1", + [code], + ); + if (dcResult.rows.length > 0) { + const dc = dcResult.rows[0]; + const now = new Date(); + const active = dc.active === true; + const notExpired = !dc.expires_at || new Date(dc.expires_at) >= now; + const started = !dc.starts_at || new Date(dc.starts_at) <= now; + const underGlobalLimit = dc.max_uses === null || dc.current_uses < dc.max_uses; + // Scope check: allow codes scoped to this service (or unscoped) + let inScope = true; + if (dc.applies_to) { + const allowed = String(dc.applies_to).split(",").map((s: string) => s.trim().toLowerCase()); + inScope = allowed.includes("fcc_carrier_registration") || allowed.includes("all"); + } + // Email allowlist check + let emailOk = true; + if (dc.allowed_emails && dc.allowed_emails.length > 0) { + const allowed = dc.allowed_emails.map((e: string) => e.toLowerCase()); + emailOk = allowed.includes(customer_email.toLowerCase().trim()); + } + if (active && notExpired && started && underGlobalLimit && inScope && emailOk) { + // Discountable base = service fee + add-ons (not state filing fees). + const discountable = BASE_FEE_CENTS + addonFeeCents; + if (dc.discount_type === "percent") { + discountCents = Math.round((discountable * dc.discount_value) / 100); + } else { + discountCents = Math.min(dc.discount_value, discountable); + } + normalizedDiscountCode = code; + } + } + } catch (dcErr) { + console.warn("[fcc-carrier-reg] Discount lookup failed (non-fatal):", dcErr); + } + } + const orderNumber = generateOrderNumber(); const result = await pool.query( @@ -118,11 +166,12 @@ router.post("/api/v1/fcc-carrier-registration", async (req: Request, res: Respon state_puc_states, service_fee_cents, formation_fee_cents, state_fee_cents, puc_fee_cents, addon_fee_cents, + discount_code, discount_cents, engagement_accepted_at, engagement_accepted_ip ) VALUES ( $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18, $19::jsonb,$20,$21,$22,$23,$24,$25,$26,$27,$28::text[], - $29,$30,$31,$32,$33,$34,$35 + $29,$30,$31,$32,$33,$34,$35,$36,$37 ) RETURNING *`, [ orderNumber, @@ -158,6 +207,8 @@ router.post("/api/v1/fcc-carrier-registration", async (req: Request, res: Respon stateFeeCents, pucFeeCents, addonFeeCents, + normalizedDiscountCode, + discountCents, engagement_accepted ? new Date().toISOString() : null, engagement_accepted ? (req.ip || req.headers["x-forwarded-for"] || null) : null, ], @@ -202,10 +253,38 @@ router.post("/api/v1/fcc-carrier-registration", async (req: Request, res: Respon } } - const totalCents = BASE_FEE_CENTS + formationFeeCents + stateFeeCents + pucFeeCents + addonFeeCents; + const subtotalCents = BASE_FEE_CENTS + formationFeeCents + stateFeeCents + pucFeeCents + addonFeeCents; + const totalCents = Math.max(0, subtotalCents - discountCents); + + // If this order used a sales agent's referral code, record a pending commission. + if (normalizedDiscountCode) { + try { + const agentCheck = await pool.query( + "SELECT sa.agent_code FROM sales_agents sa JOIN discount_codes dc ON sa.discount_code_id = dc.id WHERE dc.code = $1 AND sa.active = TRUE", + [normalizedDiscountCode], + ); + if (agentCheck.rows.length > 0) { + const { createCommission } = await import("./agents.js"); + await createCommission({ + agentCode: agentCheck.rows[0].agent_code, + orderType: "fcc_carrier_registration", + orderId: order.id, + orderNumber: orderNumber, + serviceSlug: "fcc-carrier-registration", + customerName: customer_name.trim(), + customerEmail: customer_email.toLowerCase().trim(), + orderAmountCents: totalCents, + discountCents: discountCents, + }); + } + } catch (commErr) { + console.warn("[fcc-carrier-reg] Commission creation failed (non-fatal):", commErr); + } + } console.log( - `[fcc-carrier-reg] Created ${orderNumber}: ${entity_source} for ${customer_email} — $${(totalCents / 100).toFixed(2)}`, + `[fcc-carrier-reg] Created ${orderNumber}: ${entity_source} for ${customer_email} — $${(totalCents / 100).toFixed(2)}` + + (discountCents > 0 ? ` (discount ${normalizedDiscountCode} -$${(discountCents / 100).toFixed(2)})` : ""), ); res.json({ @@ -219,6 +298,9 @@ router.post("/api/v1/fcc-carrier-registration", async (req: Request, res: Respon formation: formationFeeCents + stateFeeCents, addons: addonFeeCents, puc: pucFeeCents, + subtotal: subtotalCents, + discount_code: normalizedDiscountCode, + discount_cents: discountCents, total: totalCents, }, }); diff --git a/scripts/create_agent_jaykordic.cjs b/scripts/create_agent_jaykordic.cjs new file mode 100644 index 0000000..e793d48 --- /dev/null +++ b/scripts/create_agent_jaykordic.cjs @@ -0,0 +1,105 @@ +#!/usr/bin/env node +/** + * Create sales agent "Jay Kordic" with custom referral code JAYK05. + * - Client discount: 5% off all discountable service fees (discount_type='percent', value=5) + * - Agent commission: 15% (commission_type='percent', commission_pct=15) + * + * Idempotent: re-running updates the existing rows instead of duplicating. + * + * Usage (from api/ so it loads api/.env): + * cd api && node ../scripts/create_agent_jaykordic.cjs + */ +const fs = require("fs"); +const path = require("path"); +const { Client } = require("pg"); + +// Load DATABASE_URL from api/.env if not already in env +function loadEnv() { + if (process.env.DATABASE_URL) return; + const envPath = path.resolve(__dirname, "../api/.env"); + if (!fs.existsSync(envPath)) return; + for (const line of fs.readFileSync(envPath, "utf8").split("\n")) { + const m = line.match(/^DATABASE_URL=(.*)$/); + if (m) { process.env.DATABASE_URL = m[1].trim(); break; } + } +} + +const CODE = "REF-JAYK05"; +const AGENT_NAME = "Jay Kordic"; +const AGENT_COMPANY = "The Horizon Group"; +const AGENT_EMAIL = "jay.kordic@performancewest.net"; // placeholder; update with real payout email +const CLIENT_DISCOUNT_PCT = 5; +const COMMISSION_PCT = 15; + +(async () => { + loadEnv(); + if (!process.env.DATABASE_URL) { + console.error("DATABASE_URL not found. Run from api/ or export DATABASE_URL."); + process.exit(1); + } + const c = new Client({ connectionString: process.env.DATABASE_URL }); + await c.connect(); + try { + await c.query("BEGIN"); + + // 1) Upsert the discount code (5% off, partner attribution to Jay Kordic) + const dc = await c.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) + ON CONFLICT (code) DO UPDATE SET + description = EXCLUDED.description, + discount_type = EXCLUDED.discount_type, + discount_value = EXCLUDED.discount_value, + referral_partner = EXCLUDED.referral_partner, + referral_email = EXCLUDED.referral_email, + referral_pct = EXCLUDED.referral_pct, + active = TRUE, + updated_at = now() + RETURNING id`, + [CODE, `Sales agent: ${AGENT_NAME} (${AGENT_COMPANY})`, CLIENT_DISCOUNT_PCT, AGENT_NAME, AGENT_EMAIL, COMMISSION_PCT], + ); + const discountCodeId = dc.rows[0].id; + + // 2) Upsert the sales agent (commission paid as percent of order) + const existing = await c.query("SELECT id FROM sales_agents WHERE email = $1 OR agent_code = $2", [AGENT_EMAIL, CODE]); + if (existing.rows.length > 0) { + await c.query( + `UPDATE sales_agents SET + agent_code = $1, discount_code_id = $2, name = $3, company = $4, + commission_type = 'percent', commission_pct = $5, + active = TRUE, updated_at = now() + WHERE id = $6`, + [CODE, discountCodeId, AGENT_NAME, AGENT_COMPANY, COMMISSION_PCT, existing.rows[0].id], + ); + console.log(`Updated existing agent id=${existing.rows[0].id}`); + } else { + const ag = await c.query( + `INSERT INTO sales_agents (agent_code, discount_code_id, name, email, company, commission_type, commission_pct, active, onboarded_at) + VALUES ($1, $2, $3, $4, $5, 'percent', $6, TRUE, now()) + RETURNING id`, + [CODE, discountCodeId, AGENT_NAME, AGENT_EMAIL, AGENT_COMPANY, COMMISSION_PCT], + ); + console.log(`Created agent id=${ag.rows[0].id}`); + } + + await c.query("COMMIT"); + + // Verify + const v = await c.query( + `SELECT d.code, d.discount_type, d.discount_value, d.referral_partner, d.referral_email, d.referral_pct, d.active AS code_active, + s.agent_code, s.name, s.commission_type, s.commission_pct, s.active AS agent_active + FROM discount_codes d + LEFT JOIN sales_agents s ON s.discount_code_id = d.id + WHERE d.code = $1`, + [CODE], + ); + console.log("\nResult:"); + console.dir(v.rows[0], { depth: null }); + } catch (e) { + await c.query("ROLLBACK").catch(() => {}); + console.error("ERROR:", e.message); + process.exitCode = 1; + } finally { + await c.end(); + } +})(); diff --git a/site/public/order/fcc-carrier-registration/index.html b/site/public/order/fcc-carrier-registration/index.html index e710931..137bc21 100644 --- a/site/public/order/fcc-carrier-registration/index.html +++ b/site/public/order/fcc-carrier-registration/index.html @@ -350,6 +350,16 @@ select:focus,input:focus{outline:none;border-color:#1e3a5f;box-shadow:0 0 0 2px