new-site/scripts/workers/services/fcc_carrier_registration.py
justin b375385efd fix(email): add text/plain part to every transactional + telecom email
All transactional/worker senders built multipart/alternative (or mixed)
messages with ONLY an HTML part. A single-part multipart/alternative is
malformed and HTML-only mail is a spam-score signal -- the same class of
deliverability bug that hurt the campaign pipeline, but on the telecom /
filing / customer-transactional path (499-Q reminders, RMD/FCC filing
review links, intake/completion/delivery emails, commissions, etc).

- worker_email.send_worker_email: auto-derive plaintext from HTML when
  caller omits text= (fixes the shared helper for all current+future use)
- 16 rolled-their-own senders in scripts/workers/** + scripts/formation/
  document_delivery.py: attach html_to_text(...) plaintext sibling before
  the HTML part (job_server + document_delivery wrap text+html in an
  alternative sub-part so PDFs still attach to the mixed root)
- api/src/email.ts: add dependency-free htmlToText() and default
  sendEmail text to it (fixes checkout/webhook HTML-only sends)

Verified: all py files compile + import at runtime, api tsc passes,
htmlToText handles hrefs/lists/entities, 11 plaintext unit tests pass.
Telecom campaign 407 (Jun 8) was HTML-only + sent in the DKIM-broken
window -> 384 sent / 0 clicks (same junked-mail signature).
2026-06-17 21:07:40 -05:00

320 lines
16 KiB
Python

"""FCC Carrier / ISP Registration pipeline handler.
CRTC-style multi-step pipeline that orchestrates:
1. Formation (optional — creates formation_order, waits for completion)
2. CORES/FRN Registration
3. Form 499 Initial
4. D.C. Registered Agent
5. State PUC Registrations (per selected state)
6. RMD, CPNI, CALEA, BDC (as applicable)
7. STIR/SHAKEN + OCN (if add-ons selected)
8. Final review + client notification
Each step checks its idempotency timestamp before running.
Reuses existing service handler logic where possible.
"""
from __future__ import annotations
import logging
import os
from datetime import datetime
from typing import Optional
import psycopg2
import psycopg2.extras
logger = logging.getLogger(__name__)
DATABASE_URL = os.environ.get("DATABASE_URL", "")
class FCCCarrierRegistrationHandler:
"""Pipeline handler for FCC Carrier / ISP Registration orders."""
async def process(self, order_data: dict) -> list[str]:
order_number = order_data.get("order_number", "")
logger.info("FCCCarrierRegHandler: starting pipeline for %s", order_number)
# Load order from PG
conn = psycopg2.connect(DATABASE_URL)
try:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute(
"SELECT * FROM fcc_carrier_registrations WHERE order_number = %s",
(order_number,),
)
order = cur.fetchone()
finally:
conn.close()
if not order:
logger.error("FCCCarrierRegHandler: order %s not found", order_number)
return []
generated: list[str] = []
# ── Step 1: Formation (if needed) ─────────────────────────────────
if order["include_formation"] and not order.get("formation_completed_at"):
formation_order = order.get("formation_order_number")
if formation_order:
# Check if formation is complete
conn = psycopg2.connect(DATABASE_URL)
try:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute(
"SELECT automation_status, entity_name FROM formation_orders WHERE order_number = %s",
(formation_order,),
)
fo = cur.fetchone()
finally:
conn.close()
if fo and fo.get("automation_status") in ("Delivered", "delivered"):
self._update_step(order_number, "formation_completed_at")
self._update_status(order_number, "formation_complete")
logger.info("FCCCarrierRegHandler: formation complete for %s", order_number)
else:
# Formation still in progress — create admin todo and pause
self._update_status(order_number, "awaiting_formation")
self._create_todo(
order_number,
f"FCC Carrier Registration {order_number} is waiting for formation "
f"order {formation_order} to complete. Dispatch the formation "
f"pipeline if not already running.",
)
logger.info(
"FCCCarrierRegHandler: %s waiting for formation %s",
order_number, formation_order,
)
return []
else:
logger.warning(
"FCCCarrierRegHandler: %s needs formation but no formation_order_number",
order_number,
)
self._create_todo(
order_number,
f"FCC Carrier Registration {order_number} needs formation but "
f"formation_order_number is missing. Create one manually.",
)
return []
# ── Step 2: CORES/FRN ─────────────────────────────────────────────
if not order.get("cores_completed_at"):
self._update_status(order_number, "cores_registration")
self._create_todo(
order_number,
f"FCC Carrier Registration {order_number}\n\n"
f"Step 2: Register with FCC CORES and obtain FRN.\n"
f"Entity: {order.get('entity_legal_name', '?')}\n"
f"EIN: {order.get('ein', 'N/A')}\n"
f"Contact: {order.get('contact_name', '')} ({order.get('contact_email', '')})\n"
f"Address: {order.get('address_street', '')}, {order.get('address_city', '')} "
f"{order.get('address_state', '')} {order.get('address_zip', '')}\n\n"
f"After obtaining FRN, update the order:\n"
f" UPDATE fcc_carrier_registrations SET frn_obtained = 'XXXXXXXXXX', "
f"cores_completed_at = NOW() WHERE order_number = '{order_number}'",
)
logger.info("FCCCarrierRegHandler: %s queued for CORES registration", order_number)
return generated
# ── Step 3: Form 499 Initial ──────────────────────────────────────
if not order.get("form_499_completed_at"):
self._update_status(order_number, "form_499_initial")
self._create_todo(
order_number,
f"FCC Carrier Registration {order_number}\n\n"
f"Step 3: File Form 499 Initial Registration at USAC E-File.\n"
f"FRN: {order.get('frn_obtained') or order.get('frn', 'PENDING')}\n"
f"Entity: {order.get('entity_legal_name', '?')}\n\n"
f"After obtaining Filer ID, update the order:\n"
f" UPDATE fcc_carrier_registrations SET filer_id_obtained = 'XXXXXX', "
f"form_499_completed_at = NOW() WHERE order_number = '{order_number}'",
)
return generated
# ── Step 4: D.C. Registered Agent ─────────────────────────────────
if order["include_dc_agent"] and not order.get("dc_agent_completed_at"):
self._create_todo(
order_number,
f"FCC Carrier Registration {order_number}\n\n"
f"Step 4: Place D.C. Registered Agent wholesale order with Northwest.\n"
f"Entity: {order.get('entity_legal_name', '?')}\n"
f"FRN: {order.get('frn_obtained') or order.get('frn', '')}\n\n"
f"After placing NW order, update:\n"
f" UPDATE fcc_carrier_registrations SET dc_agent_completed_at = NOW() "
f"WHERE order_number = '{order_number}'",
)
# Don't mark complete — admin must confirm NW order is placed first
# ── Step 5: State PUC Registrations ───────────────────────────────
puc_states = order.get("state_puc_states") or []
if puc_states and not order.get("state_puc_completed_at"):
self._update_status(order_number, "state_registrations")
self._create_todo(
order_number,
f"FCC Carrier Registration {order_number}\n\n"
f"Step 5: State PUC registrations for: {', '.join(puc_states)}\n"
f"Entity: {order.get('entity_legal_name', '?')}\n\n"
f"Create individual state_puc_registrations rows or handle manually.\n"
f"After completing all states, update:\n"
f" UPDATE fcc_carrier_registrations SET state_puc_completed_at = NOW() "
f"WHERE order_number = '{order_number}'",
)
return generated
# ── Step 6: Compliance filings (RMD, CPNI, CALEA, BDC) ───────────
self._update_status(order_number, "compliance_filings")
compliance_todos = []
if order["include_rmd"] and not order.get("rmd_completed_at"):
compliance_todos.append("RMD Registration")
if order["include_cpni"] and not order.get("cpni_completed_at"):
compliance_todos.append("CPNI Certification")
if order["include_calea"] and not order.get("calea_completed_at"):
compliance_todos.append("CALEA SSI Plan")
if order["include_bdc"] and not order.get("bdc_completed_at"):
compliance_todos.append("BDC Filing")
if compliance_todos:
self._create_todo(
order_number,
f"FCC Carrier Registration {order_number}\n\n"
f"Step 6: Compliance filings needed:\n"
+ "\n".join(f" - {t}" for t in compliance_todos)
+ f"\n\nEntity: {order.get('entity_legal_name', '?')}\n"
f"FRN: {order.get('frn_obtained') or order.get('frn', '')}\n"
f"Filer ID: {order.get('filer_id_obtained') or order.get('filer_id_499', '')}\n\n"
f"Process each filing, then update the corresponding *_completed_at timestamp.",
)
# ── Step 7: Optional add-ons ──────────────────────────────────────
if order["include_stir_shaken"] and not order.get("stir_shaken_completed_at"):
self._create_todo(
order_number,
f"FCC Carrier Registration {order_number}\n\n"
f"Step 7a: STIR/SHAKEN Implementation\n"
f"Entity: {order.get('entity_legal_name', '?')}\n"
f"FRN: {order.get('frn_obtained') or order.get('frn', '')}",
)
if order["include_ocn"] and not order.get("ocn_completed_at"):
self._create_todo(
order_number,
f"FCC Carrier Registration {order_number}\n\n"
f"Step 7b: NECA OCN Registration\n"
f"Entity: {order.get('entity_legal_name', '?')}\n"
f"FRN: {order.get('frn_obtained') or order.get('frn', '')}",
)
# ── Step 8: Final review ──────────────────────────────────────────
all_done = (
(not order["include_rmd"] or order.get("rmd_completed_at"))
and (not order["include_cpni"] or order.get("cpni_completed_at"))
and (not order["include_calea"] or order.get("calea_completed_at"))
and (not order["include_bdc"] or order.get("bdc_completed_at"))
and (not order["include_stir_shaken"] or order.get("stir_shaken_completed_at"))
and (not order["include_ocn"] or order.get("ocn_completed_at"))
)
if all_done:
self._update_status(order_number, "review")
self._create_todo(
order_number,
f"FCC Carrier Registration {order_number} — ALL STEPS COMPLETE\n\n"
f"Entity: {order.get('entity_legal_name', '?')}\n"
f"FRN: {order.get('frn_obtained') or order.get('frn', '')}\n"
f"Filer ID: {order.get('filer_id_obtained') or order.get('filer_id_499', '')}\n\n"
f"Review all filings and send the client a completion summary.\n"
f"Then mark as delivered:\n"
f" UPDATE fcc_carrier_registrations SET status = 'delivered' "
f"WHERE order_number = '{order_number}'",
priority="High",
)
# Send client notification
self._send_status_email(order, "Your FCC carrier registration is nearing completion. Our team is doing a final review.")
return generated
# ── Helpers ───────────────────────────────────────────────────────────
def _update_status(self, order_number: str, status: str) -> None:
try:
conn = psycopg2.connect(DATABASE_URL)
with conn.cursor() as cur:
cur.execute(
"UPDATE fcc_carrier_registrations SET status = %s WHERE order_number = %s",
(status, order_number),
)
conn.commit()
conn.close()
except Exception as exc:
logger.warning("Could not update status for %s: %s", order_number, exc)
def _update_step(self, order_number: str, column: str) -> None:
try:
conn = psycopg2.connect(DATABASE_URL)
with conn.cursor() as cur:
cur.execute(
f"UPDATE fcc_carrier_registrations SET {column} = NOW() WHERE order_number = %s",
(order_number,),
)
conn.commit()
conn.close()
except Exception as exc:
logger.warning("Could not update step %s for %s: %s", column, order_number, exc)
def _create_todo(self, order_number: str, description: str, priority: str = "Medium") -> None:
try:
from scripts.workers.erpnext_client import ERPNextClient
ERPNextClient().create_resource("ToDo", {
"description": f"[fcc-carrier-reg] {order_number}\n\n{description}",
"priority": priority,
"role": "Accounting Advisor",
})
except Exception as exc:
logger.error("Could not create admin ToDo: %s", exc)
def _send_status_email(self, order: dict, message: str) -> None:
try:
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from scripts._email_plaintext import html_to_text
email = order.get("customer_email", "")
name = order.get("customer_name", "")
if not email:
return
first = name.split(" ")[0] if name else "there"
subject = f"FCC Carrier Registration Update — {order.get('order_number', '')}"
body = (
f"<h2>Registration Update</h2>"
f"<p>Hi {first},</p>"
f"<p>{message}</p>"
f"<p>Entity: <strong>{order.get('entity_legal_name', '')}</strong></p>"
f"<p style='font-size:12px;color:#9ca3af;'>Order: {order.get('order_number', '')}</p>"
f"<p style='font-size:11px;color:#9ca3af;'>Performance West Inc. | 1-888-411-0383</p>"
)
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = os.environ.get("SMTP_FROM", "Performance West <noreply@performancewest.net>")
msg["To"] = email
msg.attach(MIMEText(html_to_text(body), "plain"))
msg.attach(MIMEText(body, "html"))
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", "")
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
if smtp_user and smtp_pass:
server.login(smtp_user, smtp_pass)
server.send_message(msg)
except Exception as exc:
logger.warning("Could not send status email: %s", exc)