"""FCC Form 499-A shared utilities. De minimis calculator (Appendix A), safe-harbor percentage lookup, Line 612 filing-type detection, and Line 105 box-tick derivation. Used by form_499a.py, form_499_initial.py, and the /validate endpoint's Python counterpart (if we ever move validation server-side). """ from __future__ import annotations import logging from dataclasses import dataclass, field from typing import Optional import psycopg2 import os logger = logging.getLogger(__name__) # ── Line 105 box-tick derivation ──────────────────────────────────────── # Mirrors site/src/lib/line_105_catalog.ts::derivedLine105Boxes. When a # CLEC/IXC/Wireless has infra_type='reseller' or 'mvno', the handler # automatically ticks the corresponding derived Line 105 box on the form. LINE_105_BOX_NUMBERS = { "voip_interconnected": 1, "voip_non_interconnected": 2, "clec": 3, "ilec": 4, "local_reseller": 5, # derived from clec + reseller "toll_reseller": 6, # derived from ixc + reseller "ixc": 7, "wireless": 8, "mvno": 9, # derived from wireless + mvno "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, } def derived_line_105_boxes(category_id: str, infra_type: Optional[str]) -> list[int]: """Return extra Line 105 boxes to tick because of the infra_type flag.""" boxes: list[int] = [] if infra_type == "reseller": if category_id == "clec": boxes.append(5) elif category_id == "ixc": boxes.append(6) if infra_type == "mvno" and category_id == "wireless": boxes.append(9) return boxes def all_line_105_boxes_to_tick(line_105_categories: list[dict]) -> list[int]: """Return every Line 105 box number to tick for this filer.""" boxes: set[int] = set() for cat in line_105_categories or []: cat_id = cat.get("id") if cat_id and cat_id in LINE_105_BOX_NUMBERS: boxes.add(LINE_105_BOX_NUMBERS[cat_id]) boxes.update(derived_line_105_boxes(cat_id, cat.get("infra_type"))) return sorted(boxes) # ── Safe harbor lookup ────────────────────────────────────────────────── SAFE_HARBOR_DISALLOWED_CATEGORIES = {"voip_non_interconnected"} def _db_connect(): return psycopg2.connect(os.environ.get("DATABASE_URL", "")) def load_safe_harbor_pct(form_year: int, category_id: str) -> Optional[float]: """Return the safe-harbor interstate % for (year, category), or None. None is returned for categories that have no safe harbor (e.g., non-interconnected VoIP) or if the year/category combination isn't in the fcc_safe_harbor_percentages seed table. """ if category_id in SAFE_HARBOR_DISALLOWED_CATEGORIES: return None try: conn = _db_connect() with conn.cursor() as cur: cur.execute( "SELECT interstate_pct FROM fcc_safe_harbor_percentages " "WHERE form_year = %s AND line_105_category = %s", (form_year, category_id), ) row = cur.fetchone() conn.close() return float(row[0]) if row else None except Exception as exc: logger.warning("safe-harbor lookup failed: %s", exc) return None def safe_harbor_allowed(category_id: str) -> bool: return category_id not in SAFE_HARBOR_DISALLOWED_CATEGORIES # ── De minimis calculator (Appendix A, 11-line worksheet) ─────────────── @dataclass class DeMinimisWorksheet: """Appendix A de minimis determination worksheet. Every field corresponds to a line in the 2026 Form 499-A Appendix A. Mirrors the PDF layout exactly so auditors can follow along. """ form_year: int # Lines 1-4 — interstate/intl contribution bases for filer + affiliates line_1_filer_interstate_cents: int = 0 line_2_filer_intl_cents: int = 0 line_3_affiliates_interstate_cents: int = 0 line_4_affiliates_intl_cents: int = 0 # Line 5 — consolidated interstate line_5_consolidated_interstate_cents: int = 0 # Line 6 — consolidated interstate+intl (pre-LIRE exclusion) line_6_consolidated_total_cents: int = 0 # Line 7 — interstate as % of consolidated total (LIRE test) line_7_interstate_pct: float = 0.0 # Line 8 — LIRE exempt? (line_7 ≤ 12%) line_8_lire_exempt: bool = False # Line 9 — contribution base to test line_9_contribution_base_cents: int = 0 # Line 10 — year-specific factor (0.256 for 2026) line_10_factor: float = 0.0 # Line 11 — estimated annual contribution = line_9 × line_10 line_11_estimated_contrib_cents: int = 0 # Result is_de_minimis: bool = False threshold_usd: int = 10000 notes: list[str] = field(default_factory=list) def to_dict(self) -> dict: return { "form_year": self.form_year, "line_1_filer_interstate_cents": self.line_1_filer_interstate_cents, "line_2_filer_intl_cents": self.line_2_filer_intl_cents, "line_3_affiliates_interstate_cents": self.line_3_affiliates_interstate_cents, "line_4_affiliates_intl_cents": self.line_4_affiliates_intl_cents, "line_5_consolidated_interstate_cents": self.line_5_consolidated_interstate_cents, "line_6_consolidated_total_cents": self.line_6_consolidated_total_cents, "line_7_interstate_pct": self.line_7_interstate_pct, "line_8_lire_exempt": self.line_8_lire_exempt, "line_9_contribution_base_cents": self.line_9_contribution_base_cents, "line_10_factor": self.line_10_factor, "line_11_estimated_contrib_cents": self.line_11_estimated_contrib_cents, "is_de_minimis": self.is_de_minimis, "threshold_usd": self.threshold_usd, "notes": self.notes, } def load_deminimis_factor(form_year: int) -> float: """Return the Appendix A Line 10 factor for a form year. Raises ValueError if the year isn't in the seed table — an unknown form year is a code bug, not a graceful-fallback situation. """ try: conn = _db_connect() with conn.cursor() as cur: cur.execute( "SELECT factor FROM fcc_deminimis_factors WHERE form_year = %s", (form_year,), ) row = cur.fetchone() conn.close() except Exception as exc: logger.error("deminimis factor lookup failed: %s", exc) raise if not row: raise ValueError(f"No de minimis factor configured for form year {form_year}") return float(row[0]) def calculate_de_minimis( *, form_year: int, filer_total_revenue_cents: int, filer_interstate_pct: float, filer_international_pct: float, affiliates: Optional[list[dict]] = None, ) -> DeMinimisWorksheet: """Compute the Appendix A de minimis worksheet. affiliates is a list of {total_revenue_cents, interstate_pct, international_pct} records for each affiliated filer. Empty list = no affiliates. """ affiliates = affiliates or [] w = DeMinimisWorksheet(form_year=form_year) # Line 1: filer interstate revenue w.line_1_filer_interstate_cents = int( filer_total_revenue_cents * (filer_interstate_pct / 100.0) ) # Line 2: filer international revenue w.line_2_filer_intl_cents = int( filer_total_revenue_cents * (filer_international_pct / 100.0) ) # Lines 3-4: affiliates for a in affiliates: tot = int(a.get("total_revenue_cents", 0)) ipct = float(a.get("interstate_pct", 0)) intl = float(a.get("international_pct", 0)) w.line_3_affiliates_interstate_cents += int(tot * ipct / 100.0) w.line_4_affiliates_intl_cents += int(tot * intl / 100.0) # Line 5: consolidated interstate w.line_5_consolidated_interstate_cents = ( w.line_1_filer_interstate_cents + w.line_3_affiliates_interstate_cents ) # Line 6: consolidated interstate + intl w.line_6_consolidated_total_cents = w.line_5_consolidated_interstate_cents + ( w.line_2_filer_intl_cents + w.line_4_affiliates_intl_cents ) # Line 7: interstate as % of consolidated total (guard /0) if w.line_6_consolidated_total_cents > 0: w.line_7_interstate_pct = round( 100.0 * w.line_5_consolidated_interstate_cents / w.line_6_consolidated_total_cents, 4, ) else: w.line_7_interstate_pct = 0.0 # Line 8: LIRE exempt if interstate ≤ 12% of combined w.line_8_lire_exempt = w.line_7_interstate_pct <= 12.0 # Line 9: contribution base to test — interstate + (0 if LIRE else intl) intl_total = w.line_2_filer_intl_cents + w.line_4_affiliates_intl_cents w.line_9_contribution_base_cents = ( w.line_5_consolidated_interstate_cents + (0 if w.line_8_lire_exempt else intl_total) ) # Line 10: year factor w.line_10_factor = load_deminimis_factor(form_year) # Line 11: estimated annual contribution w.line_11_estimated_contrib_cents = int( w.line_9_contribution_base_cents * w.line_10_factor ) # Result: de minimis if < $10,000 w.is_de_minimis = w.line_11_estimated_contrib_cents < (w.threshold_usd * 100) if w.is_de_minimis: w.notes.append( f"De minimis: estimated contribution ${w.line_11_estimated_contrib_cents/100:,.2f}" f" < ${w.threshold_usd:,.0f} threshold." ) else: w.notes.append( f"NOT de minimis: estimated contribution ${w.line_11_estimated_contrib_cents/100:,.2f}" f" ≥ ${w.threshold_usd:,.0f} threshold." ) if w.line_8_lire_exempt: w.notes.append( f"LIRE exempt: interstate ({w.line_7_interstate_pct:.2f}%) ≤ 12% " f"of combined interstate+intl — international revenue excluded." ) return w # ── Line 612 filing-type detection ────────────────────────────────────── def detect_filing_type( *, entity: dict, current_year_filing_exists: bool = False, revised_reason: Optional[str] = None, ) -> str: """Return one of: original_april_1, registration_new_filer, revised_registration, revised_revenue. """ if not entity.get("filer_id_499"): return "registration_new_filer" if current_year_filing_exists: if revised_reason == "registration": return "revised_registration" if revised_reason == "revenue": return "revised_revenue" return "original_april_1" # ── TRS contribution base (Lines 512-514) ─────────────────────────────── # Revenue lines that roll up into the TRS contribution base. # Line 418.4 (non-interconnected VoIP) is included ONLY in TRS base — # it's excluded from USF/NANPA/LNP/ITSP bases. Line 511 subtracts. 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", # TRS-only ] def compute_trs_contribution_base(revenue_lines: dict) -> tuple[int, int, int]: """Return (line_512, line_513, line_514) in cents. line_512 = Σ(TRS_BASE_LINE_KEYS) - line_511 line_513 = line_513 (uncollectible for TRS, provided by filer) line_514 = line_512 - line_513 """ line_512 = sum(int(revenue_lines.get(k, 0) or 0) for k in TRS_BASE_LINE_KEYS) line_512 -= int(revenue_lines.get("line_511", 0) or 0) line_513 = int(revenue_lines.get("line_513", 0) or 0) line_514 = line_512 - line_513 return line_512, line_513, line_514