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>
244 lines
7.7 KiB
TypeScript
244 lines
7.7 KiB
TypeScript
// FCC Form 499-A shared utilities (TypeScript / API side)
|
|
//
|
|
// Mirrors scripts/workers/services/telecom/fcc_499_utils.py — same logic
|
|
// is available to both the API (for /validate dry-run) and the worker
|
|
// handler (for actual form submission). Keep in sync.
|
|
//
|
|
// Authority: 2026 Form 499-A Instructions.
|
|
|
|
import { pool } from "../db.js";
|
|
|
|
// ── Line 105 box-tick derivation ──────────────────────────────────────────
|
|
|
|
export const LINE_105_BOX_NUMBERS: Record<string, number> = {
|
|
voip_interconnected: 1,
|
|
voip_non_interconnected: 2,
|
|
clec: 3,
|
|
ilec: 4,
|
|
local_reseller: 5,
|
|
toll_reseller: 6,
|
|
ixc: 7,
|
|
wireless: 8,
|
|
mvno: 9,
|
|
prepaid_calling_card: 10,
|
|
private_line: 11,
|
|
satellite: 12,
|
|
payphone: 13,
|
|
osp: 14,
|
|
shared_tenant: 15,
|
|
audio_bridging: 16,
|
|
toll_free: 17,
|
|
paging: 18,
|
|
smr: 19,
|
|
fixed_wireless: 20,
|
|
mobile_satellite: 21,
|
|
other: 22,
|
|
};
|
|
|
|
export interface Line105Entry {
|
|
id: string;
|
|
rank: number;
|
|
infra_type?: "facilities" | "reseller" | "mvno";
|
|
is_tdm_service?: boolean;
|
|
}
|
|
|
|
export function derivedLine105Boxes(
|
|
categoryId: string,
|
|
infraType: string | undefined,
|
|
): number[] {
|
|
const boxes: number[] = [];
|
|
if (infraType === "reseller") {
|
|
if (categoryId === "clec") boxes.push(5);
|
|
if (categoryId === "ixc") boxes.push(6);
|
|
}
|
|
if (infraType === "mvno" && categoryId === "wireless") boxes.push(9);
|
|
return boxes;
|
|
}
|
|
|
|
export function allLine105BoxesToTick(categories: Line105Entry[]): number[] {
|
|
const boxes = new Set<number>();
|
|
for (const cat of categories ?? []) {
|
|
if (cat.id && LINE_105_BOX_NUMBERS[cat.id] !== undefined) {
|
|
boxes.add(LINE_105_BOX_NUMBERS[cat.id]);
|
|
}
|
|
for (const b of derivedLine105Boxes(cat.id, cat.infra_type)) {
|
|
boxes.add(b);
|
|
}
|
|
}
|
|
return Array.from(boxes).sort((a, b) => a - b);
|
|
}
|
|
|
|
// ── Safe harbor lookup ────────────────────────────────────────────────────
|
|
|
|
export const SAFE_HARBOR_DISALLOWED = new Set(["voip_non_interconnected"]);
|
|
|
|
export function safeHarborAllowed(categoryId: string): boolean {
|
|
return !SAFE_HARBOR_DISALLOWED.has(categoryId);
|
|
}
|
|
|
|
export async function loadSafeHarborPct(
|
|
formYear: number,
|
|
categoryId: string,
|
|
): Promise<number | null> {
|
|
if (SAFE_HARBOR_DISALLOWED.has(categoryId)) return null;
|
|
const r = await pool.query(
|
|
`SELECT interstate_pct FROM fcc_safe_harbor_percentages
|
|
WHERE form_year = $1 AND line_105_category = $2`,
|
|
[formYear, categoryId],
|
|
);
|
|
return r.rows[0] ? Number(r.rows[0].interstate_pct) : null;
|
|
}
|
|
|
|
// ── De minimis calculator (Appendix A) ───────────────────────────────────
|
|
|
|
export interface DeMinimisWorksheet {
|
|
form_year: number;
|
|
line_1_filer_interstate_cents: number;
|
|
line_2_filer_intl_cents: number;
|
|
line_3_affiliates_interstate_cents: number;
|
|
line_4_affiliates_intl_cents: number;
|
|
line_5_consolidated_interstate_cents: number;
|
|
line_6_consolidated_total_cents: number;
|
|
line_7_interstate_pct: number;
|
|
line_8_lire_exempt: boolean;
|
|
line_9_contribution_base_cents: number;
|
|
line_10_factor: number;
|
|
line_11_estimated_contrib_cents: number;
|
|
is_de_minimis: boolean;
|
|
threshold_usd: number;
|
|
notes: string[];
|
|
}
|
|
|
|
export interface AffiliateRevenue {
|
|
total_revenue_cents: number;
|
|
interstate_pct: number;
|
|
international_pct: number;
|
|
}
|
|
|
|
export async function loadDeMinimisFactor(formYear: number): Promise<number> {
|
|
const r = await pool.query(
|
|
`SELECT factor FROM fcc_deminimis_factors WHERE form_year = $1`,
|
|
[formYear],
|
|
);
|
|
if (!r.rows[0]) {
|
|
throw new Error(`No de minimis factor configured for form year ${formYear}`);
|
|
}
|
|
return Number(r.rows[0].factor);
|
|
}
|
|
|
|
export async function calculateDeMinimis(opts: {
|
|
form_year: number;
|
|
filer_total_revenue_cents: number;
|
|
filer_interstate_pct: number;
|
|
filer_international_pct: number;
|
|
affiliates?: AffiliateRevenue[];
|
|
}): Promise<DeMinimisWorksheet> {
|
|
const affiliates = opts.affiliates ?? [];
|
|
const notes: string[] = [];
|
|
|
|
const line_1 = Math.round(
|
|
opts.filer_total_revenue_cents * (opts.filer_interstate_pct / 100),
|
|
);
|
|
const line_2 = Math.round(
|
|
opts.filer_total_revenue_cents * (opts.filer_international_pct / 100),
|
|
);
|
|
let line_3 = 0;
|
|
let line_4 = 0;
|
|
for (const a of affiliates) {
|
|
line_3 += Math.round(a.total_revenue_cents * a.interstate_pct / 100);
|
|
line_4 += Math.round(a.total_revenue_cents * a.international_pct / 100);
|
|
}
|
|
const line_5 = line_1 + line_3;
|
|
const intl_total = line_2 + line_4;
|
|
const line_6 = line_5 + intl_total;
|
|
const line_7 = line_6 > 0
|
|
? Math.round(100 * line_5 / line_6 * 10000) / 10000
|
|
: 0;
|
|
const line_8 = line_7 <= 12.0;
|
|
const line_9 = line_5 + (line_8 ? 0 : intl_total);
|
|
const line_10 = await loadDeMinimisFactor(opts.form_year);
|
|
const line_11 = Math.round(line_9 * line_10);
|
|
const threshold_usd = 10000;
|
|
const is_de_minimis = line_11 < threshold_usd * 100;
|
|
|
|
if (is_de_minimis) {
|
|
notes.push(
|
|
`De minimis: estimated contribution $${(line_11 / 100).toFixed(2)} < ` +
|
|
`$${threshold_usd.toLocaleString()} threshold.`,
|
|
);
|
|
} else {
|
|
notes.push(
|
|
`NOT de minimis: estimated contribution $${(line_11 / 100).toFixed(2)} ` +
|
|
`≥ $${threshold_usd.toLocaleString()} threshold.`,
|
|
);
|
|
}
|
|
if (line_8) {
|
|
notes.push(
|
|
`LIRE exempt: interstate (${line_7.toFixed(2)}%) ≤ 12% of combined` +
|
|
` interstate+intl — international revenue excluded.`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
form_year: opts.form_year,
|
|
line_1_filer_interstate_cents: line_1,
|
|
line_2_filer_intl_cents: line_2,
|
|
line_3_affiliates_interstate_cents: line_3,
|
|
line_4_affiliates_intl_cents: line_4,
|
|
line_5_consolidated_interstate_cents: line_5,
|
|
line_6_consolidated_total_cents: line_6,
|
|
line_7_interstate_pct: line_7,
|
|
line_8_lire_exempt: line_8,
|
|
line_9_contribution_base_cents: line_9,
|
|
line_10_factor: line_10,
|
|
line_11_estimated_contrib_cents: line_11,
|
|
is_de_minimis,
|
|
threshold_usd,
|
|
notes,
|
|
};
|
|
}
|
|
|
|
// ── Line 612 filing type ─────────────────────────────────────────────────
|
|
|
|
export type FilingType =
|
|
| "original_april_1"
|
|
| "registration_new_filer"
|
|
| "revised_registration"
|
|
| "revised_revenue";
|
|
|
|
export function detectFilingType(opts: {
|
|
entity: { filer_id_499?: string | null };
|
|
current_year_filing_exists?: boolean;
|
|
revised_reason?: "registration" | "revenue" | null;
|
|
}): FilingType {
|
|
if (!opts.entity.filer_id_499) return "registration_new_filer";
|
|
if (opts.current_year_filing_exists) {
|
|
if (opts.revised_reason === "registration") return "revised_registration";
|
|
if (opts.revised_reason === "revenue") return "revised_revenue";
|
|
}
|
|
return "original_april_1";
|
|
}
|
|
|
|
// ── TRS contribution base (Lines 512-514) ────────────────────────────────
|
|
|
|
export const TRS_BASE_LINE_KEYS = [
|
|
"line_403", "line_404", "line_404_1", "line_404_3",
|
|
"line_405", "line_406", "line_407", "line_408",
|
|
"line_409", "line_410", "line_411", "line_412",
|
|
"line_413", "line_414_1", "line_414_2",
|
|
"line_415", "line_416", "line_417",
|
|
"line_418_4",
|
|
] as const;
|
|
|
|
export function computeTrsContributionBase(
|
|
revenueLines: Record<string, number | undefined | null>,
|
|
): { line_512: number; line_513: number; line_514: number } {
|
|
const sum = TRS_BASE_LINE_KEYS.reduce(
|
|
(acc, k) => acc + (Number(revenueLines[k]) || 0),
|
|
0,
|
|
);
|
|
const line_512 = sum - (Number(revenueLines.line_511) || 0);
|
|
const line_513 = Number(revenueLines.line_513) || 0;
|
|
const line_514 = line_512 - line_513;
|
|
return { line_512, line_513, line_514 };
|
|
}
|