Files
2026-05-20 15:25:32 +07:00

625 lines
26 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { siteUrl } from "@/lib/site";
import { formatRupiah } from "@/lib/utils";
/**
* Registry semua template email. Setiap entry punya:
* - param shape (kontrak data yang harus dipassing caller)
* - render function → `{ subject, html }`
*
* Style: plain HTML inline-style untuk kompatibilitas client email
* (Outlook, Gmail mobile, dst). Hindari CSS modern (flexbox, grid, var).
*/
const PRIMARY = "#0d9488"; // primary-600
const NEUTRAL_700 = "#404040";
const NEUTRAL_500 = "#737373";
const RED_600 = "#dc2626";
const AMBER_600 = "#d97706";
const EMERALD_600 = "#059669";
function shell(opts: { title: string; bodyHtml: string; ctaLabel?: string; ctaUrl?: string }): string {
const cta = opts.ctaLabel && opts.ctaUrl
? `<p style="margin: 24px 0;">
<a href="${opts.ctaUrl}" style="display:inline-block;background:${PRIMARY};color:#ffffff;text-decoration:none;padding:12px 24px;border-radius:8px;font-weight:600;font-size:14px;">${opts.ctaLabel}</a>
</p>`
: "";
return `<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>${opts.title}</title></head>
<body style="margin:0;padding:0;background:#f5f5f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:${NEUTRAL_700};">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f5f5f5;padding:24px 0;">
<tr><td align="center">
<table width="560" cellpadding="0" cellspacing="0" style="background:#ffffff;border-radius:12px;padding:32px;max-width:90%;">
<tr><td>
<p style="margin:0 0 8px;font-size:12px;font-weight:bold;color:${PRIMARY};letter-spacing:1px;">SETRIP</p>
${opts.bodyHtml}
${cta}
<hr style="border:none;border-top:1px solid #e5e5e5;margin:24px 0;">
<p style="margin:0;font-size:11px;color:${NEUTRAL_500};">
Email ini dikirim otomatis dari SeTrip. Kalau ada pertanyaan, balas email ini atau hubungi <a href="mailto:support@setrip.id" style="color:${PRIMARY};">support@setrip.id</a>.
</p>
</td></tr>
</table>
</td></tr>
</table>
</body></html>`;
}
// ============================================================================
// KYC verification
// ============================================================================
export interface KycApprovedData {
userName: string;
}
function kycApproved(d: KycApprovedData) {
return {
subject: "✅ Verifikasi organizer SeTrip kamu sudah disetujui",
html: shell({
title: "KYC Approved",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;color:${EMERALD_600};">🎉 Selamat ${d.userName}!</h1>
<p style="margin:0 0 12px;">Pengajuan verifikasi organizer kamu sudah <strong>disetujui</strong>. Mulai sekarang kamu bisa:</p>
<ul style="margin:0 0 16px;padding-left:20px;">
<li>Membuat trip <strong>berbayar</strong></li>
<li>Menerima payout otomatis ke rekening setelah trip selesai</li>
<li>Mendapat badge ✅ Verified Organizer di profil publik</li>
</ul>
`,
ctaLabel: "Buat Trip Sekarang",
ctaUrl: `${siteUrl}/create-trip`,
}),
};
}
export interface KycRejectedData {
userName: string;
rejectionReason: string;
}
function kycRejected(d: KycRejectedData) {
return {
subject: "❌ Pengajuan verifikasi SeTrip kamu ditolak",
html: shell({
title: "KYC Rejected",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;color:${RED_600};">Pengajuan ditolak</h1>
<p style="margin:0 0 12px;">Halo ${d.userName}, pengajuan verifikasi organizer kamu belum bisa kami setujui.</p>
<div style="background:#fef2f2;border-left:4px solid ${RED_600};padding:12px 16px;margin:16px 0;">
<p style="margin:0;font-weight:600;font-size:13px;">Alasan:</p>
<p style="margin:8px 0 0;font-size:14px;">${escapeHtml(d.rejectionReason)}</p>
</div>
<p style="margin:0 0 12px;">Perbaiki data sesuai catatan di atas lalu ajukan ulang lewat halaman verifikasi.</p>
`,
ctaLabel: "Perbaiki & Ajukan Ulang",
ctaUrl: `${siteUrl}/verify`,
}),
};
}
export interface KycReuploadRequestData {
userName: string;
fields: string[];
note: string;
}
function kycReuploadRequest(d: KycReuploadRequestData) {
const fieldList = d.fields
.map((f) => `<li>${escapeHtml(reuploadFieldLabel(f))}</li>`)
.join("");
return {
subject: "🔄 Admin minta kamu upload ulang data verifikasi",
html: shell({
title: "KYC Re-upload Request",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;color:${AMBER_600};">Perlu upload ulang</h1>
<p style="margin:0 0 12px;">Halo ${d.userName}, admin meminta kamu mengirim ulang beberapa data verifikasi.</p>
<div style="background:#fffbeb;border-left:4px solid ${AMBER_600};padding:12px 16px;margin:16px 0;">
<p style="margin:0;font-weight:600;font-size:13px;">Field yang perlu di-upload ulang:</p>
<ul style="margin:8px 0 0;padding-left:20px;font-size:14px;">${fieldList}</ul>
<p style="margin:12px 0 0;font-weight:600;font-size:13px;">Catatan admin:</p>
<p style="margin:4px 0 0;font-size:14px;">${escapeHtml(d.note)}</p>
</div>
<p style="margin:0 0 12px;">Buka halaman verifikasi, perbaiki field yang diminta, lalu submit ulang. Banner akan otomatis hilang setelah submit.</p>
`,
ctaLabel: "Submit Ulang",
ctaUrl: `${siteUrl}/verify`,
}),
};
}
function reuploadFieldLabel(field: string): string {
switch (field) {
case "ktpImage":
return "Foto KTP";
case "liveness":
return "Foto liveness (pegang kertas SETRIP)";
case "nik":
return "NIK";
case "bankInfo":
return "Info rekening";
case "address":
return "Alamat";
default:
return field;
}
}
// ============================================================================
// Refund
// ============================================================================
export interface RefundCreatedData {
userName: string;
tripTitle: string;
amount: number;
reason: string;
}
function refundCreated(d: RefundCreatedData) {
return {
subject: `Pengajuan refund untuk "${d.tripTitle}" sudah kami terima`,
html: shell({
title: "Refund Created",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;">Laporan refund diterima</h1>
<p style="margin:0 0 12px;">Halo ${d.userName}, kami sudah menerima laporan refund kamu.</p>
<table cellpadding="0" cellspacing="0" style="width:100%;border-collapse:collapse;margin:16px 0;background:#fafafa;border-radius:8px;">
<tr><td style="padding:12px 16px;color:${NEUTRAL_500};font-size:13px;">Trip</td><td style="padding:12px 16px;font-weight:600;">${escapeHtml(d.tripTitle)}</td></tr>
<tr><td style="padding:12px 16px;color:${NEUTRAL_500};font-size:13px;border-top:1px solid #e5e5e5;">Nominal refund</td><td style="padding:12px 16px;font-weight:600;border-top:1px solid #e5e5e5;">${formatRupiah(d.amount)}</td></tr>
<tr><td style="padding:12px 16px;color:${NEUTRAL_500};font-size:13px;border-top:1px solid #e5e5e5;">Reason</td><td style="padding:12px 16px;font-weight:600;border-top:1px solid #e5e5e5;">${escapeHtml(d.reason)}</td></tr>
</table>
<p style="margin:0;font-size:14px;color:${NEUTRAL_500};">Admin akan review dalam 13 hari kerja. Kami kabari lagi via email.</p>
`,
}),
};
}
export interface RefundSucceededData {
userName: string;
tripTitle: string;
amount: number;
adminNote: string;
}
function refundSucceeded(d: RefundSucceededData) {
return {
subject: `✅ Refund ${formatRupiah(d.amount)} sudah dikirim`,
html: shell({
title: "Refund Succeeded",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;color:${EMERALD_600};">Refund sudah dikirim</h1>
<p style="margin:0 0 12px;">Halo ${d.userName}, refund untuk trip <strong>${escapeHtml(d.tripTitle)}</strong> sudah kami transfer.</p>
<p style="margin:0 0 12px;font-size:24px;font-weight:bold;color:${EMERALD_600};">${formatRupiah(d.amount)}</p>
${d.adminNote ? `<div style="background:#f0fdf4;border-left:4px solid ${EMERALD_600};padding:12px 16px;margin:16px 0;"><p style="margin:0;font-weight:600;font-size:13px;">Catatan admin:</p><p style="margin:4px 0 0;font-size:14px;">${escapeHtml(d.adminNote)}</p></div>` : ""}
<p style="margin:0;font-size:14px;">Cek rekening kamu — biasanya masuk dalam 1×24 jam (tergantung bank). Kalau lebih dari 3 hari belum muncul, balas email ini.</p>
`,
}),
};
}
export interface RefundFailedData {
userName: string;
tripTitle: string;
amount: number;
adminNote: string;
}
function refundFailed(d: RefundFailedData) {
return {
subject: `⚠️ Refund untuk "${d.tripTitle}" gagal diproses`,
html: shell({
title: "Refund Failed",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;color:${RED_600};">Refund gagal diproses</h1>
<p style="margin:0 0 12px;">Halo ${d.userName}, refund kamu untuk trip <strong>${escapeHtml(d.tripTitle)}</strong> sebesar <strong>${formatRupiah(d.amount)}</strong> gagal diproses.</p>
<div style="background:#fef2f2;border-left:4px solid ${RED_600};padding:12px 16px;margin:16px 0;">
<p style="margin:0;font-weight:600;font-size:13px;">Catatan admin:</p>
<p style="margin:4px 0 0;font-size:14px;">${escapeHtml(d.adminNote)}</p>
</div>
<p style="margin:0;font-size:14px;">Balas email ini atau hubungi <a href="mailto:support@setrip.id" style="color:${PRIMARY};">support@setrip.id</a> untuk koordinasi lanjutan. Kami akan bantu proses ulang.</p>
`,
}),
};
}
// ============================================================================
// Payment
// ============================================================================
export interface PaymentPaidData {
userName: string;
tripTitle: string;
tripId: string;
amount: number;
}
function paymentPaid(d: PaymentPaidData) {
return {
subject: `✅ Pembayaran ${formatRupiah(d.amount)} untuk "${d.tripTitle}" terkonfirmasi`,
html: shell({
title: "Payment Confirmed",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;color:${EMERALD_600};">Pembayaran terkonfirmasi 🎉</h1>
<p style="margin:0 0 12px;">Halo ${d.userName}, pembayaran kamu untuk trip <strong>${escapeHtml(d.tripTitle)}</strong> sudah masuk.</p>
<p style="margin:0 0 12px;font-size:20px;font-weight:bold;">${formatRupiah(d.amount)}</p>
<p style="margin:0 0 12px;font-size:14px;">Sampai jumpa di trip! Detail meeting point + itinerary lengkap bisa dicek di halaman trip.</p>
`,
ctaLabel: "Lihat Detail Trip",
ctaUrl: `${siteUrl}/trips/${d.tripId}`,
}),
};
}
export interface BookingApprovedData {
userName: string;
tripTitle: string;
tripId: string;
amount: number;
}
function bookingApproved(d: BookingApprovedData) {
const isFree = d.amount <= 0;
return {
subject: isFree
? `✅ Kamu disetujui ikut "${d.tripTitle}"`
: `✅ Kamu disetujui ikut "${d.tripTitle}" — siap bayar`,
html: shell({
title: "Booking Approved",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;color:${EMERALD_600};">Disetujui organizer!</h1>
<p style="margin:0 0 12px;">Halo ${d.userName}, organizer sudah <strong>menyetujui</strong> kamu untuk ikut trip <strong>${escapeHtml(d.tripTitle)}</strong>.</p>
${
isFree
? `<p style="margin:0 0 12px;font-size:14px;">Trip ini <strong>gratis</strong> — tidak perlu bayar. Cek detail meeting point + itinerary di halaman trip.</p>`
: `<p style="margin:0 0 12px;font-size:14px;">Untuk mengamankan slot, selesaikan pembayaran <strong>${formatRupiah(d.amount)}</strong> via Midtrans (BCA VA, GoPay, QRIS, dll). Dana ditahan SeTrip sampai trip selesai (escrow).</p>`
}
`,
ctaLabel: isFree ? "Lihat Detail Trip" : "Bayar Sekarang",
ctaUrl: isFree
? `${siteUrl}/trips/${d.tripId}`
: `${siteUrl}/trips/${d.tripId}/payment`,
}),
};
}
// ============================================================================
// Account moderation
// ============================================================================
export interface AccountSuspendedData {
userName: string;
reason: string;
}
function accountSuspended(d: AccountSuspendedData) {
return {
subject: "⛔ Akun SeTrip kamu ditangguhkan",
html: shell({
title: "Account Suspended",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;color:${RED_600};">Akun ditangguhkan</h1>
<p style="margin:0 0 12px;">Halo ${d.userName}, akun SeTrip kamu sedang dalam status <strong>ditangguhkan</strong>. Selama status ini berlaku, kamu tidak bisa login atau melakukan aksi (join trip, bikin trip, dll).</p>
<div style="background:#fef2f2;border-left:4px solid ${RED_600};padding:12px 16px;margin:16px 0;">
<p style="margin:0;font-weight:600;font-size:13px;">Alasan:</p>
<p style="margin:8px 0 0;font-size:14px;">${escapeHtml(d.reason)}</p>
</div>
<p style="margin:0;font-size:14px;">Kalau menurut kamu ini kesalahan atau ingin klarifikasi, balas email ini atau hubungi <a href="mailto:support@setrip.id" style="color:${PRIMARY};">support@setrip.id</a> dengan subject &quot;Banding suspend akun&quot;.</p>
`,
}),
};
}
// ============================================================================
// PR-E3 — Phase 2: notifikasi event (join, trip dibatalkan, payout, KYC)
// ============================================================================
/** Blok refund untuk email pembatalan trip. Pesan beda kalau tidak ada refund. */
function refundBlock(refundAmount: number): string {
if (refundAmount <= 0) {
return `<p style="margin:0 0 12px;font-size:14px;">Booking kamu untuk trip ini tidak punya pembayaran yang perlu dikembalikan.</p>`;
}
return `<div style="background:#f0fdf4;border-left:4px solid ${EMERALD_600};padding:12px 16px;margin:16px 0;">
<p style="margin:0;font-weight:600;font-size:13px;">Refund kamu sedang diproses</p>
<p style="margin:8px 0 0;font-size:18px;font-weight:bold;color:${EMERALD_600};">${formatRupiah(refundAmount)}</p>
<p style="margin:8px 0 0;font-size:13px;">Admin SeTrip akan memproses transfer ke rekening kamu. Kami kabari lagi via email saat refund selesai.</p>
</div>`;
}
export interface JoinRequestData {
organizerName: string;
joinerName: string;
tripTitle: string;
tripId: string;
}
function joinRequest(d: JoinRequestData) {
return {
subject: `👋 ${d.joinerName} mau gabung trip "${d.tripTitle}"`,
html: shell({
title: "Join Request",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;">Ada yang mau gabung trip kamu</h1>
<p style="margin:0 0 12px;">Halo ${escapeHtml(d.organizerName)}, <strong>${escapeHtml(d.joinerName)}</strong> mengajukan diri untuk ikut trip <strong>${escapeHtml(d.tripTitle)}</strong>.</p>
<p style="margin:0 0 12px;font-size:14px;">Buka halaman trip untuk meninjau profil peserta lalu setujui atau tolak permintaannya.</p>
`,
ctaLabel: "Tinjau Permintaan",
ctaUrl: `${siteUrl}/trips/${d.tripId}`,
}),
};
}
export interface JoinRejectedData {
userName: string;
tripTitle: string;
}
function joinRejected(d: JoinRejectedData) {
return {
subject: `Update permintaan gabung trip "${d.tripTitle}"`,
html: shell({
title: "Join Rejected",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;">Permintaan gabung belum diterima</h1>
<p style="margin:0 0 12px;">Halo ${escapeHtml(d.userName)}, organizer belum bisa menerima permintaan kamu untuk ikut trip <strong>${escapeHtml(d.tripTitle)}</strong> kali ini.</p>
<p style="margin:0 0 12px;font-size:14px;">Jangan berkecil hati — masih banyak trip seru lain yang bisa kamu ikuti.</p>
`,
ctaLabel: "Cari Trip Lain",
ctaUrl: `${siteUrl}/trips`,
}),
};
}
export interface PaymentExpiredData {
userName: string;
tripTitle: string;
tripId: string;
amount: number;
}
function paymentExpired(d: PaymentExpiredData) {
return {
subject: `⏰ Pembayaran trip "${d.tripTitle}" belum selesai`,
html: shell({
title: "Payment Expired",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;color:${AMBER_600};">Pembayaran belum selesai</h1>
<p style="margin:0 0 12px;">Halo ${escapeHtml(d.userName)}, pembayaran kamu untuk trip <strong>${escapeHtml(d.tripTitle)}</strong> sebesar <strong>${formatRupiah(d.amount)}</strong> kadaluarsa atau gagal — slot kamu belum aman.</p>
<p style="margin:0 0 12px;font-size:14px;">Selama trip belum penuh dan belum berangkat, kamu masih bisa mengulang pembayaran.</p>
`,
ctaLabel: "Coba Bayar Lagi",
ctaUrl: `${siteUrl}/trips/${d.tripId}/payment`,
}),
};
}
export interface TripCancelledOrganizerData {
userName: string;
tripTitle: string;
refundAmount: number;
}
function tripCancelledOrganizer(d: TripCancelledOrganizerData) {
return {
subject: `❌ Trip "${d.tripTitle}" dibatalkan`,
html: shell({
title: "Trip Cancelled",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;color:${RED_600};">Trip dibatalkan</h1>
<p style="margin:0 0 12px;">Halo ${escapeHtml(d.userName)}, mohon maaf — organizer membatalkan trip <strong>${escapeHtml(d.tripTitle)}</strong>. Partisipasi kamu di trip ini otomatis dibatalkan.</p>
${refundBlock(d.refundAmount)}
`,
ctaLabel: "Cari Trip Lain",
ctaUrl: `${siteUrl}/trips`,
}),
};
}
export interface TripCancelledAdminData {
userName: string;
tripTitle: string;
reason: string;
refundAmount: number;
}
function tripCancelledAdmin(d: TripCancelledAdminData) {
return {
subject: `❌ Trip "${d.tripTitle}" dibatalkan SeTrip`,
html: shell({
title: "Trip Cancelled by Admin",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;color:${RED_600};">Trip dibatalkan SeTrip</h1>
<p style="margin:0 0 12px;">Halo ${escapeHtml(d.userName)}, trip <strong>${escapeHtml(d.tripTitle)}</strong> dibatalkan oleh tim admin SeTrip.</p>
<div style="background:#fef2f2;border-left:4px solid ${RED_600};padding:12px 16px;margin:16px 0;">
<p style="margin:0;font-weight:600;font-size:13px;">Alasan:</p>
<p style="margin:8px 0 0;font-size:14px;">${escapeHtml(d.reason)}</p>
</div>
${refundBlock(d.refundAmount)}
`,
}),
};
}
export interface PayoutReleasedData {
organizerName: string;
tripTitle: string;
amount: number;
}
function payoutReleased(d: PayoutReleasedData) {
return {
subject: `💰 Payout trip "${d.tripTitle}" masuk antrian transfer`,
html: shell({
title: "Payout Released",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;color:${EMERALD_600};">Payout siap ditransfer</h1>
<p style="margin:0 0 12px;">Halo ${escapeHtml(d.organizerName)}, masa tahan (escrow) untuk trip <strong>${escapeHtml(d.tripTitle)}</strong> sudah berakhir.</p>
<p style="margin:0 0 12px;font-size:24px;font-weight:bold;color:${EMERALD_600};">${formatRupiah(d.amount)}</p>
<p style="margin:0;font-size:14px;">Dana ini masuk antrian transfer — admin SeTrip akan memprosesnya ke rekening kamu. Kami kabari lagi saat sudah ditransfer.</p>
`,
}),
};
}
export interface PayoutPaidData {
organizerName: string;
tripTitle: string;
amount: number;
adminNote: string;
}
function payoutPaid(d: PayoutPaidData) {
return {
subject: `✅ Payout ${formatRupiah(d.amount)} sudah ditransfer`,
html: shell({
title: "Payout Paid",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;color:${EMERALD_600};">Payout sudah ditransfer</h1>
<p style="margin:0 0 12px;">Halo ${escapeHtml(d.organizerName)}, payout untuk trip <strong>${escapeHtml(d.tripTitle)}</strong> sudah kami transfer.</p>
<p style="margin:0 0 12px;font-size:24px;font-weight:bold;color:${EMERALD_600};">${formatRupiah(d.amount)}</p>
${d.adminNote ? `<div style="background:#f0fdf4;border-left:4px solid ${EMERALD_600};padding:12px 16px;margin:16px 0;"><p style="margin:0;font-weight:600;font-size:13px;">Catatan / referensi transfer:</p><p style="margin:4px 0 0;font-size:14px;">${escapeHtml(d.adminNote)}</p></div>` : ""}
<p style="margin:0;font-size:14px;">Cek rekening kamu — biasanya masuk dalam 1×24 jam tergantung bank.</p>
`,
}),
};
}
export interface AccountUnsuspendedData {
userName: string;
}
function accountUnsuspended(d: AccountUnsuspendedData) {
return {
subject: "✅ Akun SeTrip kamu aktif kembali",
html: shell({
title: "Account Unsuspended",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;color:${EMERALD_600};">Akun aktif kembali</h1>
<p style="margin:0 0 12px;">Halo ${escapeHtml(d.userName)}, penangguhan akun SeTrip kamu sudah dicabut. Kamu bisa login dan beraktivitas seperti biasa lagi.</p>
`,
ctaLabel: "Buka SeTrip",
ctaUrl: siteUrl,
}),
};
}
export interface KycSubmittedData {
userName: string;
}
function kycSubmitted(d: KycSubmittedData) {
return {
subject: "📋 Pengajuan verifikasi organizer kamu sudah masuk",
html: shell({
title: "KYC Submitted",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;">Pengajuan diterima</h1>
<p style="margin:0 0 12px;">Halo ${escapeHtml(d.userName)}, pengajuan verifikasi organizer kamu sudah masuk dan sedang antri di-review tim admin.</p>
<p style="margin:0 0 12px;font-size:14px;">Estimasi review <strong>13 hari kerja</strong>. Hasilnya kami kabari lewat email — tidak perlu submit ulang selama belum ada keputusan.</p>
`,
}),
};
}
export interface KycManualOverrideData {
userName: string;
}
function kycManualOverride(d: KycManualOverrideData) {
return {
subject: "✅ Kamu jadi organizer terverifikasi SeTrip",
html: shell({
title: "KYC Manual Override",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;color:${EMERALD_600};">🎉 Selamat ${escapeHtml(d.userName)}!</h1>
<p style="margin:0 0 12px;">Tim admin SeTrip sudah memverifikasi kamu sebagai organizer. Mulai sekarang kamu bisa membuat trip berbayar dan menerima payout.</p>
`,
ctaLabel: "Buat Trip Sekarang",
ctaUrl: `${siteUrl}/create-trip`,
}),
};
}
export interface KycReopenedData {
userName: string;
}
function kycReopened(d: KycReopenedData) {
return {
subject: "🔄 Pengajuan verifikasi kamu dibuka kembali",
html: shell({
title: "KYC Reopened",
bodyHtml: `
<h1 style="margin:0 0 16px;font-size:22px;color:${AMBER_600};">Pengajuan dibuka kembali</h1>
<p style="margin:0 0 12px;">Halo ${escapeHtml(d.userName)}, tim admin membuka kembali pengajuan verifikasi organizer kamu. Kamu bisa memperbaiki data lalu mengajukannya ulang.</p>
`,
ctaLabel: "Buka Halaman Verifikasi",
ctaUrl: `${siteUrl}/verify`,
}),
};
}
// ============================================================================
// Registry — discriminated union supaya type-safe per template.
// ============================================================================
export type EmailTemplate =
| { template: "kyc_approved"; data: KycApprovedData }
| { template: "kyc_rejected"; data: KycRejectedData }
| { template: "kyc_reupload_request"; data: KycReuploadRequestData }
| { template: "refund_created"; data: RefundCreatedData }
| { template: "refund_succeeded"; data: RefundSucceededData }
| { template: "refund_failed"; data: RefundFailedData }
| { template: "payment_paid"; data: PaymentPaidData }
| { template: "booking_approved"; data: BookingApprovedData }
| { template: "account_suspended"; data: AccountSuspendedData }
| { template: "join_request"; data: JoinRequestData }
| { template: "join_rejected"; data: JoinRejectedData }
| { template: "payment_expired"; data: PaymentExpiredData }
| { template: "trip_cancelled_organizer"; data: TripCancelledOrganizerData }
| { template: "trip_cancelled_admin"; data: TripCancelledAdminData }
| { template: "payout_released"; data: PayoutReleasedData }
| { template: "payout_paid"; data: PayoutPaidData }
| { template: "account_unsuspended"; data: AccountUnsuspendedData }
| { template: "kyc_submitted"; data: KycSubmittedData }
| { template: "kyc_manual_override"; data: KycManualOverrideData }
| { template: "kyc_reopened"; data: KycReopenedData };
export function renderEmail(input: EmailTemplate): {
subject: string;
html: string;
} {
switch (input.template) {
case "kyc_approved":
return kycApproved(input.data);
case "kyc_rejected":
return kycRejected(input.data);
case "kyc_reupload_request":
return kycReuploadRequest(input.data);
case "refund_created":
return refundCreated(input.data);
case "refund_succeeded":
return refundSucceeded(input.data);
case "refund_failed":
return refundFailed(input.data);
case "payment_paid":
return paymentPaid(input.data);
case "booking_approved":
return bookingApproved(input.data);
case "account_suspended":
return accountSuspended(input.data);
case "join_request":
return joinRequest(input.data);
case "join_rejected":
return joinRejected(input.data);
case "payment_expired":
return paymentExpired(input.data);
case "trip_cancelled_organizer":
return tripCancelledOrganizer(input.data);
case "trip_cancelled_admin":
return tripCancelledAdmin(input.data);
case "payout_released":
return payoutReleased(input.data);
case "payout_paid":
return payoutPaid(input.data);
case "account_unsuspended":
return accountUnsuspended(input.data);
case "kyc_submitted":
return kycSubmitted(input.data);
case "kyc_manual_override":
return kycManualOverride(input.data);
case "kyc_reopened":
return kycReopened(input.data);
}
}
// Escape HTML supaya tidak ada XSS via data user (mis. tripTitle dari user input).
function escapeHtml(str: string): string {
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}