Add FCC Carrier/ISP Registration: API, checkout, handler, dispatch

Phase 3-5:
- API: POST /api/v1/fcc-carrier-registration (order creation with pricing)
- API: GET /api/v1/fcc-carrier-registration/:id (status)
- API: GET /api/v1/fcc-carrier-registration/state-fees (formation fees)
- Checkout: fcc_carrier_registration order type with Stripe line items
- Payment handler: dispatch worker + send confirmation email
- Pipeline handler: 8-step CRTC-style pipeline (formation → CORES → 499 →
  DC Agent → State PUC → RMD/CPNI/CALEA/BDC → add-ons → final review)
- Job server dispatch map entry
- Service page CTA updated to link to order page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
justin 2026-04-29 08:48:36 -05:00
parent 830f5ae738
commit 2927b5cebb
7 changed files with 677 additions and 2 deletions

View file

@ -44,6 +44,7 @@ import foreignQualRouter from "./routes/foreign-qualification.js";
import corpStatusRouter from "./routes/corp-status.js";
import portalRmdReviewRouter from "./routes/portal-rmd-review.js";
import pucRouter from "./routes/puc.js";
import fccCarrierRegRouter from "./routes/fcc-carrier-registration.js";
const app = express();
@ -112,6 +113,7 @@ app.use(lnpaRegionsRouter);
app.use(fccFilingsRouter);
app.use(foreignQualRouter);
app.use(pucRouter);
app.use(fccCarrierRegRouter);
app.use(adminCryptoRouter);
// Note: identityRouter mounted above express.json() for webhook route,
// but also handles non-webhook routes (create-session, poll) which work fine with json()

View file

@ -76,7 +76,7 @@ const GATEWAY_LABELS: Record<string, string> = {
const CreateSessionSchema = z.object({
order_id: z.string().min(1),
order_type: z.enum(["canada_crtc", "formation", "bundle", "compliance", "compliance_batch"]),
order_type: z.enum(["canada_crtc", "formation", "bundle", "compliance", "compliance_batch", "fcc_carrier_registration"]),
payment_method: z.enum(["card", "ach", "paypal", "klarna", "crypto"]),
});
@ -246,6 +246,48 @@ async function fetchOrderData(
} | null> {
// ── Canada CRTC ─────────────────────────────────────────────────────────
if (order_type === "fcc_carrier_registration") {
const { rows } = await pool.query(
`SELECT * FROM fcc_carrier_registrations WHERE order_number = $1`,
[order_id],
);
if (!rows.length) return null;
const order = rows[0] as Record<string, unknown>;
if (order.payment_status !== "pending_payment") return null;
const baseCents = (order.service_fee_cents as number) || 129900;
const formCents = ((order.formation_fee_cents as number) || 0) + ((order.state_fee_cents as number) || 0);
const addonCents = (order.addon_fee_cents as number) || 0;
const pucCents = (order.puc_fee_cents as number) || 0;
const totalCents = baseCents + formCents + addonCents + pucCents;
const lineItems: Array<{price_data: {currency: "usd"; product_data: {name: string}; unit_amount: number}; quantity: number}> = [
{ price_data: { currency: "usd", product_data: { name: "FCC Carrier / ISP Registration" }, unit_amount: baseCents }, quantity: 1 },
];
if (formCents > 0) {
lineItems.push({ price_data: { currency: "usd", product_data: { name: `Business Formation (${order.formation_state || "?"} ${((order.entity_type as string) || "LLC").toUpperCase()})` }, unit_amount: formCents }, quantity: 1 });
}
if ((order.include_stir_shaken as boolean)) {
lineItems.push({ price_data: { currency: "usd", product_data: { name: "STIR/SHAKEN Implementation" }, unit_amount: 49900 }, quantity: 1 });
}
if ((order.include_ocn as boolean)) {
lineItems.push({ price_data: { currency: "usd", product_data: { name: "NECA OCN Registration" }, unit_amount: 265000 }, quantity: 1 });
}
if (pucCents > 0) {
const stateCount = ((order.state_puc_states as string[]) || []).length;
lineItems.push({ price_data: { currency: "usd", product_data: { name: `State PUC Registration (${stateCount} state${stateCount !== 1 ? "s" : ""})` }, unit_amount: pucCents }, quantity: 1 });
}
return {
order,
stripeLineItems: lineItems as Stripe.Checkout.SessionCreateParams.LineItem[],
service_cents: totalCents,
base_cents: totalCents,
customer_email: (order.customer_email as string) || "",
customer_name: (order.customer_name as string) || "",
};
}
if (order_type === "canada_crtc") {
const { rows } = await pool.query(
`SELECT * FROM canada_crtc_orders WHERE order_number = $1`,
@ -1106,6 +1148,7 @@ export async function handlePaymentComplete(
bundle: "bundle_orders",
compliance: "compliance_orders",
compliance_batch: "compliance_orders",
fcc_carrier_registration: "fcc_carrier_registrations",
};
const table = tableMap[order_type];
if (!table) return;
@ -1200,6 +1243,48 @@ export async function handlePaymentComplete(
}
// ── Advance compliance batch orders (fan out worker dispatch per service) ──
// ── FCC Carrier Registration — dispatch pipeline worker ──────────────────
if (order_type === "fcc_carrier_registration") {
const workerUrl = process.env.WORKER_URL || "http://workers:8090";
setImmediate(async () => {
try {
const dispatchRes = await fetch(`${workerUrl}/jobs`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "process_fcc_carrier_registration",
order_number: order_id,
}),
});
if (dispatchRes.ok) {
console.log(`[checkout] FCC carrier registration dispatched: ${order_id}`);
} else {
console.error(`[checkout] FCC carrier reg dispatch failed: HTTP ${dispatchRes.status}`);
}
} catch (err) {
console.error(`[checkout] FCC carrier reg dispatch error:`, err);
}
});
// Send confirmation email
try {
const { sendEmail } = await import("../email.js");
const custEmail = (order.customer_email as string) || "";
const custName = (order.customer_name as string) || "";
const firstName = custName.split(" ")[0] || custName;
await sendEmail({
to: custEmail,
subject: `FCC Carrier Registration — Order ${order_id} Confirmed`,
html: `<h2>Your FCC Carrier Registration is Underway</h2>
<p>Hi ${firstName},</p>
<p>Thank you for your order. We're now processing your carrier/ISP registration package.</p>
<p>You'll receive email updates as each step completes. If we need any additional information, we'll reach out.</p>
<p style="font-size:12px;color:#9ca3af;">Order: ${order_id}</p>
<p style="font-size:12px;color:#9ca3af;">Performance West Inc. | 525 Randall Ave Ste 100-1195, Cheyenne, WY 82001 | 1-888-411-0383</p>`,
});
} catch {}
}
if (order_type === "compliance_batch") {
// A batch order is a set of compliance_orders sharing a batch_id.
// Create ERPNext Sales Order + Invoice, then dispatch each sub-order.

View file

@ -0,0 +1,250 @@
/**
* FCC Carrier / ISP Registration order creation & status API.
*
* POST /api/v1/fcc-carrier-registration create order
* GET /api/v1/fcc-carrier-registration/:id get order status
* GET /api/v1/fcc-carrier-registration/state-fees formation fees per state
*/
import { Router, type Request, type Response } from "express";
import { pool } from "../db.js";
import { randomBytes } from "crypto";
const router = Router();
const BASE_FEE_CENTS = 129900; // $1,299
const FORMATION_MARKUP_CENTS = 2500; // $25 filing service
const STIR_SHAKEN_FEE_CENTS = 49900;
const OCN_FEE_CENTS = 265000;
const STATE_PUC_FEE_CENTS = 39900; // per state
function generateOrderNumber(): string {
const year = new Date().getFullYear();
const id = randomBytes(4).toString("hex").toUpperCase();
return `FCR-${year}-${id}`;
}
// ── GET /api/v1/fcc-carrier-registration/state-fees ─────────────────────────
router.get("/api/v1/fcc-carrier-registration/state-fees", async (_req: Request, res: Response) => {
try {
const { rows } = await pool.query(
`SELECT state_code, state_name, llc_formation_fee, corp_formation_fee,
expedited_fee, typical_processing_days
FROM state_filing_fees ORDER BY state_name`,
);
res.json({ states: rows, markup_cents: FORMATION_MARKUP_CENTS });
} catch (err) {
console.error("[fcc-carrier-reg] state-fees error:", err);
res.status(500).json({ error: "Could not load state fees" });
}
});
// ── POST /api/v1/fcc-carrier-registration ───────────────────────────────────
router.post("/api/v1/fcc-carrier-registration", async (req: Request, res: Response) => {
try {
const {
customer_name, customer_email, customer_phone,
entity_source, entity_legal_name, ein, formation_state, entity_type, frn,
contact_name, contact_email, contact_phone, contact_title,
address_street, address_city, address_state, address_zip,
service_wizard, services,
engagement_accepted,
} = req.body ?? {};
// Validate required fields
if (!customer_email || !customer_name) {
res.status(400).json({ error: "customer_name and customer_email are required." });
return;
}
if (!entity_source || !["existing", "new_formation"].includes(entity_source)) {
res.status(400).json({ error: "entity_source must be 'existing' or 'new_formation'." });
return;
}
if (!contact_name || !contact_email) {
res.status(400).json({ error: "contact_name and contact_email are required." });
return;
}
// Determine included services from wizard + checklist
const svcList: string[] = Array.isArray(services) ? services : [];
const wizardData = service_wizard || {};
const includeFormation = entity_source === "new_formation";
const includeRmd = true; // always included in base
const includeCpni = true;
const includeCalea = true;
const includeBdc = true;
const includeDcAgent = true;
const includeStirShaken = svcList.includes("stir_shaken");
const includeOcn = svcList.includes("ocn");
const statePucStates = svcList.includes("state_puc") ? (wizardData.puc_states || []) : [];
// Calculate pricing
let formationFeeCents = 0;
let stateFeeCents = 0;
if (includeFormation && formation_state) {
// Look up state filing fee
const feeCol = (entity_type === "corporation") ? "corp_formation_fee" : "llc_formation_fee";
try {
const feeResult = await pool.query(
`SELECT ${feeCol} AS fee FROM state_filing_fees WHERE state_code = $1`,
[formation_state.toUpperCase()],
);
if (feeResult.rows.length > 0 && feeResult.rows[0].fee) {
stateFeeCents = Number(feeResult.rows[0].fee);
}
} catch {}
formationFeeCents = FORMATION_MARKUP_CENTS;
}
let addonFeeCents = 0;
if (includeStirShaken) addonFeeCents += STIR_SHAKEN_FEE_CENTS;
if (includeOcn) addonFeeCents += OCN_FEE_CENTS;
const pucFeeCents = statePucStates.length * STATE_PUC_FEE_CENTS;
const orderNumber = generateOrderNumber();
const result = await pool.query(
`INSERT INTO fcc_carrier_registrations (
order_number, customer_email, customer_name, customer_phone,
entity_source, entity_legal_name, entity_type, formation_state, ein, frn,
contact_name, contact_email, contact_phone, contact_title,
address_street, address_city, address_state, address_zip,
service_wizard,
include_formation, include_dc_agent, include_rmd, include_cpni,
include_calea, include_bdc, include_stir_shaken, include_ocn,
state_puc_states,
service_fee_cents, formation_fee_cents, state_fee_cents,
puc_fee_cents, addon_fee_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
) RETURNING *`,
[
orderNumber,
customer_email.toLowerCase().trim(),
customer_name.trim(),
customer_phone || null,
entity_source,
entity_legal_name || null,
entity_type || null,
formation_state ? formation_state.toUpperCase() : null,
ein || null,
frn || null,
contact_name.trim(),
contact_email.toLowerCase().trim(),
contact_phone || null,
contact_title || null,
address_street || null,
address_city || null,
address_state ? address_state.toUpperCase() : null,
address_zip || null,
JSON.stringify(wizardData),
includeFormation,
includeDcAgent,
includeRmd,
includeCpni,
includeCalea,
includeBdc,
includeStirShaken,
includeOcn,
statePucStates,
BASE_FEE_CENTS,
formationFeeCents,
stateFeeCents,
pucFeeCents,
addonFeeCents,
engagement_accepted ? new Date().toISOString() : null,
engagement_accepted ? (req.ip || req.headers["x-forwarded-for"] || null) : null,
],
);
const order = result.rows[0];
// If formation needed, create a formation_orders row
if (includeFormation && formation_state) {
try {
const formationOrderNumber = `FO-${new Date().getFullYear()}-${randomBytes(3).toString("hex").toUpperCase()}`;
await pool.query(
`INSERT INTO formation_orders (
order_number, customer_name, customer_email, customer_phone,
state_code, entity_type, entity_name,
principal_address, principal_city, principal_state, principal_zip,
service_fee_cents, state_fee_cents, payment_status
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,'paid')`,
[
formationOrderNumber,
customer_name.trim(),
customer_email.toLowerCase().trim(),
customer_phone || null,
formation_state.toUpperCase(),
entity_type || "llc",
entity_legal_name || null,
address_street || null,
address_city || null,
address_state ? address_state.toUpperCase() : null,
address_zip || null,
FORMATION_MARKUP_CENTS,
stateFeeCents,
],
);
// Link formation order to carrier registration
await pool.query(
`UPDATE fcc_carrier_registrations SET formation_order_number = $1 WHERE order_number = $2`,
[formationOrderNumber, orderNumber],
);
} catch (formErr) {
console.warn("[fcc-carrier-reg] Formation order creation failed (non-fatal):", formErr);
}
}
const totalCents = BASE_FEE_CENTS + formationFeeCents + stateFeeCents + pucFeeCents + addonFeeCents;
console.log(
`[fcc-carrier-reg] Created ${orderNumber}: ${entity_source} for ${customer_email}$${(totalCents / 100).toFixed(2)}`,
);
res.json({
success: true,
order_number: orderNumber,
order_id: orderNumber,
order_type: "fcc_carrier_registration",
total_cents: totalCents,
pricing: {
base: BASE_FEE_CENTS,
formation: formationFeeCents + stateFeeCents,
addons: addonFeeCents,
puc: pucFeeCents,
total: totalCents,
},
});
} catch (err) {
console.error("[fcc-carrier-reg] Create error:", err);
res.status(500).json({ error: "Could not create order." });
}
});
// ── GET /api/v1/fcc-carrier-registration/:id ────────────────────────────────
router.get("/api/v1/fcc-carrier-registration/:id", async (req: Request, res: Response) => {
try {
const { rows } = await pool.query(
`SELECT * FROM fcc_carrier_registrations WHERE order_number = $1`,
[req.params.id],
);
if (rows.length === 0) {
res.status(404).json({ error: "Order not found" });
return;
}
res.json(rows[0]);
} catch (err) {
console.error("[fcc-carrier-reg] Get error:", err);
res.status(500).json({ error: "Could not load order." });
}
});
export default router;