From a404cb1b5794fc5205716409272c4b3c7041dd38 Mon Sep 17 00:00:00 2001 From: justin Date: Sun, 3 May 2026 06:15:26 -0500 Subject: [PATCH] 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) --- ...rm_499a_discontinuance_letter_generator.py | 295 ++++++++++++++++++ .../services/form_499a_discontinuance.py | 49 ++- 2 files changed, 341 insertions(+), 3 deletions(-) create mode 100644 scripts/document_gen/templates/form_499a_discontinuance_letter_generator.py diff --git a/scripts/document_gen/templates/form_499a_discontinuance_letter_generator.py b/scripts/document_gen/templates/form_499a_discontinuance_letter_generator.py new file mode 100644 index 0000000..27d4750 --- /dev/null +++ b/scripts/document_gen/templates/form_499a_discontinuance_letter_generator.py @@ -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 diff --git a/scripts/workers/services/form_499a_discontinuance.py b/scripts/workers/services/form_499a_discontinuance.py index fb9fc1d..c811c60 100644 --- a/scripts/workers/services/form_499a_discontinuance.py +++ b/scripts/workers/services/form_499a_discontinuance.py @@ -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', '')}", )