- 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
274 lines
11 KiB
Python
274 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
|
|
|
|
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()
|