new-site/scripts/workers/completion_emails.py
justin b375385efd fix(email): add text/plain part to every transactional + telecom email
All transactional/worker senders built multipart/alternative (or mixed)
messages with ONLY an HTML part. A single-part multipart/alternative is
malformed and HTML-only mail is a spam-score signal -- the same class of
deliverability bug that hurt the campaign pipeline, but on the telecom /
filing / customer-transactional path (499-Q reminders, RMD/FCC filing
review links, intake/completion/delivery emails, commissions, etc).

- worker_email.send_worker_email: auto-derive plaintext from HTML when
  caller omits text= (fixes the shared helper for all current+future use)
- 16 rolled-their-own senders in scripts/workers/** + scripts/formation/
  document_delivery.py: attach html_to_text(...) plaintext sibling before
  the HTML part (job_server + document_delivery wrap text+html in an
  alternative sub-part so PDFs still attach to the mixed root)
- api/src/email.ts: add dependency-free htmlToText() and default
  sendEmail text to it (fixes checkout/webhook HTML-only sends)

Verified: all py files compile + import at runtime, api tsc passes,
htmlToText handles hrefs/lists/entities, 11 plaintext unit tests pass.
Telecom campaign 407 (Jun 8) was HTML-only + sent in the DKIM-broken
window -> 384 sent / 0 clicks (same junked-mail signature).
2026-06-17 21:07:40 -05:00

282 lines
11 KiB
Python

"""Post-completion email flow.
Sends two emails after an order is marked completed:
1. Immediately: "Your filing is complete!" with documents
2. 24 hours later: Exit survey + referral program + review ask
Run via cron every 15 minutes:
python -m scripts.workers.completion_emails
"""
import json
import logging
import os
import smtplib
import sys
import urllib.request
from datetime import datetime, timedelta, timezone
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from scripts._email_plaintext import html_to_text
import psycopg2
LOG = logging.getLogger("workers.completion_emails")
DB_URL = os.getenv("DATABASE_URL", "")
SMTP_FROM = "noreply@performancewest.net"
SITE_URL = os.getenv("SITE_URL", "https://performancewest.net")
API_URL = os.getenv("API_URL", "https://api.performancewest.net")
GOOGLE_REVIEW_URL = os.getenv("GOOGLE_REVIEW_URL", "")
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
def send_email(to: str, subject: str, html: str):
"""Send an HTML email via local Postfix."""
msg = MIMEMultipart("alternative")
msg["From"] = f"Performance West <{SMTP_FROM}>"
msg["To"] = to
msg["Subject"] = subject
msg.attach(MIMEText(html_to_text(html), "plain"))
msg.attach(MIMEText(html, "html"))
import os as _smtp_os
with smtplib.SMTP(_smtp_os.getenv("SMTP_HOST", "co.carrierone.com"), int(_smtp_os.getenv("SMTP_PORT", "587")), timeout=30) as s:
s.starttls()
_u, _p = _smtp_os.getenv("SMTP_USER", ""), _smtp_os.getenv("SMTP_PASS", "")
if _u and _p:
s.login(_u, _p)
s.sendmail(SMTP_FROM, [to], msg.as_string())
def get_or_create_referral_code(conn, email: str, name: str) -> str:
"""Get or create a referral code for a customer."""
cur = conn.cursor()
cur.execute("SELECT code FROM referral_codes WHERE customer_email = %s", (email,))
row = cur.fetchone()
if row:
return row[0]
clean = "".join(c for c in name.upper() if c.isalpha())[:12]
code = f"REF-{clean or 'CUSTOMER'}"
# Ensure unique
cur.execute("SELECT 1 FROM referral_codes WHERE code = %s", (code,))
if cur.fetchone():
import random
code = f"{code}{random.randint(10, 99)}"
try:
cur.execute(
"INSERT INTO referral_codes (code, customer_email, customer_name) VALUES (%s, %s, %s) ON CONFLICT DO NOTHING",
(code, email, name),
)
conn.commit()
except Exception:
conn.rollback()
return code
def completion_email_html(order_number: str, service_name: str, customer_name: str) -> str:
"""Generate the completion email HTML."""
first = customer_name.split()[0] if customer_name else "there"
return f"""<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
<body style="font-family:-apple-system,system-ui,sans-serif;background:#f8fafc;margin:0;padding:0">
<div style="max-width:560px;margin:0 auto;padding:20px">
<div style="background:#1a2744;border-radius:12px 12px 0 0;padding:24px;text-align:center">
<img src="{SITE_URL}/images/logo.png" alt="Performance West" style="height:40px">
</div>
<div style="background:#fff;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px;padding:32px">
<div style="text-align:center;margin-bottom:24px">
<div style="width:56px;height:56px;background:#f0fdf4;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;margin-bottom:12px">
<span style="font-size:28px">✅</span>
</div>
<h1 style="font-size:22px;color:#1a2744;margin:0">Your {service_name} Is Complete!</h1>
</div>
<p style="font-size:15px;color:#374151;line-height:1.6">Hi {first},</p>
<p style="font-size:15px;color:#374151;line-height:1.6">
Great news — your <strong>{service_name}</strong> has been completed and filed successfully.
Order reference: <strong>{order_number}</strong>.
</p>
<p style="font-size:15px;color:#374151;line-height:1.6">
You can view your documents and track all your filings in your client portal:
</p>
<div style="text-align:center;margin:24px 0">
<a href="{SITE_URL}/portal/dashboard" style="display:inline-block;padding:12px 32px;background:#f97316;color:#fff;font-weight:700;border-radius:8px;text-decoration:none;font-size:15px">View in Portal →</a>
</div>
<p style="font-size:14px;color:#64748b;line-height:1.6">
If you have any questions about your filing, reply to this email or call us at
<a href="tel:8884110383" style="color:#f97316;font-weight:600">(888) 411-0383</a>.
</p>
<p style="font-size:14px;color:#64748b">— The Performance West Team</p>
</div>
<div style="text-align:center;padding:16px;font-size:11px;color:#94a3b8">
Performance West Inc. · (888) 411-0383 · performancewest.net
</div>
</div>
</body></html>"""
def followup_email_html(
order_number: str,
service_name: str,
customer_name: str,
referral_code: str,
) -> str:
"""Generate the 24h follow-up email with survey + referral."""
first = customer_name.split()[0] if customer_name else "there"
survey_url = f"{SITE_URL}/survey?order={order_number}"
return f"""<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"></head>
<body style="font-family:-apple-system,system-ui,sans-serif;background:#f8fafc;margin:0;padding:0">
<div style="max-width:560px;margin:0 auto;padding:20px">
<div style="background:#1a2744;border-radius:12px 12px 0 0;padding:24px;text-align:center">
<img src="{SITE_URL}/images/logo.png" alt="Performance West" style="height:40px">
</div>
<div style="background:#fff;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px;padding:32px">
<h1 style="font-size:20px;color:#1a2744;margin:0 0 16px">How did we do, {first}?</h1>
<p style="font-size:15px;color:#374151;line-height:1.6">
Your <strong>{service_name}</strong> was completed yesterday. We'd love your feedback — it takes 30 seconds.
</p>
<!-- Star rating -->
<div style="text-align:center;margin:24px 0">
<p style="font-size:14px;color:#64748b;margin:0 0 8px">Rate your experience:</p>
<div style="font-size:32px;letter-spacing:8px">
<a href="{survey_url}&rating=1" style="text-decoration:none">⭐</a>
<a href="{survey_url}&rating=2" style="text-decoration:none">⭐</a>
<a href="{survey_url}&rating=3" style="text-decoration:none">⭐</a>
<a href="{survey_url}&rating=4" style="text-decoration:none">⭐</a>
<a href="{survey_url}&rating=5" style="text-decoration:none">⭐</a>
</div>
<p style="font-size:12px;color:#94a3b8;margin:4px 0 0">Click a star to rate</p>
</div>
<div style="border-top:1px solid #e2e8f0;margin:24px 0"></div>
<!-- Referral -->
<div style="background:#eff6ff;border:1px solid #bfdbfe;border-radius:10px;padding:20px;margin-bottom:20px">
<h3 style="font-size:16px;color:#1e3a5f;margin:0 0 8px">Know another trucker?</h3>
<p style="font-size:14px;color:#1d4ed8;margin:0 0 12px;line-height:1.5">
Share your referral code and earn <strong>$25 credit</strong> for each order they place.
</p>
<div style="background:#fff;border:2px dashed #3b82f6;border-radius:8px;padding:12px;text-align:center">
<p style="font-size:12px;color:#64748b;margin:0 0 4px">Your referral code:</p>
<p style="font-size:22px;font-weight:700;color:#1e3a5f;margin:0;letter-spacing:1px">{referral_code}</p>
</div>
<p style="font-size:12px;color:#64748b;margin:8px 0 0;text-align:center">
They can enter this code at checkout. You earn $25 credit per order — no limit.
</p>
</div>
<p style="font-size:14px;color:#64748b;line-height:1.6">
Thank you for choosing Performance West. We're here whenever you need us.
</p>
<p style="font-size:14px;color:#64748b">— The Performance West Team</p>
</div>
<div style="text-align:center;padding:16px;font-size:11px;color:#94a3b8">
Performance West Inc. · (888) 411-0383 · performancewest.net
</div>
</div>
</body></html>"""
def process_completions():
"""Find completed orders and send emails."""
conn = psycopg2.connect(DB_URL)
cur = conn.cursor()
now = datetime.now(timezone.utc)
# 1. Send completion emails (order completed, email not sent yet)
cur.execute("""
SELECT order_number, customer_email, customer_name, service_name, updated_at
FROM compliance_orders
WHERE payment_status = 'paid'
AND completion_email_sent_at IS NULL
AND updated_at < NOW() - interval '5 minutes'
AND (
intake_data->>'status' = 'completed'
OR intake_data->>'status' = 'delivered'
OR intake_data->>'status' = 'filed'
)
ORDER BY updated_at
LIMIT 20
""")
completions = cur.fetchall()
for row in completions:
order_number, email, name, service_name, updated_at = row
if not email:
continue
try:
html = completion_email_html(order_number, service_name or "compliance filing", name or "")
send_email(email, f"✅ Your {service_name or 'filing'} is complete — {order_number}", html)
cur.execute(
"UPDATE compliance_orders SET completion_email_sent_at = NOW() WHERE order_number = %s",
(order_number,),
)
conn.commit()
LOG.info("[completion] Sent completion email for %s to %s", order_number, email)
except Exception as exc:
LOG.error("[completion] Failed for %s: %s", order_number, exc)
conn.rollback()
# 2. Send 24h follow-up emails (completion email sent 24h+ ago, follow-up not sent)
cur.execute("""
SELECT order_number, customer_email, customer_name, service_name
FROM compliance_orders
WHERE payment_status = 'paid'
AND completion_email_sent_at IS NOT NULL
AND completion_email_sent_at < NOW() - interval '24 hours'
AND followup_email_sent_at IS NULL
ORDER BY completion_email_sent_at
LIMIT 20
""")
followups = cur.fetchall()
for row in followups:
order_number, email, name, service_name = row
if not email:
continue
try:
referral_code = get_or_create_referral_code(conn, email, name or "")
html = followup_email_html(order_number, service_name or "compliance filing", name or "", referral_code)
send_email(email, f"How was your experience? + Earn $25 referrals", html)
cur.execute(
"UPDATE compliance_orders SET followup_email_sent_at = NOW(), referral_code = %s WHERE order_number = %s",
(referral_code, order_number),
)
conn.commit()
LOG.info("[followup] Sent follow-up email for %s to %s (ref: %s)", order_number, email, referral_code)
except Exception as exc:
LOG.error("[followup] Failed for %s: %s", order_number, exc)
conn.rollback()
conn.close()
total = len(completions) + len(followups)
if total:
LOG.info("[completion_emails] Processed %d completion + %d follow-up emails", len(completions), len(followups))
def main():
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s %(message)s",
)
process_completions()
if __name__ == "__main__":
main()