new-site/scripts/workers/services/foreign_carrier_affiliation.py
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
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>
2026-04-27 06:54:22 -05:00

256 lines
11 KiB
Python

"""Foreign Carrier Affiliation Notification handler (47 CFR § 63.11).
Section 63.11 requires U.S. carriers affiliated with a foreign carrier
serving the same route to/from the United States to file a notification
with the FCC's International Bureau. The notification goes to ECFS /
IBFS — scaffolding here uses the ECFS Express upload path (same as CPNI
and a few other proceedings).
Intake fields (intake_data.foreign_carrier):
foreign_carrier_legal_name: str
country: str (ISO-2)
ownership_pct: float
affected_routes: list[str] # ISO-2 country codes
affiliation_date: str # YYYY-MM-DD
notification_type: str # "pre-consummation" | "post-closing"
Rare-enough filing that we generate the notification letter + auto-submit
when the auto-filing toggle is on, but we prefer admin review on this one
because a miscategorized affiliation is costly.
"""
from __future__ import annotations
import logging
import os
from datetime import datetime
from typing import Optional
from .base_handler import BaseServiceHandler
from .telecom.auto_filing import check_auto_filing, request_admin_review
from .telecom.undetected_browser import undetected_browser, human_delay, type_slowly
logger = logging.getLogger(__name__)
ECFS_UPLOAD_URL = os.environ.get(
"FCC_ECFS_UPLOAD_URL", "https://www.fcc.gov/ecfs/upload/express",
)
# IB Docket 99-217 is the historical home; current filings often use IB
# Docket 04-47 (Section 63.10/63.11 proceedings). Override via env for
# deployment-specific needs.
FCC_63_11_DOCKET = os.environ.get("FCC_63_11_DOCKET", "04-47")
class ForeignCarrierAffiliationHandler(BaseServiceHandler):
SERVICE_SLUG = "fcc-63-11-notification"
SERVICE_NAME = "Foreign Carrier Affiliation Notification (47 CFR § 63.11)"
REQUIRES_LLM = False
async def process(self, order_data: dict) -> list[str]:
work_dir = self._make_work_dir()
order_number = order_data["name"]
entity = order_data.get("entity", {})
intake = order_data.get("intake_data") or {}
fc_intake = intake.get("foreign_carrier") or {}
entity_id = entity.get("id")
generated: list[str] = []
required = ["foreign_carrier_legal_name", "country",
"ownership_pct", "affected_routes", "affiliation_date"]
missing = [k for k in required if not fc_intake.get(k)]
if missing:
self._create_admin_todo(
order_number,
f"63.11 notification requires intake_data.foreign_carrier to "
f"carry: {missing}. Ask the customer to complete intake.",
)
return generated
# Generate the notification letter
letter_path = self._write_letter(
order_number=order_number, entity=entity,
fc_intake=fc_intake, work_dir=work_dir,
)
if letter_path:
generated.append(letter_path)
try:
generated.append(self._convert_to_pdf(letter_path))
except Exception as exc:
logger.warning("63.11 letter PDF conversion failed: %s", exc)
decision = check_auto_filing(order_data)
if not decision.may_submit:
request_admin_review(
order_number=order_number,
service_slug=self.SERVICE_SLUG,
service_name=self.SERVICE_NAME,
entity_name=entity.get("legal_name", ""),
frn=entity.get("frn", ""),
packet_minio_paths=[f"compliance/{order_number}/{os.path.basename(p)}" for p in generated],
admin_email=decision.admin_email,
summary=(
f"47 CFR § 63.11 affiliation notification.\n"
f"Foreign carrier: {fc_intake['foreign_carrier_legal_name']} "
f"({fc_intake['country']}).\n"
f"Ownership: {fc_intake['ownership_pct']}%.\n"
f"Affected routes: {', '.join(fc_intake.get('affected_routes', []))}.\n"
f"Affiliation date: {fc_intake['affiliation_date']}."
),
)
return generated
# Auto-submit via ECFS
conf_path, conf_num = await self._submit_to_ecfs(
order_number=order_number, entity=entity, letter_pdf=next(
(p for p in generated if p.endswith(".pdf")), letter_path,
), work_dir=work_dir,
)
if conf_path:
generated.append(conf_path)
if entity_id and conf_num:
self._persist_affiliation(entity_id, fc_intake, conf_num)
return generated
# ------------------------------------------------------------------ #
def _write_letter(
self, *, order_number: str, entity: dict, fc_intake: dict, work_dir: str,
) -> str:
from docx import Document
doc = Document()
doc.add_heading(
"Notification of Foreign Carrier Affiliation "
"(47 CFR § 63.11)", level=1,
)
doc.add_paragraph(
f"Filed: {datetime.now().strftime('%B %d, %Y')}"
)
doc.add_paragraph(
f"Filing party: {entity.get('legal_name', '')} "
f"(FRN {entity.get('frn', 'N/A')})"
)
doc.add_paragraph(
f"To: Federal Communications Commission — International Bureau"
)
doc.add_paragraph("")
doc.add_paragraph(
f"Pursuant to 47 CFR § 63.11, {entity.get('legal_name', '')} "
f"notifies the Commission of an affiliation with a foreign "
f"carrier as follows:"
)
doc.add_paragraph(
f"Foreign carrier legal name: {fc_intake['foreign_carrier_legal_name']}"
)
doc.add_paragraph(
f"Country / jurisdiction of foreign carrier: {fc_intake['country']}"
)
doc.add_paragraph(
f"Ownership interest: {fc_intake['ownership_pct']}%"
)
doc.add_paragraph(
f"Affected route(s): {', '.join(fc_intake.get('affected_routes', []))}"
)
doc.add_paragraph(
f"Affiliation date: {fc_intake['affiliation_date']} "
f"({fc_intake.get('notification_type', 'post-closing')})"
)
doc.add_paragraph("")
doc.add_paragraph(
"This notification is submitted in accordance with the "
"requirements and timing specified in 47 CFR § 63.11(a) "
"and 63.11(b). The filing party certifies that the "
"information provided is true and correct to the best of its "
"knowledge."
)
for _ in range(2):
doc.add_paragraph("")
doc.add_paragraph("_" * 45)
doc.add_paragraph(entity.get("ceo_name") or entity.get("contact_name", ""))
doc.add_paragraph(entity.get("ceo_title", "Chief Executive Officer"))
doc.add_paragraph(entity.get("legal_name", ""))
out = os.path.join(work_dir, f"fcc_63_11_letter_{order_number}.docx")
doc.save(out)
return out
async def _submit_to_ecfs(
self, *, order_number: str, entity: dict,
letter_pdf: str, work_dir: str,
) -> tuple[str | None, str]:
conf_path = os.path.join(work_dir, f"ecfs_63_11_confirmation_{order_number}.pdf")
confirmation = ""
try:
async with undetected_browser(headless=True) as (ctx, page):
await page.goto(ECFS_UPLOAD_URL, wait_until="domcontentloaded")
await human_delay(1.5, 3.0)
await type_slowly(page, 'input[name="proceedings"]', FCC_63_11_DOCKET)
await page.wait_for_selector(f'li:has-text("{FCC_63_11_DOCKET}")', timeout=10000)
await page.click(f'li:has-text("{FCC_63_11_DOCKET}")')
await human_delay()
await type_slowly(page, 'input[name="name_of_filer"]', entity.get("legal_name", ""))
await type_slowly(page, 'input[name="filer_email"]', entity.get("contact_email", ""))
await page.select_option('select[name="type_of_filing"]', label="Notification")
await page.set_input_files('input[type="file"]', letter_pdf)
await human_delay(1.0, 2.0)
await page.click('button:has-text("Continue")')
await page.wait_for_selector("text=Review", timeout=30000)
await page.click('button:has-text("Submit")')
await page.wait_for_selector("text=Confirmation", timeout=60000)
body = await page.locator("body").inner_text()
for line in body.splitlines():
if "Filing ID" in line or "Confirmation" in line:
parts = line.split(":", 1)
if len(parts) == 2 and parts[1].strip():
confirmation = parts[1].strip()
break
await page.pdf(path=conf_path, format="Letter")
return conf_path, confirmation
except Exception as exc:
logger.exception("63.11 ECFS submission failed: %s", exc)
self._create_admin_todo(
order_number,
f"63.11 ECFS submission raised: {exc}. Packet is in MinIO; "
f"file manually at https://www.fcc.gov/ecfs/ under docket {FCC_63_11_DOCKET}.",
)
return None, ""
def _persist_affiliation(self, entity_id: int, fc_intake: dict,
confirmation: str) -> None:
try:
import json
import psycopg2
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
record = dict(fc_intake)
record["filed_at"] = datetime.utcnow().isoformat()
record["ecfs_confirmation"] = confirmation
with conn.cursor() as cur:
cur.execute(
"""
UPDATE telecom_entities
SET foreign_affiliations = COALESCE(foreign_affiliations, '[]'::jsonb)
|| %s::jsonb
WHERE id = %s
""",
(json.dumps([record]), entity_id),
)
conn.commit()
conn.close()
except Exception as exc:
logger.warning("Could not persist affiliation on %s: %s", entity_id, exc)
def _create_admin_todo(self, order_number: str, description: str) -> None:
try:
from scripts.workers.erpnext_client import ERPNextClient
ERPNextClient().create_resource(
"ToDo",
{
"description": f"[{self.SERVICE_SLUG}] {order_number}\n\n{description}",
"priority": "High",
"role": "Accounting Advisor",
},
)
except Exception as exc:
logger.error("Could not create admin ToDo: %s", exc)