#!/usr/bin/env python3 """FCC Form 499-Q Quarterly Filing Notification Worker. Runs daily via cron. Finds upcoming 499-Q compliance_orders and sends reminder emails with intake links at 30, 14, and 7 days before due date. After the client completes intake, the 499-Q handler files at USAC. Lifecycle: 499-A filed → 499-Q orders created (by Form499ABundleHandler) ↓ 30 days before due → first reminder email 14 days before due → second reminder 7 days before due → urgent reminder ↓ Client fills intake → worker files 499-Q ↓ Filed → confirmation email Cron: 0 8 * * * (daily at 8am CT) """ from __future__ import annotations import json import logging import os import smtplib from datetime import date, timedelta from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from scripts._email_plaintext import html_to_text import psycopg2 import psycopg2.extras LOG = logging.getLogger("workers.quarterly_499q_notify") SMTP_HOST = os.environ.get("SMTP_HOST", "co.carrierone.com") SMTP_PORT = int(os.environ.get("SMTP_PORT", "587")) SMTP_USER = os.environ.get("SMTP_USER", "noreply@performancewest.net") SMTP_PASS = os.environ.get("SMTP_PASS", "") SMTP_FROM = os.environ.get("SMTP_FROM", "Performance West ") DOMAIN = os.environ.get("DOMAIN", "performancewest.net") def _send_email(to: str, subject: str, html: str) -> bool: try: msg = MIMEMultipart("alternative") msg["From"] = SMTP_FROM msg["To"] = to msg["Subject"] = subject msg.attach(MIMEText(html_to_text(html), "plain")) msg.attach(MIMEText(html, "html")) with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=30) as s: s.starttls() s.login(SMTP_USER, SMTP_PASS) s.send_message(msg) return True except Exception as exc: LOG.warning("Email send failed to %s: %s", to, exc) return False def _build_reminder_email( entity_name: str, quarter: str, due_date: str, days_until: int, order_number: str, filer_id: str, ) -> tuple[str, str]: """Return (subject, html_body) for a 499-Q reminder.""" urgency = "Reminder" if days_until > 14 else "Upcoming" if days_until > 7 else "Urgent" subject = f"{urgency}: FCC Form 499-Q ({quarter}) due {due_date} — {entity_name}" intake_url = f"https://{DOMAIN}/order/fcc-499q?order={order_number}" html = f"""

FCC Form 499-Q — {quarter} Filing

Hi,

This is a reminder that the FCC Form 499-Q ({quarter}) quarterly filing for {entity_name} (Filer ID: {filer_id}) is due {due_date}{f' — in {days_until} days' if days_until > 0 else ' — today'}.

The 499-Q reports your projected quarterly USF contributions based on actual telecom revenue for the prior quarter. USAC uses this to calculate your quarterly contribution payment.

Complete 499-Q Filing →

This filing is included in your 499-A + 499-Q bundle — no additional charge. If you need assistance, reply to this email or contact us at ops@performancewest.net.

{'

⚠️ Late 499-Q filings may result in estimated USF contributions calculated by USAC, which are typically higher than actual amounts.

' if days_until <= 7 else ''}
""" return subject, html def run(dry_run: bool = False) -> dict: """Check for upcoming 499-Q filings and send reminders.""" conn = psycopg2.connect(os.environ["DATABASE_URL"]) today = date.today() stats = {"checked": 0, "reminded_30d": 0, "reminded_14d": 0, "reminded_7d": 0, "skipped": 0} with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: cur.execute(""" SELECT co.order_number, co.customer_email, co.customer_name, co.intake_data, co.telecom_entity_id FROM compliance_orders co WHERE co.service_slug = 'fcc-499q' AND co.payment_status = 'paid' AND co.intake_data IS NOT NULL AND (co.intake_data->>'intake_completed')::boolean IS NOT TRUE """) rows = cur.fetchall() stats["checked"] = len(rows) for row in rows: intake = row["intake_data"] or {} due_str = intake.get("due_date") if not due_str: continue due = date.fromisoformat(due_str) days_until = (due - today).days quarter = intake.get("quarter", "?") entity_name = intake.get("entity_name", row["customer_name"]) filer_id = intake.get("filer_id_499", "") email = row["customer_email"] order_number = row["order_number"] # Already past due — skip (handled by overdue process) if days_until < -7: stats["skipped"] += 1 continue # Determine which reminder to send reminder_key = None if days_until <= 7 and not intake.get("reminder_sent_7d"): reminder_key = "reminder_sent_7d" stats["reminded_7d"] += 1 elif days_until <= 14 and not intake.get("reminder_sent_14d"): reminder_key = "reminder_sent_14d" stats["reminded_14d"] += 1 elif days_until <= 30 and not intake.get("reminder_sent_30d"): reminder_key = "reminder_sent_30d" stats["reminded_30d"] += 1 if not reminder_key: continue LOG.info( "499-Q %s %s: %s due %s (%d days) — sending %s to %s", order_number, quarter, entity_name, due_str, days_until, reminder_key, email, ) if not dry_run: subject, html = _build_reminder_email( entity_name=entity_name, quarter=quarter, due_date=due_str, days_until=max(days_until, 0), order_number=order_number, filer_id=filer_id, ) sent = _send_email(email, subject, html) if sent: # Mark reminder as sent intake[reminder_key] = True cur.execute( "UPDATE compliance_orders SET intake_data = %s, updated_at = now() WHERE order_number = %s", (json.dumps(intake), order_number), ) conn.commit() else: LOG.info("DRY RUN: would send %s to %s for %s", reminder_key, email, order_number) conn.close() LOG.info("499-Q notify: %s", stats) return stats def main(): logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s %(message)s") import argparse parser = argparse.ArgumentParser() parser.add_argument("--dry-run", action="store_true") args = parser.parse_args() run(dry_run=args.dry_run) if __name__ == "__main__": main()