diff --git a/scripts/workers/services/__init__.py b/scripts/workers/services/__init__.py index 26445f1..2b5c803 100644 --- a/scripts/workers/services/__init__.py +++ b/scripts/workers/services/__init__.py @@ -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", diff --git a/scripts/workers/services/form_499a_discontinuance.py b/scripts/workers/services/form_499a_discontinuance.py new file mode 100644 index 0000000..4347872 --- /dev/null +++ b/scripts/workers/services/form_499a_discontinuance.py @@ -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""" +
+
+

Form 499-A Discontinuance

+
+
+

We've received your request to discontinue the FCC Form 499-A filing + obligation for {entity_name} (Filer ID: {filer_id}).

+ +

We will:

+
    +
  1. File a final Form 499-A with zero revenue
  2. +
  3. Request USAC to close your filer account
  4. +
  5. Confirm discontinuance of related obligations (CPNI, RMD)
  6. +
+ +

You'll receive a confirmation email once the discontinuance is processed + by USAC. This typically takes 2-4 weeks.

+ +

+ Order: {order_number}
+ Questions? Reply to this email or contact + ops@performancewest.net. +

+
+
+ """ + msg = MIMEMultipart("alternative") + msg["From"] = os.environ.get("SMTP_FROM", "Performance West ") + 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) diff --git a/scripts/workers/services/form_499q.py b/scripts/workers/services/form_499q.py new file mode 100644 index 0000000..b0da0e4 --- /dev/null +++ b/scripts/workers/services/form_499q.py @@ -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""" +
+
+

Form 499-Q {quarter} — Filing Received

+
+
+

Your FCC Form 499-Q quarterly revenue data for {entity_name} + ({quarter}, due {due_date}) has been received.

+

We'll file this with USAC E-File and send you a confirmation with your + filing reference number once complete.

+

+ Order: {order_number}
+ Questions? Reply to this email or contact + ops@performancewest.net. +

+
+
+ """ + msg = MIMEMultipart("alternative") + msg["From"] = os.environ.get("SMTP_FROM", "Performance West ") + 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) diff --git a/site/public/order/fcc-499q/index.html b/site/public/order/fcc-499q/index.html new file mode 100644 index 0000000..4a9e4c8 --- /dev/null +++ b/site/public/order/fcc-499q/index.html @@ -0,0 +1,235 @@ + + + + + +FCC Form 499-Q Quarterly Filing — Performance West Inc. + + + + + + +
+

FCC Form 499-Q — Quarterly Filing

+

Report your projected quarterly USF contributions based on actual telecom revenue for the prior quarter.

+ +
Loading order details...
+ + + + + + +
+ + + + diff --git a/site/public/order/fcc-compliance/index.html b/site/public/order/fcc-compliance/index.html index 961a8e1..e25c021 100644 --- a/site/public/order/fcc-compliance/index.html +++ b/site/public/order/fcc-compliance/index.html @@ -145,6 +145,7 @@ var CHECK_TO_SLUG = { cpni:"cpni-certification", cpni_certification:"cpni-certification", "499a":"fcc-499a", form_499a:"fcc-499a", form_499a_zero:"fcc-499a-zero", "499a-zero":"fcc-499a-zero", + form_499a_disc:"fcc-499a-discontinuance", "499a-disc":"fcc-499a-discontinuance", "499q":"fcc-499a-499q", form_499q:"fcc-499a-499q", rmd:"rmd-filing", rmd_filing:"rmd-filing", stir:"stir-shaken", stir_shaken:"stir-shaken", @@ -161,6 +162,7 @@ var SLUG_META = { "cpni-certification": {label:"CPNI Annual Certification", price:14900, desc:"47 CFR § 64.2009 annual CPNI cert filed to FCC ECFS", order:"/order/cpni-certification"}, "fcc-499a": {label:"FCC Form 499-A Filing", price:49900, desc:"Annual USF contribution filing at USAC E-File", order:"/order/fcc-499a"}, "fcc-499a-zero": {label:"FCC Form 499-A (Zero Revenue)", price:17900, desc:"For carriers with no telecom revenue", order:"/order/fcc-499a"}, + "fcc-499a-discontinuance":{label:"Form 499-A Discontinuance", price:29900, desc:"Close out USAC filer registration", order:"/order/fcc-499a"}, "fcc-499a-499q": {label:"FCC 499-A + 499-Q Bundle", price:59900, desc:"Annual + quarterly USF projections", order:"/order/fcc-499a-499q"}, "rmd-filing": {label:"RMD Registration / Recertification", price:21900, gov_fee:10000, gov_fee_label:"FCC RMD filing fee", desc:"Robocall Mitigation Database filing + $100 FCC filing fee", order:"/order/rmd-filing"}, "stir-shaken": {label:"STIR/SHAKEN Implementation", price:49900, desc:"STIR/SHAKEN certificate + attestation setup", order:"/order/stir-shaken"}, diff --git a/site/src/pages/tools/fcc-compliance-check.astro b/site/src/pages/tools/fcc-compliance-check.astro index be0d2fb..771f4f6 100644 --- a/site/src/pages/tools/fcc-compliance-check.astro +++ b/site/src/pages/tools/fcc-compliance-check.astro @@ -682,6 +682,7 @@ import Base from "../../layouts/Base.astro"; if (check.id === "form_499a" && (status === "red" || status === "yellow")) { function resetF499() { check.status = status; + check._499aVariant = null; const cr = colorMap[status]; card.className = `${cr.bg} ${cr.border} border rounded-xl p-4 flex items-start gap-3`; card.innerHTML = inner; @@ -718,14 +719,16 @@ import Base from "../../layouts/Base.astro"; card.querySelector(".flex-1")?.appendChild(followUp); followUp.querySelector(".f499-keep")?.addEventListener("click", () => { + check._499aVariant = "zero"; showF499Result( `${eName} had no telecom revenue but wants to keep the filer registration active. A zero-revenue 499-A must be filed for each year with an active filer ID. We can file these for you.`, "yellow" ); }); followUp.querySelector(".f499-deregister")?.addEventListener("click", () => { + check._499aVariant = "discontinuance"; showF499Result( - `${eName} wants to cancel the USAC filer registration. All outstanding 499-A filings must be current first (including zero-revenue returns). We can handle the catch-up filings and submit the cancellation request to USAC on your behalf.`, + `${eName} wants to cancel the USAC filer registration. We'll file any outstanding zero-revenue 499-A returns and submit the discontinuance request to USAC on your behalf.`, "yellow" ); }); @@ -777,8 +780,14 @@ import Base from "../../layouts/Base.astro"; services.push({ id: "stir_shaken", name: "STIR/SHAKEN Implementation", desc: "", price: 499 }); break; case "form_499a": - services.push({ id: "form_499a", name: "Form 499-A Filing", desc: "with revenue", price: 499 }); - services.push({ id: "form_499a_zero", name: "Form 499-A (Zero Revenue)", desc: "no telecom revenue", price: 179, altOf: "form_499a" }); + if (check._499aVariant === "discontinuance") { + services.push({ id: "form_499a_disc", name: "Form 499-A Discontinuance", desc: "close filer account", price: 299 }); + } else if (check._499aVariant === "zero") { + services.push({ id: "form_499a_zero", name: "Form 499-A (Zero Revenue)", desc: "no telecom revenue", price: 179 }); + } else { + services.push({ id: "form_499a", name: "Form 499-A Filing", desc: "with revenue", price: 499 }); + services.push({ id: "form_499a_zero", name: "Form 499-A (Zero Revenue)", desc: "no telecom revenue", price: 179, altOf: "form_499a" }); + } if (s === "red") { services.push({ id: "dc_agent", name: "D.C. Registered Agent", desc: "", price: 99, priceLabel: "$99/yr" }); }