Phase 3-5: - API: POST /api/v1/fcc-carrier-registration (order creation with pricing) - API: GET /api/v1/fcc-carrier-registration/:id (status) - API: GET /api/v1/fcc-carrier-registration/state-fees (formation fees) - Checkout: fcc_carrier_registration order type with Stripe line items - Payment handler: dispatch worker + send confirmation email - Pipeline handler: 8-step CRTC-style pipeline (formation → CORES → 499 → DC Agent → State PUC → RMD/CPNI/CALEA/BDC → add-ons → final review) - Job server dispatch map entry - Service page CTA updated to link to order page Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
317 lines
15 KiB
Python
317 lines
15 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}'",
|
|
)
|
|
self._update_step(order_number, "dc_agent_completed_at")
|
|
|
|
# ── 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
|
|
|
|
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(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)
|