post-completion flow: survey, referral program, review ask

- Migration 081: referral_codes, referral_uses, exit_surveys tables
- API: POST /api/v1/survey, POST /api/v1/referral/check, GET /api/v1/referral/:email
- Worker: completion_emails.py — sends completion + 24h follow-up (survey + referral)
- Survey page: /survey/?order=X&rating=N — star rating, feedback, Google review ask
- Referral: REF-FIRSTNAME codes, $25 credit per referred order, no limit
- Low ratings (1-3 stars) trigger Telegram alert for admin follow-up
- Cron: every 15 minutes
This commit is contained in:
justin 2026-05-30 21:22:14 -05:00
parent 6b20ba7f08
commit ad3d189b2b
6 changed files with 655 additions and 0 deletions

View file

@ -0,0 +1,274 @@
"""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
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, "html"))
with smtplib.SMTP("localhost", 25) as s:
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()