Automate 499-Q USAC filing + discontinuance letter auto-email
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>
This commit is contained in:
parent
a404cb1b57
commit
f30b0383a9
2 changed files with 342 additions and 71 deletions
|
|
@ -125,6 +125,62 @@ class Form499ADiscontinuanceHandler(BaseComplianceHandler):
|
||||||
f"Client email: {entity.get('contact_email') or order_data.get('customer_email', '')}",
|
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
|
# Send confirmation to client
|
||||||
self._send_confirmation(
|
self._send_confirmation(
|
||||||
to=entity.get("contact_email") or order_data.get("customer_email", ""),
|
to=entity.get("contact_email") or order_data.get("customer_email", ""),
|
||||||
|
|
@ -133,7 +189,7 @@ class Form499ADiscontinuanceHandler(BaseComplianceHandler):
|
||||||
filer_id=filer_id,
|
filer_id=filer_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"status": "submitted_for_processing"}
|
return {"status": "submitted_for_processing", "letter_generated": bool(letter_path)}
|
||||||
|
|
||||||
def _send_confirmation(
|
def _send_confirmation(
|
||||||
self, to: str, entity_name: str, order_number: str, filer_id: str,
|
self, to: str, entity_name: str, order_number: str, filer_id: str,
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,30 @@
|
||||||
"""FCC Form 499-Q Quarterly Filing Handler.
|
"""FCC Form 499-Q Quarterly Filing Handler.
|
||||||
|
|
||||||
Simplified filing: the client submits quarterly revenue via the intake page,
|
After the client submits quarterly revenue via /order/fcc-499q, this handler:
|
||||||
and this handler files it at USAC E-File. Revenue calculations are minimal
|
1. Generates a 499-Q prep summary
|
||||||
compared to the 499-A — just four revenue buckets projected for the quarter.
|
2. Checks auto-filing toggle (same as 499-A)
|
||||||
|
3. If auto-filing enabled: submits to USAC E-File via Playwright
|
||||||
|
4. Captures confirmation number, sends client confirmation email
|
||||||
|
5. Records filing in the database
|
||||||
|
|
||||||
The 499-Q determines quarterly USF contribution payments.
|
The 499-Q is simpler than the 499-A — just 4 revenue categories
|
||||||
|
projected for the quarter, determining the quarterly USF contribution.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from .base_handler import BaseComplianceHandler
|
from .base_handler import BaseComplianceHandler
|
||||||
|
from .telecom.auto_filing import check_auto_filing, request_admin_review
|
||||||
|
|
||||||
logger = logging.getLogger("workers.services.form_499q")
|
logger = logging.getLogger("workers.services.form_499q")
|
||||||
|
|
||||||
|
USAC_EFILE_URL = "https://forms.universalservice.org"
|
||||||
|
|
||||||
|
|
||||||
class Form499QHandler(BaseComplianceHandler):
|
class Form499QHandler(BaseComplianceHandler):
|
||||||
SERVICE_SLUG = "fcc-499q"
|
SERVICE_SLUG = "fcc-499q"
|
||||||
|
|
@ -27,88 +36,297 @@ class Form499QHandler(BaseComplianceHandler):
|
||||||
intake_data = order_data.get("intake_data", {})
|
intake_data = order_data.get("intake_data", {})
|
||||||
|
|
||||||
if not intake_data.get("intake_completed"):
|
if not intake_data.get("intake_completed"):
|
||||||
logger.info(
|
logger.info("Form499QHandler: %s intake not completed — waiting", order_number)
|
||||||
"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
|
return None
|
||||||
|
|
||||||
quarter = intake_data.get("quarter", "?")
|
quarter = intake_data.get("quarter", "?")
|
||||||
revenue = intake_data.get("revenue", {})
|
revenue = intake_data.get("revenue", {})
|
||||||
filer_id = intake_data.get("filer_id_499") or entity.get("filer_id_499", "")
|
filer_id = intake_data.get("filer_id_499") or entity.get("filer_id_499", "")
|
||||||
frn = intake_data.get("frn") or entity.get("frn", "")
|
frn = intake_data.get("frn") or entity.get("frn", "")
|
||||||
|
legal_name = entity.get("legal_name") or intake_data.get("entity_name", "")
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Form499QHandler: processing %s %s for %s (total: $%.2f)",
|
"Form499QHandler: %s %s for %s (total: $%.2f)",
|
||||||
order_number, quarter,
|
order_number, quarter, legal_name, revenue.get("total", 0),
|
||||||
entity.get("legal_name", "?"),
|
|
||||||
revenue.get("total", 0),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create admin todo with filing instructions
|
# ── Auto-filing check ──────────────────────────────────────────
|
||||||
# (Full Playwright automation for USAC E-File 499-Q TBD)
|
decision = check_auto_filing(order_data)
|
||||||
self._create_admin_todo(
|
if not decision.may_submit:
|
||||||
order_number,
|
logger.info("Form499QHandler: staging for admin review (order=%s)", order_number)
|
||||||
f"FILE 499-Q {quarter} for {entity.get('legal_name', '?')} "
|
request_admin_review(
|
||||||
f"(FRN: {frn}, Filer ID: {filer_id})\n\n"
|
order_number=order_number,
|
||||||
f"Revenue:\n"
|
service_slug=self.SERVICE_SLUG,
|
||||||
f" Carrier's Carrier Interstate: ${revenue.get('carriers_carrier_interstate', 0):.2f}\n"
|
service_name=self.SERVICE_NAME,
|
||||||
f" Carrier's Carrier Intrastate: ${revenue.get('carriers_carrier_intrastate', 0):.2f}\n"
|
entity_name=legal_name,
|
||||||
f" End-User Interstate: ${revenue.get('end_user_interstate', 0):.2f}\n"
|
frn=frn,
|
||||||
f" End-User Intrastate: ${revenue.get('end_user_intrastate', 0):.2f}\n"
|
packet_minio_paths=[],
|
||||||
f" Total: ${revenue.get('total', 0):.2f}\n\n"
|
admin_email=decision.admin_email,
|
||||||
f"Due: {intake_data.get('due_date', '?')}\n"
|
summary=(
|
||||||
f"Parent 499-A: {intake_data.get('parent_499a_order', '?')}\n\n"
|
f"499-Q {quarter} ready for {legal_name}. "
|
||||||
f"File at: https://forms.universalservice.org/",
|
f"Filer ID: {filer_id}. Total revenue: ${revenue.get('total', 0):.2f}. "
|
||||||
|
f"Due: {intake_data.get('due_date', '?')}. "
|
||||||
|
f"CC Inter: ${revenue.get('carriers_carrier_interstate', 0):.2f}, "
|
||||||
|
f"CC Intra: ${revenue.get('carriers_carrier_intrastate', 0):.2f}, "
|
||||||
|
f"EU Inter: ${revenue.get('end_user_interstate', 0):.2f}, "
|
||||||
|
f"EU Intra: ${revenue.get('end_user_intrastate', 0):.2f}. "
|
||||||
|
f"Submit via USAC E-File at {USAC_EFILE_URL}."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# Send client confirmation that we received their data
|
||||||
|
self._send_received_email(
|
||||||
|
to=entity.get("contact_email") or order_data.get("customer_email", ""),
|
||||||
|
entity_name=legal_name,
|
||||||
|
order_number=order_number,
|
||||||
|
quarter=quarter,
|
||||||
|
due_date=intake_data.get("due_date", ""),
|
||||||
|
)
|
||||||
|
return {"status": "admin_review"}
|
||||||
|
|
||||||
|
# ── USAC E-File submission via Playwright ──────────────────────
|
||||||
|
work_dir = tempfile.mkdtemp(prefix=f"499q_{order_number}_")
|
||||||
|
confirmation_number = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
confirmation_number = await self._submit_to_usac(
|
||||||
|
order_number=order_number,
|
||||||
|
entity=entity,
|
||||||
|
intake_data=intake_data,
|
||||||
|
revenue=revenue,
|
||||||
|
work_dir=work_dir,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Form499QHandler: USAC submission failed: %s", exc)
|
||||||
|
self._create_admin_todo(
|
||||||
|
order_number,
|
||||||
|
f"499-Q {quarter} AUTO-FILING FAILED for {legal_name}\n\n"
|
||||||
|
f"Error: {exc}\n\n"
|
||||||
|
f"File manually at {USAC_EFILE_URL}\n"
|
||||||
|
f"Filer ID: {filer_id}, FRN: {frn}\n"
|
||||||
|
f"Revenue total: ${revenue.get('total', 0):.2f}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Confirmation ───────────────────────────────────────────────
|
||||||
|
if confirmation_number:
|
||||||
|
logger.info("Form499QHandler: %s filed — confirmation %s", order_number, confirmation_number)
|
||||||
|
# Record filing
|
||||||
|
try:
|
||||||
|
import psycopg2
|
||||||
|
conn = psycopg2.connect(os.environ.get("DATABASE_URL", ""))
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""UPDATE compliance_orders
|
||||||
|
SET intake_data = intake_data || %s::jsonb,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE order_number = %s""",
|
||||||
|
(json.dumps({"confirmation_number": confirmation_number, "filed_at": datetime.utcnow().isoformat()}), order_number),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Could not record confirmation: %s", exc)
|
||||||
|
|
||||||
|
self._send_confirmation_email(
|
||||||
|
to=entity.get("contact_email") or order_data.get("customer_email", ""),
|
||||||
|
entity_name=legal_name,
|
||||||
|
order_number=order_number,
|
||||||
|
quarter=quarter,
|
||||||
|
confirmation=confirmation_number,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# No confirmation — send "received" email, admin will file manually
|
||||||
|
self._send_received_email(
|
||||||
|
to=entity.get("contact_email") or order_data.get("customer_email", ""),
|
||||||
|
entity_name=legal_name,
|
||||||
|
order_number=order_number,
|
||||||
|
quarter=quarter,
|
||||||
|
due_date=intake_data.get("due_date", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"status": "filed" if confirmation_number else "pending_manual", "confirmation": confirmation_number}
|
||||||
|
|
||||||
|
async def _submit_to_usac(
|
||||||
|
self, order_number: str, entity: dict, intake_data: dict,
|
||||||
|
revenue: dict, work_dir: str,
|
||||||
|
) -> str:
|
||||||
|
"""Submit 499-Q to USAC E-File via Playwright. Returns confirmation number."""
|
||||||
|
from scripts.workers.services.telecom.human_delay import human_delay
|
||||||
|
|
||||||
|
frn = intake_data.get("frn") or entity.get("frn", "")
|
||||||
|
filer_id = intake_data.get("filer_id_499") or entity.get("filer_id_499", "")
|
||||||
|
quarter = intake_data.get("quarter", "")
|
||||||
|
|
||||||
|
USAC_STORAGE_STATE = os.environ.get(
|
||||||
|
"USAC_STORAGE_STATE",
|
||||||
|
"/app/data/usac_session.json",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Send confirmation email to client
|
if not os.path.exists(USAC_STORAGE_STATE):
|
||||||
self._send_confirmation_email(
|
self._create_admin_todo(
|
||||||
to=entity.get("contact_email") or order_data.get("customer_email", ""),
|
order_number,
|
||||||
entity_name=entity.get("legal_name", ""),
|
f"499-Q {quarter}: No USAC E-File session found. "
|
||||||
order_number=order_number,
|
f"Log in at {USAC_EFILE_URL}, export session to {USAC_STORAGE_STATE}, "
|
||||||
quarter=quarter,
|
f"then re-dispatch.",
|
||||||
due_date=intake_data.get("due_date", ""),
|
)
|
||||||
)
|
return ""
|
||||||
|
|
||||||
return {"status": "submitted_for_filing"}
|
try:
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
except ImportError:
|
||||||
|
logger.warning("Playwright not available — creating admin todo")
|
||||||
|
self._create_admin_todo(
|
||||||
|
order_number,
|
||||||
|
f"499-Q {quarter}: Playwright not installed. File manually at {USAC_EFILE_URL}.",
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
|
||||||
def _send_confirmation_email(
|
confirmation_number = ""
|
||||||
self, to: str, entity_name: str, order_number: str,
|
|
||||||
quarter: str, due_date: str,
|
async with async_playwright() as p:
|
||||||
) -> None:
|
browser = await p.chromium.launch(headless=True)
|
||||||
|
context = await browser.new_context(storage_state=USAC_STORAGE_STATE)
|
||||||
|
page = await context.new_page()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await page.goto(USAC_EFILE_URL, timeout=30000)
|
||||||
|
await human_delay()
|
||||||
|
|
||||||
|
# Navigate to Form 499-Q
|
||||||
|
await page.click('text="Form 499-Q"')
|
||||||
|
await human_delay()
|
||||||
|
|
||||||
|
# Fill FRN / Filer ID
|
||||||
|
await page.fill('input[name="frn"]', frn)
|
||||||
|
await page.fill('input[name="filer_id"]', filer_id)
|
||||||
|
await human_delay()
|
||||||
|
|
||||||
|
# Revenue fields — the 499-Q has simplified revenue blocks
|
||||||
|
# Carrier's carrier revenue
|
||||||
|
cc_inter = revenue.get("carriers_carrier_interstate", 0)
|
||||||
|
cc_intra = revenue.get("carriers_carrier_intrastate", 0)
|
||||||
|
eu_inter = revenue.get("end_user_interstate", 0)
|
||||||
|
eu_intra = revenue.get("end_user_intrastate", 0)
|
||||||
|
|
||||||
|
# Fill revenue fields (exact selectors TBD from USAC form recon)
|
||||||
|
for selector, value in [
|
||||||
|
('input[name*="cc_interstate"], input[name*="carriers_carrier_inter"]', cc_inter),
|
||||||
|
('input[name*="cc_intrastate"], input[name*="carriers_carrier_intra"]', cc_intra),
|
||||||
|
('input[name*="eu_interstate"], input[name*="end_user_inter"]', eu_inter),
|
||||||
|
('input[name*="eu_intrastate"], input[name*="end_user_intra"]', eu_intra),
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
# Try each selector variant
|
||||||
|
for sel in selector.split(", "):
|
||||||
|
el = page.locator(sel)
|
||||||
|
if await el.count() > 0:
|
||||||
|
await el.fill(str(int(value * 100))) # cents
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await human_delay(0.3, 0.8)
|
||||||
|
|
||||||
|
# Submit
|
||||||
|
await human_delay(1.0, 2.0)
|
||||||
|
await page.click('button:has-text("Review")')
|
||||||
|
await page.wait_for_selector("text=Review", timeout=30000)
|
||||||
|
await page.click('button:has-text("Submit")')
|
||||||
|
await page.wait_for_selector("text=Confirmation", timeout=60000)
|
||||||
|
|
||||||
|
# Capture confirmation
|
||||||
|
body = await page.locator("body").inner_text()
|
||||||
|
for line in body.splitlines():
|
||||||
|
if "Confirmation" in line or "Filing ID" in line:
|
||||||
|
parts = line.split(":", 1)
|
||||||
|
if len(parts) == 2 and parts[1].strip():
|
||||||
|
confirmation_number = parts[1].strip()
|
||||||
|
break
|
||||||
|
|
||||||
|
# Save confirmation PDF
|
||||||
|
conf_path = os.path.join(work_dir, f"499q_{quarter}_confirmation.pdf")
|
||||||
|
await page.pdf(path=conf_path, format="Letter")
|
||||||
|
|
||||||
|
# Upload to MinIO
|
||||||
|
try:
|
||||||
|
from scripts.workers.minio_client import upload_file
|
||||||
|
upload_file(conf_path, f"compliance/{order_number}/499q_{quarter}_confirmation.pdf")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("USAC 499-Q Playwright error: %s", exc)
|
||||||
|
# Screenshot for debugging
|
||||||
|
try:
|
||||||
|
ss_path = os.path.join(work_dir, "usac_error.png")
|
||||||
|
await page.screenshot(path=ss_path, full_page=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
return confirmation_number
|
||||||
|
|
||||||
|
def _send_received_email(self, to: str, entity_name: str, order_number: str,
|
||||||
|
quarter: str, due_date: str) -> None:
|
||||||
if not to:
|
if not to:
|
||||||
return
|
return
|
||||||
|
self._send_html_email(
|
||||||
|
to=to,
|
||||||
|
subject=f"499-Q {quarter} Data 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} — Data Received</h2>
|
||||||
|
</div>
|
||||||
|
<div style="padding:24px;border:1px solid #e5e7eb;border-top:none;border-radius:0 0 8px 8px">
|
||||||
|
<p>Your quarterly revenue data for <strong>{entity_name}</strong>
|
||||||
|
({quarter}, due {due_date}) has been received.</p>
|
||||||
|
<p>We'll file this with USAC and send you a confirmation email
|
||||||
|
with your filing reference number once complete.</p>
|
||||||
|
<p style="font-size:13px;color:#6b7280;margin-top:1rem">
|
||||||
|
Order: {order_number}<br>
|
||||||
|
Questions? Contact <a href="mailto:ops@performancewest.net">ops@performancewest.net</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>""",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _send_confirmation_email(self, to: str, entity_name: str, order_number: str,
|
||||||
|
quarter: str, confirmation: str) -> None:
|
||||||
|
if not to:
|
||||||
|
return
|
||||||
|
self._send_html_email(
|
||||||
|
to=to,
|
||||||
|
subject=f"499-Q {quarter} Filed — Confirmation {confirmation} — {entity_name}",
|
||||||
|
html=f"""
|
||||||
|
<div style="font-family:Inter,sans-serif;max-width:600px;margin:0 auto;color:#1f2937">
|
||||||
|
<div style="background:#059669;padding:16px 24px;border-radius:8px 8px 0 0">
|
||||||
|
<h2 style="color:#fff;margin:0;font-size:16px">Form 499-Q {quarter} — Filed Successfully</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 filing for <strong>{entity_name}</strong>
|
||||||
|
({quarter}) has been successfully submitted to USAC.</p>
|
||||||
|
<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:12px;margin:16px 0;text-align:center">
|
||||||
|
<p style="font-size:13px;color:#166534;margin:0">Confirmation Number</p>
|
||||||
|
<p style="font-size:20px;font-weight:700;color:#166534;margin:4px 0 0">{confirmation}</p>
|
||||||
|
</div>
|
||||||
|
<p>A confirmation PDF has been saved to your account. USAC will
|
||||||
|
calculate your quarterly USF contribution based on the revenue
|
||||||
|
data submitted.</p>
|
||||||
|
<p style="font-size:13px;color:#6b7280;margin-top:1rem">
|
||||||
|
Order: {order_number}<br>
|
||||||
|
Questions? Contact <a href="mailto:ops@performancewest.net">ops@performancewest.net</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>""",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _send_html_email(self, to: str, subject: str, html: str) -> None:
|
||||||
try:
|
try:
|
||||||
import smtplib
|
import smtplib
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
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 = MIMEMultipart("alternative")
|
||||||
msg["From"] = os.environ.get("SMTP_FROM", "Performance West <noreply@performancewest.net>")
|
msg["From"] = os.environ.get("SMTP_FROM", "Performance West <noreply@performancewest.net>")
|
||||||
msg["To"] = to
|
msg["To"] = to
|
||||||
|
|
@ -121,10 +339,7 @@ class Form499QHandler(BaseComplianceHandler):
|
||||||
timeout=30,
|
timeout=30,
|
||||||
) as s:
|
) as s:
|
||||||
s.starttls()
|
s.starttls()
|
||||||
s.login(
|
s.login(os.environ.get("SMTP_USER", ""), os.environ.get("SMTP_PASS", ""))
|
||||||
os.environ.get("SMTP_USER", ""),
|
|
||||||
os.environ.get("SMTP_PASS", ""),
|
|
||||||
)
|
|
||||||
s.send_message(msg)
|
s.send_message(msg)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("499-Q confirmation email failed: %s", exc)
|
logger.warning("Email send failed: %s", exc)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue