- ✅ - ✅ - ✅
5.9 KiB
Setrip — Admin Payment Operations Roadmap
Admin perlu visibilitas + kontrol penuh atas alur uang: payment Midtrans, refund, payout. Saat webhook gagal atau status mismatch, admin harus bisa reconcile tanpa edit DB.
Skenario nyata: webhook Midtrans drop di production.
Booking.status = AWAITING_PAYpadahal user sudah bayar (confirm email dari Midtrans). User komplain via WhatsApp. Saat ini admin harus query DB manual lalu update via Prisma Studio.
Baseline
- ✅
paymentService.reconcileFromGateway(orderId, userId)di server/services/payment.service.ts sudah call Midtrans Core API + apply state-machine. TapiuserIdcheck membatasi penggunaan ke user pemilik booking — admin perlu variant. - ✅
paymentService.handleMidtransWebhookada + idempotent viaapplyGatewayStatushelper. - ✅
Payment.rawCallbacksimpan snapshot mentah untuk audit. - ✅
Refund+Payoutmodel lengkap denganreviewedBy/processedBy/adminNote. - ❌ Tidak ada page
/admin/bookings/[id]untuk drill-down per-booking timeline. - ❌ Tidak ada admin variant
reconcileFromGateway(yang tidak butuh userId check). - ❌ Tidak ada UI yang tampilkan
Payment.rawCallbackJSON. - ❌ Tidak ada filter refund per
reason(mis. cari semua DISPUTE_RESOLVED). - ❌ Tidak ada bulk reconcile untuk stale PENDING/AWAITING payments.
Phase 1 — Booking + Payment Detail View ⏳
Admin perlu satu halaman yang tampilkan seluruh event uang untuk satu booking: payment attempts (Midtrans + manual legacy), refund history, payout status, raw callback.
Keputusan asumsi:
- Drill-down dari
/admin/trips/[id],/admin/refunds,/admin/payouts, dan global search nanti. - Tampilkan timeline chronological semua event Payment + Refund + Payout untuk booking — bukan tabel terpisah.
Payment.rawCallbackditampilkan sebagai collapsible JSON viewer (tidak default expanded — verbose).- Show juga
Booking.statushistory kalau ada (tidak ada saat ini —updatedAtjadi proxy).
| # | Item | Status | File |
|---|---|---|---|
| 1.1 | bookingRepo.findByIdForAdmin(id) — include payments (with raw), refunds, payout, trip, user, participant |
⏳ | server/repositories/booking.repo.ts |
| 1.2 | Page /admin/bookings/[id] — header (trip, user, amount, status), timeline events |
⏳ | app/admin/bookings/[id]/page.tsx |
| 1.3 | Component PaymentTimelineAdmin — render Payment + Refund + Payout sorted by createdAt |
⏳ | features/booking/components/payment-timeline-admin.tsx |
| 1.4 | Component RawCallbackViewer — collapsible <details> block dengan JSON pretty-printed |
⏳ | features/booking/components/raw-callback-viewer.tsx |
| 1.5 | Link "Lihat detail" dari /admin/refunds ke /admin/bookings/[id] |
⏳ | app/admin/refunds/page.tsx |
| 1.6 | Link "Lihat detail" dari /admin/payouts ke /admin/bookings/[id] |
⏳ | app/admin/payouts/page.tsx |
Tindakan manual: tidak ada.
Phase 2 — Admin Midtrans Reconciliation UI ⏳
Tombol di booking detail page yang panggil Midtrans Core API + apply update. Admin variant tidak butuh userId check.
Keputusan asumsi:
- Reuse internal helper
applyGatewayStatusdi server/services/payment.service.ts — sudah ekstrak. - Buat
paymentService.adminReconcile(orderId, adminId)— sama denganreconcileFromGatewaytapi:- Skip ownership check (admin bypass).
- Log
adminIddiPayment.rawCallbacksnapshot (tambah field_reconciledByAdminId).
- Server action
adminReconcileMidtransAction(orderId)guardisAdmin. - UI: tombol per Payment row di timeline. Disable kalau Payment sudah final (PAID/FAILED/EXPIRED/CANCELLED/REFUNDED) tapi tetap show last-reconciled-at.
- Tampilkan toast hasil: "Updated to PAID" / "Already PAID, no change" / "Amount mismatch (audit)".
| # | Item | Status | File |
|---|---|---|---|
| 2.1 | paymentService.adminReconcile(orderId, adminId) — variant tanpa ownership check |
⏳ | server/services/payment.service.ts |
| 2.2 | Server action adminReconcileMidtransAction(orderId) |
⏳ | features/booking/actions.ts (atau features/admin/actions.ts baru) |
| 2.3 | Tombol "Reconcile dari Midtrans" di tiap Payment Midtrans di timeline | ⏳ | features/booking/components/payment-timeline-admin.tsx |
| 2.4 | Tampilkan Payment.rejectionReason (untuk amount mismatch log) di card payment |
⏳ | features/booking/components/payment-timeline-admin.tsx |
| 2.5 | (Optional) Bulk reconcile: /admin/payments/stale — list Payment status PENDING/AWAITING > 6 jam |
⏳ | app/admin/payments/stale/page.tsx |
Tindakan manual:
- Brief admin: kapan pakai reconcile (peserta lapor "sudah bayar tapi status belum update"). Jangan dipakai untuk PAID booking (idempotent tapi noise).
Phase 3 — Dispute & Chargeback Tracking ⏳
RefundReason.DISPUTE_RESOLVED sudah ada di enum tapi tidak ada flow khusus.
Keputusan asumsi:
- Tidak buat tabel baru. Filter di refund list page cukup.
- Tambah "Chargeback note" field di Refund kalau perlu (skip untuk MVP — pakai
adminNotesaja). - Highlight visual: badge merah untuk DISPUTE_RESOLVED supaya admin treat khusus.
| # | Item | Status | File |
|---|---|---|---|
| 3.1 | Tab/filter reason di /admin/refunds — dropdown semua nilai RefundReason |
⏳ | app/admin/refunds/page.tsx |
| 3.2 | Badge khusus untuk DISPUTE_RESOLVED di refund card |
⏳ | features/refund/components/refund-review-card.tsx |
| 3.3 | Dokumentasi SOP: kapan pakai DISPUTE_RESOLVED vs reason lain |
⏳ | docs/admin/refund-reasons.md (baru) |
Tindakan manual:
- Tulis SOP dispute handling (alur bank → admin → refund creation).
- Brief admin:
DISPUTE_RESOLVEDhanya untuk chargeback yang sudah resolve via bank.