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>
256 lines
11 KiB
Python
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)
|