"""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)