Add referral/discount code to FCC carrier page + REF-JAYK05 agent
Frontend (order/fcc-carrier-registration): - Add a referral/discount code box on the review step that validates against /api/v1/discount/:code and shows the discount line + adjusted total. Discount applies to service fee + add-ons, never state filing fees. - Prefill + auto-apply from ?code= / ?ref= query param (referral links). Backend (fcc-carrier-registration route): - Accept discount_code, validate it, store discount_code/discount_cents, and subtract from the total. Checkout already reads discount_cents to apply the Stripe coupon. - Create a pending commission when the code belongs to an active sales agent. Commission fix (agents.createCommission): - Percent-type agents now earn commission_pct on ALL order types. Previously canada_crtc/formation/bundle used flat defaults and ignored percent agents. Agent: created sales agent Jay Kordic (The Horizon Group) with custom code REF-JAYK05 -> client gets 5% off discountable services, agent earns 15%. Idempotent setup script in scripts/create_agent_jaykordic.cjs.
This commit is contained in:
parent
1584a6692b
commit
53857574d3
4 changed files with 293 additions and 8 deletions
|
|
@ -37,19 +37,23 @@ export async function createCommission(params: {
|
||||||
// Calculate commission amount
|
// Calculate commission amount
|
||||||
let commissionCents = agent.commission_default_cents || 30000; // $300 default
|
let commissionCents = agent.commission_default_cents || 30000; // $300 default
|
||||||
const overrides = agent.commission_overrides || {};
|
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]) {
|
if (params.serviceSlug && overrides[params.serviceSlug]) {
|
||||||
commissionCents = 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") {
|
} else if (params.orderType === "canada_crtc") {
|
||||||
commissionCents = overrides["canada-crtc"] || 30000;
|
commissionCents = overrides["canada-crtc"] || 30000;
|
||||||
} else if (params.orderType === "formation") {
|
} else if (params.orderType === "formation") {
|
||||||
commissionCents = overrides["formation"] || 5000;
|
commissionCents = overrides["formation"] || 5000;
|
||||||
} else if (params.orderType === "bundle") {
|
} else if (params.orderType === "bundle") {
|
||||||
commissionCents = overrides["bundle"] || 10000;
|
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(
|
await pool.query(
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ router.post("/api/v1/fcc-carrier-registration", async (req: Request, res: Respon
|
||||||
address_street, address_city, address_state, address_zip,
|
address_street, address_city, address_state, address_zip,
|
||||||
service_wizard, services,
|
service_wizard, services,
|
||||||
engagement_accepted,
|
engagement_accepted,
|
||||||
|
discount_code,
|
||||||
} = req.body ?? {};
|
} = req.body ?? {};
|
||||||
|
|
||||||
// Validate required fields
|
// 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;
|
if (includeOcn) addonFeeCents += OCN_FEE_CENTS;
|
||||||
const pucFeeCents = statePucStates.length * STATE_PUC_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 orderNumber = generateOrderNumber();
|
||||||
|
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
|
|
@ -118,11 +166,12 @@ router.post("/api/v1/fcc-carrier-registration", async (req: Request, res: Respon
|
||||||
state_puc_states,
|
state_puc_states,
|
||||||
service_fee_cents, formation_fee_cents, state_fee_cents,
|
service_fee_cents, formation_fee_cents, state_fee_cents,
|
||||||
puc_fee_cents, addon_fee_cents,
|
puc_fee_cents, addon_fee_cents,
|
||||||
|
discount_code, discount_cents,
|
||||||
engagement_accepted_at, engagement_accepted_ip
|
engagement_accepted_at, engagement_accepted_ip
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,
|
$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[],
|
$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 *`,
|
) RETURNING *`,
|
||||||
[
|
[
|
||||||
orderNumber,
|
orderNumber,
|
||||||
|
|
@ -158,6 +207,8 @@ router.post("/api/v1/fcc-carrier-registration", async (req: Request, res: Respon
|
||||||
stateFeeCents,
|
stateFeeCents,
|
||||||
pucFeeCents,
|
pucFeeCents,
|
||||||
addonFeeCents,
|
addonFeeCents,
|
||||||
|
normalizedDiscountCode,
|
||||||
|
discountCents,
|
||||||
engagement_accepted ? new Date().toISOString() : null,
|
engagement_accepted ? new Date().toISOString() : null,
|
||||||
engagement_accepted ? (req.ip || req.headers["x-forwarded-for"] || null) : 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(
|
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({
|
res.json({
|
||||||
|
|
@ -219,6 +298,9 @@ router.post("/api/v1/fcc-carrier-registration", async (req: Request, res: Respon
|
||||||
formation: formationFeeCents + stateFeeCents,
|
formation: formationFeeCents + stateFeeCents,
|
||||||
addons: addonFeeCents,
|
addons: addonFeeCents,
|
||||||
puc: pucFeeCents,
|
puc: pucFeeCents,
|
||||||
|
subtotal: subtotalCents,
|
||||||
|
discount_code: normalizedDiscountCode,
|
||||||
|
discount_cents: discountCents,
|
||||||
total: totalCents,
|
total: totalCents,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
105
scripts/create_agent_jaykordic.cjs
Normal file
105
scripts/create_agent_jaykordic.cjs
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -350,6 +350,16 @@ select:focus,input:focus{outline:none;border-color:#1e3a5f;box-shadow:0 0 0 2px
|
||||||
<h2>Review & Checkout</h2>
|
<h2>Review & Checkout</h2>
|
||||||
<div id="review-content"></div>
|
<div id="review-content"></div>
|
||||||
|
|
||||||
|
<!-- Referral / discount code -->
|
||||||
|
<div style="margin-top:.75rem;padding:.75rem;border:1px dashed #d1d5db;border-radius:8px">
|
||||||
|
<label class="label" for="discount-code" style="margin-top:0">Referral or discount code</label>
|
||||||
|
<div style="display:flex;gap:.5rem">
|
||||||
|
<input type="text" id="discount-code" placeholder="e.g. REF-JAYK05" maxlength="40" style="flex:1;text-transform:uppercase">
|
||||||
|
<button type="button" class="btn btn-back" id="btn-apply-discount" style="white-space:nowrap">Apply</button>
|
||||||
|
</div>
|
||||||
|
<p id="discount-status" style="font-size:.8rem;margin-top:.4rem;display:none"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label style="display:flex;align-items:flex-start;gap:.5rem;padding:.65rem;margin-top:.75rem;border:1px solid #e5e7eb;border-radius:8px;cursor:pointer;font-size:.75rem;color:#6b7280;line-height:1.5">
|
<label style="display:flex;align-items:flex-start;gap:.5rem;padding:.65rem;margin-top:.75rem;border:1px solid #e5e7eb;border-radius:8px;cursor:pointer;font-size:.75rem;color:#6b7280;line-height:1.5">
|
||||||
<input type="checkbox" id="engage-check" required style="margin-top:2px;accent-color:#1e3a5f">
|
<input type="checkbox" id="engage-check" required style="margin-top:2px;accent-color:#1e3a5f">
|
||||||
<span>I authorize Performance West Inc. to prepare and submit regulatory filings on my behalf as described above. I understand Performance West provides compliance consulting services, not legal advice or legal representation. I confirm the information I provide is accurate to the best of my knowledge. <a href="/terms" target="_blank" style="color:#1e3a5f;text-decoration:underline">Terms of Service</a></span>
|
<span>I authorize Performance West Inc. to prepare and submit regulatory filings on my behalf as described above. I understand Performance West provides compliance consulting services, not legal advice or legal representation. I confirm the information I provide is accurate to the best of my knowledge. <a href="/terms" target="_blank" style="color:#1e3a5f;text-decoration:underline">Terms of Service</a></span>
|
||||||
|
|
@ -419,6 +429,8 @@ select:focus,input:focus{outline:none;border-color:#1e3a5f;box-shadow:0 0 0 2px
|
||||||
pucStates: [],
|
pucStates: [],
|
||||||
// Pricing
|
// Pricing
|
||||||
baseFee: 129900, formationFee: 0, stateFee: 0, pucFee: 0, addonFee: 0,
|
baseFee: 129900, formationFee: 0, stateFee: 0, pucFee: 0, addonFee: 0,
|
||||||
|
// Discount / referral
|
||||||
|
discountCode: '', discountCents: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Step navigation ──
|
// ── Step navigation ──
|
||||||
|
|
@ -790,10 +802,58 @@ select:focus,input:focus{outline:none;border-color:#1e3a5f;box-shadow:0 0 0 2px
|
||||||
wizard.addrZip = document.getElementById('addr-zip').value.trim();
|
wizard.addrZip = document.getElementById('addr-zip').value.trim();
|
||||||
buildReview();
|
buildReview();
|
||||||
showStep(5);
|
showStep(5);
|
||||||
|
// Auto-validate a prefilled referral code (e.g. from ?code= link) on first arrival
|
||||||
|
var dcInput = document.getElementById('discount-code');
|
||||||
|
if (dcInput && dcInput.value.trim() && !wizard.appliedDiscount) {
|
||||||
|
document.getElementById('btn-apply-discount').click();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
document.getElementById('btn-back-4').addEventListener('click', function() { showStep(3); });
|
document.getElementById('btn-back-4').addEventListener('click', function() { showStep(3); });
|
||||||
document.getElementById('btn-back-5').addEventListener('click', function() { showStep(4); });
|
document.getElementById('btn-back-5').addEventListener('click', function() { showStep(4); });
|
||||||
|
|
||||||
|
// ── Apply referral / discount code ──
|
||||||
|
document.getElementById('btn-apply-discount').addEventListener('click', async function() {
|
||||||
|
var btn = this;
|
||||||
|
var input = document.getElementById('discount-code');
|
||||||
|
var statusEl = document.getElementById('discount-status');
|
||||||
|
var code = input.value.trim().toUpperCase();
|
||||||
|
statusEl.style.display = 'block';
|
||||||
|
if (!code) {
|
||||||
|
// Clearing the code
|
||||||
|
wizard.appliedDiscount = null; wizard.discountCode = ''; wizard.discountCents = 0;
|
||||||
|
statusEl.style.color = '#6b7280'; statusEl.textContent = 'No code applied.';
|
||||||
|
buildReview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
btn.disabled = true; var prev = btn.textContent; btn.textContent = 'Checking...';
|
||||||
|
statusEl.style.color = '#6b7280'; statusEl.textContent = 'Validating code...';
|
||||||
|
try {
|
||||||
|
var email = wizard.contactEmail || document.getElementById('contact-email').value.trim();
|
||||||
|
var url = API + '/api/v1/discount/' + encodeURIComponent(code) +
|
||||||
|
'?service=fcc_carrier_registration' + (email ? '&email=' + encodeURIComponent(email) : '');
|
||||||
|
var resp = await fetch(url);
|
||||||
|
var data = await resp.json();
|
||||||
|
if (!resp.ok || !data.valid) {
|
||||||
|
wizard.appliedDiscount = null; wizard.discountCode = ''; wizard.discountCents = 0;
|
||||||
|
statusEl.style.color = '#dc2626';
|
||||||
|
statusEl.textContent = data.error || 'Code is not valid.';
|
||||||
|
buildReview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wizard.appliedDiscount = { discount_type: data.discount_type, discount_value: data.discount_value };
|
||||||
|
wizard.discountCode = data.code || code;
|
||||||
|
buildReview(); // recomputes wizard.discountCents against discountable base
|
||||||
|
statusEl.style.color = '#059669';
|
||||||
|
var label = data.discount_type === 'percent' ? (data.discount_value + '% off service fees') : ('$' + (data.discount_value / 100).toLocaleString() + ' off service fees');
|
||||||
|
statusEl.textContent = 'Applied ' + wizard.discountCode + ' \u2014 ' + label + (wizard.discountCents > 0 ? ' (\u2212$' + (wizard.discountCents / 100).toLocaleString() + ')' : '') + '.';
|
||||||
|
} catch (err) {
|
||||||
|
statusEl.style.color = '#dc2626';
|
||||||
|
statusEl.textContent = 'Could not validate code. Please try again.';
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false; btn.textContent = prev;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ── Build review ──
|
// ── Build review ──
|
||||||
function buildReview() {
|
function buildReview() {
|
||||||
var services = [];
|
var services = [];
|
||||||
|
|
@ -819,11 +879,29 @@ select:focus,input:focus{outline:none;border-color:#1e3a5f;box-shadow:0 0 0 2px
|
||||||
}
|
}
|
||||||
|
|
||||||
var total = 0;
|
var total = 0;
|
||||||
|
var discountableBase = 0; // service fee + add-ons; excludes formation/state filing fees
|
||||||
items.forEach(function(item) {
|
items.forEach(function(item) {
|
||||||
total += item.price;
|
total += item.price;
|
||||||
|
var isFormation = item.name.indexOf('Business Formation') === 0;
|
||||||
|
if (!isFormation) discountableBase += item.price;
|
||||||
html += '<tr style="border-bottom:1px solid #f3f4f6"><td style="padding:.35rem">' + item.name + '</td>';
|
html += '<tr style="border-bottom:1px solid #f3f4f6"><td style="padding:.35rem">' + item.name + '</td>';
|
||||||
html += '<td style="text-align:right;padding:.35rem">' + (item.price ? '$' + (item.price / 100).toLocaleString() : 'Included') + '</td></tr>';
|
html += '<td style="text-align:right;padding:.35rem">' + (item.price ? '$' + (item.price / 100).toLocaleString() : 'Included') + '</td></tr>';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Recompute discount against the current discountable base (in case services changed)
|
||||||
|
if (wizard.appliedDiscount) {
|
||||||
|
var d = wizard.appliedDiscount;
|
||||||
|
wizard.discountCents = d.discount_type === 'percent'
|
||||||
|
? Math.round((discountableBase * d.discount_value) / 100)
|
||||||
|
: Math.min(d.discount_value, discountableBase);
|
||||||
|
} else {
|
||||||
|
wizard.discountCents = 0;
|
||||||
|
}
|
||||||
|
if (wizard.discountCents > 0) {
|
||||||
|
html += '<tr style="color:#059669"><td style="padding:.35rem">Discount (' + wizard.discountCode + ')</td>';
|
||||||
|
html += '<td style="text-align:right;padding:.35rem">-$' + (wizard.discountCents / 100).toLocaleString() + '</td></tr>';
|
||||||
|
total -= wizard.discountCents;
|
||||||
|
}
|
||||||
html += '<tr style="border-top:2px solid #e5e7eb;font-weight:700"><td style="padding:.5rem">Total</td><td style="text-align:right;padding:.5rem">$' + (total / 100).toLocaleString() + '</td></tr>';
|
html += '<tr style="border-top:2px solid #e5e7eb;font-weight:700"><td style="padding:.5rem">Total</td><td style="text-align:right;padding:.5rem">$' + (total / 100).toLocaleString() + '</td></tr>';
|
||||||
html += '</table>';
|
html += '</table>';
|
||||||
|
|
||||||
|
|
@ -880,6 +958,7 @@ select:focus,input:focus{outline:none;border-color:#1e3a5f;box-shadow:0 0 0 2px
|
||||||
puc_states: wizard.pucStates,
|
puc_states: wizard.pucStates,
|
||||||
},
|
},
|
||||||
services: services,
|
services: services,
|
||||||
|
discount_code: wizard.discountCode || '',
|
||||||
engagement_accepted: true,
|
engagement_accepted: true,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
@ -954,6 +1033,21 @@ select:focus,input:focus{outline:none;border-color:#1e3a5f;box-shadow:0 0 0 2px
|
||||||
}).catch(function() {});
|
}).catch(function() {});
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Prefill referral code from ?code= / ?ref= query param ──
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
var qs = new URLSearchParams(window.location.search);
|
||||||
|
var refCode = (qs.get('code') || qs.get('ref') || '').trim().toUpperCase();
|
||||||
|
if (refCode) {
|
||||||
|
var input = document.getElementById('discount-code');
|
||||||
|
if (input) {
|
||||||
|
input.value = refCode;
|
||||||
|
wizard.discountCode = refCode; // captured even before the user reaches step 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
})();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<!-- Tawk.to Live Chat --><script>var Tawk_API=Tawk_API||{}, Tawk_LoadStart=new Date();(function(){var s1=document.createElement("script"),s0=document.getElementsByTagName("script")[0];s1.async=true;s1.src="https://embed.tawk.to/69d5a9ca0d1c3f1c37998081/1jll9ufph";s1.charset="UTF-8";s1.setAttribute("crossorigin","*");s0.parentNode.insertBefore(s1,s0);})();</script>
|
<!-- Tawk.to Live Chat --><script>var Tawk_API=Tawk_API||{}, Tawk_LoadStart=new Date();(function(){var s1=document.createElement("script"),s0=document.getElementsByTagName("script")[0];s1.async=true;s1.src="https://embed.tawk.to/69d5a9ca0d1c3f1c37998081/1jll9ufph";s1.charset="UTF-8";s1.setAttribute("crossorigin","*");s0.parentNode.insertBefore(s1,s0);})();</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue