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,47 @@
-- Referral codes and survey responses for post-completion flow
-- Referral codes (one per customer)
CREATE TABLE IF NOT EXISTS referral_codes (
id SERIAL PRIMARY KEY,
code TEXT NOT NULL UNIQUE, -- e.g. REF-MARKADAMS
customer_email TEXT NOT NULL,
customer_name TEXT,
credit_cents INTEGER DEFAULT 2500, -- $25 credit
times_used INTEGER DEFAULT 0,
total_earned_cents INTEGER DEFAULT 0,
balance_cents INTEGER DEFAULT 0, -- unused credit
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_referral_codes_email ON referral_codes(customer_email);
CREATE INDEX IF NOT EXISTS idx_referral_codes_code ON referral_codes(code);
-- Referral usage tracking
CREATE TABLE IF NOT EXISTS referral_uses (
id SERIAL PRIMARY KEY,
referral_code TEXT NOT NULL REFERENCES referral_codes(code),
referred_order TEXT NOT NULL, -- order_number of the new customer's order
referred_email TEXT NOT NULL,
credit_cents INTEGER DEFAULT 2500,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Exit survey responses
CREATE TABLE IF NOT EXISTS exit_surveys (
id SERIAL PRIMARY KEY,
order_number TEXT NOT NULL,
customer_email TEXT NOT NULL,
rating INTEGER CHECK (rating BETWEEN 1 AND 5),
feedback TEXT,
would_recommend BOOLEAN,
review_link_clicked BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_exit_surveys_order ON exit_surveys(order_number);
-- Track which completion emails have been sent (prevent duplicates)
ALTER TABLE compliance_orders
ADD COLUMN IF NOT EXISTS completion_email_sent_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS followup_email_sent_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS referral_code TEXT;

View file

@ -47,6 +47,7 @@ import portalEsignGenericRouter from "./routes/portal-esign-generic.js";
import pucRouter from "./routes/puc.js";
import fccCarrierRegRouter from "./routes/fcc-carrier-registration.js";
import dotLookupRouter from "./routes/dot-lookup.js";
import surveyRouter from "./routes/survey.js";
const app = express();
@ -120,6 +121,7 @@ app.use(foreignQualRouter);
app.use(pucRouter);
app.use(fccCarrierRegRouter);
app.use(dotLookupRouter);
app.use(surveyRouter);
app.use(adminCryptoRouter);
// Note: identityRouter mounted above express.json() for webhook route,
// but also handles non-webhook routes (create-session, poll) which work fine with json()

152
api/src/routes/survey.ts Normal file
View file

@ -0,0 +1,152 @@
/**
* Exit survey + referral endpoints.
*
* POST /api/v1/survey Submit exit survey response
* GET /api/v1/survey/:order Get survey form data (pre-fill)
* POST /api/v1/referral/check Validate a referral code
* GET /api/v1/referral/:email Get referral code + stats for a customer
*/
import { Router, type Request, type Response } from "express";
import { pool } from "../db.js";
const router = Router();
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || "";
const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID || "";
// POST /api/v1/survey — Submit exit survey
router.post("/api/v1/survey", async (req: Request, res: Response) => {
try {
const { order_number, rating, feedback, would_recommend } = req.body ?? {};
if (!order_number || !rating) {
res.status(400).json({ error: "order_number and rating required." });
return;
}
// Get customer email from order
const orderResult = await pool.query(
"SELECT customer_email, customer_name, service_name FROM compliance_orders WHERE order_number = $1",
[order_number],
);
const order = orderResult.rows[0] as Record<string, unknown> | undefined;
const email = (order?.customer_email as string) || "";
await pool.query(
`INSERT INTO exit_surveys (order_number, customer_email, rating, feedback, would_recommend)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT DO NOTHING`,
[order_number, email, rating, feedback || null, would_recommend ?? null],
);
// Telegram alert for low ratings
if (rating <= 3 && TELEGRAM_BOT_TOKEN && TELEGRAM_CHAT_ID) {
const text = `⚠️ Low survey rating\nOrder: ${order_number}\nRating: ${"⭐".repeat(rating)}\nCustomer: ${email}\nFeedback: ${feedback || "(none)"}`;
fetch(`https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chat_id: TELEGRAM_CHAT_ID, text }),
}).catch(() => {});
}
// If high rating, return Google review link
const showReview = rating >= 4;
const googleReviewUrl = process.env.GOOGLE_REVIEW_URL || "";
res.json({
success: true,
show_review: showReview,
google_review_url: googleReviewUrl || null,
referral_code: order ? await getOrCreateReferralCode(email, (order.customer_name as string) || "") : null,
});
} catch (err) {
console.error("[survey] Error:", err);
res.status(500).json({ error: "Survey submission failed." });
}
});
// POST /api/v1/referral/check — Validate a referral code
router.post("/api/v1/referral/check", async (req: Request, res: Response) => {
try {
const { code } = req.body ?? {};
if (!code) {
res.status(400).json({ valid: false, error: "code required." });
return;
}
const result = await pool.query(
"SELECT code, customer_name FROM referral_codes WHERE code = $1",
[code.toUpperCase()],
);
if (result.rows.length === 0) {
res.json({ valid: false });
return;
}
res.json({
valid: true,
referrer_name: (result.rows[0] as Record<string, unknown>).customer_name,
});
} catch {
res.status(500).json({ valid: false, error: "Check failed." });
}
});
// GET /api/v1/referral/:email — Get referral info for a customer
router.get("/api/v1/referral/:email", async (req: Request, res: Response) => {
try {
const email = req.params.email;
const result = await pool.query(
"SELECT code, times_used, total_earned_cents, balance_cents FROM referral_codes WHERE customer_email = $1",
[email],
);
if (result.rows.length === 0) {
res.json({ has_code: false });
return;
}
const r = result.rows[0] as Record<string, unknown>;
res.json({
has_code: true,
code: r.code,
times_used: r.times_used,
total_earned: ((r.total_earned_cents as number) / 100).toFixed(2),
balance: ((r.balance_cents as number) / 100).toFixed(2),
});
} catch {
res.status(500).json({ error: "Failed." });
}
});
async function getOrCreateReferralCode(email: string, name: string): Promise<string> {
// Check if already exists
const existing = await pool.query(
"SELECT code FROM referral_codes WHERE customer_email = $1",
[email],
);
if (existing.rows.length > 0) {
return (existing.rows[0] as Record<string, unknown>).code as string;
}
// Generate code from name
const cleanName = name.replace(/[^a-zA-Z]/g, "").toUpperCase().substring(0, 12);
let code = `REF-${cleanName || "CUSTOMER"}`;
// Ensure unique
const check = await pool.query("SELECT 1 FROM referral_codes WHERE code = $1", [code]);
if (check.rows.length > 0) {
code = `${code}${Math.floor(Math.random() * 99)}`;
}
await pool.query(
`INSERT INTO referral_codes (code, customer_email, customer_name)
VALUES ($1, $2, $3) ON CONFLICT (customer_email) DO NOTHING`,
[code, email, name],
);
return code;
}
export default router;

View file

@ -134,6 +134,14 @@ worker_crons:
on_calendar: "*-*-* 08:00:00 UTC"
persistent: true
# Post-completion emails — every 15 minutes.
# Sends completion confirmations + 24h follow-up (survey + referral).
- name: pw-completion-emails
description: Send post-completion and follow-up emails
module: scripts.workers.completion_emails
on_calendar: "*-*-* *:0/15:00 UTC"
persistent: false
# Florida entity downloader — daily 07:30 UTC.
# Downloads daily diffs from FL Sunbiz SFTP (free public access).
- name: pw-fl-entity-downloader

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()

View file

@ -0,0 +1,172 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex">
<title>How Did We Do? | Performance West</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<script>
window.__PW_API = (function() {
var h = window.location.hostname;
if (h === "localhost" || h === "127.0.0.1") return "http://" + h + ":3001";
if (h === "dev.performancewest.net") return "https://api.dev.performancewest.net";
return "https://api.performancewest.net";
})();
</script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, system-ui, sans-serif; background: #f8fafc; min-height: 100vh; display: flex; flex-direction: column; }
.header { background: #1a2744; padding: 20px; text-align: center; }
.header img { height: 40px; }
.main { flex: 1; padding: 24px; max-width: 520px; margin: 0 auto; width: 100%; }
.card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 32px; margin-bottom: 16px; }
h1 { font-size: 22px; color: #1a2744; margin-bottom: 8px; text-align: center; }
.subtitle { font-size: 15px; color: #64748b; text-align: center; margin-bottom: 24px; }
.stars { display: flex; justify-content: center; gap: 8px; margin-bottom: 24px; }
.star { font-size: 40px; cursor: pointer; transition: transform 0.1s; opacity: 0.3; }
.star.active { opacity: 1; }
.star:hover { transform: scale(1.2); }
textarea { width: 100%; padding: 12px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 14px; resize: vertical; min-height: 80px; font-family: inherit; }
.btn { display: block; width: 100%; padding: 14px; background: #f97316; color: #fff; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; margin-top: 16px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.review-box { background: #f0fdf4; border: 2px solid #86efac; border-radius: 12px; padding: 24px; text-align: center; margin-top: 16px; }
.referral-box { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 12px; padding: 20px; margin-top: 16px; }
.referral-code { background: #fff; border: 2px dashed #3b82f6; border-radius: 8px; padding: 12px; text-align: center; margin: 12px 0; }
.referral-code span { font-size: 24px; font-weight: 700; color: #1e3a5f; letter-spacing: 1px; }
.footer { padding: 16px; text-align: center; font-size: 11px; color: #94a3b8; border-top: 1px solid #e2e8f0; }
.hidden { display: none; }
</style>
</head>
<body>
<div class="header">
<img src="/images/logo.png" alt="Performance West">
</div>
<div class="main">
<!-- Survey form -->
<div id="survey-form" class="card">
<h1>How was your experience?</h1>
<p class="subtitle">Your feedback helps us serve truckers better.</p>
<div class="stars" id="star-row">
<span class="star" data-rating="1"></span>
<span class="star" data-rating="2"></span>
<span class="star" data-rating="3"></span>
<span class="star" data-rating="4"></span>
<span class="star" data-rating="5"></span>
</div>
<p id="rating-label" style="text-align:center;font-size:14px;color:#64748b;margin-bottom:16px"></p>
<label style="font-size:14px;font-weight:600;color:#374151;display:block;margin-bottom:6px">Anything you'd like us to know? <span style="color:#94a3b8">(optional)</span></label>
<textarea id="feedback" placeholder="What went well? What could we improve?"></textarea>
<button type="button" id="submit-btn" class="btn" disabled>Submit Feedback</button>
</div>
<!-- Thank you + review ask (shown after submit if rating >= 4) -->
<div id="thank-you" class="hidden">
<div class="card" style="text-align:center">
<div style="font-size:48px;margin-bottom:12px">🙏</div>
<h1>Thank you for your feedback!</h1>
<p class="subtitle">We appreciate you taking the time.</p>
</div>
<div id="review-ask" class="review-box hidden">
<h3 style="font-size:16px;color:#166534;margin-bottom:8px">Glad you had a great experience!</h3>
<p style="font-size:14px;color:#374151;margin-bottom:16px">Would you mind leaving us a quick Google review? It helps other truckers find us and takes about 30 seconds.</p>
<a id="google-review-link" href="#" target="_blank" style="display:inline-block;padding:12px 28px;background:#059669;color:#fff;font-weight:600;border-radius:8px;text-decoration:none;font-size:15px">Leave a Google Review →</a>
</div>
<div id="referral-section" class="referral-box hidden">
<h3 style="font-size:16px;color:#1e3a5f;margin-bottom:6px">Know another trucker who needs help?</h3>
<p style="font-size:14px;color:#1d4ed8;margin-bottom:4px">Share your referral code and earn <strong>$25 credit</strong> for each order they place.</p>
<div class="referral-code">
<p style="font-size:11px;color:#64748b;margin-bottom:4px">Your referral code:</p>
<span id="referral-code-display"></span>
</div>
<p style="font-size:12px;color:#64748b">They enter this code at checkout. You earn $25 per order — no limit.</p>
</div>
</div>
</div>
<div class="footer">
Performance West Inc. &middot; (888) 411-0383 &middot; performancewest.net
</div>
<script>
var API = window.__PW_API;
var params = new URLSearchParams(window.location.search);
var orderNumber = params.get("order") || "";
var preRating = parseInt(params.get("rating") || "0");
var selectedRating = 0;
var stars = document.querySelectorAll(".star");
var ratingLabel = document.getElementById("rating-label");
var submitBtn = document.getElementById("submit-btn");
var labels = ["", "Poor", "Fair", "Good", "Great", "Excellent"];
function setRating(r) {
selectedRating = r;
stars.forEach(function(s, i) {
s.classList.toggle("active", i < r);
});
ratingLabel.textContent = labels[r] || "";
submitBtn.disabled = false;
}
stars.forEach(function(s) {
s.addEventListener("click", function() {
setRating(parseInt(s.dataset.rating));
});
});
// Pre-select from URL param
if (preRating >= 1 && preRating <= 5) {
setRating(preRating);
}
submitBtn.addEventListener("click", function() {
if (!selectedRating || !orderNumber) return;
submitBtn.disabled = true;
submitBtn.textContent = "Submitting...";
fetch(API + "/api/v1/survey", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
order_number: orderNumber,
rating: selectedRating,
feedback: document.getElementById("feedback").value,
would_recommend: selectedRating >= 4,
}),
})
.then(function(r) { return r.json(); })
.then(function(d) {
document.getElementById("survey-form").classList.add("hidden");
document.getElementById("thank-you").classList.remove("hidden");
// Show Google review if high rating
if (d.show_review && d.google_review_url) {
var reviewEl = document.getElementById("review-ask");
reviewEl.classList.remove("hidden");
document.getElementById("google-review-link").href = d.google_review_url;
}
// Show referral code
if (d.referral_code) {
document.getElementById("referral-section").classList.remove("hidden");
document.getElementById("referral-code-display").textContent = d.referral_code;
}
})
.catch(function() {
submitBtn.textContent = "Submit Feedback";
submitBtn.disabled = false;
alert("Something went wrong. Please try again.");
});
});
</script>
</body>
</html>