- Intake email now has "I've completed the delegation →" button instead of "reply to this email" - Button links to order success page with action=usac_delegation - New API: POST /api/v1/compliance-orders/:id/usac-delegation Records confirmation timestamp in intake_data and logs it - Removed "reply to this email" from intake email Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1500 lines
56 KiB
TypeScript
1500 lines
56 KiB
TypeScript
/**
|
||
* Compliance Orders — CRUD for compliance service orders.
|
||
*
|
||
* POST /api/v1/compliance-orders
|
||
* Create a new compliance order (pending_payment).
|
||
* Returns { order_number, service_slug, ... } for checkout redirect.
|
||
*
|
||
* GET /api/v1/compliance-orders/:order_number
|
||
* Get order status (used by success page to confirm payment).
|
||
*/
|
||
|
||
import { Router } from "express";
|
||
import { pool } from "../db.js";
|
||
import { randomBytes } from "crypto";
|
||
|
||
const router = Router();
|
||
|
||
// ── Service catalog (prices in cents) ──────────────────────────────────────
|
||
const COMPLIANCE_SERVICES: Record<
|
||
string,
|
||
{ name: string; price_cents: number; gov_fee_cents?: number; gov_fee_label?: string; erpnext_item: string; discountable: boolean }
|
||
> = {
|
||
"fcc-compliance-checkup": {
|
||
name: "FCC Carrier Compliance Checkup",
|
||
price_cents: 79900,
|
||
erpnext_item: "FCC-COMPLIANCE-CHECKUP",
|
||
discountable: true,
|
||
},
|
||
"fcc-499a": {
|
||
name: "FCC Form 499-A Filing",
|
||
price_cents: 49900,
|
||
erpnext_item: "FCC-499A",
|
||
discountable: true,
|
||
},
|
||
"fcc-499a-499q": {
|
||
name: "FCC Form 499-A + 499-Q Bundle",
|
||
price_cents: 59900,
|
||
erpnext_item: "FCC-499A-499Q",
|
||
discountable: true,
|
||
},
|
||
"fcc-full-compliance": {
|
||
name: "FCC Full Compliance Bundle",
|
||
price_cents: 149900,
|
||
erpnext_item: "FCC-FULL-COMPLIANCE",
|
||
discountable: true,
|
||
},
|
||
"cpni-certification": {
|
||
name: "CPNI Annual Certification",
|
||
price_cents: 14900,
|
||
erpnext_item: "CPNI-CERT",
|
||
discountable: true,
|
||
},
|
||
"rmd-filing": {
|
||
name: "RMD Registration / Recertification",
|
||
price_cents: 21900,
|
||
gov_fee_cents: 10000,
|
||
gov_fee_label: "FCC RMD filing fee",
|
||
erpnext_item: "RMD-FILING",
|
||
discountable: true,
|
||
},
|
||
"stir-shaken": {
|
||
name: "STIR/SHAKEN Implementation Assistance",
|
||
price_cents: 49900,
|
||
erpnext_item: "STIR-SHAKEN",
|
||
discountable: true,
|
||
},
|
||
"dc-agent": {
|
||
name: "D.C. Registered Agent (Annual)",
|
||
price_cents: 9900,
|
||
erpnext_item: "DC-AGENT",
|
||
discountable: false,
|
||
},
|
||
// BDC filings — the FCC retired Form 477 in Dec 2022 and folded voice
|
||
// subscription data into BDC. Broadband-only and voice-only SKUs let
|
||
// carriers pay for just what they need; bdc-filing remains as a bundle.
|
||
"bdc-broadband": {
|
||
name: "BDC Broadband Deployment Filing",
|
||
price_cents: 19900,
|
||
erpnext_item: "BDC-BROADBAND",
|
||
discountable: true,
|
||
},
|
||
"bdc-voice": {
|
||
name: "BDC Voice Subscription Filing (formerly Form 477 Voice)",
|
||
price_cents: 14900,
|
||
erpnext_item: "BDC-VOICE",
|
||
discountable: true,
|
||
},
|
||
"bdc-filing": {
|
||
name: "BDC Filing (Broadband + Voice)",
|
||
price_cents: 29900,
|
||
erpnext_item: "BDC-FILING",
|
||
discountable: true,
|
||
},
|
||
// New-carrier entry point — register in CORES + obtain FRN
|
||
"cores-frn-registration": {
|
||
name: "CORES / FRN Registration",
|
||
price_cents: 9900,
|
||
erpnext_item: "CORES-FRN",
|
||
discountable: true,
|
||
},
|
||
// 499-A New Filer Registration (distinct from the annual revenue filing)
|
||
"fcc-499-initial": {
|
||
name: "Form 499 Initial Registration",
|
||
price_cents: 29900,
|
||
erpnext_item: "FCC-499-INITIAL",
|
||
discountable: true,
|
||
},
|
||
// CALEA System Security & Integrity plan (47 USC 229)
|
||
"calea-ssi": {
|
||
name: "CALEA SSI Plan",
|
||
price_cents: 29900,
|
||
erpnext_item: "CALEA-SSI",
|
||
discountable: true,
|
||
},
|
||
// 47 CFR § 63.11 Foreign Carrier Affiliation Notification
|
||
"fcc-63-11-notification": {
|
||
name: "Foreign Carrier Affiliation Notification (47 CFR § 63.11)",
|
||
price_cents: 34900,
|
||
erpnext_item: "FCC-63-11",
|
||
discountable: true,
|
||
},
|
||
// One-click onboarding for a brand-new carrier (composes 5 filings)
|
||
"new-carrier-bundle": {
|
||
name: "New Carrier Onboarding Bundle (FRN + 499 Initial + RMD + CPNI + CALEA)",
|
||
price_cents: 179900,
|
||
erpnext_item: "NEW-CARRIER-BUNDLE",
|
||
discountable: true,
|
||
},
|
||
// NECA OCN registration — required for VoIP/IPES carriers that need
|
||
// their own Operating Company Number for STIR/SHAKEN signing, LRN
|
||
// assignments, or direct numbering authority. NECA charges $550
|
||
// standard / $675 expedited; we pass through + margin for prep work.
|
||
"ocn-registration": {
|
||
name: "NECA OCN + Sponsoring CLEC Agreement",
|
||
price_cents: 265000,
|
||
erpnext_item: "OCN-REGISTRATION",
|
||
discountable: false,
|
||
},
|
||
"fcc-499a-discontinuance": {
|
||
name: "Form 499-A Discontinuance Filing",
|
||
price_cents: 29900,
|
||
erpnext_item: "499A-DISCONTINUANCE",
|
||
discountable: false,
|
||
},
|
||
// Standalone CDR Traffic Study — customers who want the classified
|
||
// study (unlocks the current reporting year) without buying the
|
||
// full 499-A filing service. Either slug unlocks the study via the
|
||
// cdr_study_access_grants paywall hook.
|
||
"cdr-analysis": {
|
||
name: "CDR Traffic Study (Annual)",
|
||
price_cents: 49900,
|
||
erpnext_item: "CDR-ANALYSIS",
|
||
discountable: true,
|
||
},
|
||
// CDR storage / processing tiers. Annual subscription; renew alongside
|
||
// the filing. Each tier is additive — customer picks the smallest tier
|
||
// that covers their worst of (storage bytes, classified rows).
|
||
"cdr-storage-tier1": {
|
||
name: "CDR Storage Tier 1 (50 GB / 50M calls)",
|
||
price_cents: 9900,
|
||
erpnext_item: "CDR-STORAGE-TIER1",
|
||
discountable: false,
|
||
},
|
||
"cdr-storage-tier2": {
|
||
name: "CDR Storage Tier 2 (250 GB / 250M calls)",
|
||
price_cents: 29900,
|
||
erpnext_item: "CDR-STORAGE-TIER2",
|
||
discountable: false,
|
||
},
|
||
"cdr-storage-tier3": {
|
||
name: "CDR Storage Tier 3 (1 TB / 1B calls)",
|
||
price_cents: 79900,
|
||
erpnext_item: "CDR-STORAGE-TIER3",
|
||
discountable: false,
|
||
},
|
||
// ── Foreign qualification (Certificate of Authority) ─────────────────
|
||
// Per-state fees are added on top of these flat service fees at order
|
||
// time (state fee + NWRA RA wholesale, looked up from jurisdictions +
|
||
// state_filing_fees). Handler fans out one filing per selected state.
|
||
//
|
||
// Pricing model:
|
||
// - `foreign-qualification-single`: flat service fee ($149) + state
|
||
// fee + optional NWRA RA. One state per order.
|
||
// - `foreign-qualification-multi`: discounted per-state service fee
|
||
// ($99) + state fees + RAs. For FCC carriers filing across their
|
||
// operating territory.
|
||
"foreign-qualification-single": {
|
||
name: "Foreign Qualification (One State)",
|
||
price_cents: 14900,
|
||
erpnext_item: "FOREIGN-QUAL-SINGLE",
|
||
discountable: true,
|
||
},
|
||
"foreign-qualification-multi": {
|
||
name: "Foreign Qualification (Multi-State)",
|
||
price_cents: 9900, // per-state service fee
|
||
erpnext_item: "FOREIGN-QUAL-MULTI",
|
||
discountable: true,
|
||
},
|
||
// State PUC/PSC Registration — $399 per-state service fee + state
|
||
// filing fees. Bond procurement coordinated separately.
|
||
"state-puc": {
|
||
name: "State PUC/PSC Registration",
|
||
price_cents: 39900, // per-state service fee
|
||
erpnext_item: "STATE-PUC",
|
||
discountable: true,
|
||
},
|
||
};
|
||
|
||
// ── Intake validation map ─────────────────────────────────────────────
|
||
//
|
||
// Required fields must be present in the submitted `intake_data` before
|
||
// Stripe Checkout is allowed to run. Soft fields trigger a warning but
|
||
// don't block payment — the handler degrades gracefully (e.g. blank
|
||
// signature line on a cert letter).
|
||
//
|
||
// Keys dot-nested where intake_data nests objects:
|
||
// "officer.name" → intake_data.officer.name
|
||
//
|
||
// Each handler's "what it actually needs" was audited against the
|
||
// handler file; stay in sync with those reads. Missing entries for a
|
||
// slug mean no enforcement — for discoverability, aim to keep this
|
||
// complete.
|
||
|
||
// FieldSpec shape:
|
||
// required — dotted-path keys that must be non-empty on intake_data
|
||
// soft — dotted-path keys that warn when empty but do not block
|
||
// conditional — rule objects evaluated at validate-time. Each rule has:
|
||
// when — predicate (see evalPredicate below)
|
||
// require — extra keys that become required when `when` is true
|
||
// reject — string error when `when` is true (hard block)
|
||
// validators — names of cross-field validators to run (see runCrossFieldValidators)
|
||
type ConditionalRule = {
|
||
when: string;
|
||
require?: string[];
|
||
reject?: string;
|
||
};
|
||
type FieldSpec = {
|
||
required: string[];
|
||
soft: string[];
|
||
conditional?: ConditionalRule[];
|
||
validators?: string[];
|
||
};
|
||
|
||
const REQUIRED_FIELDS: Record<string, FieldSpec> = {
|
||
"fcc-compliance-checkup": { required: [], soft: ["frn"] },
|
||
"rmd-filing": { required: ["frn", "carrier_category"], soft: ["stir_shaken_status", "analytics_systems"] },
|
||
"cpni-certification": { required: ["frn"], soft: ["officer.name", "officer.title", "complaints_count"] },
|
||
// ── 499-A family: the full audit-driven spec ───────────────────────────
|
||
"fcc-499a": {
|
||
required: [
|
||
"line_105_primary",
|
||
"line_105_categories",
|
||
"entity_structure",
|
||
"officer_1_name", "officer_1_title",
|
||
"officer_1_street", "officer_1_city", "officer_1_state", "officer_1_zip",
|
||
"jurisdictions_served",
|
||
"total_revenue_cents",
|
||
"interstate_pct", "international_pct",
|
||
"filer_id_499",
|
||
"safe_harbor_election.method",
|
||
"lnpa_region_allocations",
|
||
"filing_type",
|
||
],
|
||
soft: ["ceo_name", "trade_names", "itsp_regulatory_fee_email"],
|
||
conditional: [
|
||
{ when: "first_telecom_service_pre_1999!=true",
|
||
require: ["first_telecom_service_year", "first_telecom_service_month"] },
|
||
{ when: "officer_count_claimed>=2",
|
||
require: ["officer_2_name","officer_2_title",
|
||
"officer_2_street","officer_2_city","officer_2_state","officer_2_zip"] },
|
||
{ when: "officer_count_claimed>=3",
|
||
require: ["officer_3_name","officer_3_title",
|
||
"officer_3_street","officer_3_city","officer_3_state","officer_3_zip"] },
|
||
{ when: "affiliated_filer_name.truthy",
|
||
require: ["affiliated_filer_ein"] },
|
||
{ when: "exempt_usf|exempt_trs|exempt_nanpa|exempt_lnp|exempt_itsp",
|
||
require: ["exemption_explanation"] },
|
||
{ when: "safe_harbor_election.method=traffic_study",
|
||
require: ["traffic_study_minio_path"] },
|
||
{ when: "line_105_primary=voip_non_interconnected AND safe_harbor_election.method=safe_harbor",
|
||
reject: "Non-interconnected VoIP has no safe harbor. Select traffic study or actual data." },
|
||
{ when: "revenue.line_303.any>0",
|
||
require: ["reseller_certifications"] },
|
||
],
|
||
validators: ["lnpa_sums_100", "de_minimis_calc", "trs_base_nonnegative"],
|
||
},
|
||
"fcc-499a-499q": {
|
||
required: [
|
||
"line_105_primary", "line_105_categories",
|
||
"total_revenue_cents", "interstate_pct", "international_pct",
|
||
"filer_id_499", "safe_harbor_election.method",
|
||
],
|
||
soft: ["ceo_name", "ceo_title"],
|
||
conditional: [
|
||
{ when: "line_105_primary=voip_non_interconnected AND safe_harbor_election.method=safe_harbor",
|
||
reject: "Non-interconnected VoIP has no safe harbor. Select traffic study or actual data." },
|
||
],
|
||
validators: ["de_minimis_calc"],
|
||
},
|
||
"fcc-499-initial": {
|
||
required: [
|
||
"line_105_primary", "entity_structure",
|
||
"officer_1_name", "officer_1_title",
|
||
"officer_1_street", "officer_1_city", "officer_1_state", "officer_1_zip",
|
||
"jurisdictions_served",
|
||
"contact_name", "contact_email",
|
||
],
|
||
soft: ["ceo_name", "ein"],
|
||
conditional: [
|
||
{ when: "officer_count_claimed>=2",
|
||
require: ["officer_2_name","officer_2_title"] },
|
||
],
|
||
},
|
||
"stir-shaken": { required: ["frn"], soft: ["target_stir_shaken_status", "sti_ca_vendor"] },
|
||
"bdc-filing": { required: ["frn"], soft: ["availability_rows", "voice_subscribers"] },
|
||
"bdc-broadband": { required: ["frn", "availability_rows"], soft: [] },
|
||
"bdc-voice": { required: ["frn", "voice_subscribers"], soft: [] },
|
||
"cores-frn-registration": { required: ["legal_name", "officer.name", "officer.email", "password_recovery_email"], soft: ["ein", "address.street", "address.city"] },
|
||
"calea-ssi": { required: ["calea_ssi.law_enforcement_contact.name", "calea_ssi.law_enforcement_contact.phone", "calea_ssi.law_enforcement_contact.email_24h"], soft: ["calea_ssi.cpni_protection_officer.name", "calea_ssi.network_infrastructure_summary"] },
|
||
"fcc-63-11-notification": { required: ["foreign_carrier.foreign_carrier_legal_name", "foreign_carrier.country", "foreign_carrier.ownership_pct", "foreign_carrier.affected_routes", "foreign_carrier.affiliation_date"], soft: ["foreign_carrier.notification_type"] },
|
||
"ocn-registration": { required: ["service_category", "operating_states"], soft: ["expedited"] },
|
||
"dc-agent": { required: [], soft: [] },
|
||
"cdr-analysis": { required: ["reporting_year"], soft: ["reporting_period"] },
|
||
"fcc-full-compliance": {
|
||
required: [
|
||
"frn", "filer_id_499",
|
||
"line_105_primary", "line_105_categories",
|
||
"total_revenue_cents", "interstate_pct", "international_pct",
|
||
"safe_harbor_election.method",
|
||
],
|
||
soft: ["ceo_name", "ceo_title"],
|
||
validators: ["lnpa_sums_100", "de_minimis_calc", "trs_base_nonnegative"],
|
||
},
|
||
"new-carrier-bundle": { required: ["legal_name", "officer.name", "officer.email", "password_recovery_email", "line_105_primary"], soft: ["ein", "contact_phone"] },
|
||
// Foreign qualification — the target_states array is the critical input.
|
||
"foreign-qualification-single": { required: ["legal_name", "home_state_code", "entity_type", "target_states"], soft: ["ein"] },
|
||
"foreign-qualification-multi": { required: ["legal_name", "home_state_code", "entity_type", "target_states"], soft: ["ein"] },
|
||
};
|
||
|
||
// Entity-level requirements (e.g. "must have an FRN on file before this
|
||
// service can run"). Checked against the linked telecom_entity.
|
||
const REQUIRES_ENTITY_FRN: ReadonlySet<string> = new Set([
|
||
"rmd-filing", "cpni-certification", "fcc-499a", "fcc-499a-499q",
|
||
"fcc-499-initial", "stir-shaken", "bdc-filing", "bdc-broadband",
|
||
"bdc-voice", "calea-ssi", "fcc-63-11-notification", "fcc-full-compliance",
|
||
]);
|
||
|
||
function digField(source: Record<string, unknown>, dottedKey: string): unknown {
|
||
return dottedKey.split(".").reduce<unknown>(
|
||
(acc, part) => {
|
||
if (acc && typeof acc === "object" && !Array.isArray(acc)) {
|
||
return (acc as Record<string, unknown>)[part];
|
||
}
|
||
return undefined;
|
||
},
|
||
source,
|
||
);
|
||
}
|
||
|
||
function isEmpty(value: unknown): boolean {
|
||
if (value === undefined || value === null) return true;
|
||
if (typeof value === "string") return value.trim() === "";
|
||
if (Array.isArray(value)) return value.length === 0;
|
||
if (typeof value === "object") return Object.keys(value as object).length === 0;
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Resolve the per-order service fee. Most orders use the catalog's
|
||
* price_cents directly. The exception: for 499-A filings from a
|
||
* de minimis, VoIP-interconnected-only carrier, the price drops to
|
||
* $299 — these filings are simpler (no USF contribution math, single
|
||
* category, safe harbor eligible) so our work is lower.
|
||
*
|
||
* Qualification criteria (all must be true):
|
||
* - slug is fcc-499a | fcc-499a-499q
|
||
* - intake_data.is_deminimis === true (self-declared or from Appendix A)
|
||
* - line_105_primary === 'voip_interconnected'
|
||
* - line_105_categories has exactly one entry (voip_interconnected)
|
||
*
|
||
* If any criteria fail, the catalog price applies.
|
||
*/
|
||
function resolveOrderFeeCents(
|
||
slug: string,
|
||
service: { price_cents: number },
|
||
intake: Record<string, unknown>,
|
||
waive_deminimis: boolean,
|
||
multi_year_filings: number[] | null,
|
||
): { fee_cents: number; pricing_note?: string; multi_year_discount_pct?: number } {
|
||
const DEMINIMIS_VOIP_ONLY_PRICE = 29900;
|
||
const MULTI_YEAR_DISCOUNT_PCT = 15;
|
||
|
||
// Compute the baseline per-year price for this order.
|
||
let perYearPrice = service.price_cents;
|
||
let baselineNote: string | undefined;
|
||
if (["fcc-499a", "fcc-499a-499q"].includes(slug)) {
|
||
if (waive_deminimis) {
|
||
// Waive → full price, overrides any de minimis discount
|
||
perYearPrice = service.price_cents;
|
||
} else {
|
||
const isDemin = intake.is_deminimis === true;
|
||
const primary = (intake.line_105_primary as string) || "";
|
||
const cats = Array.isArray(intake.line_105_categories)
|
||
? (intake.line_105_categories as Array<{ id: string }>)
|
||
: [];
|
||
const voipOnly =
|
||
primary === "voip_interconnected" &&
|
||
cats.length === 1 &&
|
||
cats[0]?.id === "voip_interconnected";
|
||
if (isDemin && voipOnly) {
|
||
perYearPrice = DEMINIMIS_VOIP_ONLY_PRICE;
|
||
baselineNote =
|
||
"De minimis + VoIP-interconnected-only pricing: $299/year instead of " +
|
||
"$499 (simpler filing — single category, no USF contribution " +
|
||
"calculation, safe harbor eligible).";
|
||
}
|
||
}
|
||
}
|
||
|
||
// If multi_year_filings has 2+ years, charge (N × perYearPrice × 0.85).
|
||
const yearCount = Array.isArray(multi_year_filings) ? multi_year_filings.length : 0;
|
||
if (yearCount >= 2) {
|
||
const gross = perYearPrice * yearCount;
|
||
const discount = Math.round(gross * (MULTI_YEAR_DISCOUNT_PCT / 100));
|
||
const fee = gross - discount;
|
||
const note =
|
||
(baselineNote ? baselineNote + " " : "") +
|
||
`Multi-year discount: ${yearCount} years at $${(perYearPrice/100).toFixed(2)}/year ` +
|
||
`less ${MULTI_YEAR_DISCOUNT_PCT}% = $${(fee/100).toFixed(2)} total.`;
|
||
return { fee_cents: fee, pricing_note: note, multi_year_discount_pct: MULTI_YEAR_DISCOUNT_PCT };
|
||
}
|
||
|
||
// OCN registration: if client has existing tandem agreement, charge only $650
|
||
// (OCN application fee) instead of $2,650 (OCN + sponsoring CLEC).
|
||
if (slug === "ocn-registration" && intake.has_existing_tandem === true) {
|
||
return {
|
||
fee_cents: 65000,
|
||
pricing_note: "Client has existing tandem/interconnection agreement — OCN application only ($650).",
|
||
};
|
||
}
|
||
|
||
return { fee_cents: perYearPrice, pricing_note: baselineNote };
|
||
}
|
||
|
||
/**
|
||
* Evaluate a conditional `when` predicate against the merged data source
|
||
* (intake_data ∪ entity columns).
|
||
*
|
||
* Syntax:
|
||
* key → truthy check on the key
|
||
* key.truthy → same as above (explicit)
|
||
* key=value → string equals (case-insensitive)
|
||
* key!=value → string not-equals
|
||
* key>=N → numeric >=
|
||
* key>N → numeric >
|
||
* key.any>N → numeric > for any element of an array of {value} OR for revenue_lines.* keys
|
||
* A AND B — both predicates true
|
||
* A|B|C → any of (OR, single pipe to avoid shell quoting issues)
|
||
*/
|
||
function evalPredicate(pred: string, src: Record<string, unknown>): boolean {
|
||
const s = pred.trim();
|
||
|
||
// AND combinator
|
||
if (s.includes(" AND ")) {
|
||
return s.split(" AND ").every((p) => evalPredicate(p.trim(), src));
|
||
}
|
||
// OR combinator (pipe)
|
||
if (s.includes("|") && !s.includes("=") && !s.includes(">")) {
|
||
return s.split("|").some((p) => evalPredicate(p.trim(), src));
|
||
}
|
||
|
||
// key.any>N — numeric "any element exceeds" against a dict-of-numbers
|
||
// keyspace OR a scalar number. `revenue.line_303.any>0` should fire
|
||
// whether `line_303` is a number (cents) OR a dict like
|
||
// {intra, inter, intl}.
|
||
const anyMatch = s.match(/^(.+)\.any([>])(-?\d+(?:\.\d+)?)$/);
|
||
if (anyMatch) {
|
||
const [, key, op, numStr] = anyMatch;
|
||
const num = Number(numStr);
|
||
const bag = digField(src, key);
|
||
if (bag === null || bag === undefined) return false;
|
||
// Scalar number: compare directly
|
||
if (typeof bag === "number") {
|
||
return op === ">" ? bag > num : false;
|
||
}
|
||
// String-numeric: parse and compare
|
||
if (typeof bag === "string") {
|
||
const n = Number(bag);
|
||
return !Number.isNaN(n) && (op === ">" ? n > num : false);
|
||
}
|
||
// Array / dict: any element exceeds
|
||
if (typeof bag === "object") {
|
||
return Object.values(bag as Record<string, unknown>).some((v) => {
|
||
const n = Number(v);
|
||
return !Number.isNaN(n) && (op === ">" ? n > num : false);
|
||
});
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// key>=N, key>N
|
||
const numMatch = s.match(/^(.+?)(>=|>)(-?\d+(?:\.\d+)?)$/);
|
||
if (numMatch) {
|
||
const [, key, op, numStr] = numMatch;
|
||
const v = Number(digField(src, key.trim()));
|
||
const num = Number(numStr);
|
||
if (Number.isNaN(v)) return false;
|
||
return op === ">=" ? v >= num : v > num;
|
||
}
|
||
|
||
// key=value or key!=value
|
||
const eqMatch = s.match(/^(.+?)(!=|=)(.+)$/);
|
||
if (eqMatch) {
|
||
const [, key, op, val] = eqMatch;
|
||
const v = digField(src, key.trim());
|
||
const sv = v == null ? "" : String(v).trim().toLowerCase();
|
||
const tv = val.trim().toLowerCase();
|
||
return op === "=" ? sv === tv : sv !== tv;
|
||
}
|
||
|
||
// bare key: truthy check. isEmpty() doesn't recognize boolean false as
|
||
// empty (because for many of our fields a literal `false` string is a
|
||
// meaningful value). For THIS predicate we want classic JS truthiness:
|
||
// false / 0 / "" / null / undefined / [] / {} → not truthy.
|
||
const key = s.replace(/\.truthy$/, "");
|
||
const v = digField(src, key);
|
||
if (v === undefined || v === null) return false;
|
||
if (typeof v === "boolean") return v;
|
||
if (typeof v === "number") return v !== 0;
|
||
if (typeof v === "string") return v.trim() !== "";
|
||
if (Array.isArray(v)) return v.length > 0;
|
||
if (typeof v === "object") return Object.keys(v as object).length > 0;
|
||
return Boolean(v);
|
||
}
|
||
|
||
/**
|
||
* Run cross-field validators (LNPA sums, de minimis calc, TRS base sign).
|
||
* Returns { errors, warnings, calculations }. Errors block checkout.
|
||
*/
|
||
async function runCrossFieldValidators(
|
||
validators: string[],
|
||
src: Record<string, unknown>,
|
||
entity: Record<string, unknown>,
|
||
orderId: number,
|
||
): Promise<{
|
||
errors: string[];
|
||
warnings: string[];
|
||
calculations: Record<string, unknown>;
|
||
}> {
|
||
const errors: string[] = [];
|
||
const warnings: string[] = [];
|
||
const calculations: Record<string, unknown> = {};
|
||
|
||
for (const v of validators) {
|
||
if (v === "lnpa_sums_100") {
|
||
// Lines 503-510 columns (a) Block 3 and (b) Block 4 must each sum
|
||
// to exactly 100.00% (or 0 if no revenue in that block).
|
||
const rows = await pool.query(
|
||
`SELECT COALESCE(SUM(block_3_pct), 0) AS b3,
|
||
COALESCE(SUM(block_4_pct), 0) AS b4,
|
||
COUNT(*) AS n
|
||
FROM lnpa_region_allocations
|
||
WHERE telecom_entity_id = $1
|
||
AND reporting_year = $2`,
|
||
[entity.id, src.reporting_year ?? new Date().getUTCFullYear() - 1],
|
||
);
|
||
const r = rows.rows[0];
|
||
const b3 = Number(r.b3);
|
||
const b4 = Number(r.b4);
|
||
const n = Number(r.n);
|
||
calculations.lnpa_sums = { block_3_pct: b3, block_4_pct: b4, row_count: n };
|
||
if (n === 0) {
|
||
errors.push("LNPA region allocation (Lines 503-510) not set — required before filing.");
|
||
} else {
|
||
if (Math.abs(b3 - 100) > 0.01 && b3 !== 0) {
|
||
errors.push(`LNPA Block 3 (resale) column sums to ${b3.toFixed(2)}%, must be 100.00% or 0.`);
|
||
}
|
||
if (Math.abs(b4 - 100) > 0.01 && b4 !== 0) {
|
||
errors.push(`LNPA Block 4 (end-user) column sums to ${b4.toFixed(2)}%, must be 100.00% or 0.`);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (v === "de_minimis_calc") {
|
||
try {
|
||
const { calculateDeMinimis } = await import("../lib/fcc_499_utils.js");
|
||
const formYear = Number(src.form_year ?? new Date().getUTCFullYear());
|
||
// Pull affiliates from telecom_entities linked by affiliated_filer_ein
|
||
const affEin = (entity.affiliated_filer_ein as string) || null;
|
||
const affRows = affEin
|
||
? await pool.query(
|
||
`SELECT total_revenue_cents, interstate_pct, international_pct
|
||
FROM telecom_entities
|
||
WHERE affiliated_filer_ein = $1 AND id <> $2`,
|
||
[affEin, entity.id],
|
||
)
|
||
: { rows: [] };
|
||
const worksheet = await calculateDeMinimis({
|
||
form_year: formYear,
|
||
filer_total_revenue_cents: Number(entity.total_revenue_cents) || 0,
|
||
filer_interstate_pct: Number(entity.interstate_pct) || 0,
|
||
filer_international_pct: Number(entity.international_pct) || 0,
|
||
affiliates: affRows.rows.map((a) => ({
|
||
total_revenue_cents: Number(a.total_revenue_cents) || 0,
|
||
interstate_pct: Number(a.interstate_pct) || 0,
|
||
international_pct: Number(a.international_pct) || 0,
|
||
})),
|
||
});
|
||
calculations.de_minimis = worksheet;
|
||
// Persist for the handler + admin review
|
||
await pool.query(
|
||
`UPDATE compliance_orders
|
||
SET deminimis_worksheet_json = $2::jsonb,
|
||
deminimis_estimated_contrib_cents = $3,
|
||
deminimis_result_is_exempt = $4
|
||
WHERE id = $1`,
|
||
[
|
||
orderId,
|
||
JSON.stringify(worksheet),
|
||
worksheet.line_11_estimated_contrib_cents,
|
||
worksheet.is_de_minimis,
|
||
],
|
||
);
|
||
// Warn if the filer's self-declared `is_deminimis` disagrees
|
||
if (
|
||
typeof entity.is_deminimis === "boolean" &&
|
||
entity.is_deminimis !== worksheet.is_de_minimis
|
||
) {
|
||
warnings.push(
|
||
`De minimis mismatch: you claimed ${
|
||
entity.is_deminimis ? "exempt" : "not exempt"
|
||
} but the Appendix A calculation shows ${
|
||
worksheet.is_de_minimis ? "exempt" : "not exempt"
|
||
} (estimated $${(worksheet.line_11_estimated_contrib_cents / 100).toFixed(2)}). Please confirm.`,
|
||
);
|
||
}
|
||
} catch (exc) {
|
||
warnings.push(`De minimis calculation failed: ${(exc as Error).message}`);
|
||
}
|
||
}
|
||
|
||
if (v === "trs_base_nonnegative") {
|
||
const { computeTrsContributionBase } = await import("../lib/fcc_499_utils.js");
|
||
const revLines = (src.revenue as Record<string, unknown>) || {};
|
||
// Flatten revenue.{line_xxx} into {line_xxx: n}
|
||
const flat: Record<string, number> = {};
|
||
for (const [k, val] of Object.entries(revLines)) {
|
||
if (typeof val === "number") flat[k] = val;
|
||
else if (val && typeof val === "object" && "cents" in (val as object)) {
|
||
flat[k] = Number((val as Record<string, unknown>).cents) || 0;
|
||
}
|
||
}
|
||
const trs = computeTrsContributionBase(flat);
|
||
calculations.trs_base = trs;
|
||
if (trs.line_512 < 0) {
|
||
errors.push(
|
||
`TRS contribution base (Line 512) computes to ${trs.line_512} cents — must be >= 0. ` +
|
||
`Check Line 511 (non-contributing resellers) vs sum of Lines 403-418.4.`,
|
||
);
|
||
}
|
||
if (trs.line_513 > trs.line_512) {
|
||
warnings.push(
|
||
`TRS uncollectible (Line 513) exceeds TRS base (Line 512). ` +
|
||
`Allowed in exceptional circumstances per 2026 instructions but flagged for review.`,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
return { errors, warnings, calculations };
|
||
}
|
||
|
||
function generateOrderNumber(): string {
|
||
const hex = randomBytes(4).toString("hex").toUpperCase();
|
||
return `CO-${hex}`;
|
||
}
|
||
|
||
/**
|
||
* POST /api/v1/compliance-orders
|
||
*/
|
||
router.post("/api/v1/compliance-orders", async (req, res) => {
|
||
const {
|
||
service_slug,
|
||
customer_email,
|
||
customer_name,
|
||
customer_phone,
|
||
telecom_entity_id,
|
||
discount_code,
|
||
notes,
|
||
intake_data,
|
||
// Past-due + revision fields (migration 058)
|
||
filing_mode, // 'current' | 'past_due' | 'revised'
|
||
form_year_override, // 2015-2035; required when filing_mode != 'current'
|
||
revises_order_number, // prior order # when filing_mode = 'revised'
|
||
revised_reason, // 'registration' | 'revenue' | 'both'
|
||
// Waive de minimis exemption (migration 059) — filer qualifies as
|
||
// de minimis but elects to file as a regular contributor so they
|
||
// can get vendor-side USF waived on wholesale trunking.
|
||
waive_deminimis_exemption,
|
||
waive_deminimis_reason,
|
||
// Multi-year catch-up (migration 060) — array of reporting years.
|
||
// 2+ years gets the 15% multi-year discount.
|
||
multi_year_filings,
|
||
} = req.body ?? {};
|
||
|
||
if (!service_slug || !customer_email || !customer_name) {
|
||
res.status(400).json({
|
||
error: "service_slug, customer_email, and customer_name are required.",
|
||
});
|
||
return;
|
||
}
|
||
|
||
const service = COMPLIANCE_SERVICES[service_slug];
|
||
if (!service) {
|
||
res.status(400).json({
|
||
error: `Unknown service_slug: ${service_slug}. Valid options: ${Object.keys(COMPLIANCE_SERVICES).join(", ")}`,
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Validate entity exists if provided
|
||
if (telecom_entity_id) {
|
||
const entity = await pool.query(
|
||
"SELECT id FROM telecom_entities WHERE id = $1",
|
||
[telecom_entity_id],
|
||
);
|
||
if (entity.rows.length === 0) {
|
||
res.status(400).json({ error: `Telecom entity ${telecom_entity_id} not found.` });
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Validate multi_year_filings (if provided)
|
||
const myf: number[] | null = Array.isArray(multi_year_filings)
|
||
? multi_year_filings.filter((y: unknown) => Number.isFinite(Number(y)))
|
||
.map((y: unknown) => Number(y))
|
||
: null;
|
||
if (myf && myf.length > 0) {
|
||
if (!["fcc-499a", "fcc-499a-499q"].includes(service_slug)) {
|
||
res.status(400).json({
|
||
error: "multi_year_filings only supported for fcc-499a / fcc-499a-499q.",
|
||
});
|
||
return;
|
||
}
|
||
const unique = new Set(myf);
|
||
if (unique.size !== myf.length) {
|
||
res.status(400).json({ error: "multi_year_filings must not contain duplicates." });
|
||
return;
|
||
}
|
||
if (myf.length < 2) {
|
||
res.status(400).json({
|
||
error: "multi_year_filings must have 2 or more years (single-year orders should use form_year_override instead).",
|
||
});
|
||
return;
|
||
}
|
||
for (const y of myf) {
|
||
if (y < 2015 || y > 2035) {
|
||
res.status(400).json({ error: `multi_year_filings year out of range: ${y}` });
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Resolve per-order fee (honors de minimis + VoIP-only discount for 499-A
|
||
// unless waived, plus 15% multi-year discount for 2+ reporting years).
|
||
const { fee_cents: resolved_fee_cents, pricing_note, multi_year_discount_pct } =
|
||
resolveOrderFeeCents(
|
||
service_slug, service, (intake_data || {}) as Record<string, unknown>,
|
||
waive_deminimis_exemption === true,
|
||
myf,
|
||
);
|
||
|
||
// Apply discount if provided
|
||
let discount_cents = 0;
|
||
if (discount_code) {
|
||
try {
|
||
const disc = await pool.query(
|
||
`SELECT discount_pct, discount_flat_cents, active
|
||
FROM discount_codes
|
||
WHERE code = $1 AND active = true`,
|
||
[discount_code.toUpperCase().trim()],
|
||
);
|
||
if (disc.rows.length > 0) {
|
||
const d = disc.rows[0] as Record<string, unknown>;
|
||
if ((d.discount_pct as number) > 0) {
|
||
discount_cents = Math.round(
|
||
(resolved_fee_cents * (d.discount_pct as number)) / 100,
|
||
);
|
||
} else if ((d.discount_flat_cents as number) > 0) {
|
||
discount_cents = d.discount_flat_cents as number;
|
||
}
|
||
}
|
||
} catch {
|
||
// discount_codes table may not exist — non-fatal
|
||
}
|
||
}
|
||
|
||
// Validate filing mode fields
|
||
const mode = filing_mode || "current";
|
||
if (!["current", "past_due", "revised"].includes(mode)) {
|
||
res.status(400).json({ error: "filing_mode must be current | past_due | revised" });
|
||
return;
|
||
}
|
||
if (mode === "past_due" && !form_year_override) {
|
||
res.status(400).json({ error: "form_year_override required for past_due filings" });
|
||
return;
|
||
}
|
||
if (mode === "revised") {
|
||
if (!revises_order_number) {
|
||
res.status(400).json({ error: "revises_order_number required for revised filings" });
|
||
return;
|
||
}
|
||
if (!revised_reason || !["registration", "revenue", "both"].includes(revised_reason)) {
|
||
res.status(400).json({ error: "revised_reason must be registration | revenue | both" });
|
||
return;
|
||
}
|
||
// Verify the prior order exists and belongs to the same entity
|
||
const prior = await pool.query(
|
||
`SELECT service_slug, telecom_entity_id, intake_data, form_year_override
|
||
FROM compliance_orders WHERE order_number = $1`,
|
||
[revises_order_number],
|
||
);
|
||
if (prior.rows.length === 0) {
|
||
res.status(400).json({ error: `Prior order ${revises_order_number} not found.` });
|
||
return;
|
||
}
|
||
const priorRow = prior.rows[0];
|
||
if (telecom_entity_id && priorRow.telecom_entity_id !== telecom_entity_id) {
|
||
res.status(400).json({
|
||
error: "Revised filing must use the same telecom_entity as the prior order.",
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
|
||
const order_number = generateOrderNumber();
|
||
|
||
try {
|
||
const result = await pool.query(
|
||
`INSERT INTO compliance_orders (
|
||
order_number, service_slug, service_name, service_fee_cents,
|
||
gov_fee_cents, gov_fee_label,
|
||
telecom_entity_id, customer_email, customer_name, customer_phone,
|
||
discount_code, discount_cents, notes, intake_data,
|
||
filing_mode, form_year_override, revises_order_number, revised_reason,
|
||
waive_deminimis_exemption, waive_deminimis_reason,
|
||
multi_year_filings, multi_year_discount_pct
|
||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22)
|
||
RETURNING *`,
|
||
[
|
||
order_number,
|
||
service_slug,
|
||
service.name,
|
||
resolved_fee_cents,
|
||
service.gov_fee_cents || 0,
|
||
service.gov_fee_label || null,
|
||
telecom_entity_id || null,
|
||
customer_email.toLowerCase().trim(),
|
||
customer_name.trim(),
|
||
customer_phone || null,
|
||
discount_code || null,
|
||
discount_cents,
|
||
notes || null,
|
||
intake_data ? JSON.stringify(intake_data) : "{}",
|
||
mode,
|
||
form_year_override || null,
|
||
revises_order_number || null,
|
||
revised_reason || null,
|
||
waive_deminimis_exemption === true,
|
||
waive_deminimis_reason || null,
|
||
myf && myf.length > 0 ? myf : null,
|
||
multi_year_discount_pct || null,
|
||
],
|
||
);
|
||
|
||
console.log(
|
||
`[compliance-orders] Created ${order_number}: ${service_slug} for ${customer_email}` +
|
||
(pricing_note ? ` [${pricing_note}]` : ""),
|
||
);
|
||
|
||
res.status(201).json({ ...result.rows[0], pricing_note: pricing_note || null });
|
||
} catch (err) {
|
||
console.error("[compliance-orders] Create error:", err);
|
||
res.status(500).json({ error: "Could not create compliance order." });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* POST /api/v1/compliance-orders/batch
|
||
* Create multiple compliance orders linked by a shared batch_id.
|
||
* Applies 15% bundle discount when 2+ priced services are selected.
|
||
*/
|
||
router.post("/api/v1/compliance-orders/batch", async (req, res) => {
|
||
const {
|
||
services: rawServices,
|
||
customer_email,
|
||
customer_name,
|
||
customer_phone,
|
||
discount_code,
|
||
intake_data,
|
||
} = req.body ?? {};
|
||
|
||
if (!rawServices || !Array.isArray(rawServices) || rawServices.length === 0) {
|
||
res.status(400).json({ error: "services array is required." });
|
||
return;
|
||
}
|
||
if (!customer_email || !customer_name) {
|
||
res.status(400).json({ error: "customer_email and customer_name are required." });
|
||
return;
|
||
}
|
||
|
||
// Deduplicate and validate service slugs
|
||
let services = [...new Set(rawServices as string[])];
|
||
|
||
// If both 499a and 499a-499q selected, drop the standalone 499a
|
||
if (services.includes("fcc-499a") && services.includes("fcc-499a-499q")) {
|
||
services = services.filter(s => s !== "fcc-499a");
|
||
}
|
||
|
||
const invalid = services.filter(s => !COMPLIANCE_SERVICES[s]);
|
||
if (invalid.length > 0) {
|
||
res.status(400).json({
|
||
error: `Unknown service slugs: ${invalid.join(", ")}. Valid: ${Object.keys(COMPLIANCE_SERVICES).join(", ")}`,
|
||
});
|
||
return;
|
||
}
|
||
|
||
// At least one paid service required
|
||
const hasPaidService = services.some(s => COMPLIANCE_SERVICES[s].price_cents > 0);
|
||
if (!hasPaidService) {
|
||
res.status(400).json({ error: "At least one paid service is required." });
|
||
return;
|
||
}
|
||
|
||
// Split services into discountable vs non-discountable (e.g., RA services)
|
||
const discountableServices = services.filter(s => COMPLIANCE_SERVICES[s].discountable);
|
||
const nonDiscountableServices = services.filter(s => !COMPLIANCE_SERVICES[s].discountable);
|
||
|
||
const discountableTotal = discountableServices.reduce((sum, s) => sum + COMPLIANCE_SERVICES[s].price_cents, 0);
|
||
const nonDiscountableTotal = nonDiscountableServices.reduce((sum, s) => sum + COMPLIANCE_SERVICES[s].price_cents, 0);
|
||
// Government filing fees are passthrough — never discounted
|
||
const govFeeTotal = services.reduce((sum, s) => sum + (COMPLIANCE_SERVICES[s].gov_fee_cents || 0), 0);
|
||
const subtotal = discountableTotal + nonDiscountableTotal + govFeeTotal;
|
||
|
||
// Bundle discount applies only to discountable services
|
||
const bundleDiscountPct = discountableServices.filter(s => COMPLIANCE_SERVICES[s].price_cents > 0).length >= 2 ? 15 : 0;
|
||
const bundleDiscountCents = Math.round(discountableTotal * bundleDiscountPct / 100);
|
||
|
||
// Referral/promo code discount also applies only to discountable services
|
||
let promoDiscountCents = 0;
|
||
if (discount_code) {
|
||
try {
|
||
const disc = await pool.query(
|
||
`SELECT discount_pct, discount_flat_cents FROM discount_codes WHERE code = $1 AND active = true`,
|
||
[discount_code.toUpperCase().trim()],
|
||
);
|
||
if (disc.rows.length > 0) {
|
||
const d = disc.rows[0] as Record<string, unknown>;
|
||
const discountableAfterBundle = discountableTotal - bundleDiscountCents;
|
||
if ((d.discount_pct as number) > 0) {
|
||
promoDiscountCents = Math.round(discountableAfterBundle * (d.discount_pct as number) / 100);
|
||
} else if ((d.discount_flat_cents as number) > 0) {
|
||
promoDiscountCents = Math.min(d.discount_flat_cents as number, discountableAfterBundle);
|
||
}
|
||
}
|
||
} catch { /* discount_codes table may not exist */ }
|
||
}
|
||
|
||
const totalDiscountCents = bundleDiscountCents + promoDiscountCents;
|
||
const totalCents = subtotal - totalDiscountCents;
|
||
const batchId = `CB-${randomBytes(4).toString("hex").toUpperCase()}`;
|
||
|
||
try {
|
||
const orders: Record<string, unknown>[] = [];
|
||
let discountDistributed = 0;
|
||
const discountableCount = discountableServices.length;
|
||
let discountableIdx = 0;
|
||
|
||
for (const slug of services) {
|
||
const svc = COMPLIANCE_SERVICES[slug];
|
||
// Distribute discount proportionally — only across discountable services
|
||
// Last discountable service absorbs rounding remainder
|
||
let svcDiscount = 0;
|
||
if (svc.discountable && totalDiscountCents > 0) {
|
||
discountableIdx++;
|
||
if (discountableIdx === discountableCount) {
|
||
svcDiscount = totalDiscountCents - discountDistributed;
|
||
} else {
|
||
svcDiscount = Math.round(totalDiscountCents * svc.price_cents / (discountableTotal || 1));
|
||
}
|
||
discountDistributed += svcDiscount;
|
||
}
|
||
const orderNumber = generateOrderNumber();
|
||
|
||
const result = await pool.query(
|
||
`INSERT INTO compliance_orders (
|
||
order_number, batch_id, service_slug, service_name, service_fee_cents,
|
||
gov_fee_cents, gov_fee_label,
|
||
customer_email, customer_name, customer_phone,
|
||
discount_code, discount_cents, intake_data
|
||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
|
||
RETURNING *`,
|
||
[
|
||
orderNumber,
|
||
batchId,
|
||
slug,
|
||
svc.name,
|
||
svc.price_cents,
|
||
svc.gov_fee_cents || 0,
|
||
svc.gov_fee_label || null,
|
||
customer_email.toLowerCase().trim(),
|
||
customer_name.trim(),
|
||
customer_phone || null,
|
||
discount_code || null,
|
||
svcDiscount,
|
||
intake_data ? JSON.stringify(intake_data) : "{}",
|
||
],
|
||
);
|
||
orders.push(result.rows[0]);
|
||
}
|
||
|
||
console.log(
|
||
`[compliance-orders] Batch ${batchId}: ${services.length} orders for ${customer_email} — $${(totalCents / 100).toFixed(2)}`,
|
||
);
|
||
|
||
res.status(201).json({
|
||
batch_id: batchId,
|
||
orders,
|
||
subtotal_cents: subtotal,
|
||
gov_fee_cents: govFeeTotal,
|
||
bundle_discount_pct: bundleDiscountPct,
|
||
bundle_discount_cents: bundleDiscountCents,
|
||
promo_discount_cents: promoDiscountCents,
|
||
total_discount_cents: totalDiscountCents,
|
||
total_cents: totalCents,
|
||
});
|
||
} catch (err) {
|
||
console.error("[compliance-orders] Batch create error:", err);
|
||
res.status(500).json({ error: "Could not create compliance orders." });
|
||
}
|
||
});
|
||
|
||
|
||
/**
|
||
* GET /api/v1/compliance-orders/:id
|
||
* Supports both single order lookup (CO-XXXXXXXX) and batch lookup (CB-XXXXXXXX).
|
||
*/
|
||
router.get("/api/v1/compliance-orders/:id", async (req, res) => {
|
||
const id = req.params.id;
|
||
|
||
try {
|
||
// Check if this is a batch ID (CB-) or single order (CO-)
|
||
if (id.startsWith("CB-")) {
|
||
const result = await pool.query(
|
||
`SELECT co.*,
|
||
te.legal_name as entity_name,
|
||
te.frn as entity_frn
|
||
FROM compliance_orders co
|
||
LEFT JOIN telecom_entities te ON te.id = co.telecom_entity_id
|
||
WHERE co.batch_id = $1
|
||
ORDER BY co.created_at`,
|
||
[id],
|
||
);
|
||
|
||
if (result.rows.length === 0) {
|
||
res.status(404).json({ error: "Batch not found." });
|
||
return;
|
||
}
|
||
|
||
const orders = result.rows;
|
||
const allPaid = orders.every((o: any) => o.payment_status === "paid");
|
||
res.json({
|
||
batch_id: id,
|
||
payment_status: allPaid ? "paid" : (orders[0] as any).payment_status,
|
||
orders,
|
||
order_number: id,
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Single order lookup
|
||
const result = await pool.query(
|
||
`SELECT co.*,
|
||
te.legal_name as entity_name,
|
||
te.frn as entity_frn
|
||
FROM compliance_orders co
|
||
LEFT JOIN telecom_entities te ON te.id = co.telecom_entity_id
|
||
WHERE co.order_number = $1`,
|
||
[id],
|
||
);
|
||
|
||
if (result.rows.length === 0) {
|
||
res.status(404).json({ error: "Order not found." });
|
||
return;
|
||
}
|
||
|
||
res.json(result.rows[0]);
|
||
} catch (err) {
|
||
console.error("[compliance-orders] Get error:", err);
|
||
res.status(500).json({ error: "Could not fetch order." });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /api/v1/compliance-orders/:order_number/recommendations
|
||
*
|
||
* Returns the list of recommended remediation services produced by the
|
||
* FCC Compliance Checkup handler (see migration 047 — `recommended_slugs`
|
||
* column, populated by `_persist_recommendations` in
|
||
* `scripts/workers/services/fcc_compliance_checkup.py`).
|
||
*
|
||
* Response:
|
||
* {
|
||
* slugs: ["rmd-filing", "cpni-certification"],
|
||
* bundle_eligible: true,
|
||
* bundle_discount_pct: 15,
|
||
* individual_urls: [
|
||
* { slug, name, price_cents, checkout_url },
|
||
* ...
|
||
* ],
|
||
* bundle_url: "https://site/checkout/compliance-bundle?..."
|
||
* }
|
||
*
|
||
* Consumed by:
|
||
* - delivery_worker.py — builds the upsell block in the email
|
||
* - the compliance checkup PDF appendix (bundle_url is embedded there too)
|
||
* - the customer portal's /orders page (when it lists remediation CTAs)
|
||
*/
|
||
router.get(
|
||
"/api/v1/compliance-orders/:order_number/recommendations",
|
||
async (req, res) => {
|
||
const orderNumber = req.params.order_number;
|
||
try {
|
||
const orderResult = await pool.query(
|
||
`SELECT co.order_number, co.customer_email, co.customer_name,
|
||
co.telecom_entity_id, co.recommended_slugs
|
||
FROM compliance_orders co
|
||
WHERE co.order_number = $1`,
|
||
[orderNumber],
|
||
);
|
||
if (orderResult.rows.length === 0) {
|
||
res.status(404).json({ error: "Order not found." });
|
||
return;
|
||
}
|
||
|
||
const order = orderResult.rows[0] as {
|
||
order_number: string;
|
||
customer_email: string;
|
||
customer_name: string;
|
||
telecom_entity_id: number | null;
|
||
recommended_slugs: string[] | null;
|
||
};
|
||
|
||
const slugs = (order.recommended_slugs ?? []).filter(
|
||
(s) => s in COMPLIANCE_SERVICES,
|
||
);
|
||
|
||
// Common querystring: prefill the order form with the customer's
|
||
// email/name/entity so the checkout is one click.
|
||
const prefill = new URLSearchParams({
|
||
email: order.customer_email,
|
||
name: order.customer_name,
|
||
});
|
||
if (order.telecom_entity_id) {
|
||
prefill.set("entity", String(order.telecom_entity_id));
|
||
}
|
||
prefill.set("source", `checkup:${order.order_number}`);
|
||
|
||
const siteBase =
|
||
process.env.SITE_URL ||
|
||
(process.env.DOMAIN ? `https://${process.env.DOMAIN}` : "https://performancewest.net");
|
||
|
||
const individual_urls = slugs
|
||
.filter((s) => s !== "fcc-full-compliance")
|
||
.map((slug) => {
|
||
const svc = COMPLIANCE_SERVICES[slug];
|
||
return {
|
||
slug,
|
||
name: svc.name,
|
||
price_cents: svc.price_cents,
|
||
gov_fee_cents: svc.gov_fee_cents || 0,
|
||
gov_fee_label: svc.gov_fee_label || null,
|
||
checkout_url: `${siteBase}/order/${slug}?${prefill.toString()}`,
|
||
};
|
||
});
|
||
|
||
// Bundle URL hits the batch create endpoint, which already applies
|
||
// the 15% discount when 2+ discountable services are selected
|
||
// (compliance-orders.ts:237). We pass the slugs in querystring so
|
||
// the frontend can POST them.
|
||
const bundleSlugs = slugs.filter((s) => s !== "fcc-full-compliance");
|
||
const bundleParams = new URLSearchParams(prefill);
|
||
for (const s of bundleSlugs) {
|
||
bundleParams.append("service", s);
|
||
}
|
||
const bundle_eligible = bundleSlugs.length >= 2;
|
||
const bundle_url = bundle_eligible
|
||
? `${siteBase}/order/compliance-bundle?${bundleParams.toString()}`
|
||
: null;
|
||
|
||
res.json({
|
||
order_number: orderNumber,
|
||
slugs,
|
||
bundle_eligible,
|
||
bundle_discount_pct: bundle_eligible ? 15 : 0,
|
||
individual_urls,
|
||
bundle_url,
|
||
});
|
||
} catch (err) {
|
||
console.error("[compliance-orders] Recommendations error:", err);
|
||
res.status(500).json({ error: "Could not fetch recommendations." });
|
||
}
|
||
},
|
||
);
|
||
|
||
/**
|
||
* POST /api/v1/compliance-orders/:order_number/approve-and-file
|
||
*
|
||
* Admin-review approval endpoint. When the global auto-filing toggle in
|
||
* the ERPNext `Compliance Settings` DocType is OFF (the default), every
|
||
* FCC/USAC filing handler stages its packet for review instead of
|
||
* submitting. The admin email + ToDo each include a link to this
|
||
* endpoint; hitting it flips `custom_auto_filing_override = 1` on the
|
||
* linked Sales Order and re-dispatches the worker job so the handler
|
||
* re-runs and (this time) actually submits to the FCC.
|
||
*
|
||
* Idempotent: safe to call multiple times; the handler's own idempotency
|
||
* check prevents double-filing.
|
||
*
|
||
* Auth: requires a valid admin bearer token. During local development a
|
||
* fallback `APPROVE_FILE_TOKEN` env var is accepted.
|
||
*/
|
||
router.post(
|
||
"/api/v1/compliance-orders/:order_number/approve-and-file",
|
||
async (req, res) => {
|
||
const orderNumber = req.params.order_number;
|
||
|
||
// Token gate — cheap header check; the ERPNext side is authoritative.
|
||
const headerToken = (req.headers["authorization"] || "")
|
||
.toString()
|
||
.replace(/^Bearer\s+/i, "")
|
||
.trim();
|
||
const expected = process.env.APPROVE_FILE_TOKEN || "";
|
||
if (!expected || headerToken !== expected) {
|
||
res.status(401).json({ error: "Unauthorized" });
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const { rows } = await pool.query(
|
||
`SELECT order_number, service_slug, erpnext_sales_order,
|
||
telecom_entity_id, payment_status
|
||
FROM compliance_orders
|
||
WHERE order_number = $1`,
|
||
[orderNumber],
|
||
);
|
||
if (rows.length === 0) {
|
||
res.status(404).json({ error: "Order not found." });
|
||
return;
|
||
}
|
||
const order = rows[0] as {
|
||
order_number: string;
|
||
service_slug: string;
|
||
erpnext_sales_order: string | null;
|
||
telecom_entity_id: number | null;
|
||
payment_status: string;
|
||
};
|
||
if (order.payment_status !== "paid") {
|
||
res.status(409).json({
|
||
error: `Cannot approve-and-file: payment_status is ${order.payment_status}.`,
|
||
});
|
||
return;
|
||
}
|
||
if (!order.erpnext_sales_order) {
|
||
res.status(409).json({
|
||
error: "Order has no linked ERPNext Sales Order.",
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 1. Flip the per-order override on the Sales Order.
|
||
const { callMethod, updateResource } = await import(
|
||
"../erpnext-client.js"
|
||
);
|
||
await updateResource("Sales Order", order.erpnext_sales_order, {
|
||
custom_auto_filing_override: 1,
|
||
});
|
||
|
||
// 2. Re-dispatch the worker job for this order. The job_server.py
|
||
// ``process_compliance_service`` handler expects order_name +
|
||
// service_slug + order_number in the payload.
|
||
try {
|
||
await callMethod("frappe.client.insert", {
|
||
doc: {
|
||
doctype: "Integration Request",
|
||
integration_request_service: "Compliance Filing Rerun",
|
||
data: JSON.stringify({
|
||
action: "process_compliance_service",
|
||
payload: {
|
||
order_name: order.erpnext_sales_order,
|
||
order_number: order.order_number,
|
||
service_slug: order.service_slug,
|
||
},
|
||
}),
|
||
status: "Queued",
|
||
},
|
||
});
|
||
} catch (dispatchErr) {
|
||
// Non-fatal — the override is flipped; the worker will re-run the
|
||
// handler on the next scheduled poll or workflow transition.
|
||
console.warn(
|
||
"[compliance-orders] approve-and-file: dispatch warning (non-fatal):",
|
||
dispatchErr,
|
||
);
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
order_number: order.order_number,
|
||
sales_order: order.erpnext_sales_order,
|
||
message: "Auto-filing override set. The filing handler will re-run and submit to the FCC.",
|
||
});
|
||
} catch (err) {
|
||
console.error("[compliance-orders] approve-and-file error:", err);
|
||
res.status(500).json({ error: "Could not approve and file." });
|
||
}
|
||
},
|
||
);
|
||
|
||
/**
|
||
* POST /api/v1/compliance-orders/:order_number/validate
|
||
*
|
||
* Dry-run validator invoked by the intake wizard's Review step BEFORE
|
||
* handing the user off to Stripe Checkout. Returns:
|
||
* - 200: { ok: true, soft_warnings: [...] } — OK to proceed to checkout
|
||
* - 422: { ok: false, missing: [...], soft_warnings: [...] } — block checkout
|
||
*
|
||
* Also writes the result to compliance_orders.intake_data_validated +
|
||
* validation_errors so a subsequent checkout attempt short-circuits with
|
||
* the same error.
|
||
*/
|
||
router.post(
|
||
"/api/v1/compliance-orders/:order_number/validate",
|
||
async (req, res) => {
|
||
const orderNumber = req.params.order_number;
|
||
try {
|
||
const orderResult = await pool.query(
|
||
`SELECT co.*, te.*
|
||
FROM compliance_orders co
|
||
LEFT JOIN telecom_entities te ON te.id = co.telecom_entity_id
|
||
WHERE co.order_number = $1`,
|
||
[orderNumber],
|
||
);
|
||
if (orderResult.rows.length === 0) {
|
||
res.status(404).json({ error: "order not found" });
|
||
return;
|
||
}
|
||
const row = orderResult.rows[0] as Record<string, unknown>;
|
||
const slug = (row.service_slug as string) || "";
|
||
const spec = REQUIRED_FIELDS[slug];
|
||
if (!spec) {
|
||
await pool.query(
|
||
"UPDATE compliance_orders SET intake_data_validated=TRUE, validation_errors=NULL WHERE order_number=$1",
|
||
[orderNumber],
|
||
);
|
||
res.json({ ok: true, missing: [], soft_warnings: [], calculations: {} });
|
||
return;
|
||
}
|
||
|
||
// Merge intake_data with entity columns into one lookup source so
|
||
// predicates/validators can reference either (e.g., entity columns
|
||
// like officer_count_claimed, or intake_data paths like
|
||
// revenue.line_303).
|
||
const intake = (row.intake_data as Record<string, unknown>) || {};
|
||
const src: Record<string, unknown> = { ...row, ...intake };
|
||
|
||
// Flatten safe_harbor_election[primary].* so predicate rules can
|
||
// reference it as `safe_harbor_election.method` without knowing
|
||
// which category is primary.
|
||
const sheRaw = src.safe_harbor_election as Record<string, unknown> | undefined;
|
||
const primaryCat = (src.line_105_primary as string) || "";
|
||
if (sheRaw && primaryCat && sheRaw[primaryCat] && typeof sheRaw[primaryCat] === "object") {
|
||
src.safe_harbor_election = {
|
||
...(sheRaw as object),
|
||
...(sheRaw[primaryCat] as object), // method, pct, year, q at top level
|
||
};
|
||
}
|
||
|
||
const missing = spec.required.filter((k) => isEmpty(digField(src, k)));
|
||
const soft = spec.soft.filter((k) => isEmpty(digField(src, k)));
|
||
const rejections: string[] = [];
|
||
|
||
// ── Conditional rules ──────────────────────────────────────────
|
||
if (spec.conditional) {
|
||
for (const rule of spec.conditional) {
|
||
if (!evalPredicate(rule.when, src)) continue;
|
||
if (rule.reject) {
|
||
rejections.push(rule.reject);
|
||
continue;
|
||
}
|
||
for (const k of rule.require || []) {
|
||
if (isEmpty(digField(src, k))) missing.push(k);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Entity-level FRN gate
|
||
if (REQUIRES_ENTITY_FRN.has(slug) && !row.frn) {
|
||
missing.push("telecom_entity.frn");
|
||
}
|
||
|
||
// ── Cross-field validators ─────────────────────────────────────
|
||
let xErrors: string[] = [];
|
||
let xWarnings: string[] = [];
|
||
let calculations: Record<string, unknown> = {};
|
||
if (spec.validators && spec.validators.length > 0) {
|
||
const xv = await runCrossFieldValidators(
|
||
spec.validators, src, row, Number(row.id),
|
||
);
|
||
xErrors = xv.errors;
|
||
xWarnings = xv.warnings;
|
||
calculations = xv.calculations;
|
||
}
|
||
|
||
const allErrors = [...rejections, ...xErrors];
|
||
const ok = missing.length === 0 && allErrors.length === 0;
|
||
const result = {
|
||
ok,
|
||
missing,
|
||
rejections: allErrors,
|
||
soft_warnings: [...soft, ...xWarnings],
|
||
calculations,
|
||
checked_at: new Date().toISOString(),
|
||
};
|
||
|
||
await pool.query(
|
||
`UPDATE compliance_orders
|
||
SET intake_data_validated = $2,
|
||
validation_errors = $3::jsonb
|
||
WHERE order_number = $1`,
|
||
[orderNumber, ok, JSON.stringify(result)],
|
||
);
|
||
|
||
if (!ok) {
|
||
res.status(422).json(result);
|
||
return;
|
||
}
|
||
res.json(result);
|
||
} catch (err) {
|
||
console.error("[compliance-orders] validate error:", err);
|
||
res.status(500).json({ error: "validation failed" });
|
||
}
|
||
},
|
||
);
|
||
|
||
/**
|
||
* POST /api/v1/compliance-orders/:id/usac-delegation
|
||
* Customer confirms they've completed USAC E-File delegation.
|
||
* Updates order status and notifies the team to begin filing.
|
||
*/
|
||
router.post("/api/v1/compliance-orders/:id/usac-delegation", async (req, res) => {
|
||
const id = req.params.id;
|
||
|
||
try {
|
||
// Support both batch ID (CB-) and order number (CO-)
|
||
const whereCol = id.startsWith("CB-") ? "batch_id" : "order_number";
|
||
const result = await pool.query(
|
||
`UPDATE compliance_orders
|
||
SET intake_data = jsonb_set(
|
||
COALESCE(intake_data, '{}'::jsonb),
|
||
'{usac_delegation_confirmed}',
|
||
to_jsonb(now()::text)
|
||
),
|
||
updated_at = NOW()
|
||
WHERE ${whereCol} = $1
|
||
AND service_slug IN ('fcc-499a', 'fcc-499a-499q')
|
||
RETURNING order_number, service_slug, customer_email`,
|
||
[id],
|
||
);
|
||
|
||
if (result.rows.length === 0) {
|
||
res.status(404).json({ error: "No 499-A order found for this ID." });
|
||
return;
|
||
}
|
||
|
||
console.log(`[compliance-orders] USAC delegation confirmed for ${id} (${result.rows.length} orders)`);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: "Thank you! We've been notified that delegation is complete and will begin your filing within 1 business day.",
|
||
orders_updated: result.rows.length,
|
||
});
|
||
} catch (err) {
|
||
console.error("[compliance-orders] USAC delegation error:", err);
|
||
res.status(500).json({ error: "Could not record delegation confirmation." });
|
||
}
|
||
});
|
||
|
||
export { COMPLIANCE_SERVICES, REQUIRED_FIELDS };
|
||
export default router;
|