# 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_PAY` padahal 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](server/services/payment.service.ts) sudah call Midtrans Core API + apply state-machine. Tapi `userId` check membatasi penggunaan ke user pemilik booking — admin perlu variant. - ✅ `paymentService.handleMidtransWebhook` ada + idempotent via `applyGatewayStatus` helper. - ✅ `Payment.rawCallback` simpan snapshot mentah untuk audit. - ✅ `Refund` + `Payout` model lengkap dengan `reviewedBy`/`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.rawCallback` JSON. - ❌ 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.rawCallback` ditampilkan sebagai collapsible JSON viewer (tidak default expanded — verbose). - Show juga `Booking.status` history kalau ada (tidak ada saat ini — `updatedAt` jadi proxy). | # | Item | Status | File | |---|---|---|---| | 1.1 | `bookingRepo.findByIdForAdmin(id)` — include payments (with raw), refunds, payout, trip, user, participant | ⏳ | [server/repositories/booking.repo.ts](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 `
` 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](app/admin/refunds/page.tsx) | | 1.6 | Link "Lihat detail" dari `/admin/payouts` ke `/admin/bookings/[id]` | ⏳ | [app/admin/payouts/page.tsx](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 `applyGatewayStatus`** di [server/services/payment.service.ts](server/services/payment.service.ts) — sudah ekstrak. - Buat `paymentService.adminReconcile(orderId, adminId)` — sama dengan `reconcileFromGateway` tapi: - Skip ownership check (admin bypass). - Log `adminId` di `Payment.rawCallback` snapshot (tambah field `_reconciledByAdminId`). - Server action `adminReconcileMidtransAction(orderId)` guard `isAdmin`. - 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](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:** 1. 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 `adminNote` saja). - 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](app/admin/refunds/page.tsx) | | 3.2 | Badge khusus untuk `DISPUTE_RESOLVED` di refund card | ⏳ | [features/refund/components/refund-review-card.tsx](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:** 1. Tulis SOP dispute handling (alur bank → admin → refund creation). 2. Brief admin: `DISPUTE_RESOLVED` hanya untuk chargeback yang sudah resolve via bank.