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 ? `

${opts.ctaLabel}

` : ""; return ` ${opts.title}

SETRIP

${opts.bodyHtml} ${cta}

Email ini dikirim otomatis dari SeTrip. Kalau ada pertanyaan, balas email ini atau hubungi support@setrip.id.

`; } // ============================================================================ // 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: `

🎉 Selamat ${d.userName}!

Pengajuan verifikasi organizer kamu sudah disetujui. Mulai sekarang kamu bisa:

`, 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: `

Pengajuan ditolak

Halo ${d.userName}, pengajuan verifikasi organizer kamu belum bisa kami setujui.

Alasan:

${escapeHtml(d.rejectionReason)}

Perbaiki data sesuai catatan di atas lalu ajukan ulang lewat halaman verifikasi.

`, 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) => `
  • ${escapeHtml(reuploadFieldLabel(f))}
  • `) .join(""); return { subject: "🔄 Admin minta kamu upload ulang data verifikasi", html: shell({ title: "KYC Re-upload Request", bodyHtml: `

    Perlu upload ulang

    Halo ${d.userName}, admin meminta kamu mengirim ulang beberapa data verifikasi.

    Field yang perlu di-upload ulang:

    Catatan admin:

    ${escapeHtml(d.note)}

    Buka halaman verifikasi, perbaiki field yang diminta, lalu submit ulang. Banner akan otomatis hilang setelah submit.

    `, 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: `

    Laporan refund diterima

    Halo ${d.userName}, kami sudah menerima laporan refund kamu.

    Trip${escapeHtml(d.tripTitle)}
    Nominal refund${formatRupiah(d.amount)}
    Reason${escapeHtml(d.reason)}

    Admin akan review dalam 1–3 hari kerja. Kami kabari lagi via email.

    `, }), }; } 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: `

    Refund sudah dikirim

    Halo ${d.userName}, refund untuk trip ${escapeHtml(d.tripTitle)} sudah kami transfer.

    ${formatRupiah(d.amount)}

    ${d.adminNote ? `

    Catatan admin:

    ${escapeHtml(d.adminNote)}

    ` : ""}

    Cek rekening kamu — biasanya masuk dalam 1×24 jam (tergantung bank). Kalau lebih dari 3 hari belum muncul, balas email ini.

    `, }), }; } 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: `

    Refund gagal diproses

    Halo ${d.userName}, refund kamu untuk trip ${escapeHtml(d.tripTitle)} sebesar ${formatRupiah(d.amount)} gagal diproses.

    Catatan admin:

    ${escapeHtml(d.adminNote)}

    Balas email ini atau hubungi support@setrip.id untuk koordinasi lanjutan. Kami akan bantu proses ulang.

    `, }), }; } // ============================================================================ // 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: `

    Pembayaran terkonfirmasi 🎉

    Halo ${d.userName}, pembayaran kamu untuk trip ${escapeHtml(d.tripTitle)} sudah masuk.

    ${formatRupiah(d.amount)}

    Sampai jumpa di trip! Detail meeting point + itinerary lengkap bisa dicek di halaman trip.

    `, 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: `

    Disetujui organizer!

    Halo ${d.userName}, organizer sudah menyetujui kamu untuk ikut trip ${escapeHtml(d.tripTitle)}.

    ${ isFree ? `

    Trip ini gratis — tidak perlu bayar. Cek detail meeting point + itinerary di halaman trip.

    ` : `

    Untuk mengamankan slot, selesaikan pembayaran ${formatRupiah(d.amount)} via Midtrans (BCA VA, GoPay, QRIS, dll). Dana ditahan SeTrip sampai trip selesai (escrow).

    ` } `, 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: `

    Akun ditangguhkan

    Halo ${d.userName}, akun SeTrip kamu sedang dalam status ditangguhkan. Selama status ini berlaku, kamu tidak bisa login atau melakukan aksi (join trip, bikin trip, dll).

    Alasan:

    ${escapeHtml(d.reason)}

    Kalau menurut kamu ini kesalahan atau ingin klarifikasi, balas email ini atau hubungi support@setrip.id dengan subject "Banding suspend akun".

    `, }), }; } // ============================================================================ // 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 `

    Booking kamu untuk trip ini tidak punya pembayaran yang perlu dikembalikan.

    `; } return `

    Refund kamu sedang diproses

    ${formatRupiah(refundAmount)}

    Admin SeTrip akan memproses transfer ke rekening kamu. Kami kabari lagi via email saat refund selesai.

    `; } 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: `

    Ada yang mau gabung trip kamu

    Halo ${escapeHtml(d.organizerName)}, ${escapeHtml(d.joinerName)} mengajukan diri untuk ikut trip ${escapeHtml(d.tripTitle)}.

    Buka halaman trip untuk meninjau profil peserta lalu setujui atau tolak permintaannya.

    `, 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: `

    Permintaan gabung belum diterima

    Halo ${escapeHtml(d.userName)}, organizer belum bisa menerima permintaan kamu untuk ikut trip ${escapeHtml(d.tripTitle)} kali ini.

    Jangan berkecil hati — masih banyak trip seru lain yang bisa kamu ikuti.

    `, 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: `

    Pembayaran belum selesai

    Halo ${escapeHtml(d.userName)}, pembayaran kamu untuk trip ${escapeHtml(d.tripTitle)} sebesar ${formatRupiah(d.amount)} kadaluarsa atau gagal — slot kamu belum aman.

    Selama trip belum penuh dan belum berangkat, kamu masih bisa mengulang pembayaran.

    `, 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: `

    Trip dibatalkan

    Halo ${escapeHtml(d.userName)}, mohon maaf — organizer membatalkan trip ${escapeHtml(d.tripTitle)}. Partisipasi kamu di trip ini otomatis dibatalkan.

    ${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: `

    Trip dibatalkan SeTrip

    Halo ${escapeHtml(d.userName)}, trip ${escapeHtml(d.tripTitle)} dibatalkan oleh tim admin SeTrip.

    Alasan:

    ${escapeHtml(d.reason)}

    ${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: `

    Payout siap ditransfer

    Halo ${escapeHtml(d.organizerName)}, masa tahan (escrow) untuk trip ${escapeHtml(d.tripTitle)} sudah berakhir.

    ${formatRupiah(d.amount)}

    Dana ini masuk antrian transfer — admin SeTrip akan memprosesnya ke rekening kamu. Kami kabari lagi saat sudah ditransfer.

    `, }), }; } 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: `

    Payout sudah ditransfer

    Halo ${escapeHtml(d.organizerName)}, payout untuk trip ${escapeHtml(d.tripTitle)} sudah kami transfer.

    ${formatRupiah(d.amount)}

    ${d.adminNote ? `

    Catatan / referensi transfer:

    ${escapeHtml(d.adminNote)}

    ` : ""}

    Cek rekening kamu — biasanya masuk dalam 1×24 jam tergantung bank.

    `, }), }; } export interface AccountUnsuspendedData { userName: string; } function accountUnsuspended(d: AccountUnsuspendedData) { return { subject: "✅ Akun SeTrip kamu aktif kembali", html: shell({ title: "Account Unsuspended", bodyHtml: `

    Akun aktif kembali

    Halo ${escapeHtml(d.userName)}, penangguhan akun SeTrip kamu sudah dicabut. Kamu bisa login dan beraktivitas seperti biasa lagi.

    `, 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: `

    Pengajuan diterima

    Halo ${escapeHtml(d.userName)}, pengajuan verifikasi organizer kamu sudah masuk dan sedang antri di-review tim admin.

    Estimasi review 1–3 hari kerja. Hasilnya kami kabari lewat email — tidak perlu submit ulang selama belum ada keputusan.

    `, }), }; } export interface KycManualOverrideData { userName: string; } function kycManualOverride(d: KycManualOverrideData) { return { subject: "✅ Kamu jadi organizer terverifikasi SeTrip", html: shell({ title: "KYC Manual Override", bodyHtml: `

    🎉 Selamat ${escapeHtml(d.userName)}!

    Tim admin SeTrip sudah memverifikasi kamu sebagai organizer. Mulai sekarang kamu bisa membuat trip berbayar dan menerima payout.

    `, 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: `

    Pengajuan dibuka kembali

    Halo ${escapeHtml(d.userName)}, tim admin membuka kembali pengajuan verifikasi organizer kamu. Kamu bisa memperbaiki data lalu mengajukannya ulang.

    `, 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, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }