Add FCC Carrier/ISP Registration: API, checkout, handler, dispatch
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>
This commit is contained in:
parent
830f5ae738
commit
2927b5cebb
7 changed files with 677 additions and 2 deletions
|
|
@ -1681,6 +1681,24 @@ def handle_import_499a(payload: dict) -> dict:
|
|||
return {"success": False, "error": str(exc)}
|
||||
|
||||
|
||||
def handle_process_fcc_carrier_registration(payload: dict) -> dict:
|
||||
"""Process an FCC Carrier / ISP Registration pipeline order."""
|
||||
import asyncio
|
||||
from scripts.workers.services.fcc_carrier_registration import FCCCarrierRegistrationHandler
|
||||
|
||||
order_number = payload.get("order_number", "")
|
||||
LOG.info("Processing FCC carrier registration: %s", order_number)
|
||||
|
||||
handler = FCCCarrierRegistrationHandler()
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
files = loop.run_until_complete(handler.process({"order_number": order_number}))
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
return {"action": "process_fcc_carrier_registration", "files": files}
|
||||
|
||||
|
||||
JOB_HANDLERS = {
|
||||
"name_search": handle_name_search,
|
||||
"file_entity": handle_file_entity,
|
||||
|
|
@ -1689,6 +1707,7 @@ JOB_HANDLERS = {
|
|||
"deliver": handle_deliver,
|
||||
"send_to_attorney": handle_send_to_attorney,
|
||||
"process_compliance_service": handle_process_compliance_service,
|
||||
"process_fcc_carrier_registration": handle_process_fcc_carrier_registration,
|
||||
# Canada CRTC pipeline actions (dispatched by ERPNext webhooks)
|
||||
"register_ca_domain": handle_register_ca_domain,
|
||||
"register_awaiting_funds": handle_register_awaiting_funds,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ from .new_carrier_bundle import NewCarrierBundleHandler
|
|||
from .foreign_qualification import ForeignQualificationHandler
|
||||
# State PUC/PSC registration across US states
|
||||
from .state_puc_filing import StatePucFilingHandler
|
||||
# FCC Carrier / ISP Registration pipeline
|
||||
from .fcc_carrier_registration import FCCCarrierRegistrationHandler
|
||||
|
||||
SERVICE_HANDLERS: dict[str, type] = {
|
||||
"flsa-audit": FLSAAuditHandler,
|
||||
|
|
|
|||
317
scripts/workers/services/fcc_carrier_registration.py
Normal file
317
scripts/workers/services/fcc_carrier_registration.py
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
"""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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue