new-site/scripts/workers/cdr_unlock_nudge.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

172 lines
6.5 KiB
Python

"""
CDR unlock nudge — quarterly cron.
Emails customers whose ingestion is healthy but whose current reporting
year has no `cdr_study_access_grants` row. Upsell to the 499-A or
standalone `cdr-analysis` service so the classified study unlocks.
One nudge per profile per quarter. Tracks last_nudge_at on a simple
per-quarter key to prevent spam.
Usage:
python -m scripts.workers.cdr_unlock_nudge
CRON: 0 10 1 */3 * (10am on the 1st day of every third month)
"""
from __future__ import annotations
import logging
import os
import smtplib
import sys
from datetime import datetime, timedelta
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import psycopg2
import psycopg2.extras
log = logging.getLogger("cdr_unlock_nudge")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
DATABASE_URL = os.environ.get("DATABASE_URL", "")
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", "")
SMTP_PASS = os.environ.get("SMTP_PASS", "")
FROM_EMAIL = os.environ.get("FROM_EMAIL", "notifications@performancewest.net")
ADMIN_EMAIL = os.environ.get("ADMIN_EMAIL", "ops@performancewest.net")
SITE_URL = os.environ.get("SITE_URL", "https://performancewest.net")
MIN_ROWS_TO_NUDGE = 10_000 # don't pester customers with no real data yet
_EMAIL_HTML = """\
<!DOCTYPE html><html><body style="font-family:Arial;color:#1f2937;">
<table width="100%" cellpadding="0" cellspacing="0" style="max-width:620px;margin:20px auto;background:#fff;border-radius:8px;overflow:hidden;">
<tr><td style="background:#1a2744;padding:22px 30px;">
<h1 style="margin:0;color:#fff;font-size:22px;">Performance West</h1>
<p style="margin:3px 0 0;color:#8fa4c8;font-size:13px;">Traffic Study Ready — Current Year Locked</p>
</td></tr>
<tr><td style="padding:26px 30px;">
<p style="margin:0 0 14px;font-size:15px;">Hi {customer_name},</p>
<p style="margin:0 0 14px;font-size:15px;">
You've sent us <strong>{row_count:,} calls</strong> for {reporting_year} so far.
Your classified traffic study is ready behind {reporting_year}'s 499-A filing service — one click and the full report,
regional breakouts, and pre-filled 499-A workbook unlock in your portal.
</p>
<div style="text-align:center;margin:24px 0;">
<a href="{unlock_url}" style="display:inline-block;padding:12px 26px;background:#059669;color:#fff;border-radius:4px;
text-decoration:none;font-weight:700;">Unlock {reporting_year} Traffic Study →</a>
</div>
<p style="margin:0 0 10px;font-size:13px;color:#475569;">
Ingestion keeps running regardless — nothing's lost. This email goes out once per quarter until you pay.
Already filed elsewhere for this year? Reply and we'll stop nudging.
</p>
</td></tr>
</table></body></html>
"""
def _send(to_email: str, subject: str, body_html: str) -> bool:
if not SMTP_USER or not SMTP_PASS:
log.warning("SMTP unconfigured — would email %s: %s", to_email, subject)
return False
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = FROM_EMAIL
msg["To"] = to_email
msg["Bcc"] = ADMIN_EMAIL
msg.attach(MIMEText(body_html, "html"))
try:
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
if SMTP_PORT != 25:
server.starttls()
server.login(SMTP_USER, SMTP_PASS)
server.sendmail(FROM_EMAIL, [to_email, ADMIN_EMAIL], msg.as_string())
return True
except Exception as exc:
log.error("send to %s failed: %s", to_email, exc)
return False
def run() -> dict:
if not DATABASE_URL:
return {"error": "no DATABASE_URL"}
year = datetime.utcnow().year
quarter = (datetime.utcnow().month - 1) // 3 + 1
period_key = f"{year}Q{quarter}"
conn = psycopg2.connect(DATABASE_URL)
sent = 0
skipped = 0
try:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
# Profiles with healthy ingestion + no grant for this year
cur.execute(
"""
SELECT
p.id AS profile_id,
p.customer_id,
p.telecom_entity_id,
te.legal_name AS entity_name,
m.rows_ingested,
co_email.customer_email,
co_email.customer_name
FROM cdr_ingestion_profiles p
JOIN telecom_entities te ON te.id = p.telecom_entity_id
LEFT JOIN cdr_usage_meters m
ON m.profile_id=p.id AND m.reporting_year=%s
LEFT JOIN LATERAL (
SELECT customer_email, customer_name
FROM compliance_orders
WHERE telecom_entity_id = p.telecom_entity_id
AND payment_status='paid'
ORDER BY created_at DESC LIMIT 1
) co_email ON TRUE
WHERE m.rows_ingested >= %s
AND NOT EXISTS (
SELECT 1 FROM cdr_study_access_grants g
WHERE g.profile_id=p.id
AND g.reporting_year=%s
)
""",
(year, MIN_ROWS_TO_NUDGE, year),
)
candidates = list(cur.fetchall())
for c in candidates:
if not c.get("customer_email"):
skipped += 1
continue
unlock_url = (
f"{SITE_URL.rstrip('/')}/order/fcc-499a"
f"?entity={c['telecom_entity_id']}&year={year}&source=nudge:{period_key}"
)
html = _EMAIL_HTML.format(
customer_name=c.get("customer_name") or "there",
reporting_year=year,
row_count=c["rows_ingested"] or 0,
unlock_url=unlock_url,
)
subject = f"Unlock your {year} traffic study — {c['entity_name']}"
if _send(c["customer_email"], subject, html):
sent += 1
else:
skipped += 1
finally:
conn.close()
log.info("unlock nudge run: sent=%s skipped=%s period=%s", sent, skipped, period_key)
return {"sent": sent, "skipped": skipped, "period": period_key}
def main() -> None:
print(run())
if __name__ == "__main__":
main()