Add USAC Filer ID Deactivation Letter template
Formal letter addressed to USAC Contributor Operations requesting
deactivation of a 499 Filer ID. Covers:
- Entity identification (name, Filer ID, FRN, EIN)
- Reason for deactivation + termination date
- Final 499-A status (zero-revenue included OR filed separately)
- Successor entity info (if applicable)
- Outstanding balance acknowledgment
- Related filings confirmation (RMD, CPNI, BDC)
- Officer signature block
- Entity summary box
Handler updated to:
- Generate the letter via document_gen template
- Upload DOCX to MinIO (compliance/{order_number}/)
- Reference the letter in admin todo
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
eee2aa497b
commit
a404cb1b57
2 changed files with 341 additions and 3 deletions
|
|
@ -0,0 +1,295 @@
|
|||
"""
|
||||
Generate USAC Filer ID Deactivation Request Letter.
|
||||
|
||||
Produces a formal letter requesting USAC to deactivate a 499 Filer ID
|
||||
pursuant to the FCC Form 499-A filing obligations. The letter is
|
||||
submitted to USAC Contributor Operations (Form499@usac.org) along
|
||||
with a final 499-A filing.
|
||||
|
||||
Per 2026 FCC Form 499-A Instructions:
|
||||
- Filers that cease providing telecommunications must deactivate
|
||||
their Filer ID with USAC by submitting a letter with termination
|
||||
date and successor entity information
|
||||
- Must be submitted within 30 days of ceasing service
|
||||
- Processing takes 60-90 days
|
||||
|
||||
Usage:
|
||||
from scripts.document_gen.templates.form_499a_discontinuance_letter_generator import (
|
||||
generate_discontinuance_letter,
|
||||
)
|
||||
path = generate_discontinuance_letter(
|
||||
entity_name="Acme Telecom LLC",
|
||||
filer_id="829999",
|
||||
frn="0015341902",
|
||||
...
|
||||
)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
LOG = logging.getLogger("document_gen.discontinuance_letter")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml.ns import qn
|
||||
from docx.oxml import OxmlElement
|
||||
except ImportError:
|
||||
LOG.warning("python-docx not installed — discontinuance letter generation unavailable")
|
||||
Document = None # type: ignore
|
||||
|
||||
NAVY = RGBColor(0x1A, 0x27, 0x44) if Document else None
|
||||
BODY_SIZE = Pt(10) if Document else None
|
||||
HEADING_SIZE = Pt(12) if Document else None
|
||||
|
||||
|
||||
def _add_body(doc, text: str, bold: bool = False, italic: bool = False,
|
||||
size=None, color=None, alignment=None) -> None:
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(6)
|
||||
if alignment:
|
||||
p.alignment = alignment
|
||||
run = p.add_run(text)
|
||||
run.font.size = size or BODY_SIZE
|
||||
run.bold = bold
|
||||
run.italic = italic
|
||||
if color:
|
||||
run.font.color.rgb = color
|
||||
|
||||
|
||||
def _add_horizontal_rule(doc) -> None:
|
||||
p = doc.add_paragraph()
|
||||
pPr = p._p.get_or_add_pPr()
|
||||
pBdr = OxmlElement("w:pBdr")
|
||||
bottom = OxmlElement("w:bottom")
|
||||
bottom.set(qn("w:val"), "single")
|
||||
bottom.set(qn("w:sz"), "6")
|
||||
bottom.set(qn("w:space"), "1")
|
||||
bottom.set(qn("w:color"), "1A2744")
|
||||
pBdr.append(bottom)
|
||||
pPr.append(pBdr)
|
||||
|
||||
|
||||
def generate_discontinuance_letter(
|
||||
# Entity
|
||||
entity_name: str,
|
||||
filer_id: str = "",
|
||||
frn: str = "",
|
||||
ein: str = "",
|
||||
address: str = "",
|
||||
# Contact / Officer
|
||||
officer_name: str = "",
|
||||
officer_title: str = "",
|
||||
officer_email: str = "",
|
||||
officer_phone: str = "",
|
||||
# Discontinuance details
|
||||
termination_date: str = "",
|
||||
discontinuance_reason: str = "Company is no longer providing telecommunications services",
|
||||
successor_entity: str = "",
|
||||
successor_filer_id: str = "",
|
||||
last_filing_year: int = 0,
|
||||
# Flags
|
||||
includes_final_zero_filing: bool = True,
|
||||
outstanding_balances: bool = False,
|
||||
# Output
|
||||
output_path: str = "/tmp/discontinuance_letter.docx",
|
||||
) -> Optional[str]:
|
||||
"""Generate the USAC Filer ID Deactivation Request Letter as a DOCX file."""
|
||||
if Document is None:
|
||||
LOG.error("python-docx not installed")
|
||||
return None
|
||||
|
||||
today = datetime.now()
|
||||
today_str = today.strftime("%B %d, %Y")
|
||||
term_date = termination_date or today_str
|
||||
filing_year = last_filing_year or (today.year - 1)
|
||||
|
||||
doc = Document()
|
||||
for section in doc.sections:
|
||||
section.top_margin = Inches(1)
|
||||
section.bottom_margin = Inches(1)
|
||||
section.left_margin = Inches(1.25)
|
||||
section.right_margin = Inches(1.25)
|
||||
|
||||
# ── Date ──
|
||||
_add_body(doc, today_str, alignment=WD_ALIGN_PARAGRAPH.RIGHT)
|
||||
|
||||
# ── Addressee ──
|
||||
_add_body(doc, "USAC Contributor Operations", bold=True)
|
||||
_add_body(doc, "Universal Service Administrative Company")
|
||||
_add_body(doc, "700 12th Street NW, Suite 900")
|
||||
_add_body(doc, "Washington, DC 20005")
|
||||
_add_body(doc, "Email: Form499@usac.org")
|
||||
_add_body(doc, "Phone: (888) 641-8722")
|
||||
|
||||
_add_horizontal_rule(doc)
|
||||
|
||||
# ── Subject line ──
|
||||
_add_body(doc, "", size=Pt(4)) # spacer
|
||||
_add_body(
|
||||
doc,
|
||||
f"Re: Request for Deactivation of 499 Filer ID — {entity_name}",
|
||||
bold=True, size=HEADING_SIZE, color=NAVY,
|
||||
)
|
||||
if filer_id:
|
||||
_add_body(doc, f"Filer ID: {filer_id}", bold=True)
|
||||
if frn:
|
||||
_add_body(doc, f"FCC Registration Number (FRN): {frn}", bold=True)
|
||||
|
||||
_add_body(doc, "", size=Pt(4)) # spacer
|
||||
|
||||
# ── Body ──
|
||||
_add_body(doc, "Dear USAC Contributor Operations:")
|
||||
|
||||
# Paragraph 1: Purpose
|
||||
_add_body(
|
||||
doc,
|
||||
f"On behalf of {entity_name}"
|
||||
f"{' (Filer ID: ' + filer_id + ')' if filer_id else ''}"
|
||||
f"{' (FRN: ' + frn + ')' if frn else ''}, "
|
||||
f"this letter serves as a formal request to deactivate the above-referenced "
|
||||
f"499 Filer ID in accordance with the FCC Form 499-A filing instructions "
|
||||
f"and USAC's Filer ID management procedures."
|
||||
)
|
||||
|
||||
# Paragraph 2: Reason
|
||||
_add_body(
|
||||
doc,
|
||||
f"Reason for deactivation: {discontinuance_reason}. "
|
||||
f"The company ceased providing telecommunications services "
|
||||
f"as of {term_date}."
|
||||
)
|
||||
|
||||
# Paragraph 3: Final filing
|
||||
if includes_final_zero_filing:
|
||||
_add_body(
|
||||
doc,
|
||||
f"A final FCC Form 499-A has been filed for the {filing_year} reporting year "
|
||||
f"with zero telecommunications revenue, reflecting the cessation of all "
|
||||
f"regulated services. Line 603 exemptions (TRS, NANPA, LNP) have been "
|
||||
f"claimed with the notation 'Not in business as of {term_date}' on the "
|
||||
f"explanation line."
|
||||
)
|
||||
else:
|
||||
_add_body(
|
||||
doc,
|
||||
f"A final FCC Form 499-A for the {filing_year} reporting year has been "
|
||||
f"filed separately, reporting actual telecommunications revenue for the "
|
||||
f"period during which the company was in service. Line 603 exemptions "
|
||||
f"(TRS, NANPA, LNP) have been claimed with the notation 'Not in business "
|
||||
f"as of {term_date}' on the explanation line."
|
||||
)
|
||||
|
||||
# Paragraph 4: Successor entity (if applicable)
|
||||
if successor_entity:
|
||||
_add_body(
|
||||
doc,
|
||||
f"Successor entity information: {successor_entity}"
|
||||
f"{' (Filer ID: ' + successor_filer_id + ')' if successor_filer_id else ''}. "
|
||||
f"The successor entity has assumed responsibility for any outstanding "
|
||||
f"universal service contribution obligations, true-up payments, and "
|
||||
f"continuation of regulated services, if any."
|
||||
)
|
||||
else:
|
||||
_add_body(
|
||||
doc,
|
||||
f"There is no successor entity. {entity_name} has permanently ceased "
|
||||
f"all telecommunications operations and does not intend to resume service."
|
||||
)
|
||||
|
||||
# Paragraph 5: Outstanding balances
|
||||
if outstanding_balances:
|
||||
_add_body(
|
||||
doc,
|
||||
f"{entity_name} acknowledges that any outstanding USF contribution "
|
||||
f"obligations, true-up payments, or refunds will be settled through "
|
||||
f"the normal USAC invoicing process prior to final account closure."
|
||||
)
|
||||
else:
|
||||
_add_body(
|
||||
doc,
|
||||
f"{entity_name} believes there are no outstanding USF contribution "
|
||||
f"obligations or unpaid invoices. If any final true-up is required, "
|
||||
f"please contact the undersigned for prompt resolution."
|
||||
)
|
||||
|
||||
# Paragraph 6: Related filings
|
||||
_add_body(
|
||||
doc,
|
||||
f"In connection with this deactivation, {entity_name} confirms that "
|
||||
f"all related FCC filing obligations are being addressed, including "
|
||||
f"Robocall Mitigation Database (RMD) certification, CPNI annual "
|
||||
f"certification, and Broadband Data Collection (BDC) filings, as "
|
||||
f"applicable."
|
||||
)
|
||||
|
||||
# Paragraph 7: CORES update
|
||||
_add_body(
|
||||
doc,
|
||||
f"{entity_name} will update its FCC CORES registration to reflect "
|
||||
f"inactive status following confirmation of this deactivation."
|
||||
)
|
||||
|
||||
# Paragraph 8: Request
|
||||
_add_body(
|
||||
doc,
|
||||
f"We respectfully request that USAC process this deactivation and "
|
||||
f"confirm in writing when the Filer ID has been deactivated and "
|
||||
f"removed from future invoicing cycles. Please direct any questions "
|
||||
f"or requests for additional documentation to the contact below."
|
||||
)
|
||||
|
||||
_add_body(doc, "", size=Pt(8)) # spacer
|
||||
|
||||
# ── Closing ──
|
||||
_add_body(doc, "Respectfully submitted,")
|
||||
_add_body(doc, "", size=Pt(20)) # signature space
|
||||
|
||||
_add_body(doc, "_" * 40)
|
||||
if officer_name:
|
||||
_add_body(doc, officer_name, bold=True)
|
||||
if officer_title:
|
||||
_add_body(doc, officer_title)
|
||||
_add_body(doc, entity_name)
|
||||
if officer_email:
|
||||
_add_body(doc, f"Email: {officer_email}")
|
||||
if officer_phone:
|
||||
_add_body(doc, f"Phone: {officer_phone}")
|
||||
|
||||
_add_horizontal_rule(doc)
|
||||
|
||||
# ── Entity summary box ──
|
||||
_add_body(doc, "Entity Summary", bold=True, size=HEADING_SIZE, color=NAVY)
|
||||
summary_lines = [
|
||||
f"Legal Name: {entity_name}",
|
||||
f"Filer ID: {filer_id or 'N/A'}",
|
||||
f"FRN: {frn or 'N/A'}",
|
||||
f"EIN: {ein or 'N/A'}",
|
||||
f"Address: {address or 'N/A'}",
|
||||
f"Termination Date: {term_date}",
|
||||
f"Reason: {discontinuance_reason}",
|
||||
f"Successor Entity: {successor_entity or 'None'}",
|
||||
f"Final 499-A: {'Zero-revenue filing included' if includes_final_zero_filing else 'Filed separately with actual revenue'}",
|
||||
]
|
||||
for line in summary_lines:
|
||||
_add_body(doc, line, size=Pt(9), color=RGBColor(0x66, 0x66, 0x66))
|
||||
|
||||
# ── Footer ──
|
||||
_add_body(doc, "", size=Pt(6))
|
||||
_add_body(
|
||||
doc,
|
||||
f"Prepared by Performance West Inc. on behalf of {entity_name}. "
|
||||
f"Generated {today_str}.",
|
||||
italic=True, size=Pt(8), color=RGBColor(0x99, 0x99, 0x99),
|
||||
)
|
||||
|
||||
# Save
|
||||
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(output_path)
|
||||
LOG.info("Discontinuance letter generated: %s", output_path)
|
||||
return output_path
|
||||
|
|
@ -40,11 +40,53 @@ class Form499ADiscontinuanceHandler(BaseComplianceHandler):
|
|||
|
||||
discontinuance_reason = intake_data.get("discontinuance_reason", "Ceased providing telecommunications services")
|
||||
last_service_date = intake_data.get("last_service_date", "")
|
||||
# If ordered as standalone discontinuance ($299), includes a zero-revenue
|
||||
# final filing. If ordered with a full 499-A ($499+$299), the 499-A handler
|
||||
# files the revenue report separately — we just handle the deactivation.
|
||||
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)
|
||||
|
|
@ -79,6 +121,7 @@ class Form499ADiscontinuanceHandler(BaseComplianceHandler):
|
|||
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', '')}",
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue