new-site/scripts/workers/services/rmd_filing.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

485 lines
22 KiB
Python

"""RMD Registration / Recertification filing handler.
Flow:
1. Generate the RMD certification letter (and Exhibit A if partial STIR/SHAKEN)
using the existing document generators.
2. Convert to PDF.
3. Launch an undetected browser, navigate to the FCC Robocall Mitigation
Database portal (https://apps.fcc.gov/rmd/), log in using the CORES
session previously authorized via the chrome-extension/fcc-access-helper
for this carrier's FRN.
4. Submit the certification (or recertification) form, attaching the
signed DOCX. Capture the confirmation page and store it in MinIO as
``rmd_confirmation_{order}.pdf``.
5. On success: ``UPDATE telecom_entities SET rmd_last_cert_date = NOW(),
rmd_confirmation_number = $conf`` so the next checkup reads green.
Prerequisite: the carrier's FRN must already have filings@performancewest.net
authorized as an agent in FCC CORES. The chrome-extension helper handles
this human-in-the-loop onboarding; it is a one-time-per-carrier step.
Idempotency: if ``already_filed(entity_id, "rmd")`` is True, the handler
skips the portal submission and returns just the packet (the customer
still gets their documents for their records).
"""
from __future__ import annotations
import logging
import os
from datetime import datetime
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
logger = logging.getLogger(__name__)
FCC_RMD_URL = os.environ.get("FCC_RMD_URL", "https://apps.fcc.gov/rmd/")
FCC_CORES_STORAGE_STATE = os.environ.get(
"FCC_CORES_STORAGE_STATE",
"/app/data/fcc_cores_session.json",
)
class RMDFilingHandler(BaseServiceHandler):
SERVICE_SLUG = "rmd-filing"
SERVICE_NAME = "RMD Registration / Recertification"
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")
date_str = datetime.now().strftime("%Y%m%d")
# ── Guard: require entity data before generating documents ───────
legal_name = entity.get("legal_name", "").strip()
if not legal_name:
logger.warning(
"RMDFilingHandler: no entity data for %s — pausing for intake",
order_number,
)
self._request_entity_intake(order_data)
return []
generated: list[str] = []
# ── 1. Generate the signed-ready packet ─────────────────────────
generated.extend(self._build_packet(order_number, entity, work_dir, date_str))
# ── 2. Idempotency gate ──────────────────────────────────────────
if entity_id and filing_state.already_filed(entity_id, "rmd"):
logger.info(
"RMDFilingHandler: RMD already on file for entity %s — returning packet only",
entity_id,
)
return generated
# ── 2b. Client review gate ─────────────────────────────────────
# The client MUST review and approve the certification before we
# submit. The perjury declaration (47 CFR § 1.16) is on them.
client_approved = order_data.get("client_approved", False)
if not client_approved:
# Upload documents to MinIO for the review portal
from scripts.document_gen import MinioStorage
storage = MinioStorage()
minio_paths = []
for path in generated:
if path.endswith(".pdf"):
remote = f"compliance/{order_number}/{os.path.basename(path)}"
try:
storage.upload_file(path, remote)
minio_paths.append(remote)
except Exception as exc:
logger.warning("MinIO upload failed for %s: %s", path, exc)
# Update order with review status + send review link
try:
import psycopg2
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
cur = conn.cursor()
cur.execute(
"""UPDATE compliance_orders
SET rmd_review_status = 'pending',
rmd_packet_minio_paths = %s
WHERE order_number = %s""",
(minio_paths, order_number),
)
conn.commit()
cur.close()
conn.close()
except Exception as exc:
logger.warning("Could not update review status: %s", exc)
# Send review link email to client
try:
customer_email = order_data.get("customer_email") or entity.get("contact_email")
customer_name = order_data.get("customer_name") or entity.get("contact_name", "")
if customer_email:
# Build JWT review URL
try:
import jwt as pyjwt
except ImportError:
import PyJWT as pyjwt # type: ignore
secret = os.environ.get("CUSTOMER_JWT_SECRET", "changeme")
domain = os.environ.get("DOMAIN", "performancewest.net")
token = pyjwt.encode(
{"order_id": order_number, "order_type": "compliance", "email": customer_email},
secret, algorithm="HS256",
)
review_url = f"https://{domain}/portal/rmd-review?token={token}"
if customer_email:
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
subject = f"Action Required — Review your RMD certification for {entity.get('legal_name', order_number)}"
body = (
f"<h2>Your RMD Certification is Ready for Review</h2>"
f"<p>Hi {customer_name.split(' ')[0] if customer_name else 'there'},</p>"
f"<p>We've prepared your Robocall Mitigation Database certification for "
f"<strong>{entity.get('legal_name', '')}</strong> (FRN: {entity.get('frn', '')}).</p>"
f"<p>Before we submit this to the FCC, we need you to review the filing details "
f"and confirm everything is accurate. This is required because the certification "
f"includes a declaration under penalty of perjury (47 CFR § 1.16).</p>"
f"<p><a href='{review_url}' style='display:inline-block;background:#1e3a5f;color:#fff;"
f"padding:12px 28px;border-radius:6px;text-decoration:none;font-weight:600;'>"
f"Review & Approve Filing →</a></p>"
f"<p style='font-size:12px;color:#9ca3af;'>This link expires in 72 hours.</p>"
)
smtp_host = os.environ.get("SMTP_HOST", "co.carrierone.com")
smtp_port = int(os.environ.get("SMTP_PORT", "587"))
smtp_user = os.environ.get("SMTP_USER", "")
smtp_pass = os.environ.get("SMTP_PASS", "")
smtp_from = os.environ.get("SMTP_FROM", "Performance West <noreply@performancewest.net>")
if smtp_user and smtp_pass:
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = smtp_from
msg["To"] = customer_email
msg["Reply-To"] = "info@performancewest.net"
msg.attach(MIMEText(body, "html"))
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
logger.info("RMD review link sent to %s for %s", customer_email, order_number)
except Exception as exc:
logger.warning("Could not send RMD review email: %s", exc)
logger.info(
"RMDFilingHandler: paused for client review — order %s",
order_number,
)
return generated
# ── 2a. Auto-filing toggle — admin review when disabled ─────────
decision = check_auto_filing(order_data)
if not decision.may_submit:
logger.info(
"RMDFilingHandler: %s — staging for admin review (order=%s)",
decision.reason, order_number,
)
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"RMD certification packet ready. STIR/SHAKEN posture: "
f"{entity.get('stir_shaken_status', 'N/A')}. "
f"Clicking Approve & File re-runs this handler against the FCC "
f"RMD portal at {FCC_RMD_URL}."
),
)
return generated
# ── 3. Submit to the FCC RMD portal ──────────────────────────────
confirmation_path, confirmation_number = await self._submit_to_rmd(
order_number=order_number,
entity=entity,
packet_docx=next((p for p in generated if p.endswith(".docx")), None),
work_dir=work_dir,
)
if confirmation_path:
generated.append(confirmation_path)
# ── 4. Persist success + email confirmation to client ──────────
if entity_id and confirmation_number:
filing_state.record_rmd_filing(entity_id, confirmation_number)
if confirmation_number:
try:
from scripts.workers.job_server import _send_filing_confirmation
from scripts.document_gen import MinioStorage
customer_email = order_data.get("customer_email") or entity.get("contact_email")
customer_name = order_data.get("customer_name") or entity.get("contact_name", "")
if customer_email:
conf_paths = [p for p in generated if "confirmation" in p.lower()]
_send_filing_confirmation(
customer_email=customer_email,
customer_name=customer_name,
order_number=order_number,
service_name=self.SERVICE_NAME,
confirmation_number=confirmation_number,
authority="FCC Robocall Mitigation Database (RMD)",
minio_paths=[f"compliance/{order_number}/{os.path.basename(p)}" for p in conf_paths],
storage=MinioStorage(),
)
except Exception as exc:
logger.warning("RMD filing confirmation email failed: %s", exc)
return generated
# ------------------------------------------------------------------ #
# Packet generation (shared with the checkup handler)
# ------------------------------------------------------------------ #
def _build_packet(
self,
order_number: str,
entity: dict,
work_dir: str,
date_str: str,
) -> list[str]:
"""Produce the RMD cert letter (+ Exhibit A if partial STIR/SHAKEN)."""
from scripts.document_gen.templates.rmd_letter_generator import (
generate_rmd_letter,
_determine_primary_role,
)
files: list[str] = []
rmd_docx = os.path.join(
work_dir, f"rmd_certification_letter_{order_number}_{date_str}.docx"
)
letter = generate_rmd_letter(
entity_name=entity.get("legal_name", ""),
dba_name=entity.get("dba_name", ""),
frn=entity.get("frn", ""),
rmd_number=entity.get("rmd_number", ""),
filer_id_499=entity.get("filer_id_499", ""),
address_street=entity.get("address_street", ""),
address_city=entity.get("address_city", ""),
address_state=entity.get("address_state", ""),
address_zip=entity.get("address_zip", ""),
contact_name=entity.get("contact_name", ""),
contact_title=entity.get("contact_title", ""),
contact_email=entity.get("contact_email", ""),
contact_phone=entity.get("contact_phone", ""),
ceo_name=entity.get("ceo_name", ""),
ceo_title=entity.get("ceo_title", "Chief Executive Officer"),
carrier_category=entity.get("carrier_category", "interconnected_voip"),
infra_type=entity.get("infra_type", "facilities"),
is_wholesale=entity.get("is_wholesale", False),
is_gateway_provider=entity.get("is_gateway_provider", False),
is_international_only=entity.get("is_international_only", False),
uses_ucaas_provider=entity.get("uses_ucaas_provider", False),
carrier_metadata=entity.get("carrier_metadata", {}),
stir_shaken_status=entity.get("stir_shaken_status", "complete_implementation"),
stir_shaken_cert_authority=entity.get("stir_shaken_cert_authority", ""),
upstream_provider_name=entity.get("upstream_provider_name", ""),
upstream_provider_frn=entity.get("upstream_provider_frn", ""),
output_path=rmd_docx,
)
if letter:
files.append(letter)
try:
files.append(self._convert_to_pdf(letter))
except Exception as exc:
logger.warning("RMD letter PDF conversion failed: %s", exc)
stir_status = entity.get("stir_shaken_status", "complete_implementation")
if stir_status in (
"partial_implementation",
"robocall_mitigation_only",
"exempt_small_carrier",
):
from scripts.document_gen.templates.rmd_exhibit_a_generator import (
generate_exhibit_a,
)
role = _determine_primary_role(
is_gateway_provider=entity.get("is_gateway_provider", False),
uses_ucaas_provider=entity.get("uses_ucaas_provider", False),
is_wholesale=entity.get("is_wholesale", False),
is_international_only=entity.get("is_international_only", False),
infra_type=entity.get("infra_type", "facilities"),
)
exhibit_docx = os.path.join(
work_dir,
f"robocall_mitigation_program_{order_number}_{date_str}.docx",
)
exhibit = generate_exhibit_a(
entity_name=entity.get("legal_name", ""),
frn=entity.get("frn", ""),
carrier_role=role,
carrier_metadata=entity.get("carrier_metadata", {}),
upstream_provider_name=entity.get("upstream_provider_name", ""),
llm_generate=self._call_llm,
output_path=exhibit_docx,
)
if exhibit:
files.append(exhibit)
try:
files.append(self._convert_to_pdf(exhibit))
except Exception as exc:
logger.warning("Exhibit A PDF conversion failed: %s", exc)
return files
# ------------------------------------------------------------------ #
# FCC RMD Playwright submission
# ------------------------------------------------------------------ #
async def _submit_to_rmd(
self,
*,
order_number: str,
entity: dict,
packet_docx: str | None,
work_dir: str,
) -> tuple[str | None, str]:
"""Submit the RMD certification and return (confirmation_pdf, conf_number).
Returns ``(None, "")`` if the submission could not be attempted
(e.g. no authorized CORES session available). In that case the
customer still receives the packet; an admin ToDo is created so a
human can authorize the FRN and the handler can be replayed.
"""
frn = entity.get("frn", "").strip()
if not frn:
logger.warning("RMDFilingHandler: no FRN — skipping portal submission")
self._create_admin_todo(
order_number,
"RMD filing requires an FRN but none was on file for this carrier. "
"Capture the FRN on the telecom entity and re-dispatch.",
)
return None, ""
storage_exists = os.path.exists(FCC_CORES_STORAGE_STATE)
storage_state = FCC_CORES_STORAGE_STATE if storage_exists else None
confirmation_path = os.path.join(
work_dir, f"rmd_confirmation_{order_number}.pdf"
)
confirmation_number = ""
try:
async with undetected_browser(
headless=True,
storage_state=storage_state,
) as (ctx, page):
await page.goto(FCC_RMD_URL, wait_until="domcontentloaded")
await human_delay(1.5, 3.0)
# If we weren't handed a logged-in session, redirect to
# CORES login will fire — detect that and fall through to
# the admin-ToDo path.
if "login" in page.url.lower() or "coresWeb" in page.url:
logger.warning(
"RMDFilingHandler: CORES session not authorized for FRN %s; "
"admin must run chrome-extension FCC access helper first",
frn,
)
self._create_admin_todo(
order_number,
f"RMD portal required CORES login for FRN {frn}. "
"Run the FCC Access Helper Chrome extension to authorize "
"filings@performancewest.net on this carrier's FRN, export "
f"the session to {FCC_CORES_STORAGE_STATE}, then re-dispatch "
f"order {order_number}.",
)
return None, ""
# Navigate to the certification form. The real selectors are
# pulled from a live recon session (TODO before production —
# covered by state-automation-status.md pattern).
await page.wait_for_selector("text=Certification", timeout=20000)
await page.click("text=File Certification")
await human_delay()
# Pre-populate the form from entity data.
await page.fill('input[name="frn"]', frn)
await page.fill(
'input[name="company_legal_name"]',
entity.get("legal_name", ""),
)
# Attach the signed-ready packet DOCX (plus PDF if present).
if packet_docx:
await page.set_input_files(
'input[type="file"][name="certification_doc"]',
packet_docx,
)
# STIR/SHAKEN implementation status — value must match the
# RMD form radio options.
stir_status = entity.get("stir_shaken_status", "complete_implementation")
await page.click(f'input[name="stir_shaken_status"][value="{stir_status}"]')
await human_delay()
# Review + submit.
await page.click('button[type="submit"]')
await page.wait_for_selector(
"text=Confirmation Number", timeout=60000
)
await human_delay(2.0, 4.0)
# Capture confirmation.
body = await page.locator("body").inner_text()
for line in body.splitlines():
if "Confirmation Number" in line:
parts = line.split(":", 1)
if len(parts) == 2:
confirmation_number = parts[1].strip()
break
# Save the confirmation page as PDF.
await page.pdf(path=confirmation_path, format="Letter")
logger.info(
"RMDFilingHandler: submitted FRN %s, confirmation %s",
frn,
confirmation_number,
)
return confirmation_path, confirmation_number
except Exception as exc:
logger.exception("RMDFilingHandler: portal submission failed: %s", exc)
self._create_admin_todo(
order_number,
f"Automated RMD submission failed for FRN {frn}: {exc}. "
"Inspect worker logs for the screenshot/video, then file manually.",
)
return None, ""
# ------------------------------------------------------------------ #
# Admin fallback
# ------------------------------------------------------------------ #
def _create_admin_todo(self, order_number: str, description: str) -> None:
"""Create an ERPNext ToDo so a human can unblock the filing."""
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)