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>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
172
scripts/workers/cdr_unlock_nudge.py
Normal file
172
scripts/workers/cdr_unlock_nudge.py
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
"""
|
||||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue