- ✅ - ✅ - ✅
6.2 KiB
Setrip — Admin Audit & Investigation Roadmap
Admin perlu mencari lintas entity (booking/payment/refund/user/trip) dan export untuk compliance + investigasi dispute.
Skenario nyata: auditor bertanya "tunjukkan semua refund yang di-approve admin X di bulan Juni 2026 dengan total lebih dari Rp 5 juta". Saat ini admin harus query DB manual atau ambil screenshot satu-satu. Tidak ada cara cari berdasarkan kombinasi reviewer + tanggal + nominal.
Baseline
- ✅ Data audit sudah ada di schema:
Refund.reviewedBy/reviewedAt/adminNote,Payout.processedBy/processedAt/adminNote,OrganizerVerification.reviewedBy/reviewedAt/rejectionReason. - ✅ Existing list pages (
/admin/refunds,/admin/payouts,/admin/verifications) sudah grouping by status tab. - ❌ Tidak ada filter date range / reviewer / amount / reason.
- ❌ Tidak ada kolom "reviewer email" di list — harus klik detail.
- ❌ Tidak ada global search (cari berdasarkan email user, order id, trip id).
- ❌ Tidak ada CSV export.
- ❌ Tidak ada audit log untuk action admin di entity lain (User suspension, Trip force-cancel, Verification reopen).
Phase 1 — Filter & Search Enhancements ⏳
Sebelum bikin audit log baru, perbaiki dulu kemampuan cari & filter di list yang sudah ada.
Keputusan asumsi:
- Pakai
searchParamsdi Next.js — tidak perlu state client (server-render fast + shareable URL). - Default date range: 30 hari terakhir, supaya page tidak load semua history.
- Reviewer dropdown sumber dari
ADMIN_EMAILSenv (sudah ada).
| # | Item | Status | File |
|---|---|---|---|
| 1.1 | Filter date range (from, to) di /admin/refunds |
⏳ | app/admin/refunds/page.tsx |
| 1.2 | Filter reviewedBy (admin email dropdown) di /admin/refunds |
⏳ | app/admin/refunds/page.tsx |
| 1.3 | Filter reason di /admin/refunds (lihat juga ADMIN_PAYMENT_OPS_ROADMAP.md) |
⏳ | app/admin/refunds/page.tsx |
| 1.4 | Filter date range + processedBy di /admin/payouts |
⏳ | app/admin/payouts/page.tsx |
| 1.5 | Filter date range + reviewedBy di /admin/verifications |
⏳ | app/admin/verifications/page.tsx |
| 1.6 | Tampilkan kolom "reviewer email" + "reviewed at" di tabel/list (semua admin pages) | ⏳ | semua app/admin/*/page.tsx |
| 1.7 | Repo helper: tambah optional filter params di refundRepo.listByStatus, payoutRepo.listByStatus, organizerRepo.listByStatus |
⏳ | server/repositories/*.ts |
Tindakan manual: tidak ada.
Phase 2 — Global Search ⏳
Satu search box yang resolve ke entity detail page paling relevan.
Keputusan asumsi:
- Input string user, prefix-based dispatch:
- Email format (
@) → user search → redirect ke/admin/users/[id] - Mulai
midtrans-/manual-→ payment lookup byexternalOrderId→/admin/bookings/[bookingId] - Mulai
cm(cuid pattern) + length 25 → coba lookup berurutan: trip → booking → user - Else: full-text search di trip title/destination
- Email format (
- Pakai server action atau route handler
/api/admin/search— return list hasil + jenis entity. - UI: searchbar di admin layout (top-right) yang dropdown hasil.
| # | Item | Status | File |
|---|---|---|---|
| 2.1 | adminSearchService.resolve(query) — dispatch ke repo lookup yang tepat |
⏳ | server/services/admin-search.service.ts |
| 2.2 | Route handler /api/admin/search?q=... (GET, guard isAdmin) |
⏳ | app/api/admin/search/route.ts |
| 2.3 | Component AdminSearchBar di admin layout — debounced, dropdown hasil |
⏳ | features/admin/components/admin-search-bar.tsx |
| 2.4 | Page /admin/search?q=... untuk full results kalau dropdown limit terlampaui |
⏳ | app/admin/search/page.tsx |
Tindakan manual: tidak ada.
Phase 3 — CSV Export ⏳
Export untuk laporan keuangan & compliance.
Keputusan asumsi:
- Stream CSV via route handler — jangan load semua ke memory.
- Pakai filter yang sama dengan list page — admin pakai URL filter lalu klik "Export".
- Header CSV: human-readable bahasa Indonesia (mis. "Tanggal Approve", "Email Peserta", "Nominal Refund").
- Tidak ada Excel/xlsx — CSV cukup, mudah dibuka di Sheets/Excel.
| # | Item | Status | File |
|---|---|---|---|
| 3.1 | Helper lib/csv.ts — streamCsv(headers, rows) return Response |
⏳ | lib/csv.ts |
| 3.2 | Route /api/admin/export/refunds — pakai filter dari query string |
⏳ | app/api/admin/export/refunds/route.ts |
| 3.3 | Route /api/admin/export/payouts |
⏳ | app/api/admin/export/payouts/route.ts |
| 3.4 | Route /api/admin/export/verifications (tanpa NIK / KTP — hanya metadata) |
⏳ | app/api/admin/export/verifications/route.ts |
| 3.5 | Tombol "Export CSV" di tiap admin list page | ⏳ | semua app/admin/*/page.tsx |
Tindakan manual:
- Test export di staging — pastikan tidak leak data sensitif (NIK harus tetap encrypted/excluded).
- Update kebijakan privasi: data export hanya untuk internal compliance.
Phase 4 — Generic Admin Audit Log ⏳
Tabel AdminActionLog untuk action di entity yang belum punya audit field (User suspend, Trip force-cancel, Verification reopen, dst).
Keputusan asumsi:
- Single tabel polymorphic:
AdminActionLog { adminId, action, entityType, entityId, payload Json?, createdAt }. - Append-only, never update/delete.
- Service helper
auditLog.record(...)dipanggil eksplisit di setiap action admin (tidak via Prisma middleware — terlalu magic). - View page
/admin/audit-logdengan filteradminId,entityType,action, date range.
| # | Item | Status | File |
|---|---|---|---|
| 4.1 | Model AdminActionLog + migration |
⏳ | prisma/schema.prisma |
| 4.2 | Helper auditLog.record({ adminId, action, entityType, entityId, payload? }) |
⏳ | server/services/audit-log.service.ts |
| 4.3 | Wire auditLog.record di semua admin server action existing (refund approve/reject/mark, payout markPaid, verification approve/reject) |
⏳ | features/*/actions.ts |
| 4.4 | Page /admin/audit-log dengan filter + pagination |
⏳ | app/admin/audit-log/page.tsx |
Tindakan manual: tidak ada.