Files
setrip/EMAIL_NOTIFICATIONS_ROADMAP.md
T
2026-05-20 15:25:32 +07:00

10 KiB
Raw Blame History

Setrip — Email Notifications Roadmap

Status implementasi notifikasi email transaksional ke user & organizer. Pakai pola yang sama dengan refund/admin roadmap: per-phase checklist, idempotent, auditable.

Prinsip:

  • Transactional only di MVP — KYC, refund, payment, account moderation. Marketing/reminder belakangan.
  • Idempotent — webhook retry / cron rerun tidak boleh double-send. Pakai idempotencyKey unique constraint di EmailSent.
  • Non-blocking — server action utama tidak boleh gagal kalau email gateway down. Pattern: try send sync; kalau gagal, enqueue EmailJob untuk retry cron.
  • Audit-friendly — semua email tercatat (sent atau queued) supaya admin bisa cek "kenapa user X belum dapat email Y?".
  • Unsubscribe-aware — transactional email (refund, payment, suspend) tetap dikirim. Marketing (reminder, social signal) opt-in dengan unsubscribe link.

Progress (per 2026-05-20): PR-E1, E2, E3, E5 — foundation, transactional email, notifikasi event Phase 2, dan admin email log + retry selesai. PR-E4 (marketing/reminder) sengaja ditunda — belum dibutuhkan.


Baseline (kondisi sekarang)

  • Tidak ada email service terintegrasi.
  • Tidak ada template engine.
  • User & organizer hanya tahu state via UI — kalau tidak buka app, miss event penting (refund cair, KYC approve, dst).

Provider choice

Resend — alasan:

  • Free tier 3000 email/bulan + 100/hari (cukup untuk MVP)
  • React Email native (kalau mau upgrade dari plain HTML)
  • API simple (POST /emails)
  • DNS setup ringan (SPF + DKIM auto)

Alternatif: AWS SES (paling murah at scale), SendGrid, Mailgun. Decision: Resend untuk MVP, evaluate ulang saat lebih dari 50k email/bulan.

Library dependency: SKIP — pakai fetch ke https://api.resend.com/emails directly. Lebih ringkas, satu-satunya dependency-free option.


PR-E1 — Foundation: schema + service + cron (MVP wajib)

Tujuan: infrastructure kirim email yang idempotent + retry-able, tanpa nge-block server action.

Keputusan asumsi:

  • 2 model baru:
    • EmailSent — append-only log dengan idempotencyKey @unique. Cek di sini sebelum kirim → cegah double-send.
    • EmailJob — retry queue untuk send yang gagal sync. Status PENDING/PROCESSING/SUCCESS/FAILED, attempt counter, max 5 retry exponential.
  • Service emailService.send({ to, template, data, idempotencyKey }):
    1. Cek EmailSent by idempotencyKey → kalau exist, return early.
    2. Render template ke { subject, html }.
    3. POST ke Resend.
    4. Sukses → insert EmailSent row.
    5. Gagal → insert EmailJob row (status PENDING, attempts=1).
  • Cron /api/cron/process-email-jobs setiap 5 menit — pick PENDING + FAILED (attempts<5), retry, mark SUCCESS atau bump attempts.
  • Caller pattern: void emailService.send(...) (fire-and-forget) supaya tidak nge-block server action. Try/catch internal sudah handle error.
# Item Status File
E1.1 Model EmailSent { idempotencyKey @unique, to, template, sentAt, providerMessageId? } + migration prisma/schema.prisma
E1.2 Model EmailJob { idempotencyKey, to, subject, html, status, attempts, lastError?, scheduledAt } + migration prisma/schema.prisma
E1.3 Service lib/email/send.ts — Resend client (raw fetch) + idempotency + enqueue on failure lib/email/send.ts
E1.4 Template registry — function per template return { subject, html } lib/email/templates.ts
E1.5 Cron route /api/cron/process-email-jobs pakai runCron helper app/api/cron/process-email-jobs/route.ts
E1.6 Env: RESEND_API_KEY, EMAIL_FROM (mis. "SeTrip <no-reply@setrip.id>") .env.example

Tindakan manual ops:

  1. Buat akun Resend, verify domain setrip.id (atau pakai onboarding@resend.dev di dev).
  2. Set DNS SPF + DKIM record di provider domain.
  3. Generate API key, set env RESEND_API_KEY + EMAIL_FROM di production.
  4. Daftarkan cron baru di system crontab: */5 * * * * curl ... /api/cron/process-email-jobs.

PR-E2 — Phase 1: Wire transactional emails (MVP wajib)

Tujuan: kirim email untuk event paling kritis (KYC, refund, payment, suspend) — yang kalau miss bikin user kehilangan uang atau bingung permanen.

# Trigger Penerima Template Wire point Status
E2.1 KYC VERIFICATION_APPROVE User kyc_approved reviewVerificationAction
E2.2 KYC VERIFICATION_REJECT User kyc_rejected (sertakan reason) reviewVerificationAction
E2.3 KYC re-upload request User kyc_reupload_request (fields + note) requestReuploadAction
E2.4 Refund created (admin atau auto-trigger) User refund_created (amount + reason) createRefundAction + tripService.closeTrip (loop semua peserta PAID)
E2.5 Refund SUCCEEDED User refund_succeeded (amount, cek rekening) decideRefundAction (decision=SUCCEEDED)
E2.6 Refund FAILED User refund_failed (alasan + langkah next) decideRefundAction (decision=FAILED)
E2.7 Midtrans webhook PAID User payment_paid (terima kasih + detail booking) paymentService.applyGatewayStatus (di branch PAID success)
E2.8 Booking di-approve organizer (status → AWAITING_PAY) User booking_approved (link bayar + deadline) confirmParticipantAction
E2.9 Account suspend User account_suspended (reason + email support) suspendUserAction

E2.4 — jalur admin (createRefundAction) kirim refund_created. Untuk auto-refund saat trip dibatalkan, peserta dikabari lewat email trip_cancelled_organizer / trip_cancelled_admin (E3.4/E3.5) yang sudah memuat blok nominal refund — satu email konsolidasi, bukan dua.

Format idempotencyKey:

  • kyc_approved-<verificationId>
  • refund_succeeded-<refundId>
  • payment_paid-<paymentId>
  • booking_approved-<bookingId>
  • account_suspended-<userId>-<suspendedAt> (allow re-suspend kalau diulang)

Tindakan manual ops:

  1. Test setiap template di staging — render via Resend "Send test" atau preview HTML lokal.
  2. Pastikan EMAIL_FROM domain match SPF/DKIM supaya tidak masuk spam.

PR-E3 — Phase 2: UX enhancement (post-MVP)

Email yang berguna tapi tidak critical kalau miss.

# Trigger Penerima Template Wire point Status
E3.1 User join trip (PENDING) Organizer join_request (siapa, link approve) joinTripAction
E3.2 Organizer reject join User join_rejected (singkat) rejectParticipantAction
E3.3 Payment EXPIRED / FAILED User payment_expired (suruh start ulang) paymentService.applyGatewayStatus
E3.4 Trip CLOSED (organizer cancel) Semua peserta aktif trip_cancelled_organizer (batch) tripService.closeTrip (organizer actor)
E3.5 Trip CLOSED (admin force-cancel) Semua peserta + organizer trip_cancelled_admin (reason) tripService.closeTrip (admin actor)
E3.6 Payout RELEASED Organizer payout_released (ETA transfer dari admin) payoutService.releaseEligible
E3.7 Payout PAID Organizer payout_paid (cek rekening + adminNote) markPayoutPaidAction
E3.8 Account unsuspended User account_unsuspended unsuspendUserAction
E3.9 KYC submitted User kyc_submitted (ETA review) submitVerificationAction
E3.10 KYC manual override User kyc_manual_override (admin verified) manualOverrideVerificationAction
E3.11 KYC reopened (admin) User kyc_reopened (boleh submit ulang) reopenVerificationAction

Wire point email: semua dikirim void emailService.send(...) (fire-and-forget, idempotent). Untuk batch trip-cancelled, closeTrip mengembalikan daftar penerima + nominal refund; email dikirim oleh action setelah transaksi commit (bukan di dalam tx).


PR-E4 — Phase 3: Marketing / reminder (post-MVP, opt-in)

Email engagement — perlu user preference + unsubscribe link.

# Trigger Penerima Template Wire point
E4.1 Welcome email saat signup User welcome NextAuth events.signIn first time
E4.2 Reminder H-3 keberangkatan User trip_reminder_h3 (meeting point + itinerary) Cron daily
E4.3 Reminder H-1 keberangkatan User trip_reminder_h1 Cron daily
E4.4 Trip selesai → minta review (H+1) User CONFIRMED review_prompt Cron daily
E4.5 Review baru diterima Organizer new_review createReviewAction
E4.6 Trip jadi FULL Organizer trip_full tripService.joinTrip (saat FULL transition)

Prerequisite: tabel UserEmailPreference dengan kategori marketing / reminders + unsubscribe token. Skip sampai Phase 4.


PR-E5 — Admin UI: email log + queue retry (post-MVP)

Visibilitas admin untuk troubleshoot "kenapa user X tidak dapat email?".

# Item Status File
E5.1 Page /admin/emails — list EmailSent + EmailJob, filter recipient + template, status lewat tab app/admin/emails/page.tsx
E5.2 Tombol "Kirim ulang" untuk EmailJob gagal/antri — retry sync langsung features/email/components/email-row-actions.tsx
E5.3 Tombol "Resend" untuk EmailSent — key turunan #resend-<ts>, butuh EmailSent.html features/email/actions.ts
E5.4 Stats card di /admin/system + /admin/emails: antri, gagal 24 jam, perlu aksi manual app/admin/system/page.tsx

Tindakan manual ops:

  1. Run migration 20260520000000_add_email_sent_html (kolom EmailSent.html) di staging → production. Tanpa ini, resend (E5.3) tidak tersedia untuk email yang dikirim sebelum migration.
  2. Tambahkan /admin/emails ke admin nav — sudah dilakukan di components/admin/admin-sidebar.tsx.

Deviasi minor dari rencana awal: filter tanggal tidak diimplementasikan (list dibatasi 100 baris terbaru); filter status diwujudkan sebagai tab (Gagal / Antrian / Terkirim).


Skip / never (eksplisit)

  • SMS / WhatsApp — beda regulatory, beda cost. Stick to email.
  • Push notification (browser/mobile) — perlu PWA setup terpisah.
  • In-app inbox — komplexitas tinggi, low ROI di MVP. Email cukup.