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:
parent
830f5ae738
commit
2927b5cebb
7 changed files with 677 additions and 2 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
250
api/src/routes/fcc-carrier-registration.ts
Normal file
250
api/src/routes/fcc-carrier-registration.ts
Normal 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue