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>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
320
scripts/workers/services/form_499_initial.py
Normal file
320
scripts/workers/services/form_499_initial.py
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
"""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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue