499-Q Handler: - Auto-filing toggle integration (same as 499-A) - Playwright USAC E-File submission for quarterly form - Revenue field filling (4 categories) - Confirmation number capture + PDF save - Client receives "data received" email immediately, then "filed successfully" email with confirmation number after submission - Falls back to admin todo if Playwright/session unavailable Discontinuance Handler: - Auto-emails deactivation letter to USAC (Form499@usac.org) with DOCX attachment + entity summary in body - CC to admin email for records - Dev mode: redirects USAC email to admin instead - Client confirmation email with process timeline Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
249 lines
13 KiB
Python
249 lines
13 KiB
Python
"""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", "")
|
|
includes_zero_filing = not intake_data.get("has_separate_499a", False)
|
|
|
|
# ── Generate USAC deactivation letter ──────────────────────────
|
|
letter_path = None
|
|
try:
|
|
from scripts.document_gen.templates.form_499a_discontinuance_letter_generator import (
|
|
generate_discontinuance_letter,
|
|
)
|
|
import tempfile
|
|
work_dir = tempfile.mkdtemp(prefix=f"disc_{order_number}_")
|
|
date_str = datetime.now().strftime("%Y%m%d")
|
|
docx_path = os.path.join(
|
|
work_dir,
|
|
f"usac_deactivation_letter_{order_number}_{date_str}.docx",
|
|
)
|
|
letter_path = generate_discontinuance_letter(
|
|
entity_name=legal_name,
|
|
filer_id=filer_id,
|
|
frn=frn,
|
|
ein=entity.get("ein", ""),
|
|
address=entity.get("address", intake_data.get("address", "")),
|
|
officer_name=intake_data.get("officer_name") or entity.get("contact_name", ""),
|
|
officer_title=intake_data.get("officer_title") or entity.get("contact_title", ""),
|
|
officer_email=entity.get("contact_email") or order_data.get("customer_email", ""),
|
|
officer_phone=entity.get("contact_phone") or order_data.get("customer_phone", ""),
|
|
termination_date=last_service_date,
|
|
discontinuance_reason=discontinuance_reason,
|
|
successor_entity=intake_data.get("successor_entity", ""),
|
|
successor_filer_id=intake_data.get("successor_filer_id", ""),
|
|
last_filing_year=int(entity.get("last_filing_year") or 0),
|
|
includes_final_zero_filing=includes_zero_filing,
|
|
outstanding_balances=intake_data.get("outstanding_balances", False),
|
|
output_path=docx_path,
|
|
)
|
|
if letter_path:
|
|
logger.info("Discontinuance letter generated: %s", letter_path)
|
|
# Upload to MinIO
|
|
try:
|
|
from scripts.workers.minio_client import upload_file
|
|
minio_key = f"compliance/{order_number}/usac_deactivation_letter_{date_str}.docx"
|
|
upload_file(letter_path, minio_key)
|
|
logger.info("Uploaded to MinIO: %s", minio_key)
|
|
except Exception as exc:
|
|
logger.warning("MinIO upload failed: %s", exc)
|
|
except Exception as exc:
|
|
logger.warning("Discontinuance letter generation failed: %s", exc)
|
|
|
|
# Per FCC 499-A Instructions: discontinuance requires TWO steps:
|
|
# 1. File the final 499-A (may have actual revenue from the portion
|
|
# of the year the company operated — NOT required to be zero)
|
|
# 2. Submit a deactivation letter to USAC within 30 days of ceasing service
|
|
#
|
|
# Line 603: check TRS/LNP/NANPA exemption boxes, write
|
|
# "Not in business as of filing date" on the explanation line
|
|
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"STEP 1 — File Final 499-A {'(ZERO REVENUE — included in this order)' if includes_zero_filing else '(filed separately via full 499-A order)'}:\n"
|
|
f" Log in to USAC E-File (https://forms.universalservice.org/)\n"
|
|
f" {'File a zero-revenue 499-A (all revenue lines $0).' if includes_zero_filing else 'The full 499-A with actual revenue is being filed under a separate order.'}\n"
|
|
f" On Line 603, check all exemption boxes (TRS, LNP, NANPA)\n"
|
|
f" and write 'Not in business as of {last_service_date or 'filing date'}'\n"
|
|
f" on the explanation line.\n\n"
|
|
f"STEP 2 — Submit USAC Deactivation Letter:\n"
|
|
f" Send letter to USAC (Form499@usac.org) with:\n"
|
|
f" - Company name: {legal_name}\n"
|
|
f" - Filer ID: {filer_id}\n"
|
|
f" - FRN: {frn}\n"
|
|
f" - Termination date: {last_service_date or 'TBD'}\n"
|
|
f" - Reason: {discontinuance_reason}\n"
|
|
f" - Successor entity: {intake_data.get('successor_entity', 'None')}\n"
|
|
f" Must be submitted within 30 days of ceasing service.\n"
|
|
f" Processing takes up to 60-90 days.\n\n"
|
|
f"STEP 3 — Update CORES:\n"
|
|
f" Update FCC CORES registration to reflect inactive status.\n\n"
|
|
f"STEP 4 — Related Filings:\n"
|
|
f" Confirm CPNI, RMD, and BDC filings are also discontinued.\n\n"
|
|
f"DEACTIVATION LETTER: {'Generated — check MinIO compliance/' + order_number + '/' if letter_path else 'GENERATION FAILED — draft manually'}\n\n"
|
|
f"Client email: {entity.get('contact_email') or order_data.get('customer_email', '')}",
|
|
)
|
|
|
|
# ── Auto-email deactivation letter to USAC ──────────────────────
|
|
# On prod with auto-filing enabled, sends the letter directly.
|
|
# On dev, sends to admin for review.
|
|
usac_email = os.environ.get("USAC_DEACTIVATION_EMAIL", "Form499@usac.org")
|
|
admin_email = os.environ.get("ADMIN_EMAIL", "ops@performancewest.net")
|
|
|
|
# In dev/test mode, redirect USAC emails to admin
|
|
if os.environ.get("NODE_ENV") == "development":
|
|
usac_email = admin_email
|
|
logger.info("Dev mode: redirecting USAC deactivation to %s", usac_email)
|
|
|
|
if letter_path:
|
|
try:
|
|
import smtplib
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.mime.text import MIMEText
|
|
from email.mime.base import MIMEBase
|
|
from email import encoders
|
|
|
|
msg = MIMEMultipart()
|
|
msg["From"] = os.environ.get("SMTP_FROM", "Performance West <noreply@performancewest.net>")
|
|
msg["To"] = usac_email
|
|
msg["Cc"] = admin_email
|
|
msg["Subject"] = f"Filer ID Deactivation Request — {legal_name} (Filer ID: {filer_id})"
|
|
|
|
body = (
|
|
f"Please find attached a formal request to deactivate the 499 Filer ID "
|
|
f"for {legal_name} (Filer ID: {filer_id}, FRN: {frn}).\n\n"
|
|
f"Termination date: {last_service_date or 'See attached letter'}\n"
|
|
f"Reason: {discontinuance_reason}\n\n"
|
|
f"Please confirm deactivation at your earliest convenience.\n\n"
|
|
f"Submitted by Performance West Inc. on behalf of {legal_name}.\n"
|
|
f"Contact: {admin_email}"
|
|
)
|
|
msg.attach(MIMEText(body, "plain"))
|
|
|
|
# Attach the letter
|
|
with open(letter_path, "rb") as f:
|
|
part = MIMEBase("application", "vnd.openxmlformats-officedocument.wordprocessingml.document")
|
|
part.set_payload(f.read())
|
|
encoders.encode_base64(part)
|
|
part.add_header("Content-Disposition", f'attachment; filename="{os.path.basename(letter_path)}"')
|
|
msg.attach(part)
|
|
|
|
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)
|
|
logger.info("Deactivation letter emailed to %s (cc: %s)", usac_email, admin_email)
|
|
except Exception as exc:
|
|
logger.warning("Failed to email deactivation letter: %s", exc)
|
|
|
|
# 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", "letter_generated": bool(letter_path)}
|
|
|
|
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 your final Form 499-A reporting revenue for the period you were in service (this may be zero or actual revenue for a partial year)</li>
|
|
<li>Submit a deactivation letter to USAC requesting closure of your filer account</li>
|
|
<li>Update your FCC CORES registration to reflect inactive status</li>
|
|
<li>Confirm discontinuance of related obligations (CPNI, RMD, BDC)</li>
|
|
</ol>
|
|
|
|
<p>USAC processing takes 60-90 days. You'll receive a confirmation
|
|
email at each step. During this period, you won't receive new
|
|
invoices for USF contributions.</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)
|