/** * Foreign Qualification API. * * Customer and admin endpoints for ordering a Certificate of Authority * (aka "foreign qualification") in one or more US states against an * already-formed entity. Used by: * * - Regular incorp clients expanding to additional states. * - FCC carriers that must be authorized to do business in every * state they serve (per state PUC rules, not the FCC directly). * * Endpoints: * GET /api/v1/foreign-qualification/jurisdictions * Public catalog of eligible target states + per-entity-type quotes. * * POST /api/v1/foreign-qualification/quote * Price a multi-state COA order before checkout. * * GET /api/v1/foreign-qualification/registrations * Admin list (requires admin auth). * * GET /api/v1/foreign-qualification/registrations/:id * Admin detail. * * Order creation flows through the regular compliance_orders checkout * pipeline (/api/v1/compliance-orders) with service_slug set to * `foreign-qualification-single` or `foreign-qualification-multi`; the * intake wizard pushes the selected target states into intake_data so * the worker can fan out per-state rows in * `foreign_qualification_registrations`. */ import { Router } from "express"; import type { Request, Response } from "express"; import { pool } from "../db.js"; import { requireAdmin } from "../middleware/admin-auth.js"; const router = Router(); // Flat service-fee schedule — mirrors COMPLIANCE_SERVICES in // compliance-orders.ts. Kept in sync manually; update both sides. const SERVICE_FEE_SINGLE_CENTS = 14900; const SERVICE_FEE_MULTI_CENTS = 9900; // per-state const NWRA_RA_DEFAULT_CENTS = 12500; // $125/yr registered agent // ── GET /api/v1/foreign-qualification/jurisdictions ───────────────── // // List every US jurisdiction with foreign-qualification enabled, // including fee preview. Front-end uses this to render the state picker // with price per state. router.get( "/api/v1/foreign-qualification/jurisdictions", async (_req: Request, res: Response) => { const { rows } = await pool.query( `SELECT j.code, j.name, j.country, j.kind, j.portal_name, j.supports_foreign_qualification, f.foreign_llc_fee, f.foreign_corp_fee, f.expedited_fee, f.publication_required, f.typical_processing_days FROM jurisdictions j LEFT JOIN state_filing_fees f ON f.state_code = j.code WHERE j.country = 'US' AND j.supports_foreign_qualification = TRUE ORDER BY j.code`, ); res.json({ jurisdictions: rows }); }, ); // ── POST /api/v1/foreign-qualification/quote ───────────────────────── // // Body: { // home_state_code: "WY", // entity_type: "llc", // target_states: ["CA", "TX", "NY"], // include_ra_each: true, // expedited: false, // } // // Returns per-state breakdown + grand total. router.post( "/api/v1/foreign-qualification/quote", async (req: Request, res: Response) => { const { home_state_code, entity_type, target_states, include_ra_each = true, expedited = false, } = req.body ?? {}; if (!home_state_code || !entity_type || !Array.isArray(target_states) || target_states.length === 0) { res.status(400).json({ error: "home_state_code, entity_type, and non-empty target_states required", }); return; } const et = String(entity_type).toLowerCase(); const feeCol = et === "llc" || et === "pllc" ? "foreign_llc_fee" : et === "corporation" || et === "c_corp" || et === "s_corp" || et === "pc" || et === "nonprofit" ? "foreign_corp_fee" : null; if (!feeCol) { res.status(400).json({ error: `unsupported entity_type: ${entity_type}` }); return; } // Pull fees + sanity-check the target state is eligible. const { rows } = await pool.query( `SELECT j.code, j.name, j.supports_foreign_qualification, f.${feeCol} AS state_fee_cents, f.expedited_fee AS expedited_raw, f.publication_required FROM jurisdictions j LEFT JOIN state_filing_fees f ON f.state_code = j.code WHERE j.code = ANY($1::varchar[])`, [target_states.map((s: string) => String(s).toUpperCase())], ); const perStateServiceFee = target_states.length === 1 ? SERVICE_FEE_SINGLE_CENTS : SERVICE_FEE_MULTI_CENTS; let grand = 0; const items = rows.map((r) => { if (!r.supports_foreign_qualification) { return { state_code: r.code, error: "not_supported", }; } const stateFee = Number(r.state_fee_cents || 0); // state_filing_fees.expedited_fee is seeded inconsistently — the // same normalization we do in the Python sizer (dollars×10000 → // cents). See scripts/workers/crypto_offramp/sizer.py. const expRaw = Number(r.expedited_raw || 0); const expFee = expedited ? (expRaw > 50000 ? Math.floor(expRaw / 100) : expRaw) : 0; const raFee = include_ra_each ? NWRA_RA_DEFAULT_CENTS : 0; const publication = r.publication_required ? true : false; const total = stateFee + expFee + raFee + perStateServiceFee; grand += total; return { state_code: r.code, state_name: r.name, state_fee_cents: stateFee, expedited_fee_cents: expFee, nwra_ra_fee_cents: raFee, service_fee_cents: perStateServiceFee, total_cents: total, publication_required: publication, }; }); res.json({ home_state_code, entity_type: et, include_ra_each, expedited, per_state_service_fee_cents: perStateServiceFee, items, grand_total_cents: grand, }); }, ); // ── GET /api/v1/foreign-qualification/registrations ────────────────── router.get( "/api/v1/foreign-qualification/registrations", requireAdmin, async (req: Request, res: Response) => { const status = (req.query.status as string) || ""; const targetState = (req.query.target_state as string) || ""; const limit = Math.min(Number(req.query.limit) || 100, 500); const conditions: string[] = []; const params: (number | string)[] = []; if (status) { conditions.push(`status = $${params.length + 1}`); params.push(status); } if (targetState) { conditions.push(`target_state_code = $${params.length + 1}`); params.push(targetState.toUpperCase()); } const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : ""; params.push(limit); const { rows } = await pool.query( `SELECT * FROM v_foreign_qualifications_pipeline ${where} LIMIT $${params.length}`, params, ); res.json({ registrations: rows }); }, ); // ── GET /api/v1/foreign-qualification/registrations/:id ────────────── router.get( "/api/v1/foreign-qualification/registrations/:id", requireAdmin, async (req: Request, res: Response) => { const id = Number(req.params.id); if (!Number.isFinite(id)) { res.status(400).json({ error: "bad id" }); return; } const { rows } = await pool.query( `SELECT * FROM foreign_qualification_registrations WHERE id = $1`, [id], ); if (!rows.length) { res.status(404).json({ error: "not found" }); return; } res.json({ registration: rows[0] }); }, ); export default router;