- 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
152 lines
5 KiB
TypeScript
152 lines
5 KiB
TypeScript
/**
|
|
* 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;
|