Add 499-Q intake page, 499-Q handler, and 499-A discontinuance handler
499-Q Quarterly Filing: - Intake page at /order/fcc-499q with simplified revenue form (4 fields: carrier's carrier inter/intra, end-user inter/intra) - Zero-revenue confirmation checkbox - Handler creates admin todo with filing details + sends client email - Registers as fcc-499q in SERVICE_HANDLERS 499-A Discontinuance: - Handler creates admin todo with step-by-step USAC instructions (file zero-revenue 499-A, request account closure, confirm CPNI/RMD) - Sends client confirmation email explaining the process - Compliance checker CTA: when user selects "No — cancel registration" in the 499-A toggle, shows discontinuance option ($299) instead of standard filing - Order page maps form_499a_disc to fcc-499a-discontinuance slug Compliance checker intelligence: - 499-A toggle tracks _499aVariant (null/zero/discontinuance) - CTA adapts: revenue=standard 499-A, zero=zero-revenue, cancel=discontinuance - Reset clears variant flag Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
572f0cbf93
commit
0fc318cb38
6 changed files with 514 additions and 3 deletions
|
|
@ -18,6 +18,8 @@ from .fcc_compliance_checkup import FCCComplianceCheckupHandler
|
|||
from .rmd_filing import RMDFilingHandler
|
||||
from .cpni_certification import CPNIFilingHandler
|
||||
from .form_499a import Form499AHandler, Form499ABundleHandler
|
||||
from .form_499q import Form499QHandler
|
||||
from .form_499a_discontinuance import Form499ADiscontinuanceHandler
|
||||
from .stir_shaken import StirShakenHandler
|
||||
from .bdc_filing import BDCFilingHandler
|
||||
from .fcc_full_compliance import FullComplianceHandler
|
||||
|
|
@ -63,6 +65,8 @@ SERVICE_HANDLERS: dict[str, type] = {
|
|||
"fcc-499a": Form499AHandler,
|
||||
"fcc-499a-zero": Form499AHandler, # same handler, zero_revenue flag set from slug
|
||||
"fcc-499a-499q": Form499ABundleHandler,
|
||||
"fcc-499q": Form499QHandler,
|
||||
"fcc-499a-discontinuance": Form499ADiscontinuanceHandler,
|
||||
"stir-shaken": StirShakenHandler,
|
||||
# BDC triple — same handler, mode auto-resolved from slug
|
||||
"bdc-filing": BDCFilingHandler, # legacy alias (both)
|
||||
|
|
@ -100,6 +104,7 @@ FCC_SERVICE_SLUGS: frozenset[str] = frozenset({
|
|||
"fcc-499a-zero",
|
||||
"fcc-499a-499q",
|
||||
"fcc-499q",
|
||||
"fcc-499a-discontinuance",
|
||||
"stir-shaken",
|
||||
"bdc-filing",
|
||||
"fcc-full-compliance",
|
||||
|
|
|
|||
130
scripts/workers/services/form_499a_discontinuance.py
Normal file
130
scripts/workers/services/form_499a_discontinuance.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"""FCC Form 499-A Discontinuance Filing Handler.
|
||||
|
||||
For carriers who no longer provide telecommunications services and need
|
||||
to close out their USAC 499-A filing obligations. Files a final 499-A
|
||||
with zero revenue and requests discontinuance status from USAC.
|
||||
|
||||
This is typically for:
|
||||
- Pure broadband resale ISPs who were incorrectly filing 499-A
|
||||
- Carriers who have ceased operations
|
||||
- Companies that were acquired and the FRN is being retired
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from .base_handler import BaseComplianceHandler
|
||||
|
||||
logger = logging.getLogger("workers.services.form_499a_discontinuance")
|
||||
|
||||
|
||||
class Form499ADiscontinuanceHandler(BaseComplianceHandler):
|
||||
SERVICE_SLUG = "fcc-499a-discontinuance"
|
||||
SERVICE_NAME = "Form 499-A Discontinuance Filing"
|
||||
|
||||
async def process(self, order_data: dict) -> dict | None:
|
||||
order_number = order_data.get("order_number", "")
|
||||
entity = order_data.get("entity", {})
|
||||
intake_data = order_data.get("intake_data", {})
|
||||
|
||||
filer_id = intake_data.get("filer_id_499") or entity.get("filer_id_499", "")
|
||||
frn = intake_data.get("frn") or entity.get("frn", "")
|
||||
legal_name = entity.get("legal_name") or intake_data.get("entity_name", "")
|
||||
|
||||
logger.info(
|
||||
"Form499ADiscontinuanceHandler: %s for %s (FRN: %s, Filer ID: %s)",
|
||||
order_number, legal_name, frn, filer_id,
|
||||
)
|
||||
|
||||
discontinuance_reason = intake_data.get("discontinuance_reason", "Ceased providing telecommunications services")
|
||||
last_service_date = intake_data.get("last_service_date", "")
|
||||
|
||||
# Create admin todo with discontinuance instructions
|
||||
# (USAC E-File discontinuance is a manual process — file zero-revenue 499-A
|
||||
# then submit discontinuance request via USAC contact form)
|
||||
self._create_admin_todo(
|
||||
order_number,
|
||||
f"FILE 499-A DISCONTINUANCE for {legal_name}\n\n"
|
||||
f"FRN: {frn}\n"
|
||||
f"Filer ID: {filer_id}\n"
|
||||
f"Reason: {discontinuance_reason}\n"
|
||||
f"Last service date: {last_service_date or 'Not specified'}\n\n"
|
||||
f"Steps:\n"
|
||||
f"1. Log in to USAC E-File (https://forms.universalservice.org/)\n"
|
||||
f"2. File a final 499-A with $0 revenue for the current year\n"
|
||||
f"3. In the comments/notes section, state: "
|
||||
f"'This is a final filing. {legal_name} has discontinued all "
|
||||
f"telecommunications services as of {last_service_date or 'current date'}. "
|
||||
f"Please close this filer account.'\n"
|
||||
f"4. Contact USAC at (888) 641-8722 or usac@usac.org to confirm "
|
||||
f"discontinuance and request removal of future filing obligations\n"
|
||||
f"5. Confirm that CPNI, RMD, and other FCC filings are also discontinued\n\n"
|
||||
f"Client email: {entity.get('contact_email') or order_data.get('customer_email', '')}",
|
||||
)
|
||||
|
||||
# Send confirmation to client
|
||||
self._send_confirmation(
|
||||
to=entity.get("contact_email") or order_data.get("customer_email", ""),
|
||||
entity_name=legal_name,
|
||||
order_number=order_number,
|
||||
filer_id=filer_id,
|
||||
)
|
||||
|
||||
return {"status": "submitted_for_processing"}
|
||||
|
||||
def _send_confirmation(
|
||||
self, to: str, entity_name: str, order_number: str, filer_id: str,
|
||||
) -> None:
|
||||
if not to:
|
||||
return
|
||||
try:
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
subject = f"Form 499-A Discontinuance Filed — {entity_name}"
|
||||
html = f"""
|
||||
<div style="font-family:Inter,sans-serif;max-width:600px;margin:0 auto;color:#1f2937">
|
||||
<div style="background:#1e3a5f;padding:16px 24px;border-radius:8px 8px 0 0">
|
||||
<h2 style="color:#fff;margin:0;font-size:16px">Form 499-A Discontinuance</h2>
|
||||
</div>
|
||||
<div style="padding:24px;border:1px solid #e5e7eb;border-top:none;border-radius:0 0 8px 8px">
|
||||
<p>We've received your request to discontinue the FCC Form 499-A filing
|
||||
obligation for <strong>{entity_name}</strong> (Filer ID: {filer_id}).</p>
|
||||
|
||||
<p>We will:</p>
|
||||
<ol style="font-size:14px;color:#374151;padding-left:1.25rem">
|
||||
<li>File a final Form 499-A with zero revenue</li>
|
||||
<li>Request USAC to close your filer account</li>
|
||||
<li>Confirm discontinuance of related obligations (CPNI, RMD)</li>
|
||||
</ol>
|
||||
|
||||
<p>You'll receive a confirmation email once the discontinuance is processed
|
||||
by USAC. This typically takes 2-4 weeks.</p>
|
||||
|
||||
<p style="font-size:13px;color:#6b7280;margin-top:1rem">
|
||||
Order: {order_number}<br>
|
||||
Questions? Reply to this email or contact
|
||||
<a href="mailto:ops@performancewest.net">ops@performancewest.net</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["From"] = os.environ.get("SMTP_FROM", "Performance West <noreply@performancewest.net>")
|
||||
msg["To"] = to
|
||||
msg["Subject"] = subject
|
||||
msg.attach(MIMEText(html, "html"))
|
||||
|
||||
with smtplib.SMTP(
|
||||
os.environ.get("SMTP_HOST", "co.carrierone.com"),
|
||||
int(os.environ.get("SMTP_PORT", "587")),
|
||||
timeout=30,
|
||||
) as s:
|
||||
s.starttls()
|
||||
s.login(os.environ.get("SMTP_USER", ""), os.environ.get("SMTP_PASS", ""))
|
||||
s.send_message(msg)
|
||||
except Exception as exc:
|
||||
logger.warning("Discontinuance confirmation email failed: %s", exc)
|
||||
130
scripts/workers/services/form_499q.py
Normal file
130
scripts/workers/services/form_499q.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"""FCC Form 499-Q Quarterly Filing Handler.
|
||||
|
||||
Simplified filing: the client submits quarterly revenue via the intake page,
|
||||
and this handler files it at USAC E-File. Revenue calculations are minimal
|
||||
compared to the 499-A — just four revenue buckets projected for the quarter.
|
||||
|
||||
The 499-Q determines quarterly USF contribution payments.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from .base_handler import BaseComplianceHandler
|
||||
|
||||
logger = logging.getLogger("workers.services.form_499q")
|
||||
|
||||
|
||||
class Form499QHandler(BaseComplianceHandler):
|
||||
SERVICE_SLUG = "fcc-499q"
|
||||
SERVICE_NAME = "FCC Form 499-Q Quarterly Filing"
|
||||
|
||||
async def process(self, order_data: dict) -> dict | None:
|
||||
order_number = order_data.get("order_number", "")
|
||||
entity = order_data.get("entity", {})
|
||||
intake_data = order_data.get("intake_data", {})
|
||||
|
||||
if not intake_data.get("intake_completed"):
|
||||
logger.info(
|
||||
"Form499QHandler: %s intake not completed — waiting for client",
|
||||
order_number,
|
||||
)
|
||||
self._create_admin_todo(
|
||||
order_number,
|
||||
f"499-Q {intake_data.get('quarter', '?')} for "
|
||||
f"{entity.get('legal_name', '?')} — awaiting client intake. "
|
||||
f"Due {intake_data.get('due_date', '?')}.",
|
||||
)
|
||||
return None
|
||||
|
||||
quarter = intake_data.get("quarter", "?")
|
||||
revenue = intake_data.get("revenue", {})
|
||||
filer_id = intake_data.get("filer_id_499") or entity.get("filer_id_499", "")
|
||||
frn = intake_data.get("frn") or entity.get("frn", "")
|
||||
|
||||
logger.info(
|
||||
"Form499QHandler: processing %s %s for %s (total: $%.2f)",
|
||||
order_number, quarter,
|
||||
entity.get("legal_name", "?"),
|
||||
revenue.get("total", 0),
|
||||
)
|
||||
|
||||
# Create admin todo with filing instructions
|
||||
# (Full Playwright automation for USAC E-File 499-Q TBD)
|
||||
self._create_admin_todo(
|
||||
order_number,
|
||||
f"FILE 499-Q {quarter} for {entity.get('legal_name', '?')} "
|
||||
f"(FRN: {frn}, Filer ID: {filer_id})\n\n"
|
||||
f"Revenue:\n"
|
||||
f" Carrier's Carrier Interstate: ${revenue.get('carriers_carrier_interstate', 0):.2f}\n"
|
||||
f" Carrier's Carrier Intrastate: ${revenue.get('carriers_carrier_intrastate', 0):.2f}\n"
|
||||
f" End-User Interstate: ${revenue.get('end_user_interstate', 0):.2f}\n"
|
||||
f" End-User Intrastate: ${revenue.get('end_user_intrastate', 0):.2f}\n"
|
||||
f" Total: ${revenue.get('total', 0):.2f}\n\n"
|
||||
f"Due: {intake_data.get('due_date', '?')}\n"
|
||||
f"Parent 499-A: {intake_data.get('parent_499a_order', '?')}\n\n"
|
||||
f"File at: https://forms.universalservice.org/",
|
||||
)
|
||||
|
||||
# Send confirmation email to client
|
||||
self._send_confirmation_email(
|
||||
to=entity.get("contact_email") or order_data.get("customer_email", ""),
|
||||
entity_name=entity.get("legal_name", ""),
|
||||
order_number=order_number,
|
||||
quarter=quarter,
|
||||
due_date=intake_data.get("due_date", ""),
|
||||
)
|
||||
|
||||
return {"status": "submitted_for_filing"}
|
||||
|
||||
def _send_confirmation_email(
|
||||
self, to: str, entity_name: str, order_number: str,
|
||||
quarter: str, due_date: str,
|
||||
) -> None:
|
||||
if not to:
|
||||
return
|
||||
try:
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
subject = f"499-Q {quarter} Filing Received — {entity_name}"
|
||||
html = f"""
|
||||
<div style="font-family:Inter,sans-serif;max-width:600px;margin:0 auto;color:#1f2937">
|
||||
<div style="background:#1e3a5f;padding:16px 24px;border-radius:8px 8px 0 0">
|
||||
<h2 style="color:#fff;margin:0;font-size:16px">Form 499-Q {quarter} — Filing Received</h2>
|
||||
</div>
|
||||
<div style="padding:24px;border:1px solid #e5e7eb;border-top:none;border-radius:0 0 8px 8px">
|
||||
<p>Your FCC Form 499-Q quarterly revenue data for <strong>{entity_name}</strong>
|
||||
({quarter}, due {due_date}) has been received.</p>
|
||||
<p>We'll file this with USAC E-File and send you a confirmation with your
|
||||
filing reference number once complete.</p>
|
||||
<p style="font-size:13px;color:#6b7280;margin-top:1rem">
|
||||
Order: {order_number}<br>
|
||||
Questions? Reply to this email or contact
|
||||
<a href="mailto:ops@performancewest.net">ops@performancewest.net</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["From"] = os.environ.get("SMTP_FROM", "Performance West <noreply@performancewest.net>")
|
||||
msg["To"] = to
|
||||
msg["Subject"] = subject
|
||||
msg.attach(MIMEText(html, "html"))
|
||||
|
||||
with smtplib.SMTP(
|
||||
os.environ.get("SMTP_HOST", "co.carrierone.com"),
|
||||
int(os.environ.get("SMTP_PORT", "587")),
|
||||
timeout=30,
|
||||
) as s:
|
||||
s.starttls()
|
||||
s.login(
|
||||
os.environ.get("SMTP_USER", ""),
|
||||
os.environ.get("SMTP_PASS", ""),
|
||||
)
|
||||
s.send_message(msg)
|
||||
except Exception as exc:
|
||||
logger.warning("499-Q confirmation email failed: %s", exc)
|
||||
Loading…
Add table
Add a link
Reference in a new issue