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>
435 lines
15 KiB
TypeScript
435 lines
15 KiB
TypeScript
import { Router } from "express";
|
|
import { pool } from "../db.js";
|
|
|
|
const router = Router();
|
|
|
|
/**
|
|
* List all telecom entities for a customer.
|
|
*
|
|
* GET /api/v1/entities/telecom?customer_id=123
|
|
* GET /api/v1/entities/telecom?email=user@example.com
|
|
*/
|
|
router.get("/api/v1/entities/telecom", async (req, res) => {
|
|
const { customer_id, email } = req.query;
|
|
|
|
if (!customer_id && !email) {
|
|
res.status(400).json({ error: "Provide customer_id or email." });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let query: string;
|
|
let params: any[];
|
|
|
|
if (customer_id) {
|
|
query = "SELECT * FROM telecom_entities WHERE customer_id = $1 AND active = true ORDER BY jurisdiction, legal_name";
|
|
params = [customer_id];
|
|
} else {
|
|
query = `SELECT te.* FROM telecom_entities te
|
|
JOIN customers c ON c.id = te.customer_id
|
|
WHERE c.email = $1 AND te.active = true
|
|
ORDER BY te.jurisdiction, te.legal_name`;
|
|
params = [(email as string).toLowerCase().trim()];
|
|
}
|
|
|
|
const result = await pool.query(query, params);
|
|
|
|
res.json({
|
|
entities: result.rows,
|
|
count: result.rows.length,
|
|
});
|
|
} catch (err) {
|
|
console.error("[telecom-entities] List error:", err);
|
|
res.status(500).json({ error: "Could not fetch entities." });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Get a single telecom entity by ID.
|
|
*
|
|
* GET /api/v1/entities/telecom/:id
|
|
*/
|
|
router.get("/api/v1/entities/telecom/:id", async (req, res) => {
|
|
try {
|
|
const result = await pool.query("SELECT * FROM telecom_entities WHERE id = $1", [req.params.id]);
|
|
if (result.rows.length === 0) {
|
|
res.status(404).json({ error: "Entity not found." });
|
|
return;
|
|
}
|
|
res.json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("[telecom-entities] Get error:", err);
|
|
res.status(500).json({ error: "Could not fetch entity." });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Create a new telecom entity.
|
|
*
|
|
* POST /api/v1/entities/telecom
|
|
*/
|
|
router.post("/api/v1/entities/telecom", async (req, res) => {
|
|
const {
|
|
customer_id, customer_email,
|
|
jurisdiction, legal_name, dba_name, ein,
|
|
// FCC
|
|
frn, filer_id_499,
|
|
// CRTC
|
|
incorporation_number, incorporation_province, crtc_registration_number,
|
|
// Classification
|
|
filer_type, infra_type, is_deminimis, is_lire, service_categories,
|
|
// Contact
|
|
contact_name, contact_email, contact_phone, ceo_name, ceo_title,
|
|
// Address
|
|
address_street, address_city, address_state, address_zip,
|
|
// Revenue
|
|
last_filing_year, total_revenue_cents, interstate_pct, international_pct,
|
|
} = req.body ?? {};
|
|
|
|
if (!legal_name) {
|
|
res.status(400).json({ error: "legal_name is required." });
|
|
return;
|
|
}
|
|
|
|
// Resolve customer_id from email if needed
|
|
let resolvedCustomerId = customer_id;
|
|
if (!resolvedCustomerId && customer_email) {
|
|
const cust = await pool.query("SELECT id FROM customers WHERE email = $1", [customer_email.toLowerCase().trim()]);
|
|
if (cust.rows.length > 0) {
|
|
resolvedCustomerId = cust.rows[0].id;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const result = await pool.query(
|
|
`INSERT INTO telecom_entities (
|
|
customer_id, jurisdiction, legal_name, dba_name, ein,
|
|
frn, filer_id_499,
|
|
incorporation_number, incorporation_province, crtc_registration_number,
|
|
filer_type, infra_type, is_deminimis, is_lire, service_categories,
|
|
contact_name, contact_email, contact_phone, ceo_name, ceo_title,
|
|
address_street, address_city, address_state, address_zip,
|
|
last_filing_year, total_revenue_cents, interstate_pct, international_pct
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28)
|
|
RETURNING *`,
|
|
[
|
|
resolvedCustomerId || null,
|
|
jurisdiction || "FCC",
|
|
legal_name, dba_name || null, ein || null,
|
|
frn || null, filer_id_499 || null,
|
|
incorporation_number || null, incorporation_province || null, crtc_registration_number || null,
|
|
filer_type || null, infra_type || null,
|
|
is_deminimis === true, is_lire === true,
|
|
service_categories || null,
|
|
contact_name || null, contact_email || null, contact_phone || null,
|
|
ceo_name || null, ceo_title || null,
|
|
address_street || null, address_city || null, address_state || null, address_zip || null,
|
|
last_filing_year || null, total_revenue_cents || 0,
|
|
interstate_pct || 0, international_pct || 0,
|
|
],
|
|
);
|
|
|
|
res.status(201).json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("[telecom-entities] Create error:", err);
|
|
res.status(500).json({ error: "Could not create entity." });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Update a telecom entity.
|
|
*
|
|
* PATCH /api/v1/entities/telecom/:id
|
|
*/
|
|
router.patch("/api/v1/entities/telecom/:id", async (req, res) => {
|
|
const id = req.params.id;
|
|
const updates = req.body ?? {};
|
|
|
|
// Build dynamic SET clause from provided fields
|
|
const allowedFields = [
|
|
"legal_name", "dba_name", "ein", "jurisdiction",
|
|
"frn", "filer_id_499",
|
|
"incorporation_number", "incorporation_province", "crtc_registration_number",
|
|
"filer_type", "infra_type", "is_deminimis", "is_lire", "service_categories",
|
|
"contact_name", "contact_email", "contact_phone", "ceo_name", "ceo_title",
|
|
"address_street", "address_city", "address_state", "address_zip",
|
|
"last_filing_year", "total_revenue_cents", "interstate_pct", "international_pct",
|
|
"active", "notes",
|
|
// Carrier classification (migration 043)
|
|
"carrier_category", "is_wholesale", "is_gateway_provider",
|
|
"is_international_only", "uses_ucaas_provider", "carrier_metadata",
|
|
"stir_shaken_status", "stir_shaken_cert_authority",
|
|
"upstream_provider_name", "upstream_provider_frn",
|
|
"rmd_letter_minio_path", "rmd_letter_generated_at", "last_compliance_checkup",
|
|
// FACS (Foreign Adversary Control System) fields (migration 045)
|
|
"facs_schedule", "facs_has_foreign_adversary", "facs_filing_status",
|
|
"facs_ownership_data", "covered_authorizations", "has_section_214",
|
|
"facs_filed_at",
|
|
// Form 499-A Block 1/2 fidelity (migrations 048, 054)
|
|
"affiliated_filer_name", "affiliated_filer_ein", "management_company_name",
|
|
"trade_names",
|
|
"regulatory_contact_name", "regulatory_contact_email", "regulatory_contact_phone",
|
|
"worksheet_office_company", "worksheet_office_street", "worksheet_office_city",
|
|
"worksheet_office_state", "worksheet_office_zip",
|
|
"billing_contact_name", "billing_contact_email", "itsp_regulatory_fee_email",
|
|
"dc_agent_company", "dc_agent_street", "dc_agent_city", "dc_agent_state",
|
|
"dc_agent_zip", "dc_agent_phone", "dc_agent_email",
|
|
"officer_1_street", "officer_1_city", "officer_1_state", "officer_1_zip",
|
|
"officer_2_name", "officer_2_title", "officer_2_street", "officer_2_city",
|
|
"officer_2_state", "officer_2_zip",
|
|
"officer_3_name", "officer_3_title", "officer_3_street", "officer_3_city",
|
|
"officer_3_state", "officer_3_zip",
|
|
"officer_count_claimed", "entity_structure",
|
|
"jurisdictions_served",
|
|
"first_telecom_service_year", "first_telecom_service_month",
|
|
"first_telecom_service_pre_1999",
|
|
// Form 499-A Block 6 fidelity (migrations 048, 054)
|
|
"exempt_usf", "exempt_trs", "exempt_nanpa", "exempt_lnp", "exempt_itsp",
|
|
"exemption_explanation",
|
|
"is_state_local_gov", "is_tax_exempt_501c",
|
|
"nondisclosure_requested",
|
|
// Line 105 taxonomy (migration 053)
|
|
"line_105_primary", "line_105_categories",
|
|
"wireless_meta", "satellite_meta", "audio_bridging_meta", "private_line_circuits",
|
|
// Safe harbor election (migration 054)
|
|
"safe_harbor_election",
|
|
// CORES + CALEA + foreign carrier (migration 052)
|
|
"cores_username", "cores_password_hash", "cores_registered_at",
|
|
"calea_ssi_generated_at", "calea_ssi_reviewer_name", "calea_ssi_next_review_date",
|
|
"foreign_affiliations",
|
|
// NECA OCN (migration 048)
|
|
"ocn", "ocn_category", "ocn_assigned_at",
|
|
];
|
|
|
|
const sets: string[] = [];
|
|
const values: any[] = [];
|
|
let paramIdx = 1;
|
|
|
|
for (const field of allowedFields) {
|
|
if (field in updates) {
|
|
sets.push(`${field} = $${paramIdx}`);
|
|
values.push(updates[field]);
|
|
paramIdx++;
|
|
}
|
|
}
|
|
|
|
if (sets.length === 0) {
|
|
res.status(400).json({ error: "No valid fields to update." });
|
|
return;
|
|
}
|
|
|
|
sets.push(`updated_at = NOW()`);
|
|
values.push(id);
|
|
|
|
try {
|
|
const result = await pool.query(
|
|
`UPDATE telecom_entities SET ${sets.join(", ")} WHERE id = $${paramIdx} RETURNING *`,
|
|
values,
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
res.status(404).json({ error: "Entity not found." });
|
|
return;
|
|
}
|
|
|
|
res.json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("[telecom-entities] Update error:", err);
|
|
res.status(500).json({ error: "Could not update entity." });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Classify a telecom entity's carrier type.
|
|
*
|
|
* PATCH /api/v1/entities/telecom/:id/classify
|
|
*
|
|
* Accepts the full carrier classification from the questionnaire and
|
|
* validates field combinations (e.g. UCaaS provider requires metadata).
|
|
*/
|
|
router.patch("/api/v1/entities/telecom/:id/classify", async (req, res) => {
|
|
const id = req.params.id;
|
|
const body = req.body ?? {};
|
|
|
|
// Validate carrier_category
|
|
const validCategories = [
|
|
"interconnected_voip", "non_interconnected_voip", "clec", "ixc", "cmrs", "other",
|
|
];
|
|
if (body.carrier_category && !validCategories.includes(body.carrier_category)) {
|
|
res.status(400).json({ error: `Invalid carrier_category. Must be one of: ${validCategories.join(", ")}` });
|
|
return;
|
|
}
|
|
|
|
// Validate stir_shaken_status
|
|
const validStirShaken = [
|
|
"complete_implementation", "partial_implementation",
|
|
"robocall_mitigation_only", "exempt_small_carrier", "not_applicable",
|
|
];
|
|
if (body.stir_shaken_status && !validStirShaken.includes(body.stir_shaken_status)) {
|
|
res.status(400).json({ error: `Invalid stir_shaken_status. Must be one of: ${validStirShaken.join(", ")}` });
|
|
return;
|
|
}
|
|
|
|
// Validate UCaaS metadata
|
|
if (body.uses_ucaas_provider === true) {
|
|
const meta = body.carrier_metadata ?? {};
|
|
if (!meta.ucaas_provider) {
|
|
res.status(400).json({ error: "carrier_metadata.ucaas_provider is required when uses_ucaas_provider is true." });
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Validate gateway metadata
|
|
if (body.is_gateway_provider === true) {
|
|
const meta = body.carrier_metadata ?? {};
|
|
if (!meta.gateway_countries || !Array.isArray(meta.gateway_countries) || meta.gateway_countries.length === 0) {
|
|
res.status(400).json({ error: "carrier_metadata.gateway_countries is required when is_gateway_provider is true." });
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Validate complete STIR/SHAKEN requires cert authority
|
|
if (body.stir_shaken_status === "complete_implementation" && !body.stir_shaken_cert_authority) {
|
|
res.status(400).json({ error: "stir_shaken_cert_authority is required for complete_implementation." });
|
|
return;
|
|
}
|
|
|
|
// Build update
|
|
const classifyFields = [
|
|
"carrier_category", "is_wholesale", "is_gateway_provider",
|
|
"is_international_only", "uses_ucaas_provider", "carrier_metadata",
|
|
"stir_shaken_status", "stir_shaken_cert_authority",
|
|
"upstream_provider_name", "upstream_provider_frn",
|
|
// Also allow updating infra_type since the questionnaire captures it
|
|
"infra_type",
|
|
];
|
|
|
|
const sets: string[] = [];
|
|
const values: any[] = [];
|
|
let paramIdx = 1;
|
|
|
|
for (const field of classifyFields) {
|
|
if (field in body) {
|
|
if (field === "carrier_metadata") {
|
|
sets.push(`${field} = $${paramIdx}::jsonb`);
|
|
} else {
|
|
sets.push(`${field} = $${paramIdx}`);
|
|
}
|
|
values.push(body[field]);
|
|
paramIdx++;
|
|
}
|
|
}
|
|
|
|
if (sets.length === 0) {
|
|
res.status(400).json({ error: "No classification fields provided." });
|
|
return;
|
|
}
|
|
|
|
sets.push(`updated_at = NOW()`);
|
|
values.push(id);
|
|
|
|
try {
|
|
const result = await pool.query(
|
|
`UPDATE telecom_entities SET ${sets.join(", ")} WHERE id = $${paramIdx} RETURNING *`,
|
|
values,
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
res.status(404).json({ error: "Entity not found." });
|
|
return;
|
|
}
|
|
|
|
res.json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("[telecom-entities] Classify error:", err);
|
|
res.status(500).json({ error: "Could not classify entity." });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Run compliance check against a specific entity.
|
|
*
|
|
* GET /api/v1/entities/telecom/:id/compliance
|
|
*
|
|
* Uses the entity's FRN to run the same checks as /api/v1/fcc/lookup,
|
|
* enriched with entity-specific data (classification, filing history).
|
|
*/
|
|
router.get("/api/v1/entities/telecom/:id/compliance", async (req, res) => {
|
|
try {
|
|
const entity = await pool.query("SELECT * FROM telecom_entities WHERE id = $1", [req.params.id]);
|
|
if (entity.rows.length === 0) {
|
|
res.status(404).json({ error: "Entity not found." });
|
|
return;
|
|
}
|
|
|
|
const e = entity.rows[0];
|
|
|
|
if (e.jurisdiction === "FCC" && e.frn) {
|
|
// Proxy to the FCC lookup endpoint
|
|
const lookupUrl = `http://localhost:${process.env.PORT || 3001}/api/v1/fcc/lookup?frn=${e.frn}`;
|
|
const lookupResp = await fetch(lookupUrl, { signal: AbortSignal.timeout(30000) });
|
|
const lookupData = await lookupResp.json() as Record<string, unknown>;
|
|
|
|
res.json(Object.assign({
|
|
entity: {
|
|
id: e.id,
|
|
legal_name: e.legal_name,
|
|
jurisdiction: e.jurisdiction,
|
|
frn: e.frn,
|
|
filer_type: e.filer_type,
|
|
is_deminimis: e.is_deminimis,
|
|
is_lire: e.is_lire,
|
|
},
|
|
}, lookupData));
|
|
} else if (e.jurisdiction === "CRTC") {
|
|
// CRTC compliance checks (simplified)
|
|
const now = new Date();
|
|
const checks = [
|
|
{
|
|
id: "crtc_registration",
|
|
label: "CRTC Registration",
|
|
status: e.crtc_registration_number ? "green" : "unknown",
|
|
detail: e.crtc_registration_number
|
|
? `Registered: ${e.crtc_registration_number}`
|
|
: "Registration number not on file",
|
|
action_url: null,
|
|
due_date: null,
|
|
},
|
|
{
|
|
id: "provincial_incorporation",
|
|
label: `${e.incorporation_province || "Provincial"} Incorporation`,
|
|
status: e.incorporation_number ? "green" : "unknown",
|
|
detail: e.incorporation_number
|
|
? `${e.incorporation_province} #${e.incorporation_number}`
|
|
: "Incorporation number not on file",
|
|
action_url: null,
|
|
due_date: null,
|
|
},
|
|
];
|
|
|
|
res.json({
|
|
entity: {
|
|
id: e.id,
|
|
legal_name: e.legal_name,
|
|
jurisdiction: e.jurisdiction,
|
|
incorporation_number: e.incorporation_number,
|
|
incorporation_province: e.incorporation_province,
|
|
},
|
|
checks,
|
|
checked_at: new Date().toISOString(),
|
|
});
|
|
} else {
|
|
res.json({
|
|
entity: { id: e.id, legal_name: e.legal_name, jurisdiction: e.jurisdiction },
|
|
checks: [],
|
|
note: "No FRN on file — add an FRN to run FCC compliance checks.",
|
|
checked_at: new Date().toISOString(),
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error("[telecom-entities] Compliance check error:", err);
|
|
res.status(500).json({ error: "Compliance check failed." });
|
|
}
|
|
});
|
|
|
|
export default router;
|