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>
227 lines
7.7 KiB
TypeScript
227 lines
7.7 KiB
TypeScript
/**
|
||
* 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;
|