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>
320 lines
14 KiB
Python
320 lines
14 KiB
Python
"""Form 499-A Initial (New Filer) Registration handler.
|
|
|
|
Per USAC rules, new telecommunications providers must register with the
|
|
Universal Service Administrative Company within 30 days of offering
|
|
service. The "New Filer Registration" path at forms.universalservice.org
|
|
creates a USAC Filer ID (812xxx) that every subsequent 499-A / 499-Q
|
|
filing references.
|
|
|
|
Distinct from our annual ``Form499AHandler`` — new filers have no
|
|
revenue to report yet; we submit Blocks 1, 2, and 6 (identifying info,
|
|
contacts, officer certification) and skip revenue.
|
|
|
|
Flow:
|
|
1. Intake-driven Playwright session against
|
|
https://forms.universalservice.org → "Register as New Filer"
|
|
2. Submit Blocks 1, 2-A, 2-B, 2-C, 6
|
|
3. Capture Filer ID from confirmation
|
|
4. Persist filer_id_499 on the entity
|
|
5. Schedule next April 1 in compliance_calendar
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from datetime import date, datetime
|
|
from typing import Optional
|
|
|
|
from .base_handler import BaseServiceHandler
|
|
from .telecom import filing_state
|
|
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__)
|
|
|
|
USAC_EFILE_URL = os.environ.get(
|
|
"USAC_EFILE_URL", "https://forms.universalservice.org/",
|
|
)
|
|
|
|
|
|
class Form499InitialHandler(BaseServiceHandler):
|
|
SERVICE_SLUG = "fcc-499-initial"
|
|
SERVICE_NAME = "Form 499 Initial Registration"
|
|
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", {})
|
|
entity_id = entity.get("id")
|
|
|
|
if not entity.get("frn"):
|
|
self._create_admin_todo(
|
|
order_number,
|
|
"Form 499 Initial Registration requires an FRN. Order a "
|
|
"cores-frn-registration first, then re-dispatch this.",
|
|
)
|
|
return []
|
|
|
|
if entity.get("filer_id_499"):
|
|
logger.info(
|
|
"Form499InitialHandler: entity %s already has filer_id_499 %s — "
|
|
"skipping",
|
|
entity_id, entity.get("filer_id_499"),
|
|
)
|
|
return []
|
|
|
|
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=[],
|
|
admin_email=decision.admin_email,
|
|
summary=(
|
|
"New-filer registration at USAC E-File. Blocks 1+2+6 only "
|
|
"(no revenue to report). Will assign Filer ID on success."
|
|
),
|
|
)
|
|
return []
|
|
|
|
filer_id, confirmation_path = await self._submit_new_filer(
|
|
order_number=order_number, entity=entity, work_dir=work_dir,
|
|
)
|
|
|
|
generated: list[str] = []
|
|
if confirmation_path:
|
|
generated.append(confirmation_path)
|
|
|
|
if filer_id and entity_id:
|
|
self._persist_filer_id(entity_id, filer_id)
|
|
self._schedule_first_499a(order_number, entity)
|
|
|
|
return generated
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# USAC E-File flow — New Filer Registration
|
|
# ------------------------------------------------------------------ #
|
|
|
|
async def _submit_new_filer(
|
|
self, *, order_number: str, entity: dict, work_dir: str,
|
|
) -> tuple[str, Optional[str]]:
|
|
confirmation_path = os.path.join(
|
|
work_dir, f"usac_new_filer_confirmation_{order_number}.pdf",
|
|
)
|
|
try:
|
|
async with undetected_browser(headless=True) as (ctx, page):
|
|
await page.goto(USAC_EFILE_URL, wait_until="domcontentloaded")
|
|
await human_delay(1.5, 3.0)
|
|
|
|
# Click into New Filer Registration
|
|
await page.click("text=Register as New Filer")
|
|
await human_delay()
|
|
|
|
# Block 1 — identifying info
|
|
await type_slowly(page, 'input[name="company_name"]', entity.get("legal_name", ""))
|
|
await page.fill('input[name="frn"]', entity.get("frn", ""))
|
|
if entity.get("dba_name"):
|
|
await page.fill('input[name="dba_name"]', entity.get("dba_name", ""))
|
|
if entity.get("ein"):
|
|
await page.fill('input[name="ein"]', entity.get("ein", "").replace("-", ""))
|
|
if entity.get("affiliated_filer_name"):
|
|
await page.fill('input[name="affiliated_filer_name"]',
|
|
entity["affiliated_filer_name"])
|
|
await page.fill('input[name="affiliated_filer_ein"]',
|
|
entity.get("affiliated_filer_ein", ""))
|
|
for i, tn in enumerate((entity.get("trade_names") or [])[:10]):
|
|
try: await page.fill(f'input[name="trade_name_{i}"]', tn)
|
|
except Exception: pass
|
|
|
|
# Line 105 — multi-select ranked categories
|
|
from .telecom.fcc_499_utils import all_line_105_boxes_to_tick
|
|
categories = entity.get("line_105_categories") or []
|
|
if not categories and entity.get("carrier_category"):
|
|
# Legacy single-category fallback
|
|
categories = [{"id": entity["carrier_category"], "rank": 1,
|
|
"infra_type": entity.get("infra_type", "facilities")}]
|
|
for box_num in all_line_105_boxes_to_tick(categories):
|
|
try: await page.check(f'input[name="line_105_box_{box_num}"]')
|
|
except Exception: pass
|
|
|
|
# Block 2-A — regulatory contact (entity overrides, else PW defaults)
|
|
await page.fill('input[name="regulatory_contact_name"]',
|
|
entity.get("regulatory_contact_name")
|
|
or entity.get("contact_name", ""))
|
|
await page.fill('input[name="regulatory_contact_email"]',
|
|
entity.get("regulatory_contact_email")
|
|
or entity.get("contact_email", ""))
|
|
await page.fill('input[name="regulatory_contact_phone"]',
|
|
entity.get("regulatory_contact_phone")
|
|
or entity.get("contact_phone", ""))
|
|
|
|
# Block 2-B — D.C. Agent
|
|
# Source priority: intake_data.dc_agent → entity columns → NWRA default
|
|
intake = order_data.get("intake_data", {}) or {}
|
|
dc = intake.get("dc_agent", {}) or {}
|
|
await page.fill('input[name="dc_agent_company"]',
|
|
dc.get("company")
|
|
or entity.get("dc_agent_company",
|
|
"Northwest Registered Agent Service Inc."))
|
|
await page.fill('input[name="dc_agent_street"]',
|
|
dc.get("street")
|
|
or entity.get("dc_agent_street", "1717 N Street NW STE 1"))
|
|
await page.fill('input[name="dc_agent_city"]',
|
|
dc.get("city")
|
|
or entity.get("dc_agent_city", "Washington"))
|
|
await page.fill('input[name="dc_agent_state"]',
|
|
dc.get("state")
|
|
or entity.get("dc_agent_state", "DC"))
|
|
await page.fill('input[name="dc_agent_zip"]',
|
|
dc.get("zip")
|
|
or entity.get("dc_agent_zip", "20036"))
|
|
|
|
# Block 2-C — Officers (up to 3, with business addresses)
|
|
for i in (1, 2, 3):
|
|
name = entity.get(f"officer_{i}_name") if i > 1 else (
|
|
entity.get("officer_1_name") or entity.get("ceo_name")
|
|
)
|
|
title = entity.get(f"officer_{i}_title") if i > 1 else (
|
|
entity.get("officer_1_title") or entity.get("ceo_title")
|
|
)
|
|
if not name:
|
|
continue
|
|
await page.fill(f'input[name="officer_{i}_name"]', name)
|
|
await page.fill(f'input[name="officer_{i}_title"]', title or "")
|
|
await page.fill(f'input[name="officer_{i}_street"]',
|
|
entity.get(f"officer_{i}_street", ""))
|
|
await page.fill(f'input[name="officer_{i}_city"]',
|
|
entity.get(f"officer_{i}_city", ""))
|
|
await page.fill(f'input[name="officer_{i}_state"]',
|
|
entity.get(f"officer_{i}_state", ""))
|
|
await page.fill(f'input[name="officer_{i}_zip"]',
|
|
entity.get(f"officer_{i}_zip", ""))
|
|
|
|
# Line 227: jurisdictions multi-select
|
|
for state in entity.get("jurisdictions_served") or []:
|
|
try: await page.check(f'input[name="line_227_state_{state}"]')
|
|
except Exception: pass
|
|
|
|
# Line 228: first-service date (or pre-1999 checkbox)
|
|
if entity.get("first_telecom_service_pre_1999"):
|
|
try: await page.check('input[name="line_228_pre_1999"]')
|
|
except Exception: pass
|
|
else:
|
|
if entity.get("first_telecom_service_year"):
|
|
await page.fill('input[name="line_228_year"]',
|
|
str(entity["first_telecom_service_year"]))
|
|
if entity.get("first_telecom_service_month"):
|
|
await page.fill('input[name="line_228_month"]',
|
|
str(entity["first_telecom_service_month"]))
|
|
|
|
# Block 6 — certification signature
|
|
sig_name = entity.get("officer_1_name") or entity.get("ceo_name") \
|
|
or entity.get("contact_name", "")
|
|
await page.fill('input[name="officer_signature_name"]', sig_name)
|
|
await page.check('input[name="officer_certification"]')
|
|
|
|
await human_delay(1.5, 3.0)
|
|
await page.click('button:has-text("Submit")')
|
|
await page.wait_for_selector("text=Filer ID", timeout=90000)
|
|
|
|
body = await page.locator("body").inner_text()
|
|
filer_id = ""
|
|
import re
|
|
m = re.search(r"\bFiler ID[:\s]*(\d{6,8})\b", body)
|
|
if m:
|
|
filer_id = m.group(1)
|
|
|
|
await page.pdf(path=confirmation_path, format="Letter")
|
|
|
|
if not filer_id:
|
|
self._create_admin_todo(
|
|
order_number,
|
|
"USAC new-filer submission completed but Filer ID could "
|
|
"not be extracted. Check the confirmation PDF in MinIO; "
|
|
"update telecom_entities.filer_id_499 manually.",
|
|
)
|
|
return "", confirmation_path
|
|
|
|
logger.info(
|
|
"Form499InitialHandler: assigned Filer ID %s for %s",
|
|
filer_id, entity.get("legal_name", ""),
|
|
)
|
|
return filer_id, confirmation_path
|
|
|
|
except Exception as exc:
|
|
logger.exception("Form499InitialHandler: USAC flow failed: %s", exc)
|
|
self._create_admin_todo(
|
|
order_number,
|
|
f"USAC new-filer registration raised: {exc}. Complete manually "
|
|
f"at {USAC_EFILE_URL} and update telecom_entities.filer_id_499.",
|
|
)
|
|
return "", None
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Persistence + calendar scheduling
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def _persist_filer_id(self, entity_id: int, filer_id: str) -> None:
|
|
try:
|
|
import psycopg2
|
|
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
|
|
with conn.cursor() as cur:
|
|
cur.execute(
|
|
"UPDATE telecom_entities SET filer_id_499=%s WHERE id=%s",
|
|
(filer_id, entity_id),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
except Exception as exc:
|
|
logger.warning("Could not persist filer_id_499 on %s: %s", entity_id, exc)
|
|
|
|
def _schedule_first_499a(self, order_number: str, entity: dict) -> None:
|
|
"""Write the next April 1 deadline into ERPNext Compliance Calendar."""
|
|
try:
|
|
from scripts.workers.erpnext_client import ERPNextClient
|
|
year = datetime.utcnow().year + (
|
|
0 if datetime.utcnow().month < 4 else 1
|
|
)
|
|
due = date(year, 4, 1).strftime("%Y-%m-%d")
|
|
ERPNextClient().create_resource(
|
|
"Compliance Calendar",
|
|
{
|
|
"entity_name": entity.get("legal_name", ""),
|
|
"order_reference": order_number,
|
|
"compliance_type": "FCC Form 499-A",
|
|
"description": (
|
|
f"First annual 499-A filing for "
|
|
f"{entity.get('legal_name', '')} (Filer ID pending). "
|
|
f"Due April 1 following the reporting year."
|
|
),
|
|
"due_date": due,
|
|
"recurring": 1,
|
|
"recurrence_period":"Yearly",
|
|
"status": "Upcoming",
|
|
},
|
|
)
|
|
logger.info(
|
|
"Form499InitialHandler: scheduled first 499-A for %s on %s",
|
|
entity.get("legal_name", ""), due,
|
|
)
|
|
except Exception as exc:
|
|
logger.warning("Could not schedule first 499-A calendar entry: %s", 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)
|