new-site/scripts/workers/renewal_worker.py
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:22 -05:00

790 lines
32 KiB
Python

#!/usr/bin/env python3
"""Compliance Calendar Renewal Worker.
Runs daily via cron. Processes the renewal lifecycle:
Upcoming → Due Soon (30 days out) → Invoice Sent → Paid → Completed
New entry (next year)
Lifecycle:
1. SCAN: Find entries where reminder_date <= today and status = "Upcoming"
2. NOTIFY: Change status to "Due Soon", send reminder email to client
3. INVOICE: For billable entries (amount_usd > 0 or amount_cad > 0),
create a Sales Invoice in ERPNext grouped by customer
4. TRACK: Monitor invoice payment status via ERPNext
5. COMPLETE: When invoice is paid, mark entry as "Completed"
6. RE-CALENDAR: Create a new entry for the next recurrence period
7. OVERDUE: If due_date has passed and not paid, mark as "Overdue"
Billable items:
- Annual Maintenance Fee: $349 USD (our service fee)
- Mailbox Renewal: ~C$165 (vendor pass-through)
- BC Annual Report: C$42 (government fee pass-through)
- Domain Renewal: ~C$15 (vendor pass-through)
Non-billable items (covered by maintenance fee):
- CRTC Compliance Check, CCTS Membership, all CRTC surveys
Cron: 0 7 * * * (daily at 7am CT)
"""
import logging
import os
import sys
from datetime import datetime, timedelta
LOG = logging.getLogger("renewal_worker")
def get_erpnext_client():
"""Return an ERPNext REST client."""
from scripts.workers.erpnext_client import ERPNextClient
return ERPNextClient()
def get_smtp_config():
"""Return SMTP config from environment."""
return {
"host": os.environ.get("SMTP_HOST", "co.carrierone.com"),
"port": int(os.environ.get("SMTP_PORT", "587")),
"user": os.environ.get("SMTP_USER", "noreply@performancewest.net"),
"password": os.environ.get("SMTP_PASS", ""),
"from_addr": os.environ.get("SMTP_FROM", "noreply@performancewest.net"),
}
def send_email(to_email: str, subject: str, html_body: str):
"""Send an email via Carbonio SMTP."""
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
smtp_cfg = get_smtp_config()
msg = MIMEMultipart("alternative")
msg["From"] = f"Performance West <{smtp_cfg['from_addr']}>"
msg["To"] = to_email
msg["Subject"] = subject
msg.attach(MIMEText(html_body, "html"))
with smtplib.SMTP(smtp_cfg["host"], smtp_cfg["port"]) as server:
server.starttls()
server.login(smtp_cfg["user"], smtp_cfg["password"])
server.sendmail(smtp_cfg["from_addr"], to_email, msg.as_string())
LOG.info("Sent renewal email to %s: %s", to_email, subject)
# ────────────────────────────────────────────────────────────────────────── #
# Step 1: Transition Upcoming → Due Soon
# ────────────────────────────────────────────────────────────────────────── #
def process_upcoming_to_due_soon(erp):
"""Find entries where reminder_date <= today and status is Upcoming."""
today = datetime.utcnow().strftime("%Y-%m-%d")
entries = erp.get_list("Compliance Calendar", filters={
"status": "Upcoming",
"reminder_date": ["<=", today],
}, fields=["name", "title", "entity_name", "order_reference",
"compliance_type", "due_date", "amount_usd", "amount_cad",
"customer"],
limit_page_length=100)
LOG.info("Found %d entries transitioning Upcoming → Due Soon", len(entries))
for entry in entries:
try:
erp.update_resource("Compliance Calendar", entry["name"], {
"status": "Due Soon",
})
LOG.info(" %s: %s → Due Soon (due %s)",
entry["name"], entry["title"], entry["due_date"])
except Exception as exc:
LOG.error("Failed to update %s: %s", entry["name"], exc)
return entries
# ────────────────────────────────────────────────────────────────────────── #
# Step 2: Generate invoices for billable Due Soon entries
# ────────────────────────────────────────────────────────────────────────── #
def generate_renewal_invoices(erp):
"""Create Sales Invoices for billable compliance entries in Due Soon status.
Groups entries by customer into a single invoice per customer.
Only creates an invoice if one doesn't already exist for the entry.
"""
entries = erp.get_list("Compliance Calendar", filters={
"status": "Due Soon",
"invoice": ["is", "not set"],
}, fields=["name", "title", "entity_name", "order_reference",
"compliance_type", "due_date", "amount_usd", "amount_cad",
"customer"],
limit_page_length=100)
# Filter to only billable entries
billable = [e for e in entries if (e.get("amount_usd") or 0) > 0
or (e.get("amount_cad") or 0) > 0]
if not billable:
LOG.info("No billable entries need invoicing")
return
# Group by customer
by_customer = {}
for entry in billable:
cust = entry.get("customer")
if not cust:
# Try to get customer from the Sales Order
if entry.get("order_reference"):
try:
so = erp.get_resource("Sales Order", entry["order_reference"])
cust = so.get("customer")
except Exception:
pass
if cust:
by_customer.setdefault(cust, []).append(entry)
else:
LOG.warning("No customer found for %s — skipping invoice", entry["name"])
for customer, cust_entries in by_customer.items():
try:
_create_renewal_invoice(erp, customer, cust_entries)
except Exception as exc:
LOG.error("Failed to create renewal invoice for %s: %s", customer, exc)
def _create_renewal_invoice(erp, customer: str, entries: list):
"""Create a single Sales Invoice for a customer's renewal entries."""
# Build invoice items
items = []
total_usd = 0
for entry in entries:
amount_usd = entry.get("amount_usd") or 0
amount_cad = entry.get("amount_cad") or 0
# Convert CAD to USD for invoicing (approximate — use 0.74 rate)
# In production, this should use Bank of Canada daily rate
cad_to_usd = 0.74
entry_usd = amount_usd + (amount_cad * cad_to_usd)
if entry_usd <= 0:
continue
items.append({
"item_code": _compliance_type_to_item(entry.get("compliance_type", "")),
"description": entry.get("title", entry.get("compliance_type", "Renewal")),
"qty": 1,
"rate": round(entry_usd, 2),
})
total_usd += entry_usd
if not items:
return
# Create Sales Invoice
entity_name = entries[0].get("entity_name", "")
invoice_data = {
"doctype": "Sales Invoice",
"customer": customer,
"due_date": entries[0].get("due_date"),
"items": items,
"custom_invoice_type": "Renewal",
"remarks": f"Annual renewal for {entity_name}" if entity_name else "Annual renewal",
}
try:
inv = erp.create_resource("Sales Invoice", invoice_data)
inv_name = inv.get("name", "")
LOG.info("Created renewal invoice %s for %s ($%.2f USD, %d items)",
inv_name, customer, total_usd, len(items))
# Submit the invoice
try:
erp.submit_resource("Sales Invoice", inv_name)
LOG.info("Submitted invoice %s", inv_name)
except Exception as sub_exc:
LOG.warning("Failed to submit invoice %s: %s", inv_name, sub_exc)
# Link invoice to each compliance entry and update status
for entry in entries:
try:
erp.update_resource("Compliance Calendar", entry["name"], {
"invoice": inv_name,
"status": "Invoice Sent",
})
except Exception as link_exc:
LOG.warning("Failed to link invoice to %s: %s", entry["name"], link_exc)
# Send renewal invoice email to customer
_send_renewal_email(erp, customer, entries, inv_name, total_usd)
except Exception as exc:
LOG.error("Failed to create invoice for %s: %s", customer, exc)
raise
def _compliance_type_to_item(compliance_type: str) -> str:
"""Map compliance type to ERPNext Item code for invoicing."""
mapping = {
"Annual Maintenance Fee": "CRTC-MAINT-ANNUAL",
"Mailbox Renewal": "MAILBOX-RENEWAL",
"BC Annual Report": "BC-ANNUAL-REPORT",
"Domain Renewal": "DOMAIN-RENEWAL-CA",
}
return mapping.get(compliance_type, "COMPLIANCE-OTHER")
def _send_renewal_email(erp, customer: str, entries: list, invoice_name: str,
total_usd: float):
"""Send renewal reminder email to the customer."""
# Get customer email
try:
cust = erp.get_resource("Customer", customer)
email = cust.get("email_id", "")
cust_name = cust.get("customer_name", customer)
except Exception:
LOG.warning("Could not get customer email for %s", customer)
return
if not email:
LOG.warning("No email for customer %s — skipping renewal email", customer)
return
entity_name = entries[0].get("entity_name", cust_name)
due_date = entries[0].get("due_date", "")
items_html = ""
for entry in entries:
amt = entry.get("amount_usd") or 0
cad = entry.get("amount_cad") or 0
price = f"${amt:.2f} USD" if amt > 0 else f"C${cad:.2f}" if cad > 0 else "Included"
items_html += f"<tr><td style='padding:8px;border-bottom:1px solid #eee;'>{entry.get('title', '')}</td>"
items_html += f"<td style='padding:8px;border-bottom:1px solid #eee;text-align:right;'>{price}</td></tr>"
html = f"""
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #1e3a5f;">Annual Renewal — {entity_name}</h2>
<p>Your annual compliance renewals for <strong>{entity_name}</strong> are
coming due on <strong>{due_date}</strong>.</p>
<table style="width:100%; border-collapse:collapse; margin:20px 0;">
<thead>
<tr style="background:#1e3a5f; color:#fff;">
<th style="padding:10px; text-align:left;">Item</th>
<th style="padding:10px; text-align:right;">Amount</th>
</tr>
</thead>
<tbody>
{items_html}
<tr style="font-weight:bold; background:#f5f5f5;">
<td style="padding:10px;">Total</td>
<td style="padding:10px; text-align:right;">${total_usd:.2f} USD</td>
</tr>
</tbody>
</table>
<p>Invoice <strong>{invoice_name}</strong> has been generated. You can view
and pay your invoice at:</p>
<p style="text-align:center; margin:20px 0;">
<a href="https://portal.performancewest.net/sales-invoices/{invoice_name}"
style="background:#1e3a5f; color:#fff; padding:12px 30px; text-decoration:none;
border-radius:4px; display:inline-block;">
View Invoice &amp; Pay
</a>
</p>
<p><strong>What's included in your annual maintenance:</strong></p>
<ul>
<li>Registered agent service (BC)</li>
<li>CRTC compliance monitoring and annual review</li>
<li>BC Annual Report filing</li>
<li>REP-T/T1 survey preparation and filing assistance</li>
<li>CCTS membership maintenance</li>
<li>General compliance support</li>
</ul>
<p>If you have any questions about your renewal, reply to this email or
contact us at <a href="mailto:support@performancewest.net">support@performancewest.net</a>.</p>
<p style="color: #666; font-size: 0.9em; margin-top: 30px;">
Performance West Inc. — Canadian Telecom Carrier Registration Services
</p>
</div>
"""
try:
send_email(email, f"Annual Renewal Due — {entity_name}", html)
except Exception as exc:
LOG.warning("Failed to send renewal email to %s: %s", email, exc)
# ────────────────────────────────────────────────────────────────────────── #
# Step 3: Check payment status on Invoice Sent entries
# ────────────────────────────────────────────────────────────────────────── #
def check_invoice_payments(erp):
"""Check if invoices linked to 'Invoice Sent' entries have been paid."""
entries = erp.get_list("Compliance Calendar", filters={
"status": "Invoice Sent",
"invoice": ["is", "set"],
}, fields=["name", "title", "invoice", "entity_name", "compliance_type",
"order_reference", "due_date", "recurring", "recurrence_period",
"amount_usd", "amount_cad", "customer", "state_code",
"category", "description"],
limit_page_length=100)
LOG.info("Checking payment status for %d invoiced entries", len(entries))
for entry in entries:
inv_name = entry.get("invoice")
if not inv_name:
continue
try:
inv = erp.get_resource("Sales Invoice", inv_name)
outstanding = inv.get("outstanding_amount", 1)
status = inv.get("status", "")
if outstanding <= 0 or status == "Paid":
LOG.info(" %s: Invoice %s is PAID — completing entry",
entry["name"], inv_name)
_complete_and_recalendar(erp, entry)
elif status in ("Overdue", "Unpaid"):
# Check if past due date
due = entry.get("due_date", "")
if due and due < datetime.utcnow().strftime("%Y-%m-%d"):
erp.update_resource("Compliance Calendar", entry["name"], {
"status": "Overdue",
})
LOG.info(" %s: Marked as Overdue (due %s)", entry["name"], due)
except Exception as exc:
LOG.warning("Failed to check invoice %s: %s", inv_name, exc)
# ────────────────────────────────────────────────────────────────────────── #
# Step 4: Mark overdue entries (no invoice, past due date)
# ────────────────────────────────────────────────────────────────────────── #
def mark_overdue(erp):
"""Mark entries past their due date as Overdue."""
today = datetime.utcnow().strftime("%Y-%m-%d")
entries = erp.get_list("Compliance Calendar", filters={
"status": ["in", ["Upcoming", "Due Soon"]],
"due_date": ["<", today],
}, fields=["name", "title", "due_date"],
limit_page_length=100)
for entry in entries:
try:
erp.update_resource("Compliance Calendar", entry["name"], {
"status": "Overdue",
})
LOG.info(" %s: %s → Overdue (was due %s)",
entry["name"], entry["title"], entry["due_date"])
except Exception as exc:
LOG.warning("Failed to mark %s overdue: %s", entry["name"], exc)
# ────────────────────────────────────────────────────────────────────────── #
# Step 5: Complete paid entries and create next year's entries
# ────────────────────────────────────────────────────────────────────────── #
def _complete_and_recalendar(erp, entry: dict):
"""Mark an entry as Completed, then create the next recurrence.
This is the core renewal logic:
1. Set status = "Paid" (payment received)
2. Perform the compliance action (or create admin ToDo)
3. Set status = "Completed" with completed_at timestamp
4. If recurring, create a new entry for the next period
"""
entry_name = entry["name"]
now = datetime.utcnow()
# Mark as Paid
erp.update_resource("Compliance Calendar", entry_name, {
"status": "Paid",
})
# Create admin ToDo to perform the actual compliance action
compliance_type = entry.get("compliance_type", "")
entity_name = entry.get("entity_name", "")
_create_action_todo(erp, entry)
# Mark as Completed
erp.update_resource("Compliance Calendar", entry_name, {
"status": "Completed",
"completed_at": now.isoformat(),
})
LOG.info("Completed: %s (%s)", entry_name, entry.get("title", ""))
# Re-calendar if recurring
if entry.get("recurring"):
_create_next_recurrence(erp, entry, entry_name)
def _create_action_todo(erp, entry: dict):
"""Create an admin ToDo to perform the compliance action now that it's paid."""
compliance_type = entry.get("compliance_type", "")
entity_name = entry.get("entity_name", "")
order_ref = entry.get("order_reference", "")
# Only create ToDos for actionable items
actionable = {
"Mailbox Renewal": f"Renew Anytime Mailbox for {entity_name}. Log into AMB and process renewal payment.",
"BC Annual Report": f"File BC Annual Report for {entity_name} via Corporate Online.",
"Domain Renewal": f"Verify .ca domain auto-renewal for {entity_name} on Porkbun.",
"CRTC Compliance Check": f"Perform annual CRTC compliance review for {entity_name}.",
"CCTS Membership": f"Verify CCTS membership is current for {entity_name}.",
}
action = actionable.get(compliance_type)
if not action:
return
try:
erp.create_resource("ToDo", {
"doctype": "ToDo",
"description": f"**Renewal Action — {compliance_type}**\n\n{action}",
"reference_type": "Sales Order",
"reference_name": order_ref,
"priority": "Medium",
"status": "Open",
"allocated_to": "Administrator",
})
LOG.info("Created action ToDo for %s: %s", entity_name, compliance_type)
except Exception as exc:
LOG.warning("Failed to create action ToDo: %s", exc)
def _create_next_recurrence(erp, entry: dict, prev_entry_name: str):
"""Create a new compliance calendar entry for the next recurrence period."""
period = entry.get("recurrence_period", "Yearly")
due_date_str = entry.get("due_date", "")
if not due_date_str:
LOG.warning("No due_date on %s — cannot re-calendar", prev_entry_name)
return
# Parse current due date
try:
due_date = datetime.strptime(due_date_str, "%Y-%m-%d")
except ValueError:
LOG.warning("Invalid due_date format: %s", due_date_str)
return
# Calculate next due date
if period == "Yearly":
next_due = due_date.replace(year=due_date.year + 1)
next_remind = next_due - timedelta(days=30)
elif period == "Quarterly":
next_due = due_date + timedelta(days=90)
next_remind = next_due - timedelta(days=14)
elif period == "Monthly":
month = due_date.month + 1
year = due_date.year
if month > 12:
month = 1
year += 1
next_due = due_date.replace(year=year, month=month)
next_remind = next_due - timedelta(days=7)
else:
LOG.warning("Unknown recurrence period: %s", period)
return
# Create new entry
new_entry = {
"doctype": "Compliance Calendar",
"customer": entry.get("customer", ""),
"entity_name": entry.get("entity_name", ""),
"order_reference": entry.get("order_reference", ""),
"title": entry.get("title", ""),
"compliance_type": entry.get("compliance_type", ""),
"category": entry.get("category", "Other"),
"description": entry.get("description", ""),
"due_date": next_due.strftime("%Y-%m-%d"),
"reminder_date": next_remind.strftime("%Y-%m-%d"),
"state_code": entry.get("state_code", ""),
"amount_usd": entry.get("amount_usd", 0),
"amount_cad": entry.get("amount_cad", 0),
"recurring": 1,
"recurrence_period": period,
"renewal_of": prev_entry_name,
"status": "Upcoming",
}
try:
created = erp.create_resource("Compliance Calendar", new_entry)
LOG.info("Re-calendared: %s%s (due %s)",
prev_entry_name, created.get("name", "?"), next_due.strftime("%Y-%m-%d"))
except Exception as exc:
LOG.error("Failed to re-calendar %s: %s", prev_entry_name, exc)
# ────────────────────────────────────────────────────────────────────────── #
# Also handle direct payment events (webhook-triggered)
# ────────────────────────────────────────────────────────────────────────── #
def handle_renewal_payment(invoice_name: str):
"""Called by webhook when a renewal invoice is paid.
This provides immediate completion instead of waiting for the daily cron.
"""
erp = get_erpnext_client()
# Find all compliance entries linked to this invoice
entries = erp.get_list("Compliance Calendar", filters={
"invoice": invoice_name,
"status": ["in", ["Invoice Sent", "Overdue"]],
}, fields=["name", "title", "invoice", "entity_name", "compliance_type",
"order_reference", "due_date", "recurring", "recurrence_period",
"amount_usd", "amount_cad", "customer", "state_code",
"category", "description"],
limit_page_length=50)
LOG.info("Processing payment for invoice %s%d entries", invoice_name, len(entries))
for entry in entries:
try:
_complete_and_recalendar(erp, entry)
except Exception as exc:
LOG.error("Failed to complete %s on payment: %s", entry["name"], exc)
# ────────────────────────────────────────────────────────────────────────── #
# FCC 499-A supporting reminders — reseller certs, CALEA review, traffic study
# ────────────────────────────────────────────────────────────────────────── #
def _db_connect():
"""Return a psycopg2 connection to the main DB, or None on failure."""
try:
import psycopg2
return psycopg2.connect(os.environ.get("DATABASE_URL", ""))
except Exception as exc:
LOG.warning("No DB connection for FCC reminders: %s", exc)
return None
def process_reseller_cert_reminders():
"""Send T-30 / T-14 / T-7 / T-1 reminders for reseller certifications.
Per 2026 Form 499-A Section IV.C.4, certifications must be renewed
annually. A lapsed cert invalidates Line 303 revenue claims.
"""
conn = _db_connect()
if not conn:
return
try:
import psycopg2.extras
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
# Find active certs whose renewal_due matches one of our reminder
# offsets from today. The T-N SQL below: renewal_due - N = today.
cur.execute(
"""
SELECT rc.id, rc.reseller_legal_name, rc.reseller_filer_id_499,
rc.reseller_contact_email, rc.renewal_due,
(rc.renewal_due - CURRENT_DATE)::int AS days_left,
te.legal_name AS filer_legal_name, te.customer_id
FROM reseller_certifications rc
JOIN telecom_entities te ON te.id = rc.filer_telecom_entity_id
WHERE rc.status = 'active'
AND (rc.renewal_due - CURRENT_DATE) IN (30, 14, 7, 1)
"""
)
rows = cur.fetchall() or []
for r in rows:
days = r["days_left"]
# Look up the customer email from the customers table
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute(
"SELECT email, name FROM customers WHERE id = %s",
(r["customer_id"],),
)
cust = cur.fetchone() or {}
if not cust.get("email"):
LOG.info(
"Reseller cert reminder: no customer email for filer %s "
"(cert %s)", r["filer_legal_name"], r["id"],
)
continue
subject = (
f"Action required: Reseller certification for "
f"{r['reseller_legal_name']} expires in {days} day"
f"{'s' if days != 1 else ''}"
)
body = f"""\
<p>Hi {cust.get('name', '')},</p>
<p>Your Form 499-A reseller certification for
<strong>{r['reseller_legal_name']}</strong>
(Filer ID {r['reseller_filer_id_499']}) is due for renewal on
<strong>{r['renewal_due']}</strong> &mdash; {days} day{'s' if days != 1 else ''} from today.</p>
<p>Without a current signed certification from this reseller, you cannot
claim their revenue on Form 499-A Line 303. Under FCC Section IV.C.4,
reseller certifications must be renewed annually.</p>
<p>Log in to your Performance West portal to request an updated signed
certification, or contact your reseller directly.</p>
<p>&mdash; Performance West</p>
"""
try:
send_email(cust["email"], subject, body)
LOG.info("Sent T-%d reseller cert reminder for %s", days, r["id"])
except Exception as exc:
LOG.warning("Reseller reminder email failed for %s: %s", r["id"], exc)
except Exception as exc:
LOG.exception("process_reseller_cert_reminders: %s", exc)
finally:
try: conn.close()
except Exception: pass
def process_calea_review_reminders():
"""Annual CALEA SSI review reminder — 1 year after calea_ssi_generated_at."""
conn = _db_connect()
if not conn:
return
try:
import psycopg2.extras
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute(
"""
SELECT te.id, te.legal_name, te.customer_id,
te.calea_ssi_generated_at,
(CURRENT_DATE - te.calea_ssi_generated_at::date)::int AS days_since
FROM telecom_entities te
WHERE te.calea_ssi_generated_at IS NOT NULL
AND te.calea_ssi_generated_at::date <= CURRENT_DATE - INTERVAL '11 months'
AND (te.calea_ssi_next_review_date IS NULL
OR te.calea_ssi_next_review_date <= CURRENT_DATE + INTERVAL '30 days')
"""
)
rows = cur.fetchall() or []
for r in rows:
LOG.info(
"CALEA review due for %s (%d days since last plan)",
r["legal_name"], r["days_since"],
)
# Pull customer email similarly to above — simplified, just log
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute(
"SELECT email, name FROM customers WHERE id = %s",
(r["customer_id"],),
)
cust = cur.fetchone() or {}
if not cust.get("email"):
continue
try:
send_email(
cust["email"],
f"CALEA SSI annual review due for {r['legal_name']}",
f"""<p>Hi {cust.get('name', '')},</p>
<p>47 USC 229 + 47 CFR 1.20003 require your CALEA System Security &amp;
Integrity plan to be reviewed annually. Your current plan was generated
{r['days_since']} days ago.</p>
<p>Log into your Performance West portal to schedule the annual review
(we'll regenerate the plan with any current-year updates).</p>
<p>&mdash; Performance West</p>""",
)
except Exception as exc:
LOG.warning("CALEA reminder email failed for %s: %s", r["id"], exc)
except Exception as exc:
LOG.exception("process_calea_review_reminders: %s", exc)
finally:
try: conn.close()
except Exception: pass
def process_traffic_study_resubmissions():
"""If a traffic-study PDF was updated after USAC submission, re-stamp + re-submit.
Placeholder: real implementation would compare pdf_minio_path mtime
against usac_submitted_at and trigger the stamper + USAC upload.
For now, just log candidates for admin awareness.
"""
conn = _db_connect()
if not conn:
return
try:
import psycopg2.extras
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute(
"""
SELECT id, profile_id, reporting_year,
usac_submitted_at, fcc_compliance_ok
FROM cdr_traffic_studies
WHERE fcc_compliance_ok = TRUE
AND usac_submitted_at IS NOT NULL
AND generated_at > usac_submitted_at
LIMIT 20
"""
)
rows = cur.fetchall() or []
for r in rows:
LOG.info(
"Traffic study %s (profile %s, year %s) regenerated after USAC submission — admin re-submit advised",
r["id"], r["profile_id"], r["reporting_year"],
)
except Exception as exc:
LOG.exception("process_traffic_study_resubmissions: %s", exc)
finally:
try: conn.close()
except Exception: pass
# ────────────────────────────────────────────────────────────────────────── #
# Main — daily cron entry point
# ────────────────────────────────────────────────────────────────────────── #
def main():
"""Run all renewal processing steps."""
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
LOG.info("=== Renewal Worker starting ===")
try:
erp = get_erpnext_client()
# Step 1: Upcoming → Due Soon (triggers 30 days before due date)
process_upcoming_to_due_soon(erp)
# Step 2: Generate invoices for billable Due Soon entries
generate_renewal_invoices(erp)
# Step 3: Check invoice payments → Complete + re-calendar
check_invoice_payments(erp)
# Step 4: Mark unpaid past-due entries as Overdue
mark_overdue(erp)
# Step 5: FCC reseller certification renewal reminders (T-30/14/7/1)
process_reseller_cert_reminders()
# Step 6: CALEA SSI annual review reminders
process_calea_review_reminders()
# Step 7: Traffic study FCC re-submission check
process_traffic_study_resubmissions()
LOG.info("=== Renewal Worker complete ===")
except Exception as exc:
LOG.exception("Renewal Worker failed: %s", exc)
sys.exit(1)
if __name__ == "__main__":
main()