- ✅
- ✅ - ✅ - ✅
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# 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 `<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](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.
|
||||
Reference in New Issue
Block a user