new-site/api/src/routes/foreign-qualification.ts
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:22 -05:00

227 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;