Compare commits
8 Commits
5e0232d909
...
427bfc0447
| Author | SHA1 | Date | |
|---|---|---|---|
| 427bfc0447 | |||
| 54f4569107 | |||
| d2b0a780d5 | |||
| 22e1e8fbea | |||
| 11b2d45d20 | |||
| 744ee3446b | |||
| 9a163c4f13 | |||
| 54cd984a7e |
@@ -2,7 +2,15 @@
|
|||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"WebFetch(domain:unsplash.com)",
|
"WebFetch(domain:unsplash.com)",
|
||||||
"Bash(npx prisma *)"
|
"Bash(npx prisma *)",
|
||||||
|
"Bash(Get-ChildItem -Path \"c:\\\\development\\\\DIOS\\\\weekly-project\\\\setrip\" -Force)",
|
||||||
|
"Bash(Select-Object Name, PSIsContainer)",
|
||||||
|
"Bash(npx tsc *)",
|
||||||
|
"Bash(echo \"exitcode=$?\")",
|
||||||
|
"PowerShell(npx prisma generate 2>&1)",
|
||||||
|
"PowerShell(npx tsc --noEmit 2>&1)",
|
||||||
|
"PowerShell(npx eslint server/services/refund.service.ts server/repositories/refund.repo.ts features/refund app/admin/refunds 2>&1)",
|
||||||
|
"PowerShell(npx eslint server lib features app 2>&1)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
## Forbidden
|
## Forbidden
|
||||||
|
|
||||||
- Jangan query database langsung di component
|
- Jangan query database langsung di component
|
||||||
- Jangan buat arsitektur over-engineered
|
- Jangan buat arsitektur over-engineered, tidak apa apa jika lebih baik untuk performance dan struktur yang baik
|
||||||
- Jangan menambahkan dependency tanpa kebutuhan jelas
|
- Jangan menambahkan dependency tanpa kebutuhan jelas, tambahkan jika memang dibutuhkan dan gunakan dependency yang aman
|
||||||
|
|
||||||
## Output Style
|
## Output Style
|
||||||
|
|
||||||
|
|||||||
@@ -1,265 +0,0 @@
|
|||||||
# Setrip — Payment Roadmap
|
|
||||||
|
|
||||||
Rencana implementasi flow pembayaran: handle trip gratis, halaman detail payment, dan integrasi Midtrans.
|
|
||||||
|
|
||||||
> **Prinsip:** Sumber kebenaran pembayaran = record di tabel khusus, bukan timestamp di `TripParticipant`. Manual transfer & Midtrans = dua provider dari pipeline yang sama. Trip gratis = first-class case (bukan paid trip dengan amount 0).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Audit state sekarang (baseline)
|
|
||||||
|
|
||||||
- Field `TripParticipant.markedPaidAt` + `paymentConfirmedAt` — dual-timestamp manual.
|
|
||||||
- Flow: user klik "Saya sudah bayar" → organizer klik "Konfirmasi pembayaran". Tidak ada record transaksi terpisah, tidak ada bukti transfer, tidak ada amount snapshot.
|
|
||||||
- Tidak ada halaman `/trips/[id]/payment`. Tombol bayar inline di trip detail.
|
|
||||||
- **Bug**: trip gratis (`price === 0`) tetap melewati flow yang sama — peserta tetap harus klik "Saya sudah bayar" dan organizer tetap harus konfirmasi.
|
|
||||||
|
|
||||||
File terkait: [server/services/trip.service.ts](server/services/trip.service.ts) (`markParticipantPayment`, `confirmParticipantPayment`), [features/booking/actions.ts](features/booking/actions.ts), [features/booking/components/organizer-payment-queue.tsx](features/booking/components/organizer-payment-queue.tsx), [features/trip/components/join-trip-button.tsx](features/trip/components/join-trip-button.tsx), [server/repositories/participant.repo.ts](server/repositories/participant.repo.ts).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PR A — Free trip handling + halaman detail payment (manual flow) ✅
|
|
||||||
|
|
||||||
Selesai. `tsc --noEmit` lulus. Tanpa schema baru, tanpa migration.
|
|
||||||
|
|
||||||
**Keputusan akses control:** halaman `/trips/[id]/payment` boleh diakses peserta apapun yang aktif (PENDING/CONFIRMED). Peserta PENDING bisa lihat nominal + rekening untuk persiapan, tapi diberi notice "tunggu approve dulu sebelum transfer". Organizer trip-nya sendiri di-redirect ke trip detail.
|
|
||||||
|
|
||||||
| # | Item | Status | File |
|
|
||||||
|---|---|---|---|
|
|
||||||
| A1 | Helper `lib/trip-pricing.ts` dengan `isFreeTrip` & `isPaidTrip` | ✅ | [lib/trip-pricing.ts](lib/trip-pricing.ts) |
|
|
||||||
| A2 | Service guard: `markParticipantPayment` & `confirmParticipantPayment` reject trip gratis | ✅ | [server/services/trip.service.ts](server/services/trip.service.ts) |
|
|
||||||
| A3 | UI gate di JoinTripButton: hide flow pembayaran kalau gratis | ✅ | [features/trip/components/join-trip-button.tsx](features/trip/components/join-trip-button.tsx) |
|
|
||||||
| A4 | UI gate di trip detail: skip `OrganizerPaymentQueue` kalau gratis | ✅ | [app/trips/[id]/page.tsx](app/trips/[id]/page.tsx) |
|
|
||||||
| A5 | Halaman `/trips/[id]/payment` (server component dengan akses kontrol) | ✅ | [app/trips/[id]/payment/page.tsx](app/trips/%5Bid%5D/payment/page.tsx) |
|
|
||||||
| A6 | Konten halaman trip gratis: banner 🎉 + status keikutsertaan + CTA | ✅ | `FreeTripSection` di [page.tsx](app/trips/%5Bid%5D/payment/page.tsx) |
|
|
||||||
| A7 | Konten halaman trip berbayar: timeline + rekening organizer + tombol "Saya sudah bayar" + status | ✅ | `PaidTripSection`, `PaymentTimeline`, `BankRow` di [page.tsx](app/trips/%5Bid%5D/payment/page.tsx) |
|
|
||||||
| A8 | Link dari trip detail → `/trips/[id]/payment` (replace tombol inline) | ✅ | `showPaymentLink` di [join-trip-button.tsx](features/trip/components/join-trip-button.tsx) |
|
|
||||||
| A9 | Metadata + `robots: noindex` halaman payment | ✅ | [page.tsx](app/trips/%5Bid%5D/payment/page.tsx) — `metadata.robots: { index: false, follow: false }` |
|
|
||||||
| A+ | `MarkPaidButton` (client component) untuk action "Saya sudah bayar" | ✅ | [features/booking/components/mark-paid-button.tsx](features/booking/components/mark-paid-button.tsx) |
|
|
||||||
| A+ | `CopyButton` (client component) untuk copy nomor rekening / nominal | ✅ | [features/booking/components/copy-button.tsx](features/booking/components/copy-button.tsx) |
|
|
||||||
|
|
||||||
**Tindakan manual:** tidak ada. Tidak ada migration di PR A.
|
|
||||||
|
|
||||||
**Catatan edge case yang sudah dihandle:**
|
|
||||||
- Organizer trip-nya sendiri buka halaman → redirect ke trip detail.
|
|
||||||
- User belum login → redirect ke login dengan `callbackUrl`.
|
|
||||||
- User belum join / sudah cancel → tampil notice "kamu belum terdaftar".
|
|
||||||
- Trip dengan organizer yang belum APPROVED verifikasinya → tampil notice "rekening belum tersedia, hubungi organizer langsung" (tidak crash).
|
|
||||||
- Peserta PENDING di-warning "tunggu approve dulu sebelum transfer".
|
|
||||||
- Tombol "Saya sudah bayar" hanya muncul untuk CONFIRMED + belum mark + bank tersedia.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PR B — Refactor schema ke `Booking` + `Payment` (provider MANUAL only) ✅
|
|
||||||
|
|
||||||
Selesai. `tsc --noEmit` lulus. Pondasi untuk Midtrans sudah siap.
|
|
||||||
|
|
||||||
### Schema (target)
|
|
||||||
|
|
||||||
```prisma
|
|
||||||
model Booking {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
tripId String
|
|
||||||
trip Trip @relation(fields: [tripId], references: [id])
|
|
||||||
userId String
|
|
||||||
user User @relation(fields: [userId], references: [id])
|
|
||||||
participantId String @unique
|
|
||||||
participant TripParticipant @relation(fields: [participantId], references: [id])
|
|
||||||
|
|
||||||
/// Snapshot harga saat booking dibuat — protect dari perubahan trip.price.
|
|
||||||
amount Int
|
|
||||||
currency String @default("IDR")
|
|
||||||
status BookingStatus @default(PENDING)
|
|
||||||
|
|
||||||
payments Payment[]
|
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
@@index([tripId, status])
|
|
||||||
@@index([userId])
|
|
||||||
}
|
|
||||||
|
|
||||||
enum BookingStatus {
|
|
||||||
PENDING // join belum approve atau belum bayar
|
|
||||||
AWAITING_PAY // approved, tinggal bayar
|
|
||||||
PAID // lunas (manual confirm atau gateway settlement)
|
|
||||||
CANCELLED
|
|
||||||
REFUNDED
|
|
||||||
EXPIRED
|
|
||||||
}
|
|
||||||
|
|
||||||
model Payment {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
bookingId String
|
|
||||||
booking Booking @relation(fields: [bookingId], references: [id])
|
|
||||||
|
|
||||||
provider PaymentProvider
|
|
||||||
/// order_id eksternal — untuk MANUAL referensi internal, untuk MIDTRANS dikirim ke gateway. Unik per attempt.
|
|
||||||
externalOrderId String @unique
|
|
||||||
externalTxId String?
|
|
||||||
method String? // bca_va, gopay, qris, manual_transfer, dst
|
|
||||||
amount Int
|
|
||||||
status PaymentStatus @default(PENDING)
|
|
||||||
|
|
||||||
rawCallback Json?
|
|
||||||
snapToken String?
|
|
||||||
expiresAt DateTime?
|
|
||||||
|
|
||||||
paidAt DateTime?
|
|
||||||
failedAt DateTime?
|
|
||||||
rejectionReason String?
|
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
@@index([bookingId, status])
|
|
||||||
@@index([provider, status])
|
|
||||||
}
|
|
||||||
|
|
||||||
enum PaymentProvider {
|
|
||||||
MANUAL
|
|
||||||
MIDTRANS
|
|
||||||
}
|
|
||||||
|
|
||||||
enum PaymentStatus {
|
|
||||||
PENDING
|
|
||||||
AWAITING
|
|
||||||
PAID
|
|
||||||
FAILED
|
|
||||||
EXPIRED
|
|
||||||
CANCELLED
|
|
||||||
REFUNDED
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tugas
|
|
||||||
|
|
||||||
| # | Item | Status | File |
|
|
||||||
|---|---|---|---|
|
|
||||||
| B1 | Schema: tambah `Booking`, `Payment`, 3 enum (`BookingStatus`, `PaymentProvider`, `PaymentStatus`) + relasi di `User`, `Trip`, `TripParticipant` | ✅ | [prisma/schema.prisma](prisma/schema.prisma) |
|
|
||||||
| B2 | Migration `add_booking_payment` (CreateEnum + CreateTable + Index + FK) | ✅ | [prisma/migrations/20260508150000_add_booking_payment/migration.sql](prisma/migrations/20260508150000_add_booking_payment/migration.sql) |
|
|
||||||
| B3 | Backfill script TS (idempotent, skip baris yang sudah punya Booking) | ✅ | [prisma/backfill-bookings.ts](prisma/backfill-bookings.ts) |
|
|
||||||
| B4 | `bookingRepo` + `paymentRepo` | ✅ | [server/repositories/booking.repo.ts](server/repositories/booking.repo.ts), [server/repositories/payment.repo.ts](server/repositories/payment.repo.ts) |
|
|
||||||
| B5 | `bookingService` — `markPaidManual`, `confirmPaidManual`, `getByTripAndUser`, `getAwaitingManualForTrip` (idempotent, transactional dengan retry serialisasi) | ✅ | [server/services/booking.service.ts](server/services/booking.service.ts) |
|
|
||||||
| B6 | `tripService.markParticipantPayment` → delegate ke `bookingService.markPaidManual`. Tetap update `TripParticipant.markedPaidAt` untuk backcompat. | ✅ | [server/services/trip.service.ts](server/services/trip.service.ts) |
|
|
||||||
| B7 | `tripService.confirmParticipantPayment` → delegate ke `bookingService.confirmPaidManual`. Tetap update `paymentConfirmedAt`. | ✅ | [server/services/trip.service.ts](server/services/trip.service.ts) |
|
|
||||||
| B+ | `tripService.joinTrip` → upsert Booking PENDING (handle re-join dari CANCELLED) | ✅ | [server/services/trip.service.ts](server/services/trip.service.ts) |
|
|
||||||
| B+ | `tripService.confirmParticipant` → transition Booking PENDING → AWAITING_PAY (paid) atau PAID (free) | ✅ | [server/services/trip.service.ts](server/services/trip.service.ts) |
|
|
||||||
| B+ | `tripService.cancelJoin` & `rejectParticipant` → Booking → CANCELLED | ✅ | [server/services/trip.service.ts](server/services/trip.service.ts) |
|
|
||||||
| B8 | Halaman `/trips/[id]/payment` baca dari Booking + Payment (bukan timestamp lama) | ✅ | [app/trips/[id]/payment/page.tsx](app/trips/%5Bid%5D/payment/page.tsx) |
|
|
||||||
| B9 | `OrganizerPaymentQueue` di trip detail dapat data dari `bookingService.getAwaitingManualForTrip` | ✅ | [app/trips/[id]/page.tsx](app/trips/%5Bid%5D/page.tsx) |
|
|
||||||
| B10 | Deprecate `TripParticipant.markedPaidAt` + `paymentConfirmedAt` (komen `@deprecated`, tetap di-update untuk backcompat) | ✅ | [prisma/schema.prisma](prisma/schema.prisma) |
|
|
||||||
| B11 | Index optimization (`@@index([tripId, status])` di Booking, `@@index([provider, status])` di Payment, `@@index([userId])` di Booking) | ✅ | [prisma/schema.prisma](prisma/schema.prisma) |
|
|
||||||
|
|
||||||
**Tindakan manual (urutan penting):**
|
|
||||||
1. `npx prisma migrate deploy` — apply schema migration `20260508150000_add_booking_payment`.
|
|
||||||
2. `npx tsx prisma/backfill-bookings.ts` — populate Booking + Payment dari `TripParticipant` lama. Idempotent, aman dijalankan ulang.
|
|
||||||
3. Verifikasi: jumlah Booking aktif = jumlah TripParticipant aktif setelah backfill.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PR C — Midtrans integration (Snap + webhook) ✅
|
|
||||||
|
|
||||||
Selesai. `tsc --noEmit` lulus. Belum test live ke sandbox Midtrans — perlu env diisi + tunneling kalau dev lokal.
|
|
||||||
|
|
||||||
### Persiapan akun & env
|
|
||||||
|
|
||||||
| Env | Keterangan |
|
|
||||||
|---|---|
|
|
||||||
| `MIDTRANS_SERVER_KEY` | Server key dari dashboard Midtrans (sandbox/production sesuai mode). Rahasia. Server-side only. |
|
|
||||||
| `NEXT_PUBLIC_MIDTRANS_CLIENT_KEY` | Client key untuk Snap.js. Aman di-expose ke frontend (NEXT_PUBLIC_). |
|
|
||||||
| `NEXT_PUBLIC_MIDTRANS_IS_PRODUCTION` | `true` untuk production, `false` (atau kosong) untuk sandbox. NEXT_PUBLIC_ supaya client tahu URL Snap.js yang benar. |
|
|
||||||
|
|
||||||
`MIDTRANS_NOTIFICATION_URL` **tidak** di env — diset langsung di dashboard Midtrans ke `<NEXT_PUBLIC_SITE_URL>/api/webhooks/midtrans`.
|
|
||||||
|
|
||||||
Sudah ditambah ke [env.example](env.example).
|
|
||||||
|
|
||||||
### Tugas
|
|
||||||
|
|
||||||
| # | Item | Status | File |
|
|
||||||
|---|---|---|---|
|
|
||||||
| C1 | Update [env.example](env.example) + 3 env baru + komentar webhook URL | ✅ | [env.example](env.example) |
|
|
||||||
| C2 | `lib/midtrans.ts` — `createSnapTransaction`, `verifyMidtransSignature` (timing-safe compare), `MIDTRANS` config helper | ✅ | [lib/midtrans.ts](lib/midtrans.ts) |
|
|
||||||
| C3 | Status mapping `mapMidtransStatus(transaction_status, fraud_status)` → `PaymentStatus` | ✅ | [lib/midtrans.ts](lib/midtrans.ts) |
|
|
||||||
| C4 | `paymentService.startMidtransPayment(bookingId, userId)` — validate, reuse Payment AWAITING aktif (idempotent re-attempt), atau buat Payment baru + call Snap API + simpan token + expiresAt | ✅ | [server/services/payment.service.ts](server/services/payment.service.ts) |
|
|
||||||
| C5 | Halaman payment: tombol "Bayar online via Midtrans" + divider "atau" + tombol manual lama | ✅ | [app/trips/[id]/payment/page.tsx](app/trips/%5Bid%5D/payment/page.tsx) |
|
|
||||||
| C6 | `MidtransPayButton` client component — load Snap.js dengan `data-client-key` dinamis, `window.snap.pay(token, callbacks)`, refresh page setelah Snap close | ✅ | [features/booking/components/midtrans-pay-button.tsx](features/booking/components/midtrans-pay-button.tsx) |
|
|
||||||
| C7 | Webhook endpoint `app/api/webhooks/midtrans/route.ts` — POST, verify signature, lookup, idempotent, return 200/401 sesuai outcome | ✅ | [app/api/webhooks/midtrans/route.ts](app/api/webhooks/midtrans/route.ts) |
|
|
||||||
| C8 | `paymentService.handleMidtransWebhook` — verifikasi signature, amount check, transaction (`Payment` + `Booking` + `TripParticipant.paymentConfirmedAt` backcompat) | ✅ | [server/services/payment.service.ts](server/services/payment.service.ts) |
|
|
||||||
| C10 | Anti-replay: skip update kalau Payment sudah final (PAID/FAILED/EXPIRED/CANCELLED/REFUNDED) | ✅ | [payment.service.ts](server/services/payment.service.ts) |
|
|
||||||
| C11 | Simpan callback mentah ke `Payment.rawCallback` (audit & dispute), termasuk untuk callback yang di-skip | ✅ | [payment.service.ts](server/services/payment.service.ts) |
|
|
||||||
| C+ | Server action `startMidtransPaymentAction` (resolve booking dari tripId, bridge ke client) | ✅ | [features/booking/actions.ts](features/booking/actions.ts) |
|
|
||||||
| C+ | Retry handling: Payment row baru dengan `midtrans-{bookingId}-{retryN}` kalau attempt lama expired/failed; idempotent reuse kalau masih AWAITING | ✅ | [payment.service.ts](server/services/payment.service.ts) |
|
|
||||||
| C9 | Cron expire Payment lama | ⏸️ skipped | Housekeeping di-handle saat user start payment (auto-expire attempt yang lewat `expiresAt`). Cron formal bisa ditambah kalau perlu cleanup massal. |
|
|
||||||
| C12 | Test scenario sandbox (settlement, deny, expire) | ⏸️ manual | Perlu env Midtrans diisi + tunneling untuk dev lokal (ngrok/cloudflared). Tidak bisa otomatis dari sini. |
|
|
||||||
| C13 | Status badge real-time | ⏸️ partial | Page refresh setelah Snap close + halaman SSR pull state baru tiap reload. Polling otomatis belum diimplementasi. |
|
|
||||||
| C14 | Email/in-app notification setelah PAID | ⏳ pending | Diluar scope PR C — masuk Phase berikutnya. |
|
|
||||||
|
|
||||||
### Mapping `transaction_status` Midtrans → `PaymentStatus`
|
|
||||||
|
|
||||||
| Midtrans | Trigger lain | `PaymentStatus` |
|
|
||||||
|---|---|---|
|
|
||||||
| `capture` | `fraud_status === "accept"` | PAID |
|
|
||||||
| `capture` | `fraud_status === "challenge"` | AWAITING (review manual di dashboard Midtrans) |
|
|
||||||
| `settlement` | — | PAID |
|
|
||||||
| `pending` | — | AWAITING |
|
|
||||||
| `deny` | — | FAILED |
|
|
||||||
| `expire` | — | EXPIRED |
|
|
||||||
| `cancel` | — | CANCELLED |
|
|
||||||
| `refund` / `partial_refund` | — | REFUNDED |
|
|
||||||
|
|
||||||
### Webhook checklist (security)
|
|
||||||
|
|
||||||
1. Verify signature: `sha512(order_id + status_code + gross_amount + SERVER_KEY) === signature_key`. Mismatch → 401, log.
|
|
||||||
2. Cek `gross_amount` cocok dengan `payment.amount` — kalau tidak sama, log anomaly, jangan PAID.
|
|
||||||
3. Lookup `Payment.externalOrderId === order_id`. Tidak ada → 200 OK + log (jangan biarkan Midtrans retry forever).
|
|
||||||
4. Idempotent: kalau status sudah final, skip update tapi tetap return 200.
|
|
||||||
5. Pakai DB transaction untuk update Payment + Booking + TripParticipant bersamaan.
|
|
||||||
6. Selalu return 200 kalau request valid (mismatch signature → 401, sisanya → 200 + log).
|
|
||||||
|
|
||||||
### Hardening pasca-audit (sebelum Midtrans live) ✅
|
|
||||||
|
|
||||||
Empat fix tambahan dari audit security/correctness:
|
|
||||||
|
|
||||||
| Fix | Issue | Solusi | File |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 1 | Webhook bisa overwrite Booking CANCELLED/REFUNDED/EXPIRED jadi PAID | Re-fetch Booking di dalam serializable transaction; kalau state konflik, Payment tetap PAID (uang masuk) tapi Booking tidak di-update + `Payment.rejectionReason` di-flag untuk manual review/refund. Webhook outcome `booking_conflict` di-log warning. | [payment.service.ts](server/services/payment.service.ts), [route.ts](app/api/webhooks/midtrans/route.ts) |
|
|
||||||
| 2 | `startMidtransPayment` lupa cek trip departure date | Tambah `isTripDepartureDayPast` guard, juga di `bookingService.markPaidManual` untuk konsistensi | [payment.service.ts](server/services/payment.service.ts), [booking.service.ts](server/services/booking.service.ts) |
|
|
||||||
| 3 | `Booking` tidak punya constraint `(tripId, userId)` unique | Tambah `@@unique([tripId, userId])` + migration `20260508160000_booking_unique_trip_user`. `findByTripAndUser` switch dari `findFirst` ke `findUnique` (lebih efisien) | [schema.prisma](prisma/schema.prisma), [migration](prisma/migrations/20260508160000_booking_unique_trip_user/migration.sql), [booking.repo.ts](server/repositories/booking.repo.ts) |
|
|
||||||
| 4 | Webhook payload tidak schema-validated | Zod `midtransWebhookSchema` (passthrough untuk forward-compat). Webhook route `safeParse` → 400 kalau shape invalid. Service signature pakai type yang inferred dari schema. | [lib/midtrans.ts](lib/midtrans.ts), [route.ts](app/api/webhooks/midtrans/route.ts), [payment.service.ts](server/services/payment.service.ts) |
|
|
||||||
|
|
||||||
### Edge cases yang gampang lupa
|
|
||||||
|
|
||||||
- **Quota race**: dua user bayar bersamaan untuk slot terakhir → slot di-hold saat Booking dibuat (status AWAITING_PAY masih hitung kuota via `TripParticipant.status`). Release belum otomatis saat Payment EXPIRED — kalau perlu, tambah cron (lihat C9 yang di-skip).
|
|
||||||
- **Trip dibatalkan organizer setelah peserta bayar** → `Booking.status = REFUNDED` setelah dana balik. Implementasi refund Midtrans = PR terpisah (tidak di scope PR C ini).
|
|
||||||
- **User retry pembayaran setelah gagal** → bikin Payment baru, `externalOrderId` baru (`midtrans-{bookingId}-{retryN}`). Reuse kalau masih AWAITING & belum expired.
|
|
||||||
- **Webhook duplicate**: Midtrans bisa kirim notifikasi yang sama 2-3 kali. Idempotent: skip update kalau Payment sudah final, tapi tetap simpan callback ke `rawCallback` untuk audit.
|
|
||||||
- **Sandbox vs production**: webhook URL diset di dashboard Midtrans = `<NEXT_PUBLIC_SITE_URL>/api/webhooks/midtrans`. Dev lokal perlu tunneling (ngrok / cloudflared) supaya endpoint bisa di-reach Midtrans.
|
|
||||||
- **Booking belum approved (`PENDING`) tapi user coba bayar** — `paymentService.startMidtransPayment` reject dengan pesan jelas. UI sudah hide tombol di state ini.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ❌ Anti-list (yang harus DITOLAK kalau muncul)
|
|
||||||
|
|
||||||
- **Halaman payment yang berubah jadi marketplace checkout**: upsell ("trip serupa lebih murah"), pricing comparison, "harga turun" — semua menarik framing OTA. Halaman payment fokus pada satu transaksi: lunas atau belum.
|
|
||||||
- **Multi-method abstraksi prematur**: jangan bikin "PaymentProvider" generic untuk Stripe/Xendit/Doku sekaligus sebelum salah satu jalan. Mulai dari MANUAL + MIDTRANS, baru tambah kalau perlu.
|
|
||||||
- **Auto-refund logic kompleks** sebelum manual refund di dashboard Midtrans dipakai. Refund jarang, manual cukup di Phase awal.
|
|
||||||
- **Payment retry otomatis** dari sisi server. User harus eksplisit klik "bayar lagi" untuk attempt baru — supaya tidak ambigu siapa yang trigger.
|
|
||||||
- **Multi-currency** sebelum ada permintaan eksplisit. `currency` di schema sudah default IDR, tapi tidak perlu UI selector.
|
|
||||||
- **Saving credit card / tokenization** tanpa kebutuhan jelas. PCI scope naik drastis. Snap sudah handle tanpa simpan kartu di sisi kita.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Saran phasing
|
|
||||||
|
|
||||||
PR berurutan. Setiap PR mandiri (siap di-deploy):
|
|
||||||
|
|
||||||
1. **PR A** — Free trip handling + halaman payment (manual). Cepat, low-risk, no migration. **Mulai dari sini.**
|
|
||||||
2. **PR B** — Refactor ke `Booking` + `Payment`. Migration + backfill data lama. UI tetap mirip.
|
|
||||||
3. **PR C** — Tambah Midtrans Snap. Test di sandbox dulu sebelum production.
|
|
||||||
|
|
||||||
Pertanyaan terbuka sebelum mulai:
|
|
||||||
|
|
||||||
1. **Akses halaman payment**: hanya peserta CONFIRMED, atau juga PENDING (yang belum disetujui organizer)? Saya rekomendasi CONFIRMED only — peserta PENDING belum perlu lihat detail bayar.
|
|
||||||
2. **Snapshot amount di Booking**: kalau organizer ubah `trip.price` setelah booking dibuat, booking lama pakai harga lama atau baru? Saya rekomendasi tetap pakai snapshot lama (audit-friendly).
|
|
||||||
3. **Manual + Midtrans co-exist**: user pilih satu provider per booking, atau bisa retry dengan provider berbeda? Saya rekomendasi pilih satu — kalau gagal di Midtrans, bisa cancel dan buat Payment baru dengan provider MANUAL.
|
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
# Setrip — Refund Roadmap
|
||||||
|
|
||||||
|
Status implementasi sistem refund yang dapat dipercaya dan auditable — dari schema, policy, sampai integrasi gateway.
|
||||||
|
|
||||||
|
> **Prinsip:** refund = financial event, bukan flag boolean. Setiap rupiah yang keluar harus auditable, idempotent, dan punya state machine eksplisit. Untuk MVP: tetap simple, tapi anticipate partial refund + async gateway dari hari pertama.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit state sekarang (baseline)
|
||||||
|
|
||||||
|
**Schema (~10% sudah ada):**
|
||||||
|
- ✅ `BookingStatus.REFUNDED` & `PaymentStatus.REFUNDED` — enum value ada di [prisma/schema.prisma](prisma/schema.prisma).
|
||||||
|
- ❌ Model `Refund` belum ada — refund saat ini cuma flag, tanpa entity tersendiri.
|
||||||
|
- ❌ Tidak ada audit trail (siapa, kapan, alasan, approval).
|
||||||
|
- ❌ Tidak bisa partial refund.
|
||||||
|
- ❌ Tidak bisa multiple refund per booking (mis. refund deposit lalu sisa).
|
||||||
|
- ❌ Tidak bisa bedakan refund vs chargeback (dispute bank).
|
||||||
|
|
||||||
|
**Service/UI (~0% sudah ada):**
|
||||||
|
- ❌ Tidak ada `refundService`.
|
||||||
|
- ❌ Tidak ada flow "organizer cancel trip → auto refund peserta PAID".
|
||||||
|
- ❌ Tidak ada UI peserta untuk request cancel + refund.
|
||||||
|
- ❌ Tidak ada UI admin untuk approve/eksekusi refund.
|
||||||
|
- ❌ Tidak ada integrasi Midtrans Refund API.
|
||||||
|
- ❌ Tidak ada reconciliation harian.
|
||||||
|
|
||||||
|
**Konteks pendukung yang sudah ada:**
|
||||||
|
- ✅ `Booking` + `Payment` model dengan `amount` (Int, IDR — money math safe).
|
||||||
|
- ✅ Midtrans webhook handler ([app/api/webhooks/midtrans/route.ts](app/api/webhooks/midtrans/route.ts)) — pola untuk refund webhook bisa di-mirror.
|
||||||
|
- ✅ `bookingService` dengan transaksi serializable + retry — pola untuk `refundService`.
|
||||||
|
- ✅ Cron infra (system crontab + `CRON_SECRET`, lihat [docs/CRON_SETUP.md](docs/CRON_SETUP.md)) — siap untuk reconciliation job.
|
||||||
|
|
||||||
|
File baseline: [prisma/schema.prisma](prisma/schema.prisma), [server/services/booking.service.ts](server/services/booking.service.ts), [app/api/webhooks/midtrans/route.ts](app/api/webhooks/midtrans/route.ts).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR-R1 — Refund Schema + Service Stub (foundation) ⏳
|
||||||
|
|
||||||
|
Foundation. Bikin entity `Refund` + service stub yang cuma create record `PENDING` (belum panggil gateway). UI admin minimal: list + manual mark succeeded. Tanpa ini, semua PR berikutnya tidak bisa jalan.
|
||||||
|
|
||||||
|
**Keputusan asumsi yang diusulkan:**
|
||||||
|
- `Refund.amount` Int (IDR) — bisa < `payment.amount` untuk partial. Constraint: `SUM(refunds.amount WHERE status=SUCCEEDED) <= payment.amount` di-enforce di service layer.
|
||||||
|
- `idempotencyKey` di-generate sekali saat Refund dibuat — dipakai saat panggil gateway nanti (R-4) supaya retry tidak double-refund.
|
||||||
|
- `BookingStatus.REFUNDED` di-set sebagai **derived state** saat full refund SUCCEEDED. Untuk partial: tambah `BookingStatus.PARTIALLY_REFUNDED` (enum baru) — atau biarkan PAID + lihat Booking.refunds[]. **Saran: tambah PARTIALLY_REFUNDED** supaya filter list "refunded bookings" bisa pakai status saja, tidak perlu join.
|
||||||
|
- `RefundReason` enum lengkap dari hari pertama — supaya laporan finance tidak butuh string parsing.
|
||||||
|
- Approval admin **wajib** untuk semua refund di MVP (4-eyes principle). Auto-approve bisa di-relax di R-2 untuk SYSTEM refund.
|
||||||
|
- Admin UI sederhana — list refund PENDING + tombol approve / mark-succeeded. Reuse pattern KYC verification UI yang sudah ada.
|
||||||
|
|
||||||
|
| # | Item | Status | File |
|
||||||
|
|---|---|---|---|
|
||||||
|
| R1.1 | Model `Refund` + enum `RefundReason`, `RefundStatus`, `RefundInitiator` | ⏳ | [prisma/schema.prisma](prisma/schema.prisma) |
|
||||||
|
| R1.2 | Tambah `BookingStatus.PARTIALLY_REFUNDED` | ⏳ | [prisma/schema.prisma](prisma/schema.prisma) |
|
||||||
|
| R1.3 | Migration `add_refund_model` | ⏳ | `prisma/migrations/` |
|
||||||
|
| R1.4 | `refundRepo` (create, findById, findByBooking, listPending, updateStatus) | ⏳ | `server/repositories/refund.repo.ts` |
|
||||||
|
| R1.5 | `refundService.requestRefund(input)` — create Refund PENDING + validasi `amount <= payment.amount - totalRefunded` | ⏳ | `server/services/refund.service.ts` |
|
||||||
|
| R1.6 | `refundService.approveRefund(refundId, adminId)` — PENDING → APPROVED | ⏳ | `server/services/refund.service.ts` |
|
||||||
|
| R1.7 | `refundService.markSucceededManual(refundId, adminId)` — APPROVED → SUCCEEDED, update Booking/Payment status. Untuk MVP refund manual transfer (non-Midtrans channel). | ⏳ | `server/services/refund.service.ts` |
|
||||||
|
| R1.8 | UI admin `/admin/refunds` — list PENDING + tombol approve & mark-succeeded | ⏳ | `app/admin/refunds/page.tsx` |
|
||||||
|
|
||||||
|
**Tindakan manual:**
|
||||||
|
1. Run migration di staging → smoke test → run di production.
|
||||||
|
2. Tambah `/admin/refunds` ke admin nav.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR-R2 — Auto-Trigger Refund saat Trip CLOSED (organizer cancel) ⏳
|
||||||
|
|
||||||
|
Saat organizer batalkan trip (`status = CLOSED`), semua peserta dengan `Booking.status = PAID` otomatis di-create `Refund` record dengan `reason = ORGANIZER_CANCELLED`, `initiatedBy = SYSTEM`, full amount.
|
||||||
|
|
||||||
|
**Keputusan asumsi yang diusulkan:**
|
||||||
|
- Trigger di service `tripService.closeTrip()` (yang nge-set `status = CLOSED`). Pakai serializable transaction — close trip + create refunds atomic.
|
||||||
|
- SYSTEM refund bisa **auto-approve** (skip admin approval) — karena policy clear: organizer cancel = 100% refund. Tapi eksekusi (mark succeeded) tetap manual atau via gateway (R-4), tidak skip.
|
||||||
|
- Notifikasi peserta: kirim email/notif "Trip dibatalkan, refund Rp X sedang diproses". (Notification system di luar scope PR ini — assume sudah ada atau di-deferred.)
|
||||||
|
- Edge case: peserta yang `AWAITING_PAY` (belum bayar) tidak perlu refund — cuma update `Booking.status = CANCELLED`. Yang `PAID` saja yang dapat refund.
|
||||||
|
|
||||||
|
| # | Item | Status | File |
|
||||||
|
|---|---|---|---|
|
||||||
|
| R2.1 | `tripService.closeTrip(tripId, organizerId)` — set status CLOSED + auto-create refunds | ⏳ | [server/services/trip.service.ts](server/services/trip.service.ts) |
|
||||||
|
| R2.2 | Auto-approve untuk SYSTEM refund dengan reason ORGANIZER_CANCELLED | ⏳ | `server/services/refund.service.ts` |
|
||||||
|
| R2.3 | UI organizer: tombol "Batalkan trip" di dashboard organizer dengan confirmation modal | ⏳ | `features/trip/components/cancel-trip-button.tsx` |
|
||||||
|
| R2.4 | Server action `cancelTripAction` | ⏳ | `features/trip/actions.ts` |
|
||||||
|
|
||||||
|
**Tindakan manual:** tidak ada.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR-R3 — Self-Service User Cancel dengan Refund Window ⏳
|
||||||
|
|
||||||
|
User bisa cancel booking sendiri, refund dihitung berdasarkan kebijakan default (hardcoded dulu, jangan polymorphic).
|
||||||
|
|
||||||
|
**Keputusan asumsi yang diusulkan:**
|
||||||
|
- **Kebijakan default hardcoded** (akan jadi data-driven di R-5):
|
||||||
|
- ≥7 hari sebelum berangkat → 80% refund (organizer ambil 20% admin fee)
|
||||||
|
- 3-7 hari sebelum berangkat → 50% refund
|
||||||
|
- <3 hari atau no-show → 0% refund (tetap create Refund record amount=0 untuk audit, atau skip — pilih skip lebih sederhana)
|
||||||
|
- Konstanta di `lib/refund-policy.ts` — supaya satu sumber kebenaran, mudah diubah.
|
||||||
|
- User refund **tidak auto-approve** — tetap butuh admin approval di MVP. Alasan: cegah abuse (spam cancel), dan validasi window calculation di sisi admin.
|
||||||
|
- Setelah refund SUCCEEDED, slot di trip kembali tersedia (`status: FULL → OPEN` kalau participantCount turun).
|
||||||
|
- UI user: tombol "Cancel & request refund" di trip detail (kalau status booking = PAID dan trip belum berangkat).
|
||||||
|
|
||||||
|
| # | Item | Status | File |
|
||||||
|
|---|---|---|---|
|
||||||
|
| R3.1 | `lib/refund-policy.ts` — `calculateRefundAmount(bookingAmount, daysUntilDeparture)` | ⏳ | `lib/refund-policy.ts` |
|
||||||
|
| R3.2 | `refundService.requestUserCancellation(bookingId, userId)` — pakai policy + create Refund PENDING | ⏳ | `server/services/refund.service.ts` |
|
||||||
|
| R3.3 | UI user di trip detail: tombol "Cancel booking" + modal preview refund amount | ⏳ | `features/booking/components/cancel-booking-button.tsx` |
|
||||||
|
| R3.4 | Server action `cancelBookingAction` | ⏳ | `features/booking/actions.ts` |
|
||||||
|
| R3.5 | Display refund policy di trip detail (di blok info atau accordion) — transparency | ⏳ | [app/trips/[id]/page.tsx](app/trips/%5Bid%5D/page.tsx) |
|
||||||
|
|
||||||
|
**Tindakan manual:**
|
||||||
|
1. Tulis copy kebijakan refund untuk halaman Terms & Privacy.
|
||||||
|
2. Tambah link kebijakan di footer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR-R4 — Integrasi Midtrans Refund API (async, idempotent) ⏳
|
||||||
|
|
||||||
|
Sambungkan eksekusi refund ke Midtrans Refund API. Untuk channel yang support refund online (BCA VA, GoPay, dst). Channel manual transfer tetap mark-succeeded manual oleh admin.
|
||||||
|
|
||||||
|
**Keputusan asumsi yang diusulkan:**
|
||||||
|
- Refund API Midtrans **async** — POST refund return `pending`, callback datang via webhook (extend [app/api/webhooks/midtrans/route.ts](app/api/webhooks/midtrans/route.ts) atau buat endpoint terpisah).
|
||||||
|
- **Idempotency key** wajib di header request — pakai `Refund.idempotencyKey` yang sudah di-generate R-1.
|
||||||
|
- State transition: APPROVED → PROCESSING (saat request dikirim) → SUCCEEDED/FAILED (via webhook).
|
||||||
|
- Eksekusi via job queue (Vercel Cron daily) atau sync di server action — **saran cron** supaya retry-able kalau gateway down. Job: pick all APPROVED refunds, kirim ke Midtrans, update PROCESSING.
|
||||||
|
- Validasi `Payment.method` — kalau `manual_transfer`, refund tetap manual (no gateway call). Skip Midtrans path.
|
||||||
|
|
||||||
|
| # | Item | Status | File |
|
||||||
|
|---|---|---|---|
|
||||||
|
| R4.1 | Helper `lib/midtrans-refund.ts` — POST refund dengan idempotency key | ⏳ | `lib/midtrans-refund.ts` |
|
||||||
|
| R4.2 | `refundService.executeRefund(refundId)` — APPROVED → PROCESSING + call Midtrans | ⏳ | `server/services/refund.service.ts` |
|
||||||
|
| R4.3 | Webhook handler refund — PROCESSING → SUCCEEDED/FAILED via callback | ⏳ | [app/api/webhooks/midtrans/route.ts](app/api/webhooks/midtrans/route.ts) |
|
||||||
|
| R4.4 | Cron `/api/cron/execute-refunds` — pick APPROVED refunds, kirim ke gateway | ⏳ | `app/api/cron/execute-refunds/route.ts` |
|
||||||
|
| R4.5 | Daftarkan cron `*/15 * * * *` (every 15 min) di system crontab | ⏳ | [docs/CRON_SETUP.md](docs/CRON_SETUP.md) |
|
||||||
|
|
||||||
|
**Tindakan manual:**
|
||||||
|
1. Aktifkan Refund API di dashboard Midtrans (perlu request ke Midtrans support untuk channel tertentu).
|
||||||
|
2. Test refund di sandbox dengan dummy transaction.
|
||||||
|
3. Set webhook URL refund di Midtrans dashboard (kalau beda dari payment webhook).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR-R5 — Refund Policy Model (per-trip customization) ⏳
|
||||||
|
|
||||||
|
Pindah refund policy dari hardcoded ke data-driven. Organizer bisa pilih policy per-trip (mis. trip premium = strict cancellation).
|
||||||
|
|
||||||
|
**Keputusan asumsi yang diusulkan:**
|
||||||
|
- Model `RefundPolicy` dengan tier predefined (FLEXIBLE, MODERATE, STRICT) — **bukan** field bebas. Mengikuti pola Airbnb. Mencegah organizer set policy yang aneh-aneh dan membingungkan peserta.
|
||||||
|
- 1 Trip → 1 RefundPolicy (foreign key di Trip). Default ke MODERATE.
|
||||||
|
- Setiap policy punya array `tiers` (JSON) berisi `{ minDaysBefore: number, refundPercentage: number }`.
|
||||||
|
- Migration: existing trip default ke MODERATE.
|
||||||
|
- UI organizer di create-trip form: dropdown pilih policy, dengan preview tier-nya.
|
||||||
|
|
||||||
|
| # | Item | Status | File |
|
||||||
|
|---|---|---|---|
|
||||||
|
| R5.1 | Model `RefundPolicy` + seed 3 tier (FLEXIBLE, MODERATE, STRICT) | ⏳ | [prisma/schema.prisma](prisma/schema.prisma) |
|
||||||
|
| R5.2 | Foreign key `Trip.refundPolicyId` (default MODERATE) | ⏳ | [prisma/schema.prisma](prisma/schema.prisma) |
|
||||||
|
| R5.3 | Migration + backfill existing trip ke MODERATE | ⏳ | `prisma/migrations/` |
|
||||||
|
| R5.4 | Update `lib/refund-policy.ts` — `calculateRefundAmount` baca dari policy | ⏳ | `lib/refund-policy.ts` |
|
||||||
|
| R5.5 | UI organizer create-trip: dropdown policy + preview | ⏳ | [features/trip/components/create-trip-form.tsx](features/trip/components/create-trip-form.tsx) |
|
||||||
|
| R5.6 | UI trip detail: tampilkan policy aktif (link ke detail tier) | ⏳ | [app/trips/[id]/page.tsx](app/trips/%5Bid%5D/page.tsx) |
|
||||||
|
|
||||||
|
**Tindakan manual:**
|
||||||
|
1. Run migration + backfill.
|
||||||
|
2. Update copy halaman terms — sebut 3 policy tier.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR-R6 — Reconciliation + Dispute Model (operational maturity) ⏳
|
||||||
|
|
||||||
|
Daily job match refund di DB vs settlement Midtrans. Model Dispute terpisah untuk chargeback/komplain pasca-refund.
|
||||||
|
|
||||||
|
**Keputusan asumsi yang diusulkan:**
|
||||||
|
- Reconciliation job: pull settlement report dari Midtrans (Settlement API) → match dengan `refund.externalRefundId` → flag drift.
|
||||||
|
- Dispute model **tertunda** sampai ada chargeback riil. Pakai `RefundReason.DISPUTE_RESOLVED` dulu di Refund model. Bikin model terpisah hanya kalau volume dispute > X per bulan.
|
||||||
|
- Alert: kalau drift > 1% dari total refund harian, kirim notif ke admin (email/Slack).
|
||||||
|
|
||||||
|
| # | Item | Status | File |
|
||||||
|
|---|---|---|---|
|
||||||
|
| R6.1 | Helper `lib/midtrans-settlement.ts` — pull settlement report | ⏳ | `lib/midtrans-settlement.ts` |
|
||||||
|
| R6.2 | Cron `/api/cron/reconcile-refunds` daily | ⏳ | `app/api/cron/reconcile-refunds/route.ts` |
|
||||||
|
| R6.3 | UI admin `/admin/refunds/reconciliation` — drift report | ⏳ | `app/admin/refunds/reconciliation/page.tsx` |
|
||||||
|
| R6.4 | (opsional, defer) Model `Dispute` + flow chargeback | ⏳ | [prisma/schema.prisma](prisma/schema.prisma) |
|
||||||
|
|
||||||
|
**Tindakan manual:**
|
||||||
|
1. Set up alert channel (email atau Slack webhook).
|
||||||
|
2. Tetapkan threshold drift (saran: 1%).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❌ Anti-list (yang harus DITOLAK kalau muncul)
|
||||||
|
|
||||||
|
- **Pakai `BookingStatus.REFUNDED` sebagai sumber kebenaran tanpa Refund model** — flag-only tidak bisa partial, tidak punya audit trail. Stuck di kasus pertama.
|
||||||
|
- **Hapus Refund row kalau gagal** — never delete financial records. Set `status = FAILED` + log alasan. Audit trail wajib.
|
||||||
|
- **Sync call ke Midtrans Refund API tanpa idempotency key** — kalau retry karena timeout atau network error → double-refund. Kerugian finansial nyata.
|
||||||
|
- **Auto-execute refund tanpa approval admin di MVP** — fraud risk. Auto-approve OK untuk SYSTEM refund (organizer cancel = clear policy), tapi eksekusi tetap controlled. Bisa di-relax setelah volume + trust matang.
|
||||||
|
- **Polymorphic refund policy dari awal** — lompat langsung ke data-driven sebelum hardcoded teruji = over-engineering. Phasing R-3 (hardcoded) lalu R-5 (data-driven) lebih sehat.
|
||||||
|
- **Trigger refund di server action user-facing tanpa state machine** — user spam click → multiple refund request. Idempotency check via `(bookingId, status IN ('PENDING','APPROVED','PROCESSING'))` unique-ish.
|
||||||
|
- **Refund partial dengan float math** — selalu integer (rupiah). Hitung % dengan `Math.floor(amount * percentage / 100)` supaya tidak ada sub-rupiah.
|
||||||
|
- **Mention "kami akan refund X%" di UI tanpa lock policy** — kebijakan harus visible di trip detail SEBELUM user join, bukan kejutan saat cancel.
|
||||||
|
- **Skip approval admin untuk refund di atas threshold (mis. > 5jt)** — fraud risk internal. 4-eyes principle wajib untuk nominal besar, walau policy clear.
|
||||||
|
- **Bundle Refund + Dispute di model yang sama** — beda flow, beda inisiator (refund = merchant, dispute = bank). Separation of concern penting walau di MVP belum perlu Dispute model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Saran phasing
|
||||||
|
|
||||||
|
PR berurutan, masing-masing mandiri (siap di-deploy). **Don't bundle.**
|
||||||
|
|
||||||
|
1. **R-1** — Schema + service stub + UI admin. Foundation, blocker untuk semua PR berikut.
|
||||||
|
2. **R-2** — Auto-trigger saat organizer CLOSED. Paling sering kepakai, low-complexity. **Mulai dari sini setelah R-1.**
|
||||||
|
3. **R-3** — Self-service user cancel + hardcoded policy. Komplet flow user-side.
|
||||||
|
4. **R-4** — Midtrans Refund API. Paling kompleks (async, idempotent, webhook). Bisa hidup tanpa ini selama admin willing manual transfer.
|
||||||
|
5. **R-5** — Refund policy data-driven. Quality-of-life untuk organizer, bukan blocker.
|
||||||
|
6. **R-6** — Reconciliation + dispute. Operational maturity, untuk volume yang lebih besar.
|
||||||
|
|
||||||
|
**Bobot effort kasar:** R-1 (M) → R-2 (M) → R-3 (M) → R-4 (L) → R-5 (M) → R-6 (L).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pertanyaan terbuka sebelum mulai R-1
|
||||||
|
|
||||||
|
1. **MVP scope** — mau prioritaskan **organizer-cancel** dulu (R-2, paling sering), atau langsung **user cancel dengan window** (R-3)? Saran: R-2 dulu — clear policy, low-complexity.
|
||||||
|
2. **Approval flow** — semua refund butuh approval admin (lebih aman), atau auto-approve untuk SYSTEM refund? Saran: auto-approve SYSTEM, manual approve untuk USER + ADMIN_ADJUSTMENT.
|
||||||
|
3. **Partial refund booking status** — tambah `BookingStatus.PARTIALLY_REFUNDED` (lebih eksplisit) atau biarkan tetap PAID + lihat `booking.refunds[]`? Saran: tambah enum baru — query-friendly.
|
||||||
|
4. **Midtrans Refund API channel** — apakah anda sudah cek channel mana yang support online refund? BCA VA + GoPay biasanya support, manual_transfer pasti tidak. Cek dashboard sebelum mulai R-4.
|
||||||
|
5. **Dispute model timing** — bikin Dispute model di R-1 (early separation) atau defer sampai R-6? Saran: defer — YAGNI sampai ada kasus chargeback riil.
|
||||||
|
6. **Threshold approval admin** — ada nominal di atas mana refund wajib approval 2 admin (4-eyes)? Saran: > Rp 1jt butuh dual approval, < Rp 1jt single approval cukup.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Admin · Refund Manual",
|
||||||
|
description:
|
||||||
|
"Halaman admin untuk meninjau laporan refund dari peserta dan organizer.",
|
||||||
|
alternates: { canonical: "/admin/refunds" },
|
||||||
|
robots: { index: false, follow: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AdminRefundsLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { authOptions } from "@/lib/auth";
|
||||||
|
import { isAdminEmail } from "@/lib/admin";
|
||||||
|
import { refundRepo } from "@/server/repositories/refund.repo";
|
||||||
|
import { CreateRefundForm } from "@/features/refund/components/create-refund-form";
|
||||||
|
import {
|
||||||
|
RefundReviewCard,
|
||||||
|
type RefundCardData,
|
||||||
|
} from "@/features/refund/components/refund-review-card";
|
||||||
|
|
||||||
|
type Tab = "PENDING" | "APPROVED" | "REJECTED" | "SUCCEEDED" | "FAILED";
|
||||||
|
|
||||||
|
const TABS: { key: Tab; label: string }[] = [
|
||||||
|
{ key: "PENDING", label: "Pending" },
|
||||||
|
{ key: "APPROVED", label: "Disetujui" },
|
||||||
|
{ key: "SUCCEEDED", label: "Selesai" },
|
||||||
|
{ key: "REJECTED", label: "Ditolak" },
|
||||||
|
{ key: "FAILED", label: "Gagal" },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface PageProps {
|
||||||
|
searchParams: Promise<{ tab?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function AdminRefundsPage({ searchParams }: PageProps) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user) redirect("/login?callbackUrl=/admin/refunds");
|
||||||
|
if (!isAdminEmail(session.user.email)) {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-2xl px-4 py-12 text-center">
|
||||||
|
<p className="text-sm text-neutral-600">
|
||||||
|
Halaman ini hanya untuk admin SeTrip.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = await searchParams;
|
||||||
|
const tab: Tab = TABS.some((t) => t.key === params.tab)
|
||||||
|
? (params.tab as Tab)
|
||||||
|
: "PENDING";
|
||||||
|
|
||||||
|
const rows = await refundRepo.listByStatus(tab);
|
||||||
|
const items: RefundCardData[] = rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
amount: r.amount,
|
||||||
|
currency: r.currency,
|
||||||
|
reason: r.reason,
|
||||||
|
reportedBy: r.reportedBy,
|
||||||
|
reportNote: r.reportNote,
|
||||||
|
initiatedBy: r.initiatedBy,
|
||||||
|
status: r.status,
|
||||||
|
adminNote: r.adminNote,
|
||||||
|
createdAt: r.createdAt,
|
||||||
|
reviewedAt: r.reviewedAt,
|
||||||
|
succeededAt: r.succeededAt,
|
||||||
|
failedAt: r.failedAt,
|
||||||
|
reviewedBy: r.reviewedBy,
|
||||||
|
booking: {
|
||||||
|
id: r.booking.id,
|
||||||
|
amount: r.booking.amount,
|
||||||
|
status: r.booking.status,
|
||||||
|
trip: {
|
||||||
|
id: r.booking.trip.id,
|
||||||
|
title: r.booking.trip.title,
|
||||||
|
date: r.booking.trip.date,
|
||||||
|
},
|
||||||
|
user: r.booking.user,
|
||||||
|
payments: r.booking.payments.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
provider: p.provider,
|
||||||
|
method: p.method,
|
||||||
|
amount: p.amount,
|
||||||
|
status: p.status,
|
||||||
|
paidAt: p.paidAt,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-4xl px-4 py-8 sm:py-12">
|
||||||
|
<header className="mb-6">
|
||||||
|
<h1 className="text-2xl font-bold text-neutral-900 sm:text-3xl">
|
||||||
|
Review Refund Manual
|
||||||
|
</h1>
|
||||||
|
<p className="mt-1 text-sm text-neutral-500">
|
||||||
|
Tinjau laporan refund dari peserta dan organizer. Setiap refund harus
|
||||||
|
melalui approval admin sebelum dieksekusi.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<CreateRefundForm />
|
||||||
|
|
||||||
|
<div className="mb-6 flex flex-wrap gap-2">
|
||||||
|
{TABS.map((t) => (
|
||||||
|
<a
|
||||||
|
key={t.key}
|
||||||
|
href={`/admin/refunds?tab=${t.key}`}
|
||||||
|
className={`rounded-full px-4 py-1.5 text-sm font-semibold transition-colors ${
|
||||||
|
tab === t.key
|
||||||
|
? "bg-primary-600 text-white"
|
||||||
|
: "bg-neutral-100 text-neutral-600 hover:bg-neutral-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{items.length === 0 ? (
|
||||||
|
<div className="rounded-2xl border border-dashed border-neutral-300 bg-white p-10 text-center">
|
||||||
|
<p className="text-sm text-neutral-500">
|
||||||
|
Tidak ada refund pada status ini.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{items.map((r) => (
|
||||||
|
<RefundReviewCard key={r.id} refund={r} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { tripService } from "@/server/services/trip.service";
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cron daily — flip trip yang sudah lewat tanggal selesai dari OPEN/FULL ke
|
||||||
|
* COMPLETED. Idempotent: run berulang aman.
|
||||||
|
*
|
||||||
|
* Trigger via system crontab (lihat [docs/CRON_SETUP.md](../../../docs/CRON_SETUP.md))
|
||||||
|
* atau cron service apapun. Header wajib: `Authorization: Bearer ${CRON_SECRET}`.
|
||||||
|
*
|
||||||
|
* Set env `CRON_SECRET` (random ≥32 char) di hosting. Kalau env tidak di-set,
|
||||||
|
* endpoint hard-fail 500 supaya tidak accidentally jalan tanpa proteksi.
|
||||||
|
*/
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
const secret = process.env.CRON_SECRET;
|
||||||
|
if (!secret) {
|
||||||
|
console.error("[cron/auto-complete-trips] CRON_SECRET tidak di-set");
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Server misconfigured" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const authHeader = req.headers.get("authorization");
|
||||||
|
if (authHeader !== `Bearer ${secret}`) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await tripService.autoCompletePastTrips();
|
||||||
|
console.log("[cron/auto-complete-trips] selesai", {
|
||||||
|
count: result.count,
|
||||||
|
ids: result.ids,
|
||||||
|
});
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: true,
|
||||||
|
completed: result.count,
|
||||||
|
ids: result.ids,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[cron/auto-complete-trips] gagal", err);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Gagal menjalankan auto-complete" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,3 +75,15 @@ export type Booking = Prisma.BookingModel
|
|||||||
* (di Phase MIDTRANS nanti). Untuk MANUAL biasanya cukup 1 Payment.
|
* (di Phase MIDTRANS nanti). Untuk MANUAL biasanya cukup 1 Payment.
|
||||||
*/
|
*/
|
||||||
export type Payment = Prisma.PaymentModel
|
export type Payment = Prisma.PaymentModel
|
||||||
|
/**
|
||||||
|
* Model Refund
|
||||||
|
* Refund = financial event terpisah dari Booking. Satu Booking bisa punya
|
||||||
|
* banyak Refund (partial, multi-tahap). Setiap row auditable: kapan dibuat,
|
||||||
|
* siapa melaporkan, siapa approve, kapan SUCCEEDED. Never delete — kalau
|
||||||
|
* gagal, set status=FAILED + alasan.
|
||||||
|
*
|
||||||
|
* Di MVP refund dimasukkan admin secara manual berdasarkan laporan dari
|
||||||
|
* peserta atau organizer (via WhatsApp/email). Phase berikutnya akan
|
||||||
|
* menambah self-service flow dari user dan organizer.
|
||||||
|
*/
|
||||||
|
export type Refund = Prisma.RefundModel
|
||||||
|
|||||||
@@ -99,3 +99,15 @@ export type Booking = Prisma.BookingModel
|
|||||||
* (di Phase MIDTRANS nanti). Untuk MANUAL biasanya cukup 1 Payment.
|
* (di Phase MIDTRANS nanti). Untuk MANUAL biasanya cukup 1 Payment.
|
||||||
*/
|
*/
|
||||||
export type Payment = Prisma.PaymentModel
|
export type Payment = Prisma.PaymentModel
|
||||||
|
/**
|
||||||
|
* Model Refund
|
||||||
|
* Refund = financial event terpisah dari Booking. Satu Booking bisa punya
|
||||||
|
* banyak Refund (partial, multi-tahap). Setiap row auditable: kapan dibuat,
|
||||||
|
* siapa melaporkan, siapa approve, kapan SUCCEEDED. Never delete — kalau
|
||||||
|
* gagal, set status=FAILED + alasan.
|
||||||
|
*
|
||||||
|
* Di MVP refund dimasukkan admin secara manual berdasarkan laporan dari
|
||||||
|
* peserta atau organizer (via WhatsApp/email). Phase berikutnya akan
|
||||||
|
* menambah self-service flow dari user dan organizer.
|
||||||
|
*/
|
||||||
|
export type Refund = Prisma.RefundModel
|
||||||
|
|||||||
@@ -389,6 +389,74 @@ export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = {
|
|||||||
_max?: Prisma.NestedJsonNullableFilter<$PrismaModel>
|
_max?: Prisma.NestedJsonNullableFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EnumRefundReasonFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RefundReason | Prisma.EnumRefundReasonFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RefundReason[] | Prisma.ListEnumRefundReasonFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RefundReason[] | Prisma.ListEnumRefundReasonFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRefundReasonFilter<$PrismaModel> | $Enums.RefundReason
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumRefundReporterFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RefundReporter | Prisma.EnumRefundReporterFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RefundReporter[] | Prisma.ListEnumRefundReporterFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RefundReporter[] | Prisma.ListEnumRefundReporterFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRefundReporterFilter<$PrismaModel> | $Enums.RefundReporter
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumRefundInitiatorFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RefundInitiator | Prisma.EnumRefundInitiatorFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RefundInitiator[] | Prisma.ListEnumRefundInitiatorFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RefundInitiator[] | Prisma.ListEnumRefundInitiatorFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRefundInitiatorFilter<$PrismaModel> | $Enums.RefundInitiator
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumRefundStatusFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RefundStatus | Prisma.EnumRefundStatusFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RefundStatus[] | Prisma.ListEnumRefundStatusFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RefundStatus[] | Prisma.ListEnumRefundStatusFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRefundStatusFilter<$PrismaModel> | $Enums.RefundStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumRefundReasonWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RefundReason | Prisma.EnumRefundReasonFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RefundReason[] | Prisma.ListEnumRefundReasonFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RefundReason[] | Prisma.ListEnumRefundReasonFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRefundReasonWithAggregatesFilter<$PrismaModel> | $Enums.RefundReason
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumRefundReasonFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumRefundReasonFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumRefundReporterWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RefundReporter | Prisma.EnumRefundReporterFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RefundReporter[] | Prisma.ListEnumRefundReporterFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RefundReporter[] | Prisma.ListEnumRefundReporterFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRefundReporterWithAggregatesFilter<$PrismaModel> | $Enums.RefundReporter
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumRefundReporterFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumRefundReporterFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumRefundInitiatorWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RefundInitiator | Prisma.EnumRefundInitiatorFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RefundInitiator[] | Prisma.ListEnumRefundInitiatorFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RefundInitiator[] | Prisma.ListEnumRefundInitiatorFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRefundInitiatorWithAggregatesFilter<$PrismaModel> | $Enums.RefundInitiator
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumRefundInitiatorFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumRefundInitiatorFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EnumRefundStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RefundStatus | Prisma.EnumRefundStatusFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RefundStatus[] | Prisma.ListEnumRefundStatusFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RefundStatus[] | Prisma.ListEnumRefundStatusFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRefundStatusWithAggregatesFilter<$PrismaModel> | $Enums.RefundStatus
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumRefundStatusFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumRefundStatusFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type NestedStringFilter<$PrismaModel = never> = {
|
export type NestedStringFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||||
@@ -750,4 +818,72 @@ export type NestedJsonNullableFilterBase<$PrismaModel = never> = {
|
|||||||
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NestedEnumRefundReasonFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RefundReason | Prisma.EnumRefundReasonFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RefundReason[] | Prisma.ListEnumRefundReasonFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RefundReason[] | Prisma.ListEnumRefundReasonFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRefundReasonFilter<$PrismaModel> | $Enums.RefundReason
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumRefundReporterFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RefundReporter | Prisma.EnumRefundReporterFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RefundReporter[] | Prisma.ListEnumRefundReporterFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RefundReporter[] | Prisma.ListEnumRefundReporterFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRefundReporterFilter<$PrismaModel> | $Enums.RefundReporter
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumRefundInitiatorFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RefundInitiator | Prisma.EnumRefundInitiatorFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RefundInitiator[] | Prisma.ListEnumRefundInitiatorFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RefundInitiator[] | Prisma.ListEnumRefundInitiatorFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRefundInitiatorFilter<$PrismaModel> | $Enums.RefundInitiator
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumRefundStatusFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RefundStatus | Prisma.EnumRefundStatusFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RefundStatus[] | Prisma.ListEnumRefundStatusFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RefundStatus[] | Prisma.ListEnumRefundStatusFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRefundStatusFilter<$PrismaModel> | $Enums.RefundStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumRefundReasonWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RefundReason | Prisma.EnumRefundReasonFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RefundReason[] | Prisma.ListEnumRefundReasonFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RefundReason[] | Prisma.ListEnumRefundReasonFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRefundReasonWithAggregatesFilter<$PrismaModel> | $Enums.RefundReason
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumRefundReasonFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumRefundReasonFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumRefundReporterWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RefundReporter | Prisma.EnumRefundReporterFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RefundReporter[] | Prisma.ListEnumRefundReporterFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RefundReporter[] | Prisma.ListEnumRefundReporterFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRefundReporterWithAggregatesFilter<$PrismaModel> | $Enums.RefundReporter
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumRefundReporterFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumRefundReporterFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumRefundInitiatorWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RefundInitiator | Prisma.EnumRefundInitiatorFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RefundInitiator[] | Prisma.ListEnumRefundInitiatorFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RefundInitiator[] | Prisma.ListEnumRefundInitiatorFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRefundInitiatorWithAggregatesFilter<$PrismaModel> | $Enums.RefundInitiator
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumRefundInitiatorFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumRefundInitiatorFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedEnumRefundStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: $Enums.RefundStatus | Prisma.EnumRefundStatusFieldRefInput<$PrismaModel>
|
||||||
|
in?: $Enums.RefundStatus[] | Prisma.ListEnumRefundStatusFieldRefInput<$PrismaModel>
|
||||||
|
notIn?: $Enums.RefundStatus[] | Prisma.ListEnumRefundStatusFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedEnumRefundStatusWithAggregatesFilter<$PrismaModel> | $Enums.RefundStatus
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedEnumRefundStatusFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedEnumRefundStatusFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export const BookingStatus = {
|
|||||||
PAID: 'PAID',
|
PAID: 'PAID',
|
||||||
CANCELLED: 'CANCELLED',
|
CANCELLED: 'CANCELLED',
|
||||||
REFUNDED: 'REFUNDED',
|
REFUNDED: 'REFUNDED',
|
||||||
|
PARTIALLY_REFUNDED: 'PARTIALLY_REFUNDED',
|
||||||
EXPIRED: 'EXPIRED'
|
EXPIRED: 'EXPIRED'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
@@ -93,3 +94,45 @@ export const PaymentStatus = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type PaymentStatus = (typeof PaymentStatus)[keyof typeof PaymentStatus]
|
export type PaymentStatus = (typeof PaymentStatus)[keyof typeof PaymentStatus]
|
||||||
|
|
||||||
|
|
||||||
|
export const RefundReason = {
|
||||||
|
USER_CANCELLATION: 'USER_CANCELLATION',
|
||||||
|
ORGANIZER_CANCELLED: 'ORGANIZER_CANCELLED',
|
||||||
|
TRIP_ISSUE: 'TRIP_ISSUE',
|
||||||
|
ADMIN_ADJUSTMENT: 'ADMIN_ADJUSTMENT',
|
||||||
|
DISPUTE_RESOLVED: 'DISPUTE_RESOLVED',
|
||||||
|
OTHER: 'OTHER'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type RefundReason = (typeof RefundReason)[keyof typeof RefundReason]
|
||||||
|
|
||||||
|
|
||||||
|
export const RefundStatus = {
|
||||||
|
PENDING: 'PENDING',
|
||||||
|
APPROVED: 'APPROVED',
|
||||||
|
REJECTED: 'REJECTED',
|
||||||
|
PROCESSING: 'PROCESSING',
|
||||||
|
SUCCEEDED: 'SUCCEEDED',
|
||||||
|
FAILED: 'FAILED'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type RefundStatus = (typeof RefundStatus)[keyof typeof RefundStatus]
|
||||||
|
|
||||||
|
|
||||||
|
export const RefundInitiator = {
|
||||||
|
USER: 'USER',
|
||||||
|
ORGANIZER: 'ORGANIZER',
|
||||||
|
SYSTEM: 'SYSTEM',
|
||||||
|
ADMIN: 'ADMIN'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type RefundInitiator = (typeof RefundInitiator)[keyof typeof RefundInitiator]
|
||||||
|
|
||||||
|
|
||||||
|
export const RefundReporter = {
|
||||||
|
PARTICIPANT: 'PARTICIPANT',
|
||||||
|
ORGANIZER: 'ORGANIZER'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type RefundReporter = (typeof RefundReporter)[keyof typeof RefundReporter]
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -393,7 +393,8 @@ export const ModelName = {
|
|||||||
TripImage: 'TripImage',
|
TripImage: 'TripImage',
|
||||||
TripParticipant: 'TripParticipant',
|
TripParticipant: 'TripParticipant',
|
||||||
Booking: 'Booking',
|
Booking: 'Booking',
|
||||||
Payment: 'Payment'
|
Payment: 'Payment',
|
||||||
|
Refund: 'Refund'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||||
@@ -409,7 +410,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
omit: GlobalOmitOptions
|
omit: GlobalOmitOptions
|
||||||
}
|
}
|
||||||
meta: {
|
meta: {
|
||||||
modelProps: "user" | "userProfile" | "account" | "organizerVerification" | "trip" | "tripReview" | "tripImage" | "tripParticipant" | "booking" | "payment"
|
modelProps: "user" | "userProfile" | "account" | "organizerVerification" | "trip" | "tripReview" | "tripImage" | "tripParticipant" | "booking" | "payment" | "refund"
|
||||||
txIsolationLevel: TransactionIsolationLevel
|
txIsolationLevel: TransactionIsolationLevel
|
||||||
}
|
}
|
||||||
model: {
|
model: {
|
||||||
@@ -1153,6 +1154,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Refund: {
|
||||||
|
payload: Prisma.$RefundPayload<ExtArgs>
|
||||||
|
fields: Prisma.RefundFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.RefundFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.RefundFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.RefundFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.RefundFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.RefundFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.RefundCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.RefundCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
createManyAndReturn: {
|
||||||
|
args: Prisma.RefundCreateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>[]
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.RefundDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.RefundUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.RefundDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.RefundUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateManyAndReturn: {
|
||||||
|
args: Prisma.RefundUpdateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>[]
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.RefundUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefundPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.RefundAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateRefund>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.RefundGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.RefundGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.RefundCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.RefundCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} & {
|
} & {
|
||||||
other: {
|
other: {
|
||||||
@@ -1365,6 +1440,31 @@ export const PaymentScalarFieldEnum = {
|
|||||||
export type PaymentScalarFieldEnum = (typeof PaymentScalarFieldEnum)[keyof typeof PaymentScalarFieldEnum]
|
export type PaymentScalarFieldEnum = (typeof PaymentScalarFieldEnum)[keyof typeof PaymentScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const RefundScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
bookingId: 'bookingId',
|
||||||
|
paymentId: 'paymentId',
|
||||||
|
amount: 'amount',
|
||||||
|
currency: 'currency',
|
||||||
|
reason: 'reason',
|
||||||
|
reportedBy: 'reportedBy',
|
||||||
|
reportNote: 'reportNote',
|
||||||
|
initiatedBy: 'initiatedBy',
|
||||||
|
status: 'status',
|
||||||
|
idempotencyKey: 'idempotencyKey',
|
||||||
|
adminNote: 'adminNote',
|
||||||
|
reviewedById: 'reviewedById',
|
||||||
|
reviewedAt: 'reviewedAt',
|
||||||
|
succeededAt: 'succeededAt',
|
||||||
|
failedAt: 'failedAt',
|
||||||
|
externalRefundId: 'externalRefundId',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type RefundScalarFieldEnum = (typeof RefundScalarFieldEnum)[keyof typeof RefundScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const SortOrder = {
|
export const SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
@@ -1587,6 +1687,62 @@ export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$Prisma
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'RefundReason'
|
||||||
|
*/
|
||||||
|
export type EnumRefundReasonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RefundReason'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'RefundReason[]'
|
||||||
|
*/
|
||||||
|
export type ListEnumRefundReasonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RefundReason[]'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'RefundReporter'
|
||||||
|
*/
|
||||||
|
export type EnumRefundReporterFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RefundReporter'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'RefundReporter[]'
|
||||||
|
*/
|
||||||
|
export type ListEnumRefundReporterFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RefundReporter[]'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'RefundInitiator'
|
||||||
|
*/
|
||||||
|
export type EnumRefundInitiatorFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RefundInitiator'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'RefundInitiator[]'
|
||||||
|
*/
|
||||||
|
export type ListEnumRefundInitiatorFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RefundInitiator[]'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'RefundStatus'
|
||||||
|
*/
|
||||||
|
export type EnumRefundStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RefundStatus'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'RefundStatus[]'
|
||||||
|
*/
|
||||||
|
export type ListEnumRefundStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RefundStatus[]'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to a field of type 'Float'
|
* Reference to a field of type 'Float'
|
||||||
*/
|
*/
|
||||||
@@ -1720,6 +1876,7 @@ export type GlobalOmitConfig = {
|
|||||||
tripParticipant?: Prisma.TripParticipantOmit
|
tripParticipant?: Prisma.TripParticipantOmit
|
||||||
booking?: Prisma.BookingOmit
|
booking?: Prisma.BookingOmit
|
||||||
payment?: Prisma.PaymentOmit
|
payment?: Prisma.PaymentOmit
|
||||||
|
refund?: Prisma.RefundOmit
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Types for Logging */
|
/* Types for Logging */
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ export const ModelName = {
|
|||||||
TripImage: 'TripImage',
|
TripImage: 'TripImage',
|
||||||
TripParticipant: 'TripParticipant',
|
TripParticipant: 'TripParticipant',
|
||||||
Booking: 'Booking',
|
Booking: 'Booking',
|
||||||
Payment: 'Payment'
|
Payment: 'Payment',
|
||||||
|
Refund: 'Refund'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||||
@@ -252,6 +253,31 @@ export const PaymentScalarFieldEnum = {
|
|||||||
export type PaymentScalarFieldEnum = (typeof PaymentScalarFieldEnum)[keyof typeof PaymentScalarFieldEnum]
|
export type PaymentScalarFieldEnum = (typeof PaymentScalarFieldEnum)[keyof typeof PaymentScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const RefundScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
bookingId: 'bookingId',
|
||||||
|
paymentId: 'paymentId',
|
||||||
|
amount: 'amount',
|
||||||
|
currency: 'currency',
|
||||||
|
reason: 'reason',
|
||||||
|
reportedBy: 'reportedBy',
|
||||||
|
reportNote: 'reportNote',
|
||||||
|
initiatedBy: 'initiatedBy',
|
||||||
|
status: 'status',
|
||||||
|
idempotencyKey: 'idempotencyKey',
|
||||||
|
adminNote: 'adminNote',
|
||||||
|
reviewedById: 'reviewedById',
|
||||||
|
reviewedAt: 'reviewedAt',
|
||||||
|
succeededAt: 'succeededAt',
|
||||||
|
failedAt: 'failedAt',
|
||||||
|
externalRefundId: 'externalRefundId',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type RefundScalarFieldEnum = (typeof RefundScalarFieldEnum)[keyof typeof RefundScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const SortOrder = {
|
export const SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
|
|||||||
@@ -18,4 +18,5 @@ export type * from './models/TripImage'
|
|||||||
export type * from './models/TripParticipant'
|
export type * from './models/TripParticipant'
|
||||||
export type * from './models/Booking'
|
export type * from './models/Booking'
|
||||||
export type * from './models/Payment'
|
export type * from './models/Payment'
|
||||||
|
export type * from './models/Refund'
|
||||||
export type * from './commonInputTypes'
|
export type * from './commonInputTypes'
|
||||||
@@ -257,6 +257,7 @@ export type BookingWhereInput = {
|
|||||||
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
|
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
|
||||||
participant?: Prisma.XOR<Prisma.TripParticipantScalarRelationFilter, Prisma.TripParticipantWhereInput>
|
participant?: Prisma.XOR<Prisma.TripParticipantScalarRelationFilter, Prisma.TripParticipantWhereInput>
|
||||||
payments?: Prisma.PaymentListRelationFilter
|
payments?: Prisma.PaymentListRelationFilter
|
||||||
|
refunds?: Prisma.RefundListRelationFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingOrderByWithRelationInput = {
|
export type BookingOrderByWithRelationInput = {
|
||||||
@@ -273,6 +274,7 @@ export type BookingOrderByWithRelationInput = {
|
|||||||
user?: Prisma.UserOrderByWithRelationInput
|
user?: Prisma.UserOrderByWithRelationInput
|
||||||
participant?: Prisma.TripParticipantOrderByWithRelationInput
|
participant?: Prisma.TripParticipantOrderByWithRelationInput
|
||||||
payments?: Prisma.PaymentOrderByRelationAggregateInput
|
payments?: Prisma.PaymentOrderByRelationAggregateInput
|
||||||
|
refunds?: Prisma.RefundOrderByRelationAggregateInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingWhereUniqueInput = Prisma.AtLeast<{
|
export type BookingWhereUniqueInput = Prisma.AtLeast<{
|
||||||
@@ -293,6 +295,7 @@ export type BookingWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
|
user?: Prisma.XOR<Prisma.UserScalarRelationFilter, Prisma.UserWhereInput>
|
||||||
participant?: Prisma.XOR<Prisma.TripParticipantScalarRelationFilter, Prisma.TripParticipantWhereInput>
|
participant?: Prisma.XOR<Prisma.TripParticipantScalarRelationFilter, Prisma.TripParticipantWhereInput>
|
||||||
payments?: Prisma.PaymentListRelationFilter
|
payments?: Prisma.PaymentListRelationFilter
|
||||||
|
refunds?: Prisma.RefundListRelationFilter
|
||||||
}, "id" | "participantId" | "tripId_userId">
|
}, "id" | "participantId" | "tripId_userId">
|
||||||
|
|
||||||
export type BookingOrderByWithAggregationInput = {
|
export type BookingOrderByWithAggregationInput = {
|
||||||
@@ -338,6 +341,7 @@ export type BookingCreateInput = {
|
|||||||
user: Prisma.UserCreateNestedOneWithoutBookingsInput
|
user: Prisma.UserCreateNestedOneWithoutBookingsInput
|
||||||
participant: Prisma.TripParticipantCreateNestedOneWithoutBookingInput
|
participant: Prisma.TripParticipantCreateNestedOneWithoutBookingInput
|
||||||
payments?: Prisma.PaymentCreateNestedManyWithoutBookingInput
|
payments?: Prisma.PaymentCreateNestedManyWithoutBookingInput
|
||||||
|
refunds?: Prisma.RefundCreateNestedManyWithoutBookingInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingUncheckedCreateInput = {
|
export type BookingUncheckedCreateInput = {
|
||||||
@@ -351,6 +355,7 @@ export type BookingUncheckedCreateInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
payments?: Prisma.PaymentUncheckedCreateNestedManyWithoutBookingInput
|
payments?: Prisma.PaymentUncheckedCreateNestedManyWithoutBookingInput
|
||||||
|
refunds?: Prisma.RefundUncheckedCreateNestedManyWithoutBookingInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingUpdateInput = {
|
export type BookingUpdateInput = {
|
||||||
@@ -364,6 +369,7 @@ export type BookingUpdateInput = {
|
|||||||
user?: Prisma.UserUpdateOneRequiredWithoutBookingsNestedInput
|
user?: Prisma.UserUpdateOneRequiredWithoutBookingsNestedInput
|
||||||
participant?: Prisma.TripParticipantUpdateOneRequiredWithoutBookingNestedInput
|
participant?: Prisma.TripParticipantUpdateOneRequiredWithoutBookingNestedInput
|
||||||
payments?: Prisma.PaymentUpdateManyWithoutBookingNestedInput
|
payments?: Prisma.PaymentUpdateManyWithoutBookingNestedInput
|
||||||
|
refunds?: Prisma.RefundUpdateManyWithoutBookingNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingUncheckedUpdateInput = {
|
export type BookingUncheckedUpdateInput = {
|
||||||
@@ -377,6 +383,7 @@ export type BookingUncheckedUpdateInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
payments?: Prisma.PaymentUncheckedUpdateManyWithoutBookingNestedInput
|
payments?: Prisma.PaymentUncheckedUpdateManyWithoutBookingNestedInput
|
||||||
|
refunds?: Prisma.RefundUncheckedUpdateManyWithoutBookingNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingCreateManyInput = {
|
export type BookingCreateManyInput = {
|
||||||
@@ -615,6 +622,20 @@ export type BookingUpdateOneRequiredWithoutPaymentsNestedInput = {
|
|||||||
update?: Prisma.XOR<Prisma.XOR<Prisma.BookingUpdateToOneWithWhereWithoutPaymentsInput, Prisma.BookingUpdateWithoutPaymentsInput>, Prisma.BookingUncheckedUpdateWithoutPaymentsInput>
|
update?: Prisma.XOR<Prisma.XOR<Prisma.BookingUpdateToOneWithWhereWithoutPaymentsInput, Prisma.BookingUpdateWithoutPaymentsInput>, Prisma.BookingUncheckedUpdateWithoutPaymentsInput>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BookingCreateNestedOneWithoutRefundsInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.BookingCreateWithoutRefundsInput, Prisma.BookingUncheckedCreateWithoutRefundsInput>
|
||||||
|
connectOrCreate?: Prisma.BookingCreateOrConnectWithoutRefundsInput
|
||||||
|
connect?: Prisma.BookingWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BookingUpdateOneRequiredWithoutRefundsNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.BookingCreateWithoutRefundsInput, Prisma.BookingUncheckedCreateWithoutRefundsInput>
|
||||||
|
connectOrCreate?: Prisma.BookingCreateOrConnectWithoutRefundsInput
|
||||||
|
upsert?: Prisma.BookingUpsertWithoutRefundsInput
|
||||||
|
connect?: Prisma.BookingWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.BookingUpdateToOneWithWhereWithoutRefundsInput, Prisma.BookingUpdateWithoutRefundsInput>, Prisma.BookingUncheckedUpdateWithoutRefundsInput>
|
||||||
|
}
|
||||||
|
|
||||||
export type BookingCreateWithoutUserInput = {
|
export type BookingCreateWithoutUserInput = {
|
||||||
id?: string
|
id?: string
|
||||||
amount: number
|
amount: number
|
||||||
@@ -625,6 +646,7 @@ export type BookingCreateWithoutUserInput = {
|
|||||||
trip: Prisma.TripCreateNestedOneWithoutBookingsInput
|
trip: Prisma.TripCreateNestedOneWithoutBookingsInput
|
||||||
participant: Prisma.TripParticipantCreateNestedOneWithoutBookingInput
|
participant: Prisma.TripParticipantCreateNestedOneWithoutBookingInput
|
||||||
payments?: Prisma.PaymentCreateNestedManyWithoutBookingInput
|
payments?: Prisma.PaymentCreateNestedManyWithoutBookingInput
|
||||||
|
refunds?: Prisma.RefundCreateNestedManyWithoutBookingInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingUncheckedCreateWithoutUserInput = {
|
export type BookingUncheckedCreateWithoutUserInput = {
|
||||||
@@ -637,6 +659,7 @@ export type BookingUncheckedCreateWithoutUserInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
payments?: Prisma.PaymentUncheckedCreateNestedManyWithoutBookingInput
|
payments?: Prisma.PaymentUncheckedCreateNestedManyWithoutBookingInput
|
||||||
|
refunds?: Prisma.RefundUncheckedCreateNestedManyWithoutBookingInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingCreateOrConnectWithoutUserInput = {
|
export type BookingCreateOrConnectWithoutUserInput = {
|
||||||
@@ -690,6 +713,7 @@ export type BookingCreateWithoutTripInput = {
|
|||||||
user: Prisma.UserCreateNestedOneWithoutBookingsInput
|
user: Prisma.UserCreateNestedOneWithoutBookingsInput
|
||||||
participant: Prisma.TripParticipantCreateNestedOneWithoutBookingInput
|
participant: Prisma.TripParticipantCreateNestedOneWithoutBookingInput
|
||||||
payments?: Prisma.PaymentCreateNestedManyWithoutBookingInput
|
payments?: Prisma.PaymentCreateNestedManyWithoutBookingInput
|
||||||
|
refunds?: Prisma.RefundCreateNestedManyWithoutBookingInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingUncheckedCreateWithoutTripInput = {
|
export type BookingUncheckedCreateWithoutTripInput = {
|
||||||
@@ -702,6 +726,7 @@ export type BookingUncheckedCreateWithoutTripInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
payments?: Prisma.PaymentUncheckedCreateNestedManyWithoutBookingInput
|
payments?: Prisma.PaymentUncheckedCreateNestedManyWithoutBookingInput
|
||||||
|
refunds?: Prisma.RefundUncheckedCreateNestedManyWithoutBookingInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingCreateOrConnectWithoutTripInput = {
|
export type BookingCreateOrConnectWithoutTripInput = {
|
||||||
@@ -740,6 +765,7 @@ export type BookingCreateWithoutParticipantInput = {
|
|||||||
trip: Prisma.TripCreateNestedOneWithoutBookingsInput
|
trip: Prisma.TripCreateNestedOneWithoutBookingsInput
|
||||||
user: Prisma.UserCreateNestedOneWithoutBookingsInput
|
user: Prisma.UserCreateNestedOneWithoutBookingsInput
|
||||||
payments?: Prisma.PaymentCreateNestedManyWithoutBookingInput
|
payments?: Prisma.PaymentCreateNestedManyWithoutBookingInput
|
||||||
|
refunds?: Prisma.RefundCreateNestedManyWithoutBookingInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingUncheckedCreateWithoutParticipantInput = {
|
export type BookingUncheckedCreateWithoutParticipantInput = {
|
||||||
@@ -752,6 +778,7 @@ export type BookingUncheckedCreateWithoutParticipantInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
payments?: Prisma.PaymentUncheckedCreateNestedManyWithoutBookingInput
|
payments?: Prisma.PaymentUncheckedCreateNestedManyWithoutBookingInput
|
||||||
|
refunds?: Prisma.RefundUncheckedCreateNestedManyWithoutBookingInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingCreateOrConnectWithoutParticipantInput = {
|
export type BookingCreateOrConnectWithoutParticipantInput = {
|
||||||
@@ -780,6 +807,7 @@ export type BookingUpdateWithoutParticipantInput = {
|
|||||||
trip?: Prisma.TripUpdateOneRequiredWithoutBookingsNestedInput
|
trip?: Prisma.TripUpdateOneRequiredWithoutBookingsNestedInput
|
||||||
user?: Prisma.UserUpdateOneRequiredWithoutBookingsNestedInput
|
user?: Prisma.UserUpdateOneRequiredWithoutBookingsNestedInput
|
||||||
payments?: Prisma.PaymentUpdateManyWithoutBookingNestedInput
|
payments?: Prisma.PaymentUpdateManyWithoutBookingNestedInput
|
||||||
|
refunds?: Prisma.RefundUpdateManyWithoutBookingNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingUncheckedUpdateWithoutParticipantInput = {
|
export type BookingUncheckedUpdateWithoutParticipantInput = {
|
||||||
@@ -792,6 +820,7 @@ export type BookingUncheckedUpdateWithoutParticipantInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
payments?: Prisma.PaymentUncheckedUpdateManyWithoutBookingNestedInput
|
payments?: Prisma.PaymentUncheckedUpdateManyWithoutBookingNestedInput
|
||||||
|
refunds?: Prisma.RefundUncheckedUpdateManyWithoutBookingNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingCreateWithoutPaymentsInput = {
|
export type BookingCreateWithoutPaymentsInput = {
|
||||||
@@ -804,6 +833,7 @@ export type BookingCreateWithoutPaymentsInput = {
|
|||||||
trip: Prisma.TripCreateNestedOneWithoutBookingsInput
|
trip: Prisma.TripCreateNestedOneWithoutBookingsInput
|
||||||
user: Prisma.UserCreateNestedOneWithoutBookingsInput
|
user: Prisma.UserCreateNestedOneWithoutBookingsInput
|
||||||
participant: Prisma.TripParticipantCreateNestedOneWithoutBookingInput
|
participant: Prisma.TripParticipantCreateNestedOneWithoutBookingInput
|
||||||
|
refunds?: Prisma.RefundCreateNestedManyWithoutBookingInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingUncheckedCreateWithoutPaymentsInput = {
|
export type BookingUncheckedCreateWithoutPaymentsInput = {
|
||||||
@@ -816,6 +846,7 @@ export type BookingUncheckedCreateWithoutPaymentsInput = {
|
|||||||
status?: $Enums.BookingStatus
|
status?: $Enums.BookingStatus
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
refunds?: Prisma.RefundUncheckedCreateNestedManyWithoutBookingInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingCreateOrConnectWithoutPaymentsInput = {
|
export type BookingCreateOrConnectWithoutPaymentsInput = {
|
||||||
@@ -844,6 +875,7 @@ export type BookingUpdateWithoutPaymentsInput = {
|
|||||||
trip?: Prisma.TripUpdateOneRequiredWithoutBookingsNestedInput
|
trip?: Prisma.TripUpdateOneRequiredWithoutBookingsNestedInput
|
||||||
user?: Prisma.UserUpdateOneRequiredWithoutBookingsNestedInput
|
user?: Prisma.UserUpdateOneRequiredWithoutBookingsNestedInput
|
||||||
participant?: Prisma.TripParticipantUpdateOneRequiredWithoutBookingNestedInput
|
participant?: Prisma.TripParticipantUpdateOneRequiredWithoutBookingNestedInput
|
||||||
|
refunds?: Prisma.RefundUpdateManyWithoutBookingNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingUncheckedUpdateWithoutPaymentsInput = {
|
export type BookingUncheckedUpdateWithoutPaymentsInput = {
|
||||||
@@ -856,6 +888,75 @@ export type BookingUncheckedUpdateWithoutPaymentsInput = {
|
|||||||
status?: Prisma.EnumBookingStatusFieldUpdateOperationsInput | $Enums.BookingStatus
|
status?: Prisma.EnumBookingStatusFieldUpdateOperationsInput | $Enums.BookingStatus
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
refunds?: Prisma.RefundUncheckedUpdateManyWithoutBookingNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BookingCreateWithoutRefundsInput = {
|
||||||
|
id?: string
|
||||||
|
amount: number
|
||||||
|
currency?: string
|
||||||
|
status?: $Enums.BookingStatus
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
trip: Prisma.TripCreateNestedOneWithoutBookingsInput
|
||||||
|
user: Prisma.UserCreateNestedOneWithoutBookingsInput
|
||||||
|
participant: Prisma.TripParticipantCreateNestedOneWithoutBookingInput
|
||||||
|
payments?: Prisma.PaymentCreateNestedManyWithoutBookingInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BookingUncheckedCreateWithoutRefundsInput = {
|
||||||
|
id?: string
|
||||||
|
tripId: string
|
||||||
|
userId: string
|
||||||
|
participantId: string
|
||||||
|
amount: number
|
||||||
|
currency?: string
|
||||||
|
status?: $Enums.BookingStatus
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
payments?: Prisma.PaymentUncheckedCreateNestedManyWithoutBookingInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BookingCreateOrConnectWithoutRefundsInput = {
|
||||||
|
where: Prisma.BookingWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.BookingCreateWithoutRefundsInput, Prisma.BookingUncheckedCreateWithoutRefundsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BookingUpsertWithoutRefundsInput = {
|
||||||
|
update: Prisma.XOR<Prisma.BookingUpdateWithoutRefundsInput, Prisma.BookingUncheckedUpdateWithoutRefundsInput>
|
||||||
|
create: Prisma.XOR<Prisma.BookingCreateWithoutRefundsInput, Prisma.BookingUncheckedCreateWithoutRefundsInput>
|
||||||
|
where?: Prisma.BookingWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BookingUpdateToOneWithWhereWithoutRefundsInput = {
|
||||||
|
where?: Prisma.BookingWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.BookingUpdateWithoutRefundsInput, Prisma.BookingUncheckedUpdateWithoutRefundsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BookingUpdateWithoutRefundsInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
amount?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
currency?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
status?: Prisma.EnumBookingStatusFieldUpdateOperationsInput | $Enums.BookingStatus
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
trip?: Prisma.TripUpdateOneRequiredWithoutBookingsNestedInput
|
||||||
|
user?: Prisma.UserUpdateOneRequiredWithoutBookingsNestedInput
|
||||||
|
participant?: Prisma.TripParticipantUpdateOneRequiredWithoutBookingNestedInput
|
||||||
|
payments?: Prisma.PaymentUpdateManyWithoutBookingNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BookingUncheckedUpdateWithoutRefundsInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
tripId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
userId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
participantId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
amount?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
currency?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
status?: Prisma.EnumBookingStatusFieldUpdateOperationsInput | $Enums.BookingStatus
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
payments?: Prisma.PaymentUncheckedUpdateManyWithoutBookingNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingCreateManyUserInput = {
|
export type BookingCreateManyUserInput = {
|
||||||
@@ -879,6 +980,7 @@ export type BookingUpdateWithoutUserInput = {
|
|||||||
trip?: Prisma.TripUpdateOneRequiredWithoutBookingsNestedInput
|
trip?: Prisma.TripUpdateOneRequiredWithoutBookingsNestedInput
|
||||||
participant?: Prisma.TripParticipantUpdateOneRequiredWithoutBookingNestedInput
|
participant?: Prisma.TripParticipantUpdateOneRequiredWithoutBookingNestedInput
|
||||||
payments?: Prisma.PaymentUpdateManyWithoutBookingNestedInput
|
payments?: Prisma.PaymentUpdateManyWithoutBookingNestedInput
|
||||||
|
refunds?: Prisma.RefundUpdateManyWithoutBookingNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingUncheckedUpdateWithoutUserInput = {
|
export type BookingUncheckedUpdateWithoutUserInput = {
|
||||||
@@ -891,6 +993,7 @@ export type BookingUncheckedUpdateWithoutUserInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
payments?: Prisma.PaymentUncheckedUpdateManyWithoutBookingNestedInput
|
payments?: Prisma.PaymentUncheckedUpdateManyWithoutBookingNestedInput
|
||||||
|
refunds?: Prisma.RefundUncheckedUpdateManyWithoutBookingNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingUncheckedUpdateManyWithoutUserInput = {
|
export type BookingUncheckedUpdateManyWithoutUserInput = {
|
||||||
@@ -925,6 +1028,7 @@ export type BookingUpdateWithoutTripInput = {
|
|||||||
user?: Prisma.UserUpdateOneRequiredWithoutBookingsNestedInput
|
user?: Prisma.UserUpdateOneRequiredWithoutBookingsNestedInput
|
||||||
participant?: Prisma.TripParticipantUpdateOneRequiredWithoutBookingNestedInput
|
participant?: Prisma.TripParticipantUpdateOneRequiredWithoutBookingNestedInput
|
||||||
payments?: Prisma.PaymentUpdateManyWithoutBookingNestedInput
|
payments?: Prisma.PaymentUpdateManyWithoutBookingNestedInput
|
||||||
|
refunds?: Prisma.RefundUpdateManyWithoutBookingNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingUncheckedUpdateWithoutTripInput = {
|
export type BookingUncheckedUpdateWithoutTripInput = {
|
||||||
@@ -937,6 +1041,7 @@ export type BookingUncheckedUpdateWithoutTripInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
payments?: Prisma.PaymentUncheckedUpdateManyWithoutBookingNestedInput
|
payments?: Prisma.PaymentUncheckedUpdateManyWithoutBookingNestedInput
|
||||||
|
refunds?: Prisma.RefundUncheckedUpdateManyWithoutBookingNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingUncheckedUpdateManyWithoutTripInput = {
|
export type BookingUncheckedUpdateManyWithoutTripInput = {
|
||||||
@@ -957,10 +1062,12 @@ export type BookingUncheckedUpdateManyWithoutTripInput = {
|
|||||||
|
|
||||||
export type BookingCountOutputType = {
|
export type BookingCountOutputType = {
|
||||||
payments: number
|
payments: number
|
||||||
|
refunds: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookingCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type BookingCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
payments?: boolean | BookingCountOutputTypeCountPaymentsArgs
|
payments?: boolean | BookingCountOutputTypeCountPaymentsArgs
|
||||||
|
refunds?: boolean | BookingCountOutputTypeCountRefundsArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -980,6 +1087,13 @@ export type BookingCountOutputTypeCountPaymentsArgs<ExtArgs extends runtime.Type
|
|||||||
where?: Prisma.PaymentWhereInput
|
where?: Prisma.PaymentWhereInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BookingCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type BookingCountOutputTypeCountRefundsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.RefundWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type BookingSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type BookingSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
@@ -995,6 +1109,7 @@ export type BookingSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||||
participant?: boolean | Prisma.TripParticipantDefaultArgs<ExtArgs>
|
participant?: boolean | Prisma.TripParticipantDefaultArgs<ExtArgs>
|
||||||
payments?: boolean | Prisma.Booking$paymentsArgs<ExtArgs>
|
payments?: boolean | Prisma.Booking$paymentsArgs<ExtArgs>
|
||||||
|
refunds?: boolean | Prisma.Booking$refundsArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.BookingCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.BookingCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["booking"]>
|
}, ExtArgs["result"]["booking"]>
|
||||||
|
|
||||||
@@ -1046,6 +1161,7 @@ export type BookingInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
|
||||||
participant?: boolean | Prisma.TripParticipantDefaultArgs<ExtArgs>
|
participant?: boolean | Prisma.TripParticipantDefaultArgs<ExtArgs>
|
||||||
payments?: boolean | Prisma.Booking$paymentsArgs<ExtArgs>
|
payments?: boolean | Prisma.Booking$paymentsArgs<ExtArgs>
|
||||||
|
refunds?: boolean | Prisma.Booking$refundsArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.BookingCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.BookingCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type BookingIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type BookingIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
@@ -1066,6 +1182,7 @@ export type $BookingPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
|
|||||||
user: Prisma.$UserPayload<ExtArgs>
|
user: Prisma.$UserPayload<ExtArgs>
|
||||||
participant: Prisma.$TripParticipantPayload<ExtArgs>
|
participant: Prisma.$TripParticipantPayload<ExtArgs>
|
||||||
payments: Prisma.$PaymentPayload<ExtArgs>[]
|
payments: Prisma.$PaymentPayload<ExtArgs>[]
|
||||||
|
refunds: Prisma.$RefundPayload<ExtArgs>[]
|
||||||
}
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
id: string
|
||||||
@@ -1475,6 +1592,7 @@ export interface Prisma__BookingClient<T, Null = never, ExtArgs extends runtime.
|
|||||||
user<T extends Prisma.UserDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.UserDefaultArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
user<T extends Prisma.UserDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.UserDefaultArgs<ExtArgs>>): Prisma.Prisma__UserClient<runtime.Types.Result.GetResult<Prisma.$UserPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
||||||
participant<T extends Prisma.TripParticipantDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.TripParticipantDefaultArgs<ExtArgs>>): Prisma.Prisma__TripParticipantClient<runtime.Types.Result.GetResult<Prisma.$TripParticipantPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
participant<T extends Prisma.TripParticipantDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.TripParticipantDefaultArgs<ExtArgs>>): Prisma.Prisma__TripParticipantClient<runtime.Types.Result.GetResult<Prisma.$TripParticipantPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
||||||
payments<T extends Prisma.Booking$paymentsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Booking$paymentsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$PaymentPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
payments<T extends Prisma.Booking$paymentsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Booking$paymentsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$PaymentPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
|
refunds<T extends Prisma.Booking$refundsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Booking$refundsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RefundPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
/**
|
/**
|
||||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||||
@@ -1937,6 +2055,30 @@ export type Booking$paymentsArgs<ExtArgs extends runtime.Types.Extensions.Intern
|
|||||||
distinct?: Prisma.PaymentScalarFieldEnum | Prisma.PaymentScalarFieldEnum[]
|
distinct?: Prisma.PaymentScalarFieldEnum | Prisma.PaymentScalarFieldEnum[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Booking.refunds
|
||||||
|
*/
|
||||||
|
export type Booking$refundsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the Refund
|
||||||
|
*/
|
||||||
|
select?: Prisma.RefundSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the Refund
|
||||||
|
*/
|
||||||
|
omit?: Prisma.RefundOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.RefundInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.RefundWhereInput
|
||||||
|
orderBy?: Prisma.RefundOrderByWithRelationInput | Prisma.RefundOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.RefundWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.RefundScalarFieldEnum | Prisma.RefundScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Booking without action
|
* Booking without action
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ export type PaymentWhereInput = {
|
|||||||
createdAt?: Prisma.DateTimeFilter<"Payment"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Payment"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"Payment"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Payment"> | Date | string
|
||||||
booking?: Prisma.XOR<Prisma.BookingScalarRelationFilter, Prisma.BookingWhereInput>
|
booking?: Prisma.XOR<Prisma.BookingScalarRelationFilter, Prisma.BookingWhereInput>
|
||||||
|
refunds?: Prisma.RefundListRelationFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PaymentOrderByWithRelationInput = {
|
export type PaymentOrderByWithRelationInput = {
|
||||||
@@ -322,6 +323,7 @@ export type PaymentOrderByWithRelationInput = {
|
|||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
booking?: Prisma.BookingOrderByWithRelationInput
|
booking?: Prisma.BookingOrderByWithRelationInput
|
||||||
|
refunds?: Prisma.RefundOrderByRelationAggregateInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PaymentWhereUniqueInput = Prisma.AtLeast<{
|
export type PaymentWhereUniqueInput = Prisma.AtLeast<{
|
||||||
@@ -345,6 +347,7 @@ export type PaymentWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
createdAt?: Prisma.DateTimeFilter<"Payment"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Payment"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"Payment"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Payment"> | Date | string
|
||||||
booking?: Prisma.XOR<Prisma.BookingScalarRelationFilter, Prisma.BookingWhereInput>
|
booking?: Prisma.XOR<Prisma.BookingScalarRelationFilter, Prisma.BookingWhereInput>
|
||||||
|
refunds?: Prisma.RefundListRelationFilter
|
||||||
}, "id" | "externalOrderId">
|
}, "id" | "externalOrderId">
|
||||||
|
|
||||||
export type PaymentOrderByWithAggregationInput = {
|
export type PaymentOrderByWithAggregationInput = {
|
||||||
@@ -410,6 +413,7 @@ export type PaymentCreateInput = {
|
|||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
booking: Prisma.BookingCreateNestedOneWithoutPaymentsInput
|
booking: Prisma.BookingCreateNestedOneWithoutPaymentsInput
|
||||||
|
refunds?: Prisma.RefundCreateNestedManyWithoutPaymentInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PaymentUncheckedCreateInput = {
|
export type PaymentUncheckedCreateInput = {
|
||||||
@@ -429,6 +433,7 @@ export type PaymentUncheckedCreateInput = {
|
|||||||
rejectionReason?: string | null
|
rejectionReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
refunds?: Prisma.RefundUncheckedCreateNestedManyWithoutPaymentInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PaymentUpdateInput = {
|
export type PaymentUpdateInput = {
|
||||||
@@ -448,6 +453,7 @@ export type PaymentUpdateInput = {
|
|||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
booking?: Prisma.BookingUpdateOneRequiredWithoutPaymentsNestedInput
|
booking?: Prisma.BookingUpdateOneRequiredWithoutPaymentsNestedInput
|
||||||
|
refunds?: Prisma.RefundUpdateManyWithoutPaymentNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PaymentUncheckedUpdateInput = {
|
export type PaymentUncheckedUpdateInput = {
|
||||||
@@ -467,6 +473,7 @@ export type PaymentUncheckedUpdateInput = {
|
|||||||
rejectionReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
rejectionReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
refunds?: Prisma.RefundUncheckedUpdateManyWithoutPaymentNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PaymentCreateManyInput = {
|
export type PaymentCreateManyInput = {
|
||||||
@@ -598,6 +605,11 @@ export type PaymentSumOrderByAggregateInput = {
|
|||||||
amount?: Prisma.SortOrder
|
amount?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PaymentNullableScalarRelationFilter = {
|
||||||
|
is?: Prisma.PaymentWhereInput | null
|
||||||
|
isNot?: Prisma.PaymentWhereInput | null
|
||||||
|
}
|
||||||
|
|
||||||
export type PaymentCreateNestedManyWithoutBookingInput = {
|
export type PaymentCreateNestedManyWithoutBookingInput = {
|
||||||
create?: Prisma.XOR<Prisma.PaymentCreateWithoutBookingInput, Prisma.PaymentUncheckedCreateWithoutBookingInput> | Prisma.PaymentCreateWithoutBookingInput[] | Prisma.PaymentUncheckedCreateWithoutBookingInput[]
|
create?: Prisma.XOR<Prisma.PaymentCreateWithoutBookingInput, Prisma.PaymentUncheckedCreateWithoutBookingInput> | Prisma.PaymentCreateWithoutBookingInput[] | Prisma.PaymentUncheckedCreateWithoutBookingInput[]
|
||||||
connectOrCreate?: Prisma.PaymentCreateOrConnectWithoutBookingInput | Prisma.PaymentCreateOrConnectWithoutBookingInput[]
|
connectOrCreate?: Prisma.PaymentCreateOrConnectWithoutBookingInput | Prisma.PaymentCreateOrConnectWithoutBookingInput[]
|
||||||
@@ -648,6 +660,22 @@ export type EnumPaymentStatusFieldUpdateOperationsInput = {
|
|||||||
set?: $Enums.PaymentStatus
|
set?: $Enums.PaymentStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PaymentCreateNestedOneWithoutRefundsInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.PaymentCreateWithoutRefundsInput, Prisma.PaymentUncheckedCreateWithoutRefundsInput>
|
||||||
|
connectOrCreate?: Prisma.PaymentCreateOrConnectWithoutRefundsInput
|
||||||
|
connect?: Prisma.PaymentWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaymentUpdateOneWithoutRefundsNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.PaymentCreateWithoutRefundsInput, Prisma.PaymentUncheckedCreateWithoutRefundsInput>
|
||||||
|
connectOrCreate?: Prisma.PaymentCreateOrConnectWithoutRefundsInput
|
||||||
|
upsert?: Prisma.PaymentUpsertWithoutRefundsInput
|
||||||
|
disconnect?: Prisma.PaymentWhereInput | boolean
|
||||||
|
delete?: Prisma.PaymentWhereInput | boolean
|
||||||
|
connect?: Prisma.PaymentWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.PaymentUpdateToOneWithWhereWithoutRefundsInput, Prisma.PaymentUpdateWithoutRefundsInput>, Prisma.PaymentUncheckedUpdateWithoutRefundsInput>
|
||||||
|
}
|
||||||
|
|
||||||
export type PaymentCreateWithoutBookingInput = {
|
export type PaymentCreateWithoutBookingInput = {
|
||||||
id?: string
|
id?: string
|
||||||
provider: $Enums.PaymentProvider
|
provider: $Enums.PaymentProvider
|
||||||
@@ -664,6 +692,7 @@ export type PaymentCreateWithoutBookingInput = {
|
|||||||
rejectionReason?: string | null
|
rejectionReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
refunds?: Prisma.RefundCreateNestedManyWithoutPaymentInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PaymentUncheckedCreateWithoutBookingInput = {
|
export type PaymentUncheckedCreateWithoutBookingInput = {
|
||||||
@@ -682,6 +711,7 @@ export type PaymentUncheckedCreateWithoutBookingInput = {
|
|||||||
rejectionReason?: string | null
|
rejectionReason?: string | null
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
refunds?: Prisma.RefundUncheckedCreateNestedManyWithoutPaymentInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PaymentCreateOrConnectWithoutBookingInput = {
|
export type PaymentCreateOrConnectWithoutBookingInput = {
|
||||||
@@ -732,6 +762,98 @@ export type PaymentScalarWhereInput = {
|
|||||||
updatedAt?: Prisma.DateTimeFilter<"Payment"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Payment"> | Date | string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PaymentCreateWithoutRefundsInput = {
|
||||||
|
id?: string
|
||||||
|
provider: $Enums.PaymentProvider
|
||||||
|
externalOrderId: string
|
||||||
|
externalTxId?: string | null
|
||||||
|
method?: string | null
|
||||||
|
amount: number
|
||||||
|
status?: $Enums.PaymentStatus
|
||||||
|
rawCallback?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
|
||||||
|
snapToken?: string | null
|
||||||
|
expiresAt?: Date | string | null
|
||||||
|
paidAt?: Date | string | null
|
||||||
|
failedAt?: Date | string | null
|
||||||
|
rejectionReason?: string | null
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
booking: Prisma.BookingCreateNestedOneWithoutPaymentsInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaymentUncheckedCreateWithoutRefundsInput = {
|
||||||
|
id?: string
|
||||||
|
bookingId: string
|
||||||
|
provider: $Enums.PaymentProvider
|
||||||
|
externalOrderId: string
|
||||||
|
externalTxId?: string | null
|
||||||
|
method?: string | null
|
||||||
|
amount: number
|
||||||
|
status?: $Enums.PaymentStatus
|
||||||
|
rawCallback?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
|
||||||
|
snapToken?: string | null
|
||||||
|
expiresAt?: Date | string | null
|
||||||
|
paidAt?: Date | string | null
|
||||||
|
failedAt?: Date | string | null
|
||||||
|
rejectionReason?: string | null
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaymentCreateOrConnectWithoutRefundsInput = {
|
||||||
|
where: Prisma.PaymentWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.PaymentCreateWithoutRefundsInput, Prisma.PaymentUncheckedCreateWithoutRefundsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaymentUpsertWithoutRefundsInput = {
|
||||||
|
update: Prisma.XOR<Prisma.PaymentUpdateWithoutRefundsInput, Prisma.PaymentUncheckedUpdateWithoutRefundsInput>
|
||||||
|
create: Prisma.XOR<Prisma.PaymentCreateWithoutRefundsInput, Prisma.PaymentUncheckedCreateWithoutRefundsInput>
|
||||||
|
where?: Prisma.PaymentWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaymentUpdateToOneWithWhereWithoutRefundsInput = {
|
||||||
|
where?: Prisma.PaymentWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.PaymentUpdateWithoutRefundsInput, Prisma.PaymentUncheckedUpdateWithoutRefundsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaymentUpdateWithoutRefundsInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
provider?: Prisma.EnumPaymentProviderFieldUpdateOperationsInput | $Enums.PaymentProvider
|
||||||
|
externalOrderId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
externalTxId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
method?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
amount?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
status?: Prisma.EnumPaymentStatusFieldUpdateOperationsInput | $Enums.PaymentStatus
|
||||||
|
rawCallback?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
|
||||||
|
snapToken?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
expiresAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
paidAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
failedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
rejectionReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
booking?: Prisma.BookingUpdateOneRequiredWithoutPaymentsNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaymentUncheckedUpdateWithoutRefundsInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
bookingId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
provider?: Prisma.EnumPaymentProviderFieldUpdateOperationsInput | $Enums.PaymentProvider
|
||||||
|
externalOrderId?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
externalTxId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
method?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
amount?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
status?: Prisma.EnumPaymentStatusFieldUpdateOperationsInput | $Enums.PaymentStatus
|
||||||
|
rawCallback?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue
|
||||||
|
snapToken?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
expiresAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
paidAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
failedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
rejectionReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
}
|
||||||
|
|
||||||
export type PaymentCreateManyBookingInput = {
|
export type PaymentCreateManyBookingInput = {
|
||||||
id?: string
|
id?: string
|
||||||
provider: $Enums.PaymentProvider
|
provider: $Enums.PaymentProvider
|
||||||
@@ -766,6 +888,7 @@ export type PaymentUpdateWithoutBookingInput = {
|
|||||||
rejectionReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
rejectionReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
refunds?: Prisma.RefundUpdateManyWithoutPaymentNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PaymentUncheckedUpdateWithoutBookingInput = {
|
export type PaymentUncheckedUpdateWithoutBookingInput = {
|
||||||
@@ -784,6 +907,7 @@ export type PaymentUncheckedUpdateWithoutBookingInput = {
|
|||||||
rejectionReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
rejectionReason?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
refunds?: Prisma.RefundUncheckedUpdateManyWithoutPaymentNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PaymentUncheckedUpdateManyWithoutBookingInput = {
|
export type PaymentUncheckedUpdateManyWithoutBookingInput = {
|
||||||
@@ -805,6 +929,35 @@ export type PaymentUncheckedUpdateManyWithoutBookingInput = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count Type PaymentCountOutputType
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type PaymentCountOutputType = {
|
||||||
|
refunds: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaymentCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
refunds?: boolean | PaymentCountOutputTypeCountRefundsArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PaymentCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type PaymentCountOutputTypeDefaultArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the PaymentCountOutputType
|
||||||
|
*/
|
||||||
|
select?: Prisma.PaymentCountOutputTypeSelect<ExtArgs> | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PaymentCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type PaymentCountOutputTypeCountRefundsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.RefundWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type PaymentSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type PaymentSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
@@ -824,6 +977,8 @@ export type PaymentSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
booking?: boolean | Prisma.BookingDefaultArgs<ExtArgs>
|
booking?: boolean | Prisma.BookingDefaultArgs<ExtArgs>
|
||||||
|
refunds?: boolean | Prisma.Payment$refundsArgs<ExtArgs>
|
||||||
|
_count?: boolean | Prisma.PaymentCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["payment"]>
|
}, ExtArgs["result"]["payment"]>
|
||||||
|
|
||||||
export type PaymentSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type PaymentSelectCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
@@ -888,6 +1043,8 @@ export type PaymentSelectScalar = {
|
|||||||
export type PaymentOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "bookingId" | "provider" | "externalOrderId" | "externalTxId" | "method" | "amount" | "status" | "rawCallback" | "snapToken" | "expiresAt" | "paidAt" | "failedAt" | "rejectionReason" | "createdAt" | "updatedAt", ExtArgs["result"]["payment"]>
|
export type PaymentOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "bookingId" | "provider" | "externalOrderId" | "externalTxId" | "method" | "amount" | "status" | "rawCallback" | "snapToken" | "expiresAt" | "paidAt" | "failedAt" | "rejectionReason" | "createdAt" | "updatedAt", ExtArgs["result"]["payment"]>
|
||||||
export type PaymentInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type PaymentInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
booking?: boolean | Prisma.BookingDefaultArgs<ExtArgs>
|
booking?: boolean | Prisma.BookingDefaultArgs<ExtArgs>
|
||||||
|
refunds?: boolean | Prisma.Payment$refundsArgs<ExtArgs>
|
||||||
|
_count?: boolean | Prisma.PaymentCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type PaymentIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type PaymentIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
booking?: boolean | Prisma.BookingDefaultArgs<ExtArgs>
|
booking?: boolean | Prisma.BookingDefaultArgs<ExtArgs>
|
||||||
@@ -900,6 +1057,7 @@ export type $PaymentPayload<ExtArgs extends runtime.Types.Extensions.InternalArg
|
|||||||
name: "Payment"
|
name: "Payment"
|
||||||
objects: {
|
objects: {
|
||||||
booking: Prisma.$BookingPayload<ExtArgs>
|
booking: Prisma.$BookingPayload<ExtArgs>
|
||||||
|
refunds: Prisma.$RefundPayload<ExtArgs>[]
|
||||||
}
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
id: string
|
||||||
@@ -1332,6 +1490,7 @@ readonly fields: PaymentFieldRefs;
|
|||||||
export interface Prisma__PaymentClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
export interface Prisma__PaymentClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
||||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||||
booking<T extends Prisma.BookingDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.BookingDefaultArgs<ExtArgs>>): Prisma.Prisma__BookingClient<runtime.Types.Result.GetResult<Prisma.$BookingPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
booking<T extends Prisma.BookingDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.BookingDefaultArgs<ExtArgs>>): Prisma.Prisma__BookingClient<runtime.Types.Result.GetResult<Prisma.$BookingPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
|
||||||
|
refunds<T extends Prisma.Payment$refundsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Payment$refundsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RefundPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
/**
|
/**
|
||||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||||
@@ -1777,6 +1936,30 @@ export type PaymentDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Inter
|
|||||||
limit?: number
|
limit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payment.refunds
|
||||||
|
*/
|
||||||
|
export type Payment$refundsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the Refund
|
||||||
|
*/
|
||||||
|
select?: Prisma.RefundSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the Refund
|
||||||
|
*/
|
||||||
|
omit?: Prisma.RefundOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.RefundInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.RefundWhereInput
|
||||||
|
orderBy?: Prisma.RefundOrderByWithRelationInput | Prisma.RefundOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.RefundWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.RefundScalarFieldEnum | Prisma.RefundScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Payment without action
|
* Payment without action
|
||||||
*/
|
*/
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -229,6 +229,7 @@ export type UserWhereInput = {
|
|||||||
bookings?: Prisma.BookingListRelationFilter
|
bookings?: Prisma.BookingListRelationFilter
|
||||||
organizerVerification?: Prisma.XOR<Prisma.OrganizerVerificationNullableScalarRelationFilter, Prisma.OrganizerVerificationWhereInput> | null
|
organizerVerification?: Prisma.XOR<Prisma.OrganizerVerificationNullableScalarRelationFilter, Prisma.OrganizerVerificationWhereInput> | null
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationListRelationFilter
|
reviewedVerifications?: Prisma.OrganizerVerificationListRelationFilter
|
||||||
|
reviewedRefunds?: Prisma.RefundListRelationFilter
|
||||||
profile?: Prisma.XOR<Prisma.UserProfileNullableScalarRelationFilter, Prisma.UserProfileWhereInput> | null
|
profile?: Prisma.XOR<Prisma.UserProfileNullableScalarRelationFilter, Prisma.UserProfileWhereInput> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +251,7 @@ export type UserOrderByWithRelationInput = {
|
|||||||
bookings?: Prisma.BookingOrderByRelationAggregateInput
|
bookings?: Prisma.BookingOrderByRelationAggregateInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationOrderByWithRelationInput
|
organizerVerification?: Prisma.OrganizerVerificationOrderByWithRelationInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationOrderByRelationAggregateInput
|
reviewedVerifications?: Prisma.OrganizerVerificationOrderByRelationAggregateInput
|
||||||
|
reviewedRefunds?: Prisma.RefundOrderByRelationAggregateInput
|
||||||
profile?: Prisma.UserProfileOrderByWithRelationInput
|
profile?: Prisma.UserProfileOrderByWithRelationInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +276,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
bookings?: Prisma.BookingListRelationFilter
|
bookings?: Prisma.BookingListRelationFilter
|
||||||
organizerVerification?: Prisma.XOR<Prisma.OrganizerVerificationNullableScalarRelationFilter, Prisma.OrganizerVerificationWhereInput> | null
|
organizerVerification?: Prisma.XOR<Prisma.OrganizerVerificationNullableScalarRelationFilter, Prisma.OrganizerVerificationWhereInput> | null
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationListRelationFilter
|
reviewedVerifications?: Prisma.OrganizerVerificationListRelationFilter
|
||||||
|
reviewedRefunds?: Prisma.RefundListRelationFilter
|
||||||
profile?: Prisma.XOR<Prisma.UserProfileNullableScalarRelationFilter, Prisma.UserProfileWhereInput> | null
|
profile?: Prisma.XOR<Prisma.UserProfileNullableScalarRelationFilter, Prisma.UserProfileWhereInput> | null
|
||||||
}, "id" | "email">
|
}, "id" | "email">
|
||||||
|
|
||||||
@@ -327,6 +330,7 @@ export type UserCreateInput = {
|
|||||||
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
||||||
|
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
|
||||||
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,6 +352,7 @@ export type UserUncheckedCreateInput = {
|
|||||||
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,6 +374,7 @@ export type UserUpdateInput = {
|
|||||||
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
|
||||||
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,6 +396,7 @@ export type UserUncheckedUpdateInput = {
|
|||||||
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,6 +622,22 @@ export type UserUpdateOneRequiredWithoutBookingsNestedInput = {
|
|||||||
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutBookingsInput, Prisma.UserUpdateWithoutBookingsInput>, Prisma.UserUncheckedUpdateWithoutBookingsInput>
|
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutBookingsInput, Prisma.UserUpdateWithoutBookingsInput>, Prisma.UserUncheckedUpdateWithoutBookingsInput>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserCreateNestedOneWithoutReviewedRefundsInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.UserCreateWithoutReviewedRefundsInput, Prisma.UserUncheckedCreateWithoutReviewedRefundsInput>
|
||||||
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutReviewedRefundsInput
|
||||||
|
connect?: Prisma.UserWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateOneWithoutReviewedRefundsNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.UserCreateWithoutReviewedRefundsInput, Prisma.UserUncheckedCreateWithoutReviewedRefundsInput>
|
||||||
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutReviewedRefundsInput
|
||||||
|
upsert?: Prisma.UserUpsertWithoutReviewedRefundsInput
|
||||||
|
disconnect?: Prisma.UserWhereInput | boolean
|
||||||
|
delete?: Prisma.UserWhereInput | boolean
|
||||||
|
connect?: Prisma.UserWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutReviewedRefundsInput, Prisma.UserUpdateWithoutReviewedRefundsInput>, Prisma.UserUncheckedUpdateWithoutReviewedRefundsInput>
|
||||||
|
}
|
||||||
|
|
||||||
export type UserCreateWithoutProfileInput = {
|
export type UserCreateWithoutProfileInput = {
|
||||||
id?: string
|
id?: string
|
||||||
name: string
|
name: string
|
||||||
@@ -633,6 +656,7 @@ export type UserCreateWithoutProfileInput = {
|
|||||||
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
||||||
|
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedCreateWithoutProfileInput = {
|
export type UserUncheckedCreateWithoutProfileInput = {
|
||||||
@@ -653,6 +677,7 @@ export type UserUncheckedCreateWithoutProfileInput = {
|
|||||||
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCreateOrConnectWithoutProfileInput = {
|
export type UserCreateOrConnectWithoutProfileInput = {
|
||||||
@@ -689,6 +714,7 @@ export type UserUpdateWithoutProfileInput = {
|
|||||||
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedUpdateWithoutProfileInput = {
|
export type UserUncheckedUpdateWithoutProfileInput = {
|
||||||
@@ -709,6 +735,7 @@ export type UserUncheckedUpdateWithoutProfileInput = {
|
|||||||
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCreateWithoutAccountsInput = {
|
export type UserCreateWithoutAccountsInput = {
|
||||||
@@ -728,6 +755,7 @@ export type UserCreateWithoutAccountsInput = {
|
|||||||
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
||||||
|
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
|
||||||
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,6 +776,7 @@ export type UserUncheckedCreateWithoutAccountsInput = {
|
|||||||
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,6 +813,7 @@ export type UserUpdateWithoutAccountsInput = {
|
|||||||
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
|
||||||
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,6 +834,7 @@ export type UserUncheckedUpdateWithoutAccountsInput = {
|
|||||||
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -824,6 +855,7 @@ export type UserCreateWithoutOrganizerVerificationInput = {
|
|||||||
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
||||||
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
||||||
|
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
|
||||||
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -844,6 +876,7 @@ export type UserUncheckedCreateWithoutOrganizerVerificationInput = {
|
|||||||
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
||||||
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -869,6 +902,7 @@ export type UserCreateWithoutReviewedVerificationsInput = {
|
|||||||
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
||||||
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
||||||
|
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
|
||||||
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -889,6 +923,7 @@ export type UserUncheckedCreateWithoutReviewedVerificationsInput = {
|
|||||||
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
||||||
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -925,6 +960,7 @@ export type UserUpdateWithoutOrganizerVerificationInput = {
|
|||||||
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
||||||
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
|
||||||
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,6 +981,7 @@ export type UserUncheckedUpdateWithoutOrganizerVerificationInput = {
|
|||||||
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
||||||
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -976,6 +1013,7 @@ export type UserUpdateWithoutReviewedVerificationsInput = {
|
|||||||
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
||||||
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
|
||||||
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -996,6 +1034,7 @@ export type UserUncheckedUpdateWithoutReviewedVerificationsInput = {
|
|||||||
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
||||||
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1016,6 +1055,7 @@ export type UserCreateWithoutTripsInput = {
|
|||||||
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
||||||
|
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
|
||||||
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1036,6 +1076,7 @@ export type UserUncheckedCreateWithoutTripsInput = {
|
|||||||
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1072,6 +1113,7 @@ export type UserUpdateWithoutTripsInput = {
|
|||||||
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
|
||||||
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1092,6 +1134,7 @@ export type UserUncheckedUpdateWithoutTripsInput = {
|
|||||||
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1112,6 +1155,7 @@ export type UserCreateWithoutTripReviewsInput = {
|
|||||||
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
||||||
|
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
|
||||||
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1132,6 +1176,7 @@ export type UserUncheckedCreateWithoutTripReviewsInput = {
|
|||||||
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1168,6 +1213,7 @@ export type UserUpdateWithoutTripReviewsInput = {
|
|||||||
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
|
||||||
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1188,6 +1234,7 @@ export type UserUncheckedUpdateWithoutTripReviewsInput = {
|
|||||||
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1208,6 +1255,7 @@ export type UserCreateWithoutParticipationsInput = {
|
|||||||
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
||||||
|
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
|
||||||
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1228,6 +1276,7 @@ export type UserUncheckedCreateWithoutParticipationsInput = {
|
|||||||
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1264,6 +1313,7 @@ export type UserUpdateWithoutParticipationsInput = {
|
|||||||
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
|
||||||
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1284,6 +1334,7 @@ export type UserUncheckedUpdateWithoutParticipationsInput = {
|
|||||||
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1304,6 +1355,7 @@ export type UserCreateWithoutBookingsInput = {
|
|||||||
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
||||||
|
reviewedRefunds?: Prisma.RefundCreateNestedManyWithoutReviewedByInput
|
||||||
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1324,6 +1376,7 @@ export type UserUncheckedCreateWithoutBookingsInput = {
|
|||||||
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1360,6 +1413,7 @@ export type UserUpdateWithoutBookingsInput = {
|
|||||||
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUpdateManyWithoutReviewedByNestedInput
|
||||||
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1380,6 +1434,107 @@ export type UserUncheckedUpdateWithoutBookingsInput = {
|
|||||||
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
||||||
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
||||||
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
|
reviewedRefunds?: Prisma.RefundUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
|
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateWithoutReviewedRefundsInput = {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password?: string | null
|
||||||
|
image?: string | null
|
||||||
|
emailVerified?: Date | string | null
|
||||||
|
acceptedTermsAndPrivacy?: boolean
|
||||||
|
acceptedAt?: Date | string | null
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
accounts?: Prisma.AccountCreateNestedManyWithoutUserInput
|
||||||
|
trips?: Prisma.TripCreateNestedManyWithoutOrganizerInput
|
||||||
|
participations?: Prisma.TripParticipantCreateNestedManyWithoutUserInput
|
||||||
|
tripReviews?: Prisma.TripReviewCreateNestedManyWithoutUserInput
|
||||||
|
bookings?: Prisma.BookingCreateNestedManyWithoutUserInput
|
||||||
|
organizerVerification?: Prisma.OrganizerVerificationCreateNestedOneWithoutUserInput
|
||||||
|
reviewedVerifications?: Prisma.OrganizerVerificationCreateNestedManyWithoutReviewedByInput
|
||||||
|
profile?: Prisma.UserProfileCreateNestedOneWithoutUserInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUncheckedCreateWithoutReviewedRefundsInput = {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password?: string | null
|
||||||
|
image?: string | null
|
||||||
|
emailVerified?: Date | string | null
|
||||||
|
acceptedTermsAndPrivacy?: boolean
|
||||||
|
acceptedAt?: Date | string | null
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
accounts?: Prisma.AccountUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
trips?: Prisma.TripUncheckedCreateNestedManyWithoutOrganizerInput
|
||||||
|
participations?: Prisma.TripParticipantUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
tripReviews?: Prisma.TripReviewUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
bookings?: Prisma.BookingUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedCreateNestedOneWithoutUserInput
|
||||||
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedCreateNestedManyWithoutReviewedByInput
|
||||||
|
profile?: Prisma.UserProfileUncheckedCreateNestedOneWithoutUserInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateOrConnectWithoutReviewedRefundsInput = {
|
||||||
|
where: Prisma.UserWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.UserCreateWithoutReviewedRefundsInput, Prisma.UserUncheckedCreateWithoutReviewedRefundsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpsertWithoutReviewedRefundsInput = {
|
||||||
|
update: Prisma.XOR<Prisma.UserUpdateWithoutReviewedRefundsInput, Prisma.UserUncheckedUpdateWithoutReviewedRefundsInput>
|
||||||
|
create: Prisma.XOR<Prisma.UserCreateWithoutReviewedRefundsInput, Prisma.UserUncheckedCreateWithoutReviewedRefundsInput>
|
||||||
|
where?: Prisma.UserWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateToOneWithWhereWithoutReviewedRefundsInput = {
|
||||||
|
where?: Prisma.UserWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.UserUpdateWithoutReviewedRefundsInput, Prisma.UserUncheckedUpdateWithoutReviewedRefundsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateWithoutReviewedRefundsInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
accounts?: Prisma.AccountUpdateManyWithoutUserNestedInput
|
||||||
|
trips?: Prisma.TripUpdateManyWithoutOrganizerNestedInput
|
||||||
|
participations?: Prisma.TripParticipantUpdateManyWithoutUserNestedInput
|
||||||
|
tripReviews?: Prisma.TripReviewUpdateManyWithoutUserNestedInput
|
||||||
|
bookings?: Prisma.BookingUpdateManyWithoutUserNestedInput
|
||||||
|
organizerVerification?: Prisma.OrganizerVerificationUpdateOneWithoutUserNestedInput
|
||||||
|
reviewedVerifications?: Prisma.OrganizerVerificationUpdateManyWithoutReviewedByNestedInput
|
||||||
|
profile?: Prisma.UserProfileUpdateOneWithoutUserNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUncheckedUpdateWithoutReviewedRefundsInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
password?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
image?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||||
|
emailVerified?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
acceptedTermsAndPrivacy?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
acceptedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
accounts?: Prisma.AccountUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
trips?: Prisma.TripUncheckedUpdateManyWithoutOrganizerNestedInput
|
||||||
|
participations?: Prisma.TripParticipantUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
tripReviews?: Prisma.TripReviewUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
bookings?: Prisma.BookingUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
organizerVerification?: Prisma.OrganizerVerificationUncheckedUpdateOneWithoutUserNestedInput
|
||||||
|
reviewedVerifications?: Prisma.OrganizerVerificationUncheckedUpdateManyWithoutReviewedByNestedInput
|
||||||
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
profile?: Prisma.UserProfileUncheckedUpdateOneWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1395,6 +1550,7 @@ export type UserCountOutputType = {
|
|||||||
tripReviews: number
|
tripReviews: number
|
||||||
bookings: number
|
bookings: number
|
||||||
reviewedVerifications: number
|
reviewedVerifications: number
|
||||||
|
reviewedRefunds: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
@@ -1404,6 +1560,7 @@ export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.I
|
|||||||
tripReviews?: boolean | UserCountOutputTypeCountTripReviewsArgs
|
tripReviews?: boolean | UserCountOutputTypeCountTripReviewsArgs
|
||||||
bookings?: boolean | UserCountOutputTypeCountBookingsArgs
|
bookings?: boolean | UserCountOutputTypeCountBookingsArgs
|
||||||
reviewedVerifications?: boolean | UserCountOutputTypeCountReviewedVerificationsArgs
|
reviewedVerifications?: boolean | UserCountOutputTypeCountReviewedVerificationsArgs
|
||||||
|
reviewedRefunds?: boolean | UserCountOutputTypeCountReviewedRefundsArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1458,6 +1615,13 @@ export type UserCountOutputTypeCountReviewedVerificationsArgs<ExtArgs extends ru
|
|||||||
where?: Prisma.OrganizerVerificationWhereInput
|
where?: Prisma.OrganizerVerificationWhereInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type UserCountOutputTypeCountReviewedRefundsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.RefundWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
@@ -1477,6 +1641,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
|
|||||||
bookings?: boolean | Prisma.User$bookingsArgs<ExtArgs>
|
bookings?: boolean | Prisma.User$bookingsArgs<ExtArgs>
|
||||||
organizerVerification?: boolean | Prisma.User$organizerVerificationArgs<ExtArgs>
|
organizerVerification?: boolean | Prisma.User$organizerVerificationArgs<ExtArgs>
|
||||||
reviewedVerifications?: boolean | Prisma.User$reviewedVerificationsArgs<ExtArgs>
|
reviewedVerifications?: boolean | Prisma.User$reviewedVerificationsArgs<ExtArgs>
|
||||||
|
reviewedRefunds?: boolean | Prisma.User$reviewedRefundsArgs<ExtArgs>
|
||||||
profile?: boolean | Prisma.User$profileArgs<ExtArgs>
|
profile?: boolean | Prisma.User$profileArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["user"]>
|
}, ExtArgs["result"]["user"]>
|
||||||
@@ -1529,6 +1694,7 @@ export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
bookings?: boolean | Prisma.User$bookingsArgs<ExtArgs>
|
bookings?: boolean | Prisma.User$bookingsArgs<ExtArgs>
|
||||||
organizerVerification?: boolean | Prisma.User$organizerVerificationArgs<ExtArgs>
|
organizerVerification?: boolean | Prisma.User$organizerVerificationArgs<ExtArgs>
|
||||||
reviewedVerifications?: boolean | Prisma.User$reviewedVerificationsArgs<ExtArgs>
|
reviewedVerifications?: boolean | Prisma.User$reviewedVerificationsArgs<ExtArgs>
|
||||||
|
reviewedRefunds?: boolean | Prisma.User$reviewedRefundsArgs<ExtArgs>
|
||||||
profile?: boolean | Prisma.User$profileArgs<ExtArgs>
|
profile?: boolean | Prisma.User$profileArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
@@ -1545,6 +1711,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
bookings: Prisma.$BookingPayload<ExtArgs>[]
|
bookings: Prisma.$BookingPayload<ExtArgs>[]
|
||||||
organizerVerification: Prisma.$OrganizerVerificationPayload<ExtArgs> | null
|
organizerVerification: Prisma.$OrganizerVerificationPayload<ExtArgs> | null
|
||||||
reviewedVerifications: Prisma.$OrganizerVerificationPayload<ExtArgs>[]
|
reviewedVerifications: Prisma.$OrganizerVerificationPayload<ExtArgs>[]
|
||||||
|
reviewedRefunds: Prisma.$RefundPayload<ExtArgs>[]
|
||||||
profile: Prisma.$UserProfilePayload<ExtArgs> | null
|
profile: Prisma.$UserProfilePayload<ExtArgs> | null
|
||||||
}
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
@@ -1971,6 +2138,7 @@ export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Typ
|
|||||||
bookings<T extends Prisma.User$bookingsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$bookingsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$BookingPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
bookings<T extends Prisma.User$bookingsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$bookingsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$BookingPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
organizerVerification<T extends Prisma.User$organizerVerificationArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$organizerVerificationArgs<ExtArgs>>): Prisma.Prisma__OrganizerVerificationClient<runtime.Types.Result.GetResult<Prisma.$OrganizerVerificationPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
organizerVerification<T extends Prisma.User$organizerVerificationArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$organizerVerificationArgs<ExtArgs>>): Prisma.Prisma__OrganizerVerificationClient<runtime.Types.Result.GetResult<Prisma.$OrganizerVerificationPayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||||
reviewedVerifications<T extends Prisma.User$reviewedVerificationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$reviewedVerificationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$OrganizerVerificationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
reviewedVerifications<T extends Prisma.User$reviewedVerificationsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$reviewedVerificationsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$OrganizerVerificationPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
|
reviewedRefunds<T extends Prisma.User$reviewedRefundsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$reviewedRefundsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RefundPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
profile<T extends Prisma.User$profileArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$profileArgs<ExtArgs>>): Prisma.Prisma__UserProfileClient<runtime.Types.Result.GetResult<Prisma.$UserProfilePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
profile<T extends Prisma.User$profileArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$profileArgs<ExtArgs>>): Prisma.Prisma__UserProfileClient<runtime.Types.Result.GetResult<Prisma.$UserProfilePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions>
|
||||||
/**
|
/**
|
||||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
@@ -2566,6 +2734,30 @@ export type User$reviewedVerificationsArgs<ExtArgs extends runtime.Types.Extensi
|
|||||||
distinct?: Prisma.OrganizerVerificationScalarFieldEnum | Prisma.OrganizerVerificationScalarFieldEnum[]
|
distinct?: Prisma.OrganizerVerificationScalarFieldEnum | Prisma.OrganizerVerificationScalarFieldEnum[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User.reviewedRefunds
|
||||||
|
*/
|
||||||
|
export type User$reviewedRefundsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the Refund
|
||||||
|
*/
|
||||||
|
select?: Prisma.RefundSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the Refund
|
||||||
|
*/
|
||||||
|
omit?: Prisma.RefundOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.RefundInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.RefundWhereInput
|
||||||
|
orderBy?: Prisma.RefundOrderByWithRelationInput | Prisma.RefundOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.RefundWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.RefundScalarFieldEnum | Prisma.RefundScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User.profile
|
* User.profile
|
||||||
*/
|
*/
|
||||||
|
|||||||
+88
-4
@@ -6,17 +6,21 @@ import Image from "next/image";
|
|||||||
import { authOptions } from "@/lib/auth";
|
import { authOptions } from "@/lib/auth";
|
||||||
import { tripService } from "@/server/services/trip.service";
|
import { tripService } from "@/server/services/trip.service";
|
||||||
import { bookingService } from "@/server/services/booking.service";
|
import { bookingService } from "@/server/services/booking.service";
|
||||||
|
import { bookingRepo } from "@/server/repositories/booking.repo";
|
||||||
import { trustService } from "@/server/services/trust.service";
|
import { trustService } from "@/server/services/trust.service";
|
||||||
import { formatRupiah } from "@/lib/utils";
|
import { formatRupiah } from "@/lib/utils";
|
||||||
import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates";
|
import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates";
|
||||||
import { siteConfig, siteUrl, absoluteUrl } from "@/lib/site";
|
import { siteConfig, siteUrl, absoluteUrl } from "@/lib/site";
|
||||||
import { JoinTripButton } from "@/features/trip/components/join-trip-button";
|
import { JoinTripButton } from "@/features/trip/components/join-trip-button";
|
||||||
|
import { CancelTripButton } from "@/features/trip/components/cancel-trip-button";
|
||||||
|
import { CancelBookingButton } from "@/features/booking/components/cancel-booking-button";
|
||||||
import { OrganizerJoinRequests } from "@/features/trip/components/organizer-join-requests";
|
import { OrganizerJoinRequests } from "@/features/trip/components/organizer-join-requests";
|
||||||
import { OrganizerTrustPanel } from "@/features/trip/components/organizer-trust-panel";
|
import { OrganizerTrustPanel } from "@/features/trip/components/organizer-trust-panel";
|
||||||
import { TripProgramBlock } from "@/features/trip/components/trip-program-block";
|
import { TripProgramBlock } from "@/features/trip/components/trip-program-block";
|
||||||
import { OrganizerPaymentQueue } from "@/features/booking/components/organizer-payment-queue";
|
import { OrganizerPaymentQueue } from "@/features/booking/components/organizer-payment-queue";
|
||||||
import { ImageGallery } from "@/features/trip/components/image-gallery";
|
import { ImageGallery } from "@/features/trip/components/image-gallery";
|
||||||
import { TripReviewSection } from "@/features/review/components/trip-review-section";
|
import { TripReviewSection } from "@/features/review/components/trip-review-section";
|
||||||
|
import { RefundPolicySection } from "@/features/refund/components/refund-policy-section";
|
||||||
import { categoryMeta } from "@/lib/activity-category";
|
import { categoryMeta } from "@/lib/activity-category";
|
||||||
import { vibeMeta } from "@/lib/vibe";
|
import { vibeMeta } from "@/lib/vibe";
|
||||||
import { isFreeTrip } from "@/lib/trip-pricing";
|
import { isFreeTrip } from "@/lib/trip-pricing";
|
||||||
@@ -24,6 +28,7 @@ import {
|
|||||||
isPastTripLastDayForReview,
|
isPastTripLastDayForReview,
|
||||||
isTripDepartureDayPast,
|
isTripDepartureDayPast,
|
||||||
} from "@/lib/trip-dates";
|
} from "@/lib/trip-dates";
|
||||||
|
import { previewRefund } from "@/lib/refund-policy";
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params,
|
params,
|
||||||
@@ -137,6 +142,31 @@ export default async function TripDetailPage({
|
|||||||
? await bookingService.getAwaitingManualForTrip(trip.id)
|
? await bookingService.getAwaitingManualForTrip(trip.id)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
// Booking peserta saat ini — dipakai untuk render CancelBookingButton vs
|
||||||
|
// tombol "Batal Ikut" biasa. Hanya untuk non-organizer yang ikut trip.
|
||||||
|
const myBooking =
|
||||||
|
session?.user && !isOrganizer && currentParticipation
|
||||||
|
? await bookingService.getByTripAndUser(trip.id, session.user.id)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Untuk CancelTripButton: jumlah booking PAID/PARTIALLY_REFUNDED (yang akan
|
||||||
|
// auto-refund). Hanya dihitung saat organizer mengakses trip yang masih
|
||||||
|
// bisa dibatalkan.
|
||||||
|
const canOrganizerCancel =
|
||||||
|
isOrganizer &&
|
||||||
|
(trip.status === "OPEN" || trip.status === "FULL") &&
|
||||||
|
!isDeparturePast;
|
||||||
|
const paidBookingCount = canOrganizerCancel
|
||||||
|
? await bookingRepo.countSettledForTrip(trip.id)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Preview refund untuk CancelBookingButton (server-side supaya konsisten
|
||||||
|
// dengan service yang juga pakai policy yang sama).
|
||||||
|
const refundPreview =
|
||||||
|
myBooking && myBooking.status === "PAID" && !isDeparturePast
|
||||||
|
? previewRefund(myBooking.amount, trip.date)
|
||||||
|
: null;
|
||||||
|
|
||||||
const catMeta = categoryMeta(trip.category);
|
const catMeta = categoryMeta(trip.category);
|
||||||
|
|
||||||
const tripUrl = absoluteUrl(`/trips/${trip.id}`);
|
const tripUrl = absoluteUrl(`/trips/${trip.id}`);
|
||||||
@@ -344,10 +374,22 @@ export default async function TripDetailPage({
|
|||||||
|
|
||||||
{/* Participant Progress */}
|
{/* Participant Progress */}
|
||||||
<div className="rounded-xl border border-neutral-200 p-3 sm:p-4">
|
<div className="rounded-xl border border-neutral-200 p-3 sm:p-4">
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between gap-2">
|
||||||
<span className="text-xs font-semibold text-neutral-700 sm:text-sm">
|
<div className="flex items-center gap-2">
|
||||||
Peserta
|
<span className="text-xs font-semibold text-neutral-700 sm:text-sm">
|
||||||
</span>
|
Peserta
|
||||||
|
</span>
|
||||||
|
{spotsLeft > 0 && spotsLeft <= 3 && (
|
||||||
|
<span className="rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-bold text-amber-800 sm:text-[11px]">
|
||||||
|
⚡ Tinggal {spotsLeft} spot!
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{spotsLeft <= 0 && (
|
||||||
|
<span className="rounded-full bg-neutral-200 px-2 py-0.5 text-[10px] font-bold text-neutral-700 sm:text-[11px]">
|
||||||
|
Penuh
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<span className="text-xs font-bold text-neutral-800 sm:text-sm">
|
<span className="text-xs font-bold text-neutral-800 sm:text-sm">
|
||||||
{participantCount}{" "}
|
{participantCount}{" "}
|
||||||
<span className="font-normal text-neutral-400">
|
<span className="font-normal text-neutral-400">
|
||||||
@@ -383,6 +425,23 @@ export default async function TripDetailPage({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
{confirmedCount > 0 && (
|
||||||
|
<p className="mt-2 text-[11px] text-neutral-600 sm:text-xs">
|
||||||
|
<span aria-hidden>👥</span> Sudah join:{" "}
|
||||||
|
<span className="font-medium text-neutral-800">
|
||||||
|
{confirmedParticipants
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((p) => p.user.name.split(" ")[0])
|
||||||
|
.join(", ")}
|
||||||
|
</span>
|
||||||
|
{confirmedCount > 3 && (
|
||||||
|
<span className="text-neutral-500">
|
||||||
|
{" "}
|
||||||
|
+{confirmedCount - 3} lainnya
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TripProgramBlock
|
<TripProgramBlock
|
||||||
@@ -451,8 +510,33 @@ export default async function TripDetailPage({
|
|||||||
isFull={spotsLeft <= 0}
|
isFull={spotsLeft <= 0}
|
||||||
tripStatus={trip.status}
|
tripStatus={trip.status}
|
||||||
isDeparturePast={isDeparturePast}
|
isDeparturePast={isDeparturePast}
|
||||||
|
hideCancelButton={!!refundPreview}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Peserta PAID: cancel + request refund (lewat policy default). */}
|
||||||
|
{refundPreview && (
|
||||||
|
<CancelBookingButton
|
||||||
|
tripId={trip.id}
|
||||||
|
preview={{
|
||||||
|
days: refundPreview.days,
|
||||||
|
refundAmount: refundPreview.refundAmount,
|
||||||
|
bookingAmount: refundPreview.bookingAmount,
|
||||||
|
tierLabel: refundPreview.tier.label,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Organizer: batalkan trip (auto-refund peserta PAID). */}
|
||||||
|
{canOrganizerCancel && (
|
||||||
|
<CancelTripButton
|
||||||
|
tripId={trip.id}
|
||||||
|
paidParticipantCount={paidBookingCount}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Kebijakan refund — transparency sebelum user cancel. */}
|
||||||
|
{!tripIsFree && <RefundPolicySection />}
|
||||||
|
|
||||||
<TripReviewSection
|
<TripReviewSection
|
||||||
tripId={trip.id}
|
tripId={trip.id}
|
||||||
reviews={trip.reviews.map((r) => ({
|
reviews={trip.reviews.map((r) => ({
|
||||||
|
|||||||
@@ -158,7 +158,14 @@ function FreeTripSection({
|
|||||||
bookingStatus,
|
bookingStatus,
|
||||||
}: {
|
}: {
|
||||||
tripId: string;
|
tripId: string;
|
||||||
bookingStatus: "PENDING" | "AWAITING_PAY" | "PAID" | "CANCELLED" | "REFUNDED" | "EXPIRED";
|
bookingStatus:
|
||||||
|
| "PENDING"
|
||||||
|
| "AWAITING_PAY"
|
||||||
|
| "PAID"
|
||||||
|
| "CANCELLED"
|
||||||
|
| "REFUNDED"
|
||||||
|
| "PARTIALLY_REFUNDED"
|
||||||
|
| "EXPIRED";
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section className="rounded-2xl border border-emerald-200 bg-emerald-50/60 p-6 text-center shadow-sm sm:p-8">
|
<section className="rounded-2xl border border-emerald-200 bg-emerald-50/60 p-6 text-center shadow-sm sm:p-8">
|
||||||
@@ -208,7 +215,14 @@ async function PaidTripSection({
|
|||||||
organizerId: string;
|
organizerId: string;
|
||||||
organizerName: string;
|
organizerName: string;
|
||||||
price: number;
|
price: number;
|
||||||
bookingStatus: "PENDING" | "AWAITING_PAY" | "PAID" | "CANCELLED" | "REFUNDED" | "EXPIRED";
|
bookingStatus:
|
||||||
|
| "PENDING"
|
||||||
|
| "AWAITING_PAY"
|
||||||
|
| "PAID"
|
||||||
|
| "CANCELLED"
|
||||||
|
| "REFUNDED"
|
||||||
|
| "PARTIALLY_REFUNDED"
|
||||||
|
| "EXPIRED";
|
||||||
paymentMarkedAt: Date | null;
|
paymentMarkedAt: Date | null;
|
||||||
paymentPaidAt: Date | null;
|
paymentPaidAt: Date | null;
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ import Link from "next/link";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { profileService } from "@/server/services/profile.service";
|
import { profileService } from "@/server/services/profile.service";
|
||||||
|
import { trustService } from "@/server/services/trust.service";
|
||||||
|
import { reviewService } from "@/server/services/review.service";
|
||||||
import { TripCard } from "@/features/trip/components/trip-card";
|
import { TripCard } from "@/features/trip/components/trip-card";
|
||||||
import { ProfileTripRow } from "@/features/profile/components/profile-trip-row";
|
import { ProfileTripRow } from "@/features/profile/components/profile-trip-row";
|
||||||
|
import { OrganizerStatsPanel } from "@/features/profile/components/organizer-stats-panel";
|
||||||
|
import { OrganizerReviewsList } from "@/features/review/components/organizer-reviews-list";
|
||||||
import { siteConfig } from "@/lib/site";
|
import { siteConfig } from "@/lib/site";
|
||||||
import { vibeMeta } from "@/lib/vibe";
|
import { vibeMeta } from "@/lib/vibe";
|
||||||
|
|
||||||
@@ -45,6 +49,16 @@ export default async function PublicProfilePage({ params }: PageProps) {
|
|||||||
year: "numeric",
|
year: "numeric",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Trust panel hanya relevan untuk user yang berperan organizer.
|
||||||
|
// Hindari query Prisma yang nggak perlu untuk user yang murni peserta.
|
||||||
|
const isOrganizerProfile = organizedTrips.length > 0 || isVerifiedOrganizer;
|
||||||
|
const [organizerTrust, organizerReviews] = isOrganizerProfile
|
||||||
|
? await Promise.all([
|
||||||
|
trustService.getOrganizerTrust(user.id),
|
||||||
|
reviewService.getReviewsByOrganizer(user.id),
|
||||||
|
])
|
||||||
|
: [null, []];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-4xl px-4 py-6 sm:py-8">
|
<div className="mx-auto max-w-4xl px-4 py-6 sm:py-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -156,6 +170,15 @@ export default async function PublicProfilePage({ params }: PageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{organizerTrust && <OrganizerStatsPanel trust={organizerTrust} />}
|
||||||
|
|
||||||
|
{organizerTrust && organizerReviews.length > 0 && (
|
||||||
|
<OrganizerReviewsList
|
||||||
|
reviews={organizerReviews}
|
||||||
|
totalCount={organizerTrust.reviewCount}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Empty profile hint */}
|
{/* Empty profile hint */}
|
||||||
{!profile && (
|
{!profile && (
|
||||||
<p className="mt-5 rounded-xl border border-dashed border-neutral-200 bg-neutral-50 px-4 py-3 text-center text-xs text-neutral-500">
|
<p className="mt-5 rounded-xl border border-dashed border-neutral-200 bg-neutral-50 px-4 py-3 text-center text-xs text-neutral-500">
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Cron Setup (PM2 / self-hosted Linux)
|
||||||
|
|
||||||
|
Aplikasi punya beberapa endpoint cron yang harus di-trigger periodik dari luar. Karena deploy pakai PM2 di VPS Linux (bukan Vercel), trigger schedule pakai **system crontab** — zero dependency, OS-native.
|
||||||
|
|
||||||
|
## Daftar cron job
|
||||||
|
|
||||||
|
| Endpoint | Schedule | Tujuan |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET /api/cron/auto-complete-trips` | `0 18 * * *` (18:00 UTC = 01:00 WIB) | Flip trip yang sudah lewat tanggal selesai dari `OPEN`/`FULL` ke `COMPLETED`. |
|
||||||
|
|
||||||
|
## Setup di server
|
||||||
|
|
||||||
|
### 1. Set `CRON_SECRET` di env production
|
||||||
|
|
||||||
|
Generate random secret 32 byte:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl rand -hex 32
|
||||||
|
```
|
||||||
|
|
||||||
|
Tambah ke file `.env` yang dibaca PM2 (atau yang pasti ter-load saat process boot):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CRON_SECRET="<hasil-openssl-tadi>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart PM2 supaya proses re-load env:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 restart setrip --update-env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Daftarkan crontab
|
||||||
|
|
||||||
|
Edit crontab user yang punya akses ke env (biasanya user yang menjalankan PM2):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
crontab -e
|
||||||
|
```
|
||||||
|
|
||||||
|
Tambah baris (ganti `https://your-domain.com` dan `<CRON_SECRET>` sesuai env yang di-set di step 1):
|
||||||
|
|
||||||
|
```cron
|
||||||
|
# Setrip — auto-complete trips harian (jam 01:00 WIB)
|
||||||
|
0 18 * * * curl -fsS -H "Authorization: Bearer <CRON_SECRET>" https://your-domain.com/api/cron/auto-complete-trips >> /var/log/setrip-cron.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
Verifikasi crontab tersimpan:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
crontab -l
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Siapkan file log
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo touch /var/log/setrip-cron.log
|
||||||
|
sudo chown $(whoami) /var/log/setrip-cron.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test manual
|
||||||
|
|
||||||
|
Sebelum tunggu schedule, panggil endpoint langsung untuk verifikasi:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://your-domain.com/api/cron/auto-complete-trips
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected response:**
|
||||||
|
|
||||||
|
- Belum ada trip yang lewat: `{"ok":true,"completed":0,"ids":[]}`
|
||||||
|
- Ada trip yang lewat: `{"ok":true,"completed":2,"ids":["clx...","cly..."]}`
|
||||||
|
|
||||||
|
**Kalau dapat 401:** `CRON_SECRET` di env tidak match dengan header. Cek `pm2 env <id>`.
|
||||||
|
|
||||||
|
**Kalau dapat 500:** `CRON_SECRET` belum di-set di env.
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
Tail log cron:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tail -f /var/log/setrip-cron.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Cek log app PM2 (untuk `console.log` dari endpoint):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pm2 logs setrip --lines 100 | grep cron
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Cron jalan tapi tidak ada efek di DB:**
|
||||||
|
- Cek `pm2 logs setrip` untuk error.
|
||||||
|
- Verifikasi waktu server: `date` (output harus UTC kalau pakai schedule UTC).
|
||||||
|
|
||||||
|
**Cron tidak jalan sama sekali:**
|
||||||
|
- Cek service cron aktif: `systemctl status cron` (Debian/Ubuntu) atau `systemctl status crond` (RHEL/CentOS).
|
||||||
|
- Cek crontab terdaftar di user yang benar: `sudo crontab -u $(whoami) -l`.
|
||||||
|
|
||||||
|
**Secret bocor:**
|
||||||
|
- Generate ulang `CRON_SECRET`, update di `.env` + crontab line, restart PM2.
|
||||||
|
|
||||||
|
## Hari kalau pindah ke Vercel / PaaS lain
|
||||||
|
|
||||||
|
Tinggal hapus crontab line + bikin `vercel.json` (atau equivalent platform). Endpoint sudah platform-agnostic — proteksinya sama (header `Authorization: Bearer <CRON_SECRET>`).
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
# Setrip — Trust & Trip Detail Roadmap
|
||||||
|
|
||||||
|
Status implementasi yang menaikkan kepercayaan calon peserta — trip detail experience yang meyakinkan + sistem reputasi organizer yang transparan.
|
||||||
|
|
||||||
|
> **Prinsip:** trust = fungsi dari (a) kelengkapan informasi trip dan (b) reputasi organizer yang transparan. Setiap fitur dievaluasi: apakah memberi calon peserta alasan obyektif untuk percaya?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit state sekarang (baseline)
|
||||||
|
|
||||||
|
**Trip detail (~80% sudah ada):**
|
||||||
|
- ✅ Itinerary, include/exclude, meeting point — schema lengkap di `Trip`, ditampilkan via `TripProgramBlock`.
|
||||||
|
- ✅ Participant preview (kartu confirmed peserta dengan avatar, kota, interests).
|
||||||
|
- ✅ Slot tersisa (X / Y) dengan progress bar berwarna.
|
||||||
|
- ⚠️ Urgency message ada tapi tidak mencolok saat slot menipis.
|
||||||
|
- ⚠️ Itinerary text bebas — organizer butuh hint format supaya konsisten isi lengkap.
|
||||||
|
|
||||||
|
**Review/trust (~40% sudah ada):**
|
||||||
|
- ✅ Model `TripReview` (rating + comment per trip+user, unique).
|
||||||
|
- ✅ Trust panel di trip detail (verified badge, trips created, avg rating, review count).
|
||||||
|
- ❌ Profil organizer publik `/u/[id]` belum tampilkan rating/review aggregate.
|
||||||
|
- ❌ Belum ada total participants served, completion rate, rating breakdown.
|
||||||
|
- ❌ Belum ada list ulasan terkumpul per organizer.
|
||||||
|
- ❌ `TripStatus.COMPLETED` enum-nya ada tapi tidak pernah di-set.
|
||||||
|
|
||||||
|
File baseline: [app/trips/[id]/page.tsx](app/trips/%5Bid%5D/page.tsx), [server/services/trust.service.ts](server/services/trust.service.ts), [server/services/review.service.ts](server/services/review.service.ts), [app/u/[id]/page.tsx](app/u/%5Bid%5D/page.tsx).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR-1 — Trip Detail Polish (UI only) ✅
|
||||||
|
|
||||||
|
Selesai. Cosmetic. Tidak ada migration, tidak ada perubahan service/repo.
|
||||||
|
|
||||||
|
| # | Item | Status | File |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1.1 | Urgency badge mencolok saat `spotsLeft <= 3` ("⚡ Tinggal X spot!") di header progress bar | ✅ | [app/trips/[id]/page.tsx](app/trips/%5Bid%5D/page.tsx) |
|
||||||
|
| 1.2 | Participant preview ringkas di blok progress ("👥 Sudah join: Andi, Rina, Budi +4") — first impression tanpa scroll | ✅ | [app/trips/[id]/page.tsx](app/trips/%5Bid%5D/page.tsx) |
|
||||||
|
| 1.3 | Hint deskriptif + placeholder lebih konkret di field itinerary form create-trip | ✅ | [features/trip/components/create-trip-form.tsx](features/trip/components/create-trip-form.tsx) |
|
||||||
|
|
||||||
|
**Tindakan manual:** tidak ada.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR-2 — Organizer Trust Aggregates (service + UI, tanpa migration) ✅
|
||||||
|
|
||||||
|
Selesai. `tsc --noEmit` lulus. Tanpa schema baru, tanpa migration.
|
||||||
|
|
||||||
|
**Keputusan asumsi yang dipakai:**
|
||||||
|
- `tripsCompleted` ≠ `Trip.status = COMPLETED` (status itu tidak pernah di-set). Pakai `endDate < now()` (fallback `date < now()`) AND `status != CLOSED`.
|
||||||
|
- `tripsCancelled` = `Trip.status = CLOSED` (organizer batalkan trip eksplisit).
|
||||||
|
- `completionRate` butuh sample ≥ 3 (`COMPLETION_RATE_MIN_SAMPLE` di [lib/trust.ts](lib/trust.ts)) supaya tidak menyesatkan organizer baru.
|
||||||
|
- Rating breakdown di-render sebagai bar chart kecil (visual cue lebih kuat dari angka mentah).
|
||||||
|
- `OrganizerStatsPanel` di profil publik tidak di-render untuk user yang murni peserta — query Prisma juga di-skip kalau `organizedTrips.length === 0 && !isVerifiedOrganizer`.
|
||||||
|
- Trip detail: stat box "Trip dibuat" diganti jadi **"Trip selesai"** (lebih meaningful) + tambah **"Peserta dilayani"**. Total 3 stat box, masih kompak.
|
||||||
|
|
||||||
|
| # | Item | Status | File |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 2.1 | Extend `OrganizerTrust` type: `tripsCompleted`, `tripsCancelled`, `totalParticipantsServed`, `completionRate`, `ratingBreakdown` | ✅ | [server/services/trust.service.ts](server/services/trust.service.ts) |
|
||||||
|
| 2.2 | `tripsCompleted` di-derive dari `endDate < now()` (fallback `date < now()`) AND `status != CLOSED` | ✅ | [server/services/trust.service.ts](server/services/trust.service.ts) |
|
||||||
|
| 2.3 | `totalParticipantsServed` = count `TripParticipant CONFIRMED` di trip yang sudah lewat & tidak dibatalkan | ✅ | [server/services/trust.service.ts](server/services/trust.service.ts) |
|
||||||
|
| 2.4 | `completionRate` = `tripsCompleted / (tripsCompleted + tripsCancelled)`. Null bila sample < 3 | ✅ | [server/services/trust.service.ts](server/services/trust.service.ts), [lib/trust.ts](lib/trust.ts) |
|
||||||
|
| 2.5 | `ratingBreakdown` via `prisma.tripReview.groupBy({ by: ['rating'] })` | ✅ | [server/services/trust.service.ts](server/services/trust.service.ts) |
|
||||||
|
| 2.6 | Komponen `OrganizerStatsPanel` (badges + 4 stat box + bar chart breakdown) | ✅ | [features/profile/components/organizer-stats-panel.tsx](features/profile/components/organizer-stats-panel.tsx) |
|
||||||
|
| 2.7 | Update `OrganizerTrustPanel` di trip detail — Trip selesai (+ subtitle "berjalan"), Peserta dilayani, Rating | ✅ | [features/trip/components/organizer-trust-panel.tsx](features/trip/components/organizer-trust-panel.tsx) |
|
||||||
|
| 2.8 | Render `OrganizerStatsPanel` di `/u/[id]` (skip query untuk non-organizer) | ✅ | [app/u/[id]/page.tsx](app/u/%5Bid%5D/page.tsx) |
|
||||||
|
|
||||||
|
**Tindakan manual:** tidak ada.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR-3 — Organizer Reviews Aggregator (service + UI, tanpa migration) ✅
|
||||||
|
|
||||||
|
Selesai. `tsc --noEmit` lulus. Tanpa schema baru, tanpa migration.
|
||||||
|
|
||||||
|
**Keputusan asumsi yang dipakai:**
|
||||||
|
- Default limit 20 ulasan terbaru — cukup untuk MVP, tidak perlu pagination dulu.
|
||||||
|
- Komponen RSC (server component, no `"use client"`) — pure render, tidak ada interaktivitas.
|
||||||
|
- Tipe `OrganizerReviewItem` di-extract dari `Awaited<ReturnType<typeof reviewRepo.findByOrganizer>>[number]` supaya schema repo = sumber kebenaran tanpa duplikasi tipe.
|
||||||
|
- Fetch trust + reviews via single `Promise.all` di `/u/[id]` (paralel, hemat 1 round-trip).
|
||||||
|
- Komponen di-skip kalau `reviews.length === 0` — biar tidak makin "kosong" di profil organizer baru. Stats panel sudah punya pesan "Belum ada ulasan".
|
||||||
|
- Rating ditampilkan sebagai bintang penuh (`★★★★☆`) bukan angka — visual cue lebih kuat untuk testimoni.
|
||||||
|
- Header section: "X terbaru dari Y ulasan" kalau di-limit, atau cuma "Y ulasan" kalau seluruh list ditampilkan.
|
||||||
|
|
||||||
|
| # | Item | Status | File |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 3.1 | `reviewService.getReviewsByOrganizer(organizerId, limit?)` + tipe `OrganizerReviewItem` | ✅ | [server/services/review.service.ts](server/services/review.service.ts) |
|
||||||
|
| 3.2 | Repo helper `findByOrganizer` (default limit 20, urut newest, include user + trip) | ✅ | [server/repositories/review.repo.ts](server/repositories/review.repo.ts) |
|
||||||
|
| 3.3 | Komponen `OrganizerReviewsList` (avatar + name + bintang + trip link + tanggal + comment) | ✅ | [features/review/components/organizer-reviews-list.tsx](features/review/components/organizer-reviews-list.tsx) |
|
||||||
|
| 3.4 | Render di `/u/[id]` di bawah `OrganizerStatsPanel`, fetch via `Promise.all` paralel | ✅ | [app/u/[id]/page.tsx](app/u/%5Bid%5D/page.tsx) |
|
||||||
|
|
||||||
|
**Tindakan manual:** tidak ada.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR-4 — Trip Completion Mechanism (cron daily) ✅
|
||||||
|
|
||||||
|
Selesai. Pilihan **B** (cron daily) — `Trip.status = COMPLETED` di-set otomatis untuk trip yang `endDate` (atau `date` kalau endDate null) sudah lewat hari ini UTC.
|
||||||
|
|
||||||
|
**Keputusan asumsi yang dipakai:**
|
||||||
|
- Cutoff = `utcStartOfDay(new Date())` (start of today UTC). Trip dengan `endDate < cutoff` di-flip; trip yang berakhir *hari ini* belum.
|
||||||
|
- Hanya trip dengan status `OPEN` atau `FULL` yang di-flip. `CLOSED` tidak disentuh (organizer eksplisit membatalkan; tetap dibedakan dari COMPLETED untuk perhitungan `tripsCancelled` di trust panel).
|
||||||
|
- Idempotent: dua kali run di hari yang sama, run kedua match 0 row.
|
||||||
|
- Endpoint diproteksi via `Authorization: Bearer ${CRON_SECRET}`. Kalau `CRON_SECRET` tidak di-set, endpoint hard-fail 500 (mencegah accidentally jalan tanpa proteksi).
|
||||||
|
- Schedule cron: `0 18 * * *` (jam 18:00 UTC = 01:00 WIB hari berikutnya) — buffer ~7 jam pasca-akhir hari WIB sebelum flip.
|
||||||
|
- **`trustService` tetap pakai computed-from-`endDate`** (tidak diganti ke `status = COMPLETED`). Alasan: trust calc tetap correct walau cron telat / down sehari, dan backward-compat untuk trip lama yang dibuat sebelum cron aktif.
|
||||||
|
- Vercel Cron via `vercel.json` — host lain tinggal panggil endpoint yang sama dari cron eksternal apa saja (GitHub Actions, cron-job.org, dst) dengan header yang sama.
|
||||||
|
|
||||||
|
| # | Item | Status | File |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 4.1 | Repo helper `bulkCompletePastTrips(cutoff)` (idempotent, batch update) | ✅ | [server/repositories/trip.repo.ts](server/repositories/trip.repo.ts) |
|
||||||
|
| 4.2 | Service `tripService.autoCompletePastTrips()` | ✅ | [server/services/trip.service.ts](server/services/trip.service.ts) |
|
||||||
|
| 4.3 | API route `/api/cron/auto-complete-trips` (GET, proteksi `CRON_SECRET`) | ✅ | [app/api/cron/auto-complete-trips/route.ts](app/api/cron/auto-complete-trips/route.ts) |
|
||||||
|
| 4.4 | Schedule `0 18 * * *` di Vercel Cron | ✅ | [vercel.json](vercel.json) |
|
||||||
|
|
||||||
|
**Tindakan manual:**
|
||||||
|
1. Set env `CRON_SECRET` di hosting (random ≥32 char). Generate cepat: `openssl rand -hex 32`.
|
||||||
|
2. Kalau host bukan Vercel: panggil endpoint dari cron eksternal apa saja (GitHub Actions schedule, cron-job.org, EasyCron, dst) dengan header `Authorization: Bearer ${CRON_SECRET}`. `vercel.json` bisa dihapus.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❌ Anti-list (yang harus DITOLAK kalau muncul)
|
||||||
|
|
||||||
|
- **Model `OrganizerReview` terpisah** — `TripReview` sudah cukup, 1 trip = 1 organizer. Bikin model baru = duplikasi data + sumber kebenaran ambigu.
|
||||||
|
- **Denormalisasi cache** (mis. `User.cachedAvgRating`) sebelum aggregate query terbukti lambat. Premature optimization → drift jadi tech debt cepat.
|
||||||
|
- **Auto-hapus review buruk** atau organizer "respon" review (untuk MVP). Bisa nanti — fokus dulu menampilkan data jujur.
|
||||||
|
- **"Trust score" gabungan satu angka** — kasih breakdown agar calon peserta evaluasi sendiri. Single number gampang dimanipulasi & menyesatkan.
|
||||||
|
- **Review user/peserta** (no-show, kooperatif?) — itu C6 di [SOCIAL_ROADMAP.md](SOCIAL_ROADMAP.md). Scope berbeda, jangan campur.
|
||||||
|
- **Rating dengan setengah bintang / kustom 1-10** — tetap 1-5 integer (sudah di schema). Granularitas lebih halus tidak meningkatkan trust, hanya menambah noise.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Saran phasing
|
||||||
|
|
||||||
|
PR berurutan. Setiap PR mandiri (siap di-deploy):
|
||||||
|
|
||||||
|
1. **PR-1** — Trip detail polish. Cepat, low-risk, no migration. **Mulai dari sini.**
|
||||||
|
2. **PR-2** — Trust aggregates di service + UI. Read-only, no migration.
|
||||||
|
3. **PR-3** — Reviews list per organizer di service + UI. Read-only, no migration.
|
||||||
|
4. **PR-4** — Diskusi opsi completion mechanism (atau skip kalau opsi C dipilih).
|
||||||
|
|
||||||
|
**Pertanyaan terbuka sebelum PR-2:**
|
||||||
|
1. Apakah `tripsCompleted` count termasuk trip dengan `status = CLOSED` yang `endDate < now()`? Saran: tidak — CLOSED = dibatalkan, dipisah ke `tripsCancelled`.
|
||||||
|
2. Threshold minimum supaya `completionRate` ditampilkan? Saran: min 3 trip selesai supaya angka tidak menyesatkan (1 trip dibatalkan dari 1 trip = 0% looks bad untuk organizer baru).
|
||||||
|
3. Tampilkan rating breakdown sebagai bar chart atau hanya angka? Saran: bar chart kecil — visual cue lebih kuat untuk credibility.
|
||||||
+10
-1
@@ -26,4 +26,13 @@ NEXT_PUBLIC_MIDTRANS_CLIENT_KEY=
|
|||||||
# 'true' untuk production, 'false' atau kosong untuk sandbox.
|
# 'true' untuk production, 'false' atau kosong untuk sandbox.
|
||||||
# Dibaca di server (untuk Snap API endpoint) DAN client (untuk Snap.js URL).
|
# Dibaca di server (untuk Snap API endpoint) DAN client (untuk Snap.js URL).
|
||||||
NEXT_PUBLIC_MIDTRANS_IS_PRODUCTION=false
|
NEXT_PUBLIC_MIDTRANS_IS_PRODUCTION=false
|
||||||
# Webhook URL di Midtrans dashboard harus diset ke: <NEXT_PUBLIC_SITE_URL>/api/webhooks/midtrans
|
# Webhook URL di Midtrans dashboard harus diset ke: <NEXT_PUBLIC_SITE_URL>/api/webhooks/midtrans
|
||||||
|
|
||||||
|
|
||||||
|
# === Cron jobs (auto-complete trip, dst) ===
|
||||||
|
# Bearer token yang harus di-kirim cron eksternal (system crontab / Vercel Cron / dst)
|
||||||
|
# saat memanggil endpoint `/api/cron/*`. Kalau kosong, endpoint hard-fail 500.
|
||||||
|
# Generate ≥32-byte hex secret:
|
||||||
|
# openssl rand -hex 32
|
||||||
|
# Setup detail: lihat docs/CRON_SETUP.md
|
||||||
|
CRON_SECRET=
|
||||||
@@ -5,6 +5,7 @@ import { authOptions } from "@/lib/auth";
|
|||||||
import { tripService } from "@/server/services/trip.service";
|
import { tripService } from "@/server/services/trip.service";
|
||||||
import { paymentService } from "@/server/services/payment.service";
|
import { paymentService } from "@/server/services/payment.service";
|
||||||
import { bookingService } from "@/server/services/booking.service";
|
import { bookingService } from "@/server/services/booking.service";
|
||||||
|
import { refundService } from "@/server/services/refund.service";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
|
|
||||||
export async function markParticipantPaidAction(tripId: string) {
|
export async function markParticipantPaidAction(tripId: string) {
|
||||||
@@ -94,3 +95,45 @@ export async function confirmParticipantPaymentAction(
|
|||||||
return { error: (err as Error).message };
|
return { error: (err as Error).message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Peserta cancel booking PAID dengan refund request. Server menghitung
|
||||||
|
* nominal refund pakai policy default (lib/refund-policy.ts) — client
|
||||||
|
* cuma kirim bookingId untuk cegah tampering.
|
||||||
|
*/
|
||||||
|
export async function cancelBookingWithRefundAction(tripId: string) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user) {
|
||||||
|
return { error: "Kamu harus login terlebih dahulu" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const booking = await bookingService.getByTripAndUser(
|
||||||
|
tripId,
|
||||||
|
session.user.id
|
||||||
|
);
|
||||||
|
if (!booking) {
|
||||||
|
return { error: "Kamu tidak terdaftar di trip ini" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await refundService.requestUserCancellation({
|
||||||
|
bookingId: booking.id,
|
||||||
|
userId: session.user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
revalidatePath(`/trips/${tripId}`);
|
||||||
|
revalidatePath("/trips");
|
||||||
|
revalidatePath("/");
|
||||||
|
revalidatePath("/profile");
|
||||||
|
revalidatePath("/admin/refunds");
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true as const,
|
||||||
|
kind: result.kind,
|
||||||
|
refundAmount: result.refundAmount,
|
||||||
|
days: result.days,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return { error: (err as Error).message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { cancelBookingWithRefundAction } from "@/features/booking/actions";
|
||||||
|
import { formatRupiah } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface CancelBookingButtonProps {
|
||||||
|
tripId: string;
|
||||||
|
/** Hasil preview server-side (dihitung di trip detail page). */
|
||||||
|
preview: {
|
||||||
|
days: number;
|
||||||
|
refundAmount: number;
|
||||||
|
bookingAmount: number;
|
||||||
|
tierLabel: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerResult =
|
||||||
|
| { kind: "REFUND_PENDING"; refundAmount: number; days: number }
|
||||||
|
| { kind: "CANCELLED_NO_REFUND"; days: number };
|
||||||
|
|
||||||
|
export function CancelBookingButton({ tripId, preview }: CancelBookingButtonProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [result, setResult] = useState<ServerResult | null>(null);
|
||||||
|
|
||||||
|
async function handleConfirm() {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
const res = await cancelBookingWithRefundAction(tripId);
|
||||||
|
setLoading(false);
|
||||||
|
if ("error" in res) {
|
||||||
|
setError(res.error ?? "Terjadi kesalahan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setResult({
|
||||||
|
kind: res.kind,
|
||||||
|
refundAmount: res.refundAmount,
|
||||||
|
days: res.days,
|
||||||
|
} as ServerResult);
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result?.kind === "REFUND_PENDING") {
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">
|
||||||
|
<p className="font-semibold">Request refund dibuat.</p>
|
||||||
|
<p className="mt-1 text-xs">
|
||||||
|
Refund <span className="font-bold">{formatRupiah(result.refundAmount)}</span>{" "}
|
||||||
|
menunggu review admin. Setelah disetujui dan ditransfer manual, slot
|
||||||
|
kamu di trip akan otomatis dibebaskan.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result?.kind === "CANCELLED_NO_REFUND") {
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-neutral-200 bg-neutral-50 p-4 text-sm text-neutral-700">
|
||||||
|
<p className="font-semibold">Booking dibatalkan.</p>
|
||||||
|
<p className="mt-1 text-xs">
|
||||||
|
Pembatalan di H-{result.days} berada di luar window refund — tidak
|
||||||
|
ada nominal yang dikembalikan.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
className="w-full rounded-xl border-2 border-red-200 py-3 text-sm font-bold text-red-600 transition-colors hover:bg-red-50"
|
||||||
|
>
|
||||||
|
Cancel & Request Refund
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const percentage = preview.bookingAmount
|
||||||
|
? Math.floor((preview.refundAmount * 100) / preview.bookingAmount)
|
||||||
|
: 0;
|
||||||
|
const noRefund = preview.refundAmount === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3 rounded-2xl border-2 border-red-200 bg-red-50 p-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-red-900">Cancel booking?</p>
|
||||||
|
<p className="mt-1 text-xs text-red-800/80">
|
||||||
|
Kamu cancel di <span className="font-semibold">H-{preview.days}</span>{" "}
|
||||||
|
dari tanggal berangkat.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-xl border border-red-200 bg-white p-3">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-500">
|
||||||
|
Estimasi refund (sesuai policy)
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-lg font-bold text-neutral-900">
|
||||||
|
{formatRupiah(preview.refundAmount)}
|
||||||
|
{!noRefund && (
|
||||||
|
<span className="ml-2 text-xs font-medium text-neutral-500">
|
||||||
|
({percentage}% dari {formatRupiah(preview.bookingAmount)})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-[11px] text-neutral-500">
|
||||||
|
Tier: {preview.tierLabel}
|
||||||
|
</p>
|
||||||
|
{noRefund ? (
|
||||||
|
<p className="mt-2 text-xs text-red-700">
|
||||||
|
⚠️ Di luar window refund — uang tidak dikembalikan. Booking akan
|
||||||
|
di-cancel langsung.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="mt-2 text-xs text-neutral-600">
|
||||||
|
Refund akan masuk antrian review admin. Setelah disetujui & uang
|
||||||
|
ditransfer, booking otomatis ditandai{" "}
|
||||||
|
{percentage === 100 ? "REFUNDED" : "PARTIALLY_REFUNDED"}.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-lg bg-white px-3 py-2 text-xs font-medium text-red-700">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-xl bg-red-600 px-4 py-2 text-sm font-bold text-white hover:bg-red-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading
|
||||||
|
? "Memproses…"
|
||||||
|
: noRefund
|
||||||
|
? "Konfirmasi Cancel"
|
||||||
|
: "Konfirmasi & Request Refund"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
setError("");
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-xl border border-red-200 bg-white px-4 py-2 text-sm font-medium text-red-700 hover:bg-red-100 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import type { OrganizerTrust } from "@/server/services/trust.service";
|
||||||
|
|
||||||
|
interface OrganizerStatsPanelProps {
|
||||||
|
trust: OrganizerTrust;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Panel reputasi organizer untuk halaman profil publik /u/[id].
|
||||||
|
* Tidak render kalau user belum punya history sebagai organizer.
|
||||||
|
*/
|
||||||
|
export function OrganizerStatsPanel({ trust }: OrganizerStatsPanelProps) {
|
||||||
|
const {
|
||||||
|
isVerified,
|
||||||
|
isTripLeader,
|
||||||
|
tripsCreated,
|
||||||
|
tripsCompleted,
|
||||||
|
totalParticipantsServed,
|
||||||
|
completionRate,
|
||||||
|
avgRating,
|
||||||
|
reviewCount,
|
||||||
|
ratingBreakdown,
|
||||||
|
} = trust;
|
||||||
|
|
||||||
|
if (tripsCreated === 0 && reviewCount === 0 && !isVerified) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxBreakdown = Math.max(
|
||||||
|
ratingBreakdown[1],
|
||||||
|
ratingBreakdown[2],
|
||||||
|
ratingBreakdown[3],
|
||||||
|
ratingBreakdown[4],
|
||||||
|
ratingBreakdown[5],
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="mt-5 rounded-2xl border border-neutral-200 bg-white p-5 shadow-sm sm:p-6">
|
||||||
|
<h2 className="mb-3 text-sm font-bold text-neutral-700 sm:text-base">
|
||||||
|
Reputasi sebagai organizer
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{(isVerified || isTripLeader) && (
|
||||||
|
<div className="mb-4 flex flex-wrap gap-2">
|
||||||
|
{isVerified && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1 rounded-full bg-primary-100 px-2.5 py-0.5 text-[11px] font-bold uppercase tracking-wide text-primary-800"
|
||||||
|
title="Identitas organizer telah diverifikasi (KTP & rekening)"
|
||||||
|
>
|
||||||
|
✅ Verified Organizer
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isTripLeader && (
|
||||||
|
<span className="inline-flex items-center rounded-full bg-secondary-100 px-2.5 py-0.5 text-[11px] font-bold uppercase tracking-wide text-secondary-900">
|
||||||
|
Trip leader
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2.5 sm:grid-cols-4">
|
||||||
|
<Stat
|
||||||
|
label="Trip selesai"
|
||||||
|
value={tripsCompleted.toString()}
|
||||||
|
tone="primary"
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
label="Peserta dilayani"
|
||||||
|
value={totalParticipantsServed.toString()}
|
||||||
|
tone="secondary"
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
label="Completion rate"
|
||||||
|
value={
|
||||||
|
completionRate != null
|
||||||
|
? `${Math.round(completionRate * 100)}%`
|
||||||
|
: "—"
|
||||||
|
}
|
||||||
|
subtitle={
|
||||||
|
completionRate == null ? "Belum cukup data" : undefined
|
||||||
|
}
|
||||||
|
tone="neutral"
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
label="Rating"
|
||||||
|
value={avgRating != null ? `${avgRating} ★` : "—"}
|
||||||
|
subtitle={
|
||||||
|
reviewCount > 0
|
||||||
|
? `${reviewCount} ulasan`
|
||||||
|
: "Belum ada ulasan"
|
||||||
|
}
|
||||||
|
tone="amber"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{reviewCount > 0 && (
|
||||||
|
<div className="mt-4 border-t border-neutral-100 pt-4">
|
||||||
|
<h3 className="mb-2 text-xs font-semibold text-neutral-600">
|
||||||
|
Distribusi rating
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{([5, 4, 3, 2, 1] as const).map((star) => {
|
||||||
|
const count = ratingBreakdown[star];
|
||||||
|
const percent = (count / maxBreakdown) * 100;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={star}
|
||||||
|
className="flex items-center gap-2 text-xs"
|
||||||
|
>
|
||||||
|
<span className="w-8 shrink-0 font-medium text-neutral-600">
|
||||||
|
{star} ★
|
||||||
|
</span>
|
||||||
|
<div className="h-2 flex-1 overflow-hidden rounded-full bg-neutral-100">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-amber-400 transition-all"
|
||||||
|
style={{ width: `${percent}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="w-8 shrink-0 text-right text-neutral-500">
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TONE_CLASSES = {
|
||||||
|
primary: { bg: "bg-primary-50", value: "text-primary-700" },
|
||||||
|
secondary: { bg: "bg-secondary-50", value: "text-secondary-700" },
|
||||||
|
neutral: { bg: "bg-neutral-50", value: "text-neutral-800" },
|
||||||
|
amber: { bg: "bg-amber-50", value: "text-amber-700" },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
interface StatProps {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
subtitle?: string;
|
||||||
|
tone: keyof typeof TONE_CLASSES;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Stat({ label, value, subtitle, tone }: StatProps) {
|
||||||
|
const cls = TONE_CLASSES[tone];
|
||||||
|
return (
|
||||||
|
<div className={`rounded-xl px-3 py-2.5 ${cls.bg}`}>
|
||||||
|
<p className="text-[10px] font-medium uppercase tracking-wide text-neutral-500 sm:text-[11px]">
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
<p className={`mt-0.5 text-lg font-bold sm:text-xl ${cls.value}`}>
|
||||||
|
{value}
|
||||||
|
</p>
|
||||||
|
{subtitle && (
|
||||||
|
<p className="text-[10px] text-neutral-400">{subtitle}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { authOptions } from "@/lib/auth";
|
||||||
|
import { isAdminEmail } from "@/lib/admin";
|
||||||
|
import { refundService } from "@/server/services/refund.service";
|
||||||
|
import { createRefundSchema, refundDecisionSchema } from "./schemas";
|
||||||
|
|
||||||
|
async function requireAdmin() {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user || !isAdminEmail(session.user.email)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return session.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createRefundAction(formData: FormData) {
|
||||||
|
const admin = await requireAdmin();
|
||||||
|
if (!admin) return { error: "Tidak memiliki akses admin" };
|
||||||
|
|
||||||
|
const parsed = createRefundSchema.safeParse({
|
||||||
|
bookingId: formData.get("bookingId") as string,
|
||||||
|
reason: formData.get("reason") as string,
|
||||||
|
reportedBy: formData.get("reportedBy") as string,
|
||||||
|
reportNote: formData.get("reportNote") as string,
|
||||||
|
amount: (formData.get("amount") as string) || undefined,
|
||||||
|
});
|
||||||
|
if (!parsed.success) {
|
||||||
|
return { error: parsed.error.issues[0].message };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await refundService.requestRefund({
|
||||||
|
bookingId: parsed.data.bookingId,
|
||||||
|
reason: parsed.data.reason,
|
||||||
|
reportedBy: parsed.data.reportedBy,
|
||||||
|
reportNote: parsed.data.reportNote,
|
||||||
|
amount: parsed.data.amount,
|
||||||
|
initiatedByAdminId: admin.id,
|
||||||
|
});
|
||||||
|
revalidatePath("/admin/refunds");
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
return { error: (err as Error).message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decideRefundAction(formData: FormData) {
|
||||||
|
const admin = await requireAdmin();
|
||||||
|
if (!admin) return { error: "Tidak memiliki akses admin" };
|
||||||
|
|
||||||
|
const parsed = refundDecisionSchema.safeParse({
|
||||||
|
refundId: formData.get("refundId") as string,
|
||||||
|
decision: formData.get("decision") as string,
|
||||||
|
adminNote: (formData.get("adminNote") as string) || undefined,
|
||||||
|
});
|
||||||
|
if (!parsed.success) {
|
||||||
|
return { error: parsed.error.issues[0].message };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { refundId, decision, adminNote } = parsed.data;
|
||||||
|
const needsNote = decision === "REJECT" || decision === "SUCCEEDED" || decision === "FAILED";
|
||||||
|
if (needsNote && (!adminNote || !adminNote.trim())) {
|
||||||
|
return { error: "Catatan/alasan admin wajib diisi untuk tindakan ini" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (decision === "APPROVE") {
|
||||||
|
await refundService.approveRefund({
|
||||||
|
refundId,
|
||||||
|
adminId: admin.id,
|
||||||
|
adminNote,
|
||||||
|
});
|
||||||
|
} else if (decision === "REJECT") {
|
||||||
|
await refundService.rejectRefund({
|
||||||
|
refundId,
|
||||||
|
adminId: admin.id,
|
||||||
|
adminNote: adminNote!,
|
||||||
|
});
|
||||||
|
} else if (decision === "SUCCEEDED") {
|
||||||
|
await refundService.markSucceededManual({
|
||||||
|
refundId,
|
||||||
|
adminId: admin.id,
|
||||||
|
adminNote: adminNote!,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await refundService.markFailed({
|
||||||
|
refundId,
|
||||||
|
adminId: admin.id,
|
||||||
|
adminNote: adminNote!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
revalidatePath("/admin/refunds");
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
return { error: (err as Error).message };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { createRefundAction } from "@/features/refund/actions";
|
||||||
|
|
||||||
|
const REASON_OPTIONS = [
|
||||||
|
{ value: "USER_CANCELLATION", label: "Peserta cancel sendiri" },
|
||||||
|
{ value: "ORGANIZER_CANCELLED", label: "Organizer batalkan trip" },
|
||||||
|
{ value: "TRIP_ISSUE", label: "Masalah saat/setelah trip" },
|
||||||
|
{ value: "ADMIN_ADJUSTMENT", label: "Penyesuaian admin" },
|
||||||
|
{ value: "DISPUTE_RESOLVED", label: "Hasil dispute / chargeback" },
|
||||||
|
{ value: "OTHER", label: "Lain-lain" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const REPORTER_OPTIONS = [
|
||||||
|
{ value: "PARTICIPANT", label: "Peserta" },
|
||||||
|
{ value: "ORGANIZER", label: "Organizer" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function CreateRefundForm() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault();
|
||||||
|
setError("");
|
||||||
|
setLoading(true);
|
||||||
|
const fd = new FormData(e.currentTarget);
|
||||||
|
const result = await createRefundAction(fd);
|
||||||
|
setLoading(false);
|
||||||
|
if (result.error) {
|
||||||
|
setError(result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(e.target as HTMLFormElement).reset();
|
||||||
|
setOpen(false);
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
return (
|
||||||
|
<div className="mb-6 flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-700"
|
||||||
|
>
|
||||||
|
+ Catat Laporan Refund
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
className="mb-6 space-y-4 rounded-2xl border border-neutral-200 bg-white p-5 shadow-sm sm:p-6"
|
||||||
|
>
|
||||||
|
<header className="flex items-start justify-between gap-3 border-b border-neutral-100 pb-3">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-base font-bold text-neutral-900">
|
||||||
|
Catat Laporan Refund Manual
|
||||||
|
</h2>
|
||||||
|
<p className="mt-0.5 text-xs text-neutral-500">
|
||||||
|
Masukkan laporan yang diterima dari peserta atau organizer (via
|
||||||
|
WhatsApp/email). Refund akan masuk antrian PENDING untuk di-review.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
setError("");
|
||||||
|
}}
|
||||||
|
className="rounded-lg px-2 py-1 text-xs font-medium text-neutral-500 hover:bg-neutral-100"
|
||||||
|
>
|
||||||
|
Tutup
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-lg bg-red-50 px-3 py-2 text-xs text-red-600">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<Field label="Booking ID" required>
|
||||||
|
<input
|
||||||
|
name="bookingId"
|
||||||
|
required
|
||||||
|
placeholder="cuid booking yang dilaporkan"
|
||||||
|
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2 text-sm font-mono focus:bg-white"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Pelapor" required>
|
||||||
|
<select
|
||||||
|
name="reportedBy"
|
||||||
|
required
|
||||||
|
defaultValue=""
|
||||||
|
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2 text-sm focus:bg-white"
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
Pilih pelapor
|
||||||
|
</option>
|
||||||
|
{REPORTER_OPTIONS.map((o) => (
|
||||||
|
<option key={o.value} value={o.value}>
|
||||||
|
{o.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Alasan" required>
|
||||||
|
<select
|
||||||
|
name="reason"
|
||||||
|
required
|
||||||
|
defaultValue=""
|
||||||
|
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2 text-sm focus:bg-white"
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
Pilih alasan
|
||||||
|
</option>
|
||||||
|
{REASON_OPTIONS.map((o) => (
|
||||||
|
<option key={o.value} value={o.value}>
|
||||||
|
{o.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label="Nominal (IDR)"
|
||||||
|
hint="Kosongkan untuk full remaining"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
name="amount"
|
||||||
|
inputMode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
placeholder="mis. 500000"
|
||||||
|
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2 text-sm focus:bg-white"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Field label="Isi laporan" required>
|
||||||
|
<textarea
|
||||||
|
name="reportNote"
|
||||||
|
required
|
||||||
|
rows={3}
|
||||||
|
placeholder="Salin/ringkas laporan dari peserta/organizer"
|
||||||
|
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2 text-sm focus:bg-white"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 border-t border-neutral-100 pt-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
setError("");
|
||||||
|
}}
|
||||||
|
className="rounded-xl border border-neutral-200 px-4 py-2 text-sm font-medium text-neutral-600 hover:bg-neutral-50"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? "Menyimpan…" : "Simpan Laporan"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Field({
|
||||||
|
label,
|
||||||
|
hint,
|
||||||
|
required,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
hint?: string;
|
||||||
|
required?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className="block">
|
||||||
|
<div className="mb-1 flex items-baseline justify-between gap-2">
|
||||||
|
<span className="text-xs font-semibold uppercase tracking-wide text-neutral-600">
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-red-500"> *</span>}
|
||||||
|
</span>
|
||||||
|
{hint && <span className="text-xs text-neutral-400">{hint}</span>}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { getRefundPolicyTiers } from "@/lib/refund-policy";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display kebijakan refund default di trip detail. Sumber tier:
|
||||||
|
* lib/refund-policy.ts. Compact agar tidak mendominasi page.
|
||||||
|
*/
|
||||||
|
export function RefundPolicySection() {
|
||||||
|
const tiers = getRefundPolicyTiers();
|
||||||
|
return (
|
||||||
|
<details className="rounded-xl border border-neutral-200 bg-neutral-50/60 p-3 text-xs sm:text-sm">
|
||||||
|
<summary className="cursor-pointer select-none font-semibold text-neutral-700">
|
||||||
|
🛟 Kebijakan refund saat peserta cancel
|
||||||
|
</summary>
|
||||||
|
<div className="mt-2 space-y-2 text-neutral-600">
|
||||||
|
<p className="text-[11px] text-neutral-500 sm:text-xs">
|
||||||
|
Kebijakan ini berlaku saat <strong>peserta</strong> cancel booking
|
||||||
|
yang sudah lunas. Kalau <strong>organizer</strong> membatalkan trip,
|
||||||
|
peserta yang sudah bayar selalu dapat refund 100%.
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{tiers.map((t) => (
|
||||||
|
<li key={t.minDaysBefore} className="flex items-baseline gap-2">
|
||||||
|
<span
|
||||||
|
className={`inline-flex min-w-[3rem] justify-center rounded-full px-2 py-0.5 text-[10px] font-bold ${
|
||||||
|
t.refundPercentage >= 80
|
||||||
|
? "bg-primary-100 text-primary-700"
|
||||||
|
: t.refundPercentage >= 50
|
||||||
|
? "bg-amber-100 text-amber-700"
|
||||||
|
: "bg-red-100 text-red-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t.refundPercentage}%
|
||||||
|
</span>
|
||||||
|
<span>{t.label}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<p className="text-[11px] text-neutral-500 sm:text-xs">
|
||||||
|
Refund diproses manual oleh admin SeTrip — perlu 1–3 hari kerja
|
||||||
|
setelah disetujui untuk uang masuk ke rekening kamu.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { decideRefundAction } from "@/features/refund/actions";
|
||||||
|
import { formatRupiah } from "@/lib/utils";
|
||||||
|
|
||||||
|
type RefundStatus =
|
||||||
|
| "PENDING"
|
||||||
|
| "APPROVED"
|
||||||
|
| "REJECTED"
|
||||||
|
| "PROCESSING"
|
||||||
|
| "SUCCEEDED"
|
||||||
|
| "FAILED";
|
||||||
|
|
||||||
|
type Decision = "APPROVE" | "REJECT" | "SUCCEEDED" | "FAILED";
|
||||||
|
|
||||||
|
export type RefundCardData = {
|
||||||
|
id: string;
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
reason: string;
|
||||||
|
reportedBy: "PARTICIPANT" | "ORGANIZER";
|
||||||
|
reportNote: string;
|
||||||
|
initiatedBy: string;
|
||||||
|
status: RefundStatus;
|
||||||
|
adminNote: string | null;
|
||||||
|
createdAt: Date;
|
||||||
|
reviewedAt: Date | null;
|
||||||
|
succeededAt: Date | null;
|
||||||
|
failedAt: Date | null;
|
||||||
|
reviewedBy: { id: string; name: string; email: string } | null;
|
||||||
|
booking: {
|
||||||
|
id: string;
|
||||||
|
amount: number;
|
||||||
|
status: string;
|
||||||
|
trip: { id: string; title: string; date: Date };
|
||||||
|
user: { id: string; name: string; email: string };
|
||||||
|
payments: {
|
||||||
|
id: string;
|
||||||
|
provider: string;
|
||||||
|
method: string | null;
|
||||||
|
amount: number;
|
||||||
|
status: string;
|
||||||
|
paidAt: Date | null;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatDate(d: Date): string {
|
||||||
|
return new Date(d).toLocaleString("id-ID", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const REASON_LABEL: Record<string, string> = {
|
||||||
|
USER_CANCELLATION: "Peserta cancel",
|
||||||
|
ORGANIZER_CANCELLED: "Organizer batalkan",
|
||||||
|
TRIP_ISSUE: "Masalah trip",
|
||||||
|
ADMIN_ADJUSTMENT: "Penyesuaian admin",
|
||||||
|
DISPUTE_RESOLVED: "Dispute resolved",
|
||||||
|
OTHER: "Lain-lain",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RefundReviewCard({ refund }: { refund: RefundCardData }) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [openAction, setOpenAction] = useState<Decision | null>(null);
|
||||||
|
const [note, setNote] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const paidPayment = refund.booking.payments.find(
|
||||||
|
(p) => p.status === "PAID" || p.status === "REFUNDED"
|
||||||
|
);
|
||||||
|
|
||||||
|
async function submit(decision: Decision) {
|
||||||
|
setError("");
|
||||||
|
setLoading(true);
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.set("refundId", refund.id);
|
||||||
|
fd.set("decision", decision);
|
||||||
|
if (note.trim()) fd.set("adminNote", note);
|
||||||
|
const result = await decideRefundAction(fd);
|
||||||
|
setLoading(false);
|
||||||
|
if (result.error) {
|
||||||
|
setError(result.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOpenAction(null);
|
||||||
|
setNote("");
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="rounded-2xl border border-neutral-200 bg-white p-5 shadow-sm sm:p-6">
|
||||||
|
<header className="flex flex-wrap items-start justify-between gap-3 border-b border-neutral-100 pb-4">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h3 className="truncate text-base font-bold text-neutral-900">
|
||||||
|
{refund.booking.trip.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-0.5 text-xs text-neutral-500">
|
||||||
|
Dilaporkan {formatDate(refund.createdAt)} oleh{" "}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{refund.reportedBy === "PARTICIPANT" ? "Peserta" : "Organizer"}
|
||||||
|
</span>
|
||||||
|
{" · "}
|
||||||
|
<span className="font-mono">{refund.id.slice(0, 8)}…</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<StatusPill status={refund.status} />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="mt-4 grid gap-4 sm:grid-cols-2">
|
||||||
|
<Field
|
||||||
|
label="Peserta booking"
|
||||||
|
value={`${refund.booking.user.name} · ${refund.booking.user.email}`}
|
||||||
|
/>
|
||||||
|
<Field label="Booking ID" value={refund.booking.id} mono />
|
||||||
|
<Field
|
||||||
|
label="Tanggal trip"
|
||||||
|
value={formatDate(refund.booking.trip.date)}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Alasan"
|
||||||
|
value={REASON_LABEL[refund.reason] ?? refund.reason}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Nominal refund"
|
||||||
|
value={`${formatRupiah(refund.amount)} ${refund.currency !== "IDR" ? `(${refund.currency})` : ""}`}
|
||||||
|
highlight
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Total dibayar"
|
||||||
|
value={
|
||||||
|
paidPayment
|
||||||
|
? `${formatRupiah(paidPayment.amount)} · ${paidPayment.provider} ${paidPayment.method ?? ""}`
|
||||||
|
: "—"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 rounded-xl bg-neutral-50 p-3 text-sm text-neutral-700">
|
||||||
|
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-neutral-500">
|
||||||
|
Isi laporan
|
||||||
|
</p>
|
||||||
|
<p className="whitespace-pre-wrap">{refund.reportNote}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{refund.adminNote && (
|
||||||
|
<div className="mt-3 rounded-xl bg-blue-50 p-3 text-sm text-blue-800">
|
||||||
|
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-blue-600">
|
||||||
|
Catatan admin
|
||||||
|
</p>
|
||||||
|
<p className="whitespace-pre-wrap">{refund.adminNote}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{refund.reviewedBy && refund.reviewedAt && (
|
||||||
|
<p className="mt-3 text-xs text-neutral-500">
|
||||||
|
Diproses oleh {refund.reviewedBy.name} pada{" "}
|
||||||
|
{formatDate(refund.reviewedAt)}
|
||||||
|
{refund.succeededAt && ` · uang keluar ${formatDate(refund.succeededAt)}`}
|
||||||
|
{refund.failedAt && ` · gagal ${formatDate(refund.failedAt)}`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(refund.status === "PENDING" || refund.status === "APPROVED") && (
|
||||||
|
<div className="mt-5 border-t border-neutral-100 pt-4">
|
||||||
|
{error && (
|
||||||
|
<div className="mb-3 rounded-lg bg-red-50 px-3 py-2 text-xs text-red-600">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{openAction ? (
|
||||||
|
<ActionForm
|
||||||
|
decision={openAction}
|
||||||
|
note={note}
|
||||||
|
setNote={setNote}
|
||||||
|
loading={loading}
|
||||||
|
onCancel={() => {
|
||||||
|
setOpenAction(null);
|
||||||
|
setNote("");
|
||||||
|
setError("");
|
||||||
|
}}
|
||||||
|
onConfirm={() => submit(openAction)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{refund.status === "PENDING" && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpenAction("APPROVE")}
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
✅ Setujui
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpenAction("REJECT")}
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-xl border border-red-200 bg-white px-4 py-2 text-sm font-bold text-red-600 hover:bg-red-50 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
❌ Tolak
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{refund.status === "APPROVED" && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpenAction("SUCCEEDED")}
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
💸 Tandai sudah ditransfer
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpenAction("FAILED")}
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-xl border border-red-200 bg-white px-4 py-2 text-sm font-bold text-red-600 hover:bg-red-50 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
⚠️ Tandai gagal
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActionForm({
|
||||||
|
decision,
|
||||||
|
note,
|
||||||
|
setNote,
|
||||||
|
loading,
|
||||||
|
onCancel,
|
||||||
|
onConfirm,
|
||||||
|
}: {
|
||||||
|
decision: Decision;
|
||||||
|
note: string;
|
||||||
|
setNote: (v: string) => void;
|
||||||
|
loading: boolean;
|
||||||
|
onCancel: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}) {
|
||||||
|
const cfg = {
|
||||||
|
APPROVE: {
|
||||||
|
label: "Setujui Refund",
|
||||||
|
placeholder: "Catatan untuk approval (opsional)",
|
||||||
|
required: false,
|
||||||
|
btnLabel: "Konfirmasi Setuju",
|
||||||
|
btnClass: "bg-primary-600 hover:bg-primary-700",
|
||||||
|
},
|
||||||
|
REJECT: {
|
||||||
|
label: "Tolak Refund",
|
||||||
|
placeholder: "Alasan penolakan (wajib)",
|
||||||
|
required: true,
|
||||||
|
btnLabel: "Konfirmasi Tolak",
|
||||||
|
btnClass: "bg-red-600 hover:bg-red-700",
|
||||||
|
},
|
||||||
|
SUCCEEDED: {
|
||||||
|
label: "Tandai Sudah Transfer",
|
||||||
|
placeholder: "Referensi transfer / nomor mutasi bank (wajib)",
|
||||||
|
required: true,
|
||||||
|
btnLabel: "Tandai SUCCEEDED",
|
||||||
|
btnClass: "bg-primary-600 hover:bg-primary-700",
|
||||||
|
},
|
||||||
|
FAILED: {
|
||||||
|
label: "Tandai Gagal",
|
||||||
|
placeholder: "Alasan gagal (wajib)",
|
||||||
|
required: true,
|
||||||
|
btnLabel: "Tandai FAILED",
|
||||||
|
btnClass: "bg-red-600 hover:bg-red-700",
|
||||||
|
},
|
||||||
|
}[decision];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-600">
|
||||||
|
{cfg.label}
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
value={note}
|
||||||
|
onChange={(e) => setNote(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
placeholder={cfg.placeholder}
|
||||||
|
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2 text-sm focus:bg-white"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onConfirm}
|
||||||
|
disabled={loading || (cfg.required && !note.trim())}
|
||||||
|
className={`rounded-xl px-4 py-2 text-sm font-bold text-white disabled:opacity-50 ${cfg.btnClass}`}
|
||||||
|
>
|
||||||
|
{loading ? "Memproses…" : cfg.btnLabel}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-xl border border-neutral-200 bg-white px-4 py-2 text-sm font-medium text-neutral-600 hover:bg-neutral-50 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Field({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
mono,
|
||||||
|
highlight,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
mono?: boolean;
|
||||||
|
highlight?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-500">
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className={`mt-0.5 text-sm ${mono ? "font-mono" : ""} ${
|
||||||
|
highlight ? "font-bold text-primary-700" : "text-neutral-800"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusPill({ status }: { status: RefundStatus }) {
|
||||||
|
const cfg: Record<RefundStatus, { label: string; cls: string }> = {
|
||||||
|
PENDING: {
|
||||||
|
label: "Pending Review",
|
||||||
|
cls: "bg-amber-50 text-amber-700 ring-amber-200",
|
||||||
|
},
|
||||||
|
APPROVED: {
|
||||||
|
label: "Disetujui",
|
||||||
|
cls: "bg-blue-50 text-blue-700 ring-blue-200",
|
||||||
|
},
|
||||||
|
REJECTED: { label: "Ditolak", cls: "bg-red-50 text-red-700 ring-red-200" },
|
||||||
|
PROCESSING: {
|
||||||
|
label: "Diproses",
|
||||||
|
cls: "bg-violet-50 text-violet-700 ring-violet-200",
|
||||||
|
},
|
||||||
|
SUCCEEDED: {
|
||||||
|
label: "Selesai",
|
||||||
|
cls: "bg-primary-50 text-primary-700 ring-primary-200",
|
||||||
|
},
|
||||||
|
FAILED: { label: "Gagal", cls: "bg-red-50 text-red-700 ring-red-200" },
|
||||||
|
};
|
||||||
|
const c = cfg[status];
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ring-1 ${c.cls}`}
|
||||||
|
>
|
||||||
|
{c.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
import { LIMITS } from "@/lib/limits";
|
||||||
|
|
||||||
|
const reasonValues = [
|
||||||
|
"USER_CANCELLATION",
|
||||||
|
"ORGANIZER_CANCELLED",
|
||||||
|
"TRIP_ISSUE",
|
||||||
|
"ADMIN_ADJUSTMENT",
|
||||||
|
"DISPUTE_RESOLVED",
|
||||||
|
"OTHER",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const reporterValues = ["PARTICIPANT", "ORGANIZER"] as const;
|
||||||
|
|
||||||
|
const refundNote = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(3, "Isi catatan minimal 3 karakter")
|
||||||
|
.max(
|
||||||
|
LIMITS.MAX_REFUND_NOTE_LENGTH,
|
||||||
|
`Catatan maksimal ${LIMITS.MAX_REFUND_NOTE_LENGTH} karakter`
|
||||||
|
);
|
||||||
|
|
||||||
|
export const createRefundSchema = z.object({
|
||||||
|
bookingId: z.string().trim().min(1, "Booking ID wajib"),
|
||||||
|
reason: z.enum(reasonValues, { error: "Alasan tidak valid" }),
|
||||||
|
reportedBy: z.enum(reporterValues, { error: "Pelapor tidak valid" }),
|
||||||
|
reportNote: refundNote,
|
||||||
|
/** Kosong = full remaining. Angka positif (IDR) untuk partial. */
|
||||||
|
amount: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.optional()
|
||||||
|
.transform((v) => (v && v.length > 0 ? Number(v.replace(/[^\d]/g, "")) : undefined))
|
||||||
|
.refine(
|
||||||
|
(n) => n === undefined || (Number.isInteger(n) && n > 0),
|
||||||
|
"Nominal harus bilangan bulat positif"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const refundDecisionSchema = z.object({
|
||||||
|
refundId: z.string().trim().min(1, "Refund ID wajib"),
|
||||||
|
decision: z.enum(["APPROVE", "REJECT", "SUCCEEDED", "FAILED"], {
|
||||||
|
error: "Keputusan tidak valid",
|
||||||
|
}),
|
||||||
|
adminNote: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(
|
||||||
|
LIMITS.MAX_REFUND_NOTE_LENGTH,
|
||||||
|
`Catatan maksimal ${LIMITS.MAX_REFUND_NOTE_LENGTH} karakter`
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateRefundInput = z.infer<typeof createRefundSchema>;
|
||||||
|
export type RefundDecisionInput = z.infer<typeof refundDecisionSchema>;
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
import type { OrganizerReviewItem } from "@/server/services/review.service";
|
||||||
|
|
||||||
|
interface OrganizerReviewsListProps {
|
||||||
|
reviews: OrganizerReviewItem[];
|
||||||
|
/// Total review keseluruhan organizer (bisa lebih besar dari `reviews.length`
|
||||||
|
/// kalau di-limit di service). Kalau tidak diberikan, fallback ke `reviews.length`.
|
||||||
|
totalCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OrganizerReviewsList({
|
||||||
|
reviews,
|
||||||
|
totalCount,
|
||||||
|
}: OrganizerReviewsListProps) {
|
||||||
|
if (reviews.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = totalCount ?? reviews.length;
|
||||||
|
const showingMore = total > reviews.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="mt-5 rounded-2xl border border-neutral-200 bg-white p-5 shadow-sm sm:p-6">
|
||||||
|
<div className="mb-4 flex flex-wrap items-baseline justify-between gap-2">
|
||||||
|
<h2 className="text-sm font-bold text-neutral-700 sm:text-base">
|
||||||
|
Ulasan dari peserta
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-neutral-500">
|
||||||
|
{showingMore
|
||||||
|
? `${reviews.length} terbaru dari ${total} ulasan`
|
||||||
|
: `${total} ulasan`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{reviews.map((r) => (
|
||||||
|
<li
|
||||||
|
key={r.id}
|
||||||
|
className="rounded-xl border border-neutral-100 bg-neutral-50/60 px-3 py-3 sm:px-4 sm:py-3.5"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
{r.user.image ? (
|
||||||
|
<Image
|
||||||
|
src={r.user.image}
|
||||||
|
alt=""
|
||||||
|
width={36}
|
||||||
|
height={36}
|
||||||
|
className="h-9 w-9 shrink-0 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary-600 text-xs font-bold text-white">
|
||||||
|
{r.user.name.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5">
|
||||||
|
<Link
|
||||||
|
href={`/u/${r.user.id}`}
|
||||||
|
className="text-xs font-semibold text-neutral-800 hover:text-primary-700 sm:text-sm"
|
||||||
|
>
|
||||||
|
{r.user.name}
|
||||||
|
</Link>
|
||||||
|
<span className="text-xs font-bold text-amber-600">
|
||||||
|
{"★".repeat(r.rating)}
|
||||||
|
<span className="text-neutral-300">
|
||||||
|
{"★".repeat(5 - r.rating)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[11px] text-neutral-500 sm:text-xs">
|
||||||
|
via{" "}
|
||||||
|
<Link
|
||||||
|
href={`/trips/${r.trip.id}`}
|
||||||
|
className="text-neutral-600 underline-offset-2 hover:text-primary-700 hover:underline"
|
||||||
|
>
|
||||||
|
{r.trip.title}
|
||||||
|
</Link>
|
||||||
|
<span className="text-neutral-400">
|
||||||
|
{" "}
|
||||||
|
· {formatReviewDate(r.createdAt)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{r.comment && (
|
||||||
|
<p className="mt-2 whitespace-pre-wrap text-xs leading-relaxed text-neutral-700 sm:text-sm">
|
||||||
|
{r.comment}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatReviewDate(date: Date): string {
|
||||||
|
return new Date(date).toLocaleDateString("id-ID", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -180,3 +180,27 @@ export async function rejectParticipantAction(
|
|||||||
return { error: (err as Error).message };
|
return { error: (err as Error).message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function cancelTripAction(tripId: string) {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
if (!session?.user) {
|
||||||
|
return { error: "Kamu harus login terlebih dahulu" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await tripService.closeTrip(tripId, session.user.id);
|
||||||
|
revalidatePath(`/trips/${tripId}`);
|
||||||
|
revalidatePath("/trips");
|
||||||
|
revalidatePath("/");
|
||||||
|
revalidatePath("/profile");
|
||||||
|
revalidatePath("/admin/refunds");
|
||||||
|
return {
|
||||||
|
success: true as const,
|
||||||
|
refundCount: result.refundsCreated.length,
|
||||||
|
cancelledCount: result.cancelledBookings.length,
|
||||||
|
skippedCount: result.skippedBookings.length,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return { error: (err as Error).message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { cancelTripAction } from "@/features/trip/actions";
|
||||||
|
|
||||||
|
interface CancelTripButtonProps {
|
||||||
|
tripId: string;
|
||||||
|
/** Jumlah peserta dengan booking PAID — preview impact. */
|
||||||
|
paidParticipantCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CancelTripButton({
|
||||||
|
tripId,
|
||||||
|
paidParticipantCount,
|
||||||
|
}: CancelTripButtonProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [confirmText, setConfirmText] = useState("");
|
||||||
|
const [result, setResult] = useState<
|
||||||
|
| { refundCount: number; cancelledCount: number; skippedCount: number }
|
||||||
|
| null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
async function handleConfirm() {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
const res = await cancelTripAction(tripId);
|
||||||
|
setLoading(false);
|
||||||
|
if ("error" in res) {
|
||||||
|
setError(res.error ?? "Terjadi kesalahan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setResult({
|
||||||
|
refundCount: res.refundCount,
|
||||||
|
cancelledCount: res.cancelledCount,
|
||||||
|
skippedCount: res.skippedCount,
|
||||||
|
});
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">
|
||||||
|
<p className="font-semibold">Trip dibatalkan.</p>
|
||||||
|
<ul className="mt-2 list-inside list-disc space-y-0.5 text-xs">
|
||||||
|
<li>{result.refundCount} refund dibuat (menunggu admin transfer)</li>
|
||||||
|
<li>
|
||||||
|
{result.cancelledCount} booking belum-bayar di-cancel langsung
|
||||||
|
</li>
|
||||||
|
{result.skippedCount > 0 && (
|
||||||
|
<li>
|
||||||
|
{result.skippedCount} booking di-skip (sudah punya refund aktif —
|
||||||
|
admin akan handle manual)
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
className="w-full rounded-xl border-2 border-red-200 py-3 text-sm font-bold text-red-600 transition-colors hover:bg-red-50"
|
||||||
|
>
|
||||||
|
Batalkan Trip
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requireConfirm = paidParticipantCount > 0;
|
||||||
|
const canSubmit = !requireConfirm || confirmText.trim() === "BATAL";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3 rounded-2xl border-2 border-red-200 bg-red-50 p-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-red-900">Yakin batalkan trip ini?</p>
|
||||||
|
<p className="mt-1 text-xs text-red-800/80">
|
||||||
|
Aksi ini <span className="font-semibold">tidak bisa di-undo</span>.
|
||||||
|
Trip akan ditandai CLOSED dan semua peserta dibatalkan.
|
||||||
|
{paidParticipantCount > 0 && (
|
||||||
|
<>
|
||||||
|
{" "}Sistem akan otomatis membuat{" "}
|
||||||
|
<span className="font-bold">
|
||||||
|
{paidParticipantCount} refund
|
||||||
|
</span>{" "}
|
||||||
|
full amount untuk peserta yang sudah membayar — admin SeTrip akan
|
||||||
|
memproses transfer.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{requireConfirm && (
|
||||||
|
<label className="block">
|
||||||
|
<span className="text-xs font-semibold uppercase tracking-wide text-red-700">
|
||||||
|
Ketik <span className="font-mono">BATAL</span> untuk konfirmasi
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
value={confirmText}
|
||||||
|
onChange={(e) => setConfirmText(e.target.value)}
|
||||||
|
placeholder="BATAL"
|
||||||
|
className="mt-1 w-full rounded-xl border border-red-300 bg-white px-3 py-2 font-mono text-sm focus:border-red-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-lg bg-white px-3 py-2 text-xs font-medium text-red-700">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={loading || !canSubmit}
|
||||||
|
className="rounded-xl bg-red-600 px-4 py-2 text-sm font-bold text-white hover:bg-red-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? "Memproses…" : "Ya, Batalkan Trip"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
setConfirmText("");
|
||||||
|
setError("");
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-xl border border-red-200 bg-white px-4 py-2 text-sm font-medium text-red-700 hover:bg-red-100 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -248,15 +248,21 @@ export function CreateTripForm({ isVerifiedOrganizer }: CreateTripFormProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="itinerary" className="mb-1.5 block text-sm font-semibold text-neutral-700">
|
<label htmlFor="itinerary" className="mb-1 block text-sm font-semibold text-neutral-700">
|
||||||
Itinerary
|
Itinerary
|
||||||
</label>
|
</label>
|
||||||
|
<p className="mb-1.5 text-[11px] text-neutral-500">
|
||||||
|
Tulis per hari supaya peserta tahu alur — itinerary lengkap bikin
|
||||||
|
trust naik drastis.
|
||||||
|
</p>
|
||||||
<textarea
|
<textarea
|
||||||
id="itinerary"
|
id="itinerary"
|
||||||
name="itinerary"
|
name="itinerary"
|
||||||
rows={5}
|
rows={6}
|
||||||
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2.5 text-sm text-neutral-800 placeholder:text-neutral-400 focus:bg-white"
|
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-4 py-2.5 text-sm text-neutral-800 placeholder:text-neutral-400 focus:bg-white"
|
||||||
placeholder={"Hari 1: …\nHari 2: …"}
|
placeholder={
|
||||||
|
"Hari 1: 05:00 kumpul di meeting point\n07:00 berangkat\n12:00 ishoma di rest area\n16:00 sampai basecamp, briefing\n\nHari 2: 04:00 summit attack\n08:00 kembali ke basecamp\n..."
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ interface JoinTripButtonProps {
|
|||||||
tripStatus: string;
|
tripStatus: string;
|
||||||
/** Tanggal berangkat trip sudah lewat */
|
/** Tanggal berangkat trip sudah lewat */
|
||||||
isDeparturePast?: boolean;
|
isDeparturePast?: boolean;
|
||||||
|
/** Sembunyikan tombol cancel — dipakai saat booking PAID dan parent
|
||||||
|
* menampilkan CancelBookingButton (refund flow) di tempat terpisah. */
|
||||||
|
hideCancelButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function JoinTripButton({
|
export function JoinTripButton({
|
||||||
@@ -36,6 +39,7 @@ export function JoinTripButton({
|
|||||||
isFull,
|
isFull,
|
||||||
tripStatus,
|
tripStatus,
|
||||||
isDeparturePast,
|
isDeparturePast,
|
||||||
|
hideCancelButton,
|
||||||
}: JoinTripButtonProps) {
|
}: JoinTripButtonProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -163,13 +167,15 @@ export function JoinTripButton({
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{isJoined ? (
|
{isJoined ? (
|
||||||
<button
|
hideCancelButton ? null : (
|
||||||
onClick={handleCancel}
|
<button
|
||||||
disabled={loading}
|
onClick={handleCancel}
|
||||||
className="w-full rounded-xl border-2 border-red-200 py-3 text-sm font-bold text-red-600 transition-colors hover:bg-red-50 disabled:opacity-50"
|
disabled={loading}
|
||||||
>
|
className="w-full rounded-xl border-2 border-red-200 py-3 text-sm font-bold text-red-600 transition-colors hover:bg-red-50 disabled:opacity-50"
|
||||||
{loading ? "Memproses..." : "Batal Ikut"}
|
>
|
||||||
</button>
|
{loading ? "Memproses..." : "Batal Ikut"}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={handleJoin}
|
onClick={handleJoin}
|
||||||
|
|||||||
@@ -56,10 +56,23 @@ export function OrganizerTrustPanel({
|
|||||||
<div className="flex flex-1 flex-wrap gap-3 border-t border-neutral-100 pt-3 sm:border-t-0 sm:border-l sm:pl-4 sm:pt-0">
|
<div className="flex flex-1 flex-wrap gap-3 border-t border-neutral-100 pt-3 sm:border-t-0 sm:border-l sm:pl-4 sm:pt-0">
|
||||||
<div className="min-w-[100px] rounded-lg bg-white/80 px-3 py-2 shadow-sm ring-1 ring-neutral-100">
|
<div className="min-w-[100px] rounded-lg bg-white/80 px-3 py-2 shadow-sm ring-1 ring-neutral-100">
|
||||||
<p className="text-[10px] font-medium text-neutral-500 sm:text-xs">
|
<p className="text-[10px] font-medium text-neutral-500 sm:text-xs">
|
||||||
Trip dibuat
|
Trip selesai
|
||||||
</p>
|
</p>
|
||||||
<p className="text-lg font-bold text-neutral-800">
|
<p className="text-lg font-bold text-primary-700">
|
||||||
{trust.tripsCreated}
|
{trust.tripsCompleted}
|
||||||
|
</p>
|
||||||
|
{trust.tripsCreated > trust.tripsCompleted && (
|
||||||
|
<p className="text-[10px] text-neutral-400">
|
||||||
|
+ {trust.tripsCreated - trust.tripsCompleted} berjalan
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-[100px] rounded-lg bg-white/80 px-3 py-2 shadow-sm ring-1 ring-neutral-100">
|
||||||
|
<p className="text-[10px] font-medium text-neutral-500 sm:text-xs">
|
||||||
|
Peserta dilayani
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font-bold text-secondary-700">
|
||||||
|
{trust.totalParticipantsServed}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-[100px] rounded-lg bg-white/80 px-3 py-2 shadow-sm ring-1 ring-neutral-100">
|
<div className="min-w-[100px] rounded-lg bg-white/80 px-3 py-2 shadow-sm ring-1 ring-neutral-100">
|
||||||
|
|||||||
@@ -31,4 +31,6 @@ export const LIMITS = {
|
|||||||
MAX_BANK_ACCOUNT_NUMBER_LENGTH: 32,
|
MAX_BANK_ACCOUNT_NUMBER_LENGTH: 32,
|
||||||
MAX_REJECTION_REASON_LENGTH: 500,
|
MAX_REJECTION_REASON_LENGTH: 500,
|
||||||
NIK_LENGTH: 16,
|
NIK_LENGTH: 16,
|
||||||
|
/** Catatan laporan dari peserta/organizer + catatan admin pada refund. */
|
||||||
|
MAX_REFUND_NOTE_LENGTH: 1000,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
+11
-1
@@ -162,7 +162,17 @@ export function verifyMidtransSignature(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Map kombinasi `transaction_status` + `fraud_status` Midtrans ke `PaymentStatus` internal.
|
* Map kombinasi `transaction_status` + `fraud_status` Midtrans ke `PaymentStatus` internal.
|
||||||
* Tabel rujukan ada di PAYMENT_ROADMAP.md PR C.
|
*
|
||||||
|
* | Midtrans | fraud_status | PaymentStatus |
|
||||||
|
* |---------------------|--------------|---------------|
|
||||||
|
* | capture | accept | PAID |
|
||||||
|
* | capture | challenge | AWAITING |
|
||||||
|
* | settlement | — | PAID |
|
||||||
|
* | pending | — | AWAITING |
|
||||||
|
* | deny | — | FAILED |
|
||||||
|
* | expire | — | EXPIRED |
|
||||||
|
* | cancel | — | CANCELLED |
|
||||||
|
* | refund / partial | — | REFUNDED |
|
||||||
*/
|
*/
|
||||||
export function mapMidtransStatus(
|
export function mapMidtransStatus(
|
||||||
transactionStatus: string,
|
transactionStatus: string,
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Refund policy hardcoded untuk MVP (PR-R3). Akan jadi data-driven di PR-R5.
|
||||||
|
*
|
||||||
|
* Aturan: hitung persentase refund berdasarkan jarak hari ke tanggal berangkat
|
||||||
|
* (UTC calendar day). Selalu integer rupiah — pakai Math.floor supaya tidak
|
||||||
|
* ada sub-rupiah dan total refund tidak pernah melebihi nominal yang dibayar.
|
||||||
|
*
|
||||||
|
* Tier:
|
||||||
|
* - ≥7 hari sebelum berangkat → 80% refund (organizer ambil 20% admin fee)
|
||||||
|
* - 3–6 hari sebelum berangkat → 50% refund
|
||||||
|
* - <3 hari sebelum berangkat / sudah lewat → 0% (tidak ada refund)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { utcStartOfDay } from "@/lib/trip-dates";
|
||||||
|
|
||||||
|
export interface RefundTier {
|
||||||
|
/** Minimum jumlah hari sebelum berangkat untuk tier ini. */
|
||||||
|
minDaysBefore: number;
|
||||||
|
/** Persentase nominal yang di-refund (0–100). */
|
||||||
|
refundPercentage: number;
|
||||||
|
/** Label untuk UI. */
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIERS: RefundTier[] = [
|
||||||
|
{ minDaysBefore: 7, refundPercentage: 80, label: "≥ 7 hari sebelum berangkat" },
|
||||||
|
{ minDaysBefore: 3, refundPercentage: 50, label: "3–6 hari sebelum berangkat" },
|
||||||
|
{ minDaysBefore: 0, refundPercentage: 0, label: "Kurang dari 3 hari / sudah lewat" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getRefundPolicyTiers(): RefundTier[] {
|
||||||
|
return TIERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jumlah hari kalender UTC dari sekarang ke tanggal berangkat. Negative kalau
|
||||||
|
* tanggal sudah lewat. Pakai start-of-day UTC supaya jam tidak mempengaruhi.
|
||||||
|
*/
|
||||||
|
export function daysUntilDeparture(
|
||||||
|
departureDate: Date,
|
||||||
|
now: Date = new Date()
|
||||||
|
): number {
|
||||||
|
const todayMs = utcStartOfDay(now).getTime();
|
||||||
|
const depMs = utcStartOfDay(departureDate).getTime();
|
||||||
|
const oneDayMs = 24 * 60 * 60 * 1000;
|
||||||
|
return Math.floor((depMs - todayMs) / oneDayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tier aktif untuk jumlah hari yang diberikan. */
|
||||||
|
export function getTierForDays(days: number): RefundTier {
|
||||||
|
for (const tier of TIERS) {
|
||||||
|
if (days >= tier.minDaysBefore) {
|
||||||
|
return tier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TIERS[TIERS.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hitung nominal refund (IDR integer) berdasarkan harga booking dan jarak ke
|
||||||
|
* tanggal berangkat. Floor supaya tidak pernah > bookingAmount.
|
||||||
|
*/
|
||||||
|
export function calculateRefundAmount(
|
||||||
|
bookingAmount: number,
|
||||||
|
days: number
|
||||||
|
): number {
|
||||||
|
if (bookingAmount <= 0) return 0;
|
||||||
|
const tier = getTierForDays(days);
|
||||||
|
return Math.floor((bookingAmount * tier.refundPercentage) / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefundPreview {
|
||||||
|
days: number;
|
||||||
|
tier: RefundTier;
|
||||||
|
refundAmount: number;
|
||||||
|
bookingAmount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bundle lengkap untuk display di UI — preview cancel booking. */
|
||||||
|
export function previewRefund(
|
||||||
|
bookingAmount: number,
|
||||||
|
departureDate: Date,
|
||||||
|
now: Date = new Date()
|
||||||
|
): RefundPreview {
|
||||||
|
const days = daysUntilDeparture(departureDate, now);
|
||||||
|
const tier = getTierForDays(days);
|
||||||
|
return {
|
||||||
|
days,
|
||||||
|
tier,
|
||||||
|
refundAmount: calculateRefundAmount(bookingAmount, days),
|
||||||
|
bookingAmount,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
/** Minimal trip sebagai organizer untuk badge "Trip leader" (heuristik MVP). */
|
/** Minimal trip sebagai organizer untuk badge "Trip leader" (heuristik MVP). */
|
||||||
export const TRIP_LEADER_MIN_TRIPS = 2;
|
export const TRIP_LEADER_MIN_TRIPS = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal sample (trip selesai + trip dibatalkan) sebelum completion rate
|
||||||
|
* ditampilkan ke publik. Mencegah angka menyesatkan untuk organizer baru:
|
||||||
|
* mis. 1 trip dibatalkan dari 1 trip = 0% — tidak fair sebagai sinyal trust.
|
||||||
|
*/
|
||||||
|
export const COMPLETION_RATE_MIN_SAMPLE = 3;
|
||||||
|
|
||||||
/** Bentuk data minimal untuk cek status verifikasi organizer. */
|
/** Bentuk data minimal untuk cek status verifikasi organizer. */
|
||||||
type WithOrganizerVerification = {
|
type WithOrganizerVerification = {
|
||||||
organizerVerification?: { status: "PENDING" | "APPROVED" | "REJECTED" } | null;
|
organizerVerification?: { status: "PENDING" | "APPROVED" | "REJECTED" } | null;
|
||||||
|
|||||||
Generated
+44
-44
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "setrip",
|
"name": "setrip",
|
||||||
"version": "0.10.0",
|
"version": "0.10.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "setrip",
|
"name": "setrip",
|
||||||
"version": "0.10.0",
|
"version": "0.10.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next-auth/prisma-adapter": "^1.0.7",
|
"@next-auth/prisma-adapter": "^1.0.7",
|
||||||
"@prisma/adapter-pg": "^7.7.0",
|
"@prisma/adapter-pg": "^7.7.0",
|
||||||
@@ -1657,9 +1657,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "16.2.5",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz",
|
||||||
"integrity": "sha512-Lb9ElHD2klcyeVD25vW+siPFqz9QMzDUSgvFZNO+dZEKoMHex4viJhVuzBhrXKqb+UKnih7mVYbt50/7KLsSCA==",
|
"integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@next/eslint-plugin-next": {
|
"node_modules/@next/eslint-plugin-next": {
|
||||||
@@ -1673,9 +1673,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-arm64": {
|
"node_modules/@next/swc-darwin-arm64": {
|
||||||
"version": "16.2.5",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz",
|
||||||
"integrity": "sha512-BW+8PGVmsruomXHsitD8JG6gny9lEdobctjBwvtPF8AKtxGDR7nR35FOl/oK9UAPXBOBm+vx0k8qtpeHOXQMGQ==",
|
"integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1689,9 +1689,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-x64": {
|
"node_modules/@next/swc-darwin-x64": {
|
||||||
"version": "16.2.5",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz",
|
||||||
"integrity": "sha512-ZoCGnCl9LlQJWmqXrZAUlNxvuNmclvE+7zUif+nDydkkehl9FKxHJ+wxSQMj+C37BYFerKiEdX9s9o02ir975Q==",
|
"integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1705,9 +1705,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||||
"version": "16.2.5",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz",
|
||||||
"integrity": "sha512-AwcZzMChaWkOTZt3vu+2ZMIj8g4dYQY+B8VUVhlFSQ2JtvyZpefyYHTe00D6b6L7BysYw7vl3zsvs9jix8tl5Q==",
|
"integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1724,9 +1724,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-musl": {
|
"node_modules/@next/swc-linux-arm64-musl": {
|
||||||
"version": "16.2.5",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz",
|
||||||
"integrity": "sha512-QqMgqWbCBFsfiQ7BF3dUlW8HJy1LWhpcqbTpoHMWA9IV+TnWwDKozQJA5NdIAHjQ00yX2Q7AUkLr/XK4n77q8A==",
|
"integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1743,9 +1743,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-gnu": {
|
"node_modules/@next/swc-linux-x64-gnu": {
|
||||||
"version": "16.2.5",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz",
|
||||||
"integrity": "sha512-3hzeiFGZtyATVx9pCeuzTshXmh50vHZitqaeZiyJZaUmjQyrfjsVUgS8apOj1vEJCIpKJM/55F45yPAV2kpjsA==",
|
"integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1762,9 +1762,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-musl": {
|
"node_modules/@next/swc-linux-x64-musl": {
|
||||||
"version": "16.2.5",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz",
|
||||||
"integrity": "sha512-0mzZV/mAt7Qj2tYNdTB6AqrS8dwng/AQLSYC5Z1YLpZdi2wxqKDPK7RY2RvjB1fXyJfOfdA3l/yTF5yLi+WfuQ==",
|
"integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1781,9 +1781,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||||
"version": "16.2.5",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz",
|
||||||
"integrity": "sha512-f/H4nZ2zJBvA8/+HpsB9mNonF9zfQoAU6D0WxJrfzhJDvJLfngVN85oqxUyrDVK99DIFfFYhLpGa5K+c5uotSw==",
|
"integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1797,9 +1797,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-x64-msvc": {
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
"version": "16.2.5",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz",
|
||||||
"integrity": "sha512-nuP7DHs4koAojsIxVPkihNgKiRUKtCU65j5X6DAbSy8VBrfT/o90bCLLHPf51JEdOZwZMFzM6e0NiGWfIWjVAg==",
|
"integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -4870,9 +4870,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -6563,12 +6563,12 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "16.2.5",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-16.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz",
|
||||||
"integrity": "sha512-TkVTm9F2WEulkgGljm4wPwNgvCCWCVw6StUHsZb8WZpHFRjepoUWg3d7L4IMg7IyjcJ4Co9eVhpro8e8O+KarQ==",
|
"integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "16.2.5",
|
"@next/env": "16.2.6",
|
||||||
"@swc/helpers": "0.5.15",
|
"@swc/helpers": "0.5.15",
|
||||||
"baseline-browser-mapping": "^2.9.19",
|
"baseline-browser-mapping": "^2.9.19",
|
||||||
"caniuse-lite": "^1.0.30001579",
|
"caniuse-lite": "^1.0.30001579",
|
||||||
@@ -6582,14 +6582,14 @@
|
|||||||
"node": ">=20.9.0"
|
"node": ">=20.9.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@next/swc-darwin-arm64": "16.2.5",
|
"@next/swc-darwin-arm64": "16.2.6",
|
||||||
"@next/swc-darwin-x64": "16.2.5",
|
"@next/swc-darwin-x64": "16.2.6",
|
||||||
"@next/swc-linux-arm64-gnu": "16.2.5",
|
"@next/swc-linux-arm64-gnu": "16.2.6",
|
||||||
"@next/swc-linux-arm64-musl": "16.2.5",
|
"@next/swc-linux-arm64-musl": "16.2.6",
|
||||||
"@next/swc-linux-x64-gnu": "16.2.5",
|
"@next/swc-linux-x64-gnu": "16.2.6",
|
||||||
"@next/swc-linux-x64-musl": "16.2.5",
|
"@next/swc-linux-x64-musl": "16.2.6",
|
||||||
"@next/swc-win32-arm64-msvc": "16.2.5",
|
"@next/swc-win32-arm64-msvc": "16.2.6",
|
||||||
"@next/swc-win32-x64-msvc": "16.2.5",
|
"@next/swc-win32-x64-msvc": "16.2.6",
|
||||||
"sharp": "^0.34.5"
|
"sharp": "^0.34.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "setrip",
|
"name": "setrip",
|
||||||
"version": "0.10.0",
|
"version": "0.10.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "BookingStatus" ADD VALUE 'PARTIALLY_REFUNDED';
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "RefundReason" AS ENUM ('USER_CANCELLATION', 'ORGANIZER_CANCELLED', 'TRIP_ISSUE', 'ADMIN_ADJUSTMENT', 'DISPUTE_RESOLVED', 'OTHER');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "RefundStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED', 'PROCESSING', 'SUCCEEDED', 'FAILED');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "RefundInitiator" AS ENUM ('USER', 'ORGANIZER', 'SYSTEM', 'ADMIN');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "RefundReporter" AS ENUM ('PARTICIPANT', 'ORGANIZER');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Refund" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"bookingId" TEXT NOT NULL,
|
||||||
|
"paymentId" TEXT,
|
||||||
|
"amount" INTEGER NOT NULL,
|
||||||
|
"currency" TEXT NOT NULL DEFAULT 'IDR',
|
||||||
|
"reason" "RefundReason" NOT NULL,
|
||||||
|
"reportedBy" "RefundReporter" NOT NULL,
|
||||||
|
"reportNote" TEXT NOT NULL,
|
||||||
|
"initiatedBy" "RefundInitiator" NOT NULL DEFAULT 'ADMIN',
|
||||||
|
"status" "RefundStatus" NOT NULL DEFAULT 'PENDING',
|
||||||
|
"idempotencyKey" TEXT NOT NULL,
|
||||||
|
"adminNote" TEXT,
|
||||||
|
"reviewedById" TEXT,
|
||||||
|
"reviewedAt" TIMESTAMP(3),
|
||||||
|
"succeededAt" TIMESTAMP(3),
|
||||||
|
"failedAt" TIMESTAMP(3),
|
||||||
|
"externalRefundId" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Refund_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Refund_idempotencyKey_key" ON "Refund"("idempotencyKey");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Refund_bookingId_status_idx" ON "Refund"("bookingId", "status");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Refund_status_createdAt_idx" ON "Refund"("status", "createdAt");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Refund" ADD CONSTRAINT "Refund_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Refund" ADD CONSTRAINT "Refund_paymentId_fkey" FOREIGN KEY ("paymentId") REFERENCES "Payment"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Refund" ADD CONSTRAINT "Refund_reviewedById_fkey" FOREIGN KEY ("reviewedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -32,6 +32,8 @@ model User {
|
|||||||
organizerVerification OrganizerVerification? @relation("OrganizerVerificationOwner")
|
organizerVerification OrganizerVerification? @relation("OrganizerVerificationOwner")
|
||||||
reviewedVerifications OrganizerVerification[] @relation("OrganizerVerificationReviewer")
|
reviewedVerifications OrganizerVerification[] @relation("OrganizerVerificationReviewer")
|
||||||
|
|
||||||
|
reviewedRefunds Refund[] @relation("RefundReviewer")
|
||||||
|
|
||||||
profile UserProfile?
|
profile UserProfile?
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,6 +260,7 @@ model Booking {
|
|||||||
status BookingStatus @default(PENDING)
|
status BookingStatus @default(PENDING)
|
||||||
|
|
||||||
payments Payment[]
|
payments Payment[]
|
||||||
|
refunds Refund[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -275,6 +278,7 @@ enum BookingStatus {
|
|||||||
PAID
|
PAID
|
||||||
CANCELLED
|
CANCELLED
|
||||||
REFUNDED
|
REFUNDED
|
||||||
|
PARTIALLY_REFUNDED
|
||||||
EXPIRED
|
EXPIRED
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,6 +311,8 @@ model Payment {
|
|||||||
failedAt DateTime?
|
failedAt DateTime?
|
||||||
rejectionReason String?
|
rejectionReason String?
|
||||||
|
|
||||||
|
refunds Refund[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@ -328,3 +334,105 @@ enum PaymentStatus {
|
|||||||
CANCELLED
|
CANCELLED
|
||||||
REFUNDED
|
REFUNDED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Refund = financial event terpisah dari Booking. Satu Booking bisa punya
|
||||||
|
/// banyak Refund (partial, multi-tahap). Setiap row auditable: kapan dibuat,
|
||||||
|
/// siapa melaporkan, siapa approve, kapan SUCCEEDED. Never delete — kalau
|
||||||
|
/// gagal, set status=FAILED + alasan.
|
||||||
|
///
|
||||||
|
/// Di MVP refund dimasukkan admin secara manual berdasarkan laporan dari
|
||||||
|
/// peserta atau organizer (via WhatsApp/email). Phase berikutnya akan
|
||||||
|
/// menambah self-service flow dari user dan organizer.
|
||||||
|
model Refund {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
bookingId String
|
||||||
|
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Restrict)
|
||||||
|
|
||||||
|
/// Payment yang di-refund. Opsional di MVP (manual transfer bisa tidak
|
||||||
|
/// terikat ke Payment row tertentu); wajib saat integrasi Midtrans (R-4).
|
||||||
|
paymentId String?
|
||||||
|
payment Payment? @relation(fields: [paymentId], references: [id], onDelete: Restrict)
|
||||||
|
|
||||||
|
/// Nominal refund dalam satuan terkecil (IDR rupiah, integer). Boleh < total
|
||||||
|
/// payment untuk partial. Service layer enforce SUM(SUCCEEDED) <= payment.amount.
|
||||||
|
amount Int
|
||||||
|
currency String @default("IDR")
|
||||||
|
|
||||||
|
reason RefundReason
|
||||||
|
|
||||||
|
/// Siapa yang melaporkan kebutuhan refund ini ke admin.
|
||||||
|
reportedBy RefundReporter
|
||||||
|
/// Isi laporan dari peserta/organizer yang admin terima (mis. WA, email).
|
||||||
|
reportNote String
|
||||||
|
|
||||||
|
/// Pihak yang membuat record di sistem. Di MVP selalu ADMIN; saat self-service
|
||||||
|
/// nanti USER/ORGANIZER, dan SYSTEM untuk auto-trigger dari trip dibatalkan.
|
||||||
|
initiatedBy RefundInitiator @default(ADMIN)
|
||||||
|
|
||||||
|
status RefundStatus @default(PENDING)
|
||||||
|
|
||||||
|
/// Idempotency key, dipakai saat panggil Midtrans Refund API di R-4. Generate
|
||||||
|
/// sekali saat create supaya retry gateway tidak double-refund.
|
||||||
|
idempotencyKey String @unique
|
||||||
|
|
||||||
|
/// Catatan admin: alasan tolak, referensi transfer manual, dst. Bebas teks.
|
||||||
|
adminNote String?
|
||||||
|
|
||||||
|
/// Admin yang terakhir mengubah status (approve/reject/mark-succeeded/failed).
|
||||||
|
reviewedById String?
|
||||||
|
reviewedBy User? @relation("RefundReviewer", fields: [reviewedById], references: [id], onDelete: SetNull)
|
||||||
|
reviewedAt DateTime?
|
||||||
|
|
||||||
|
succeededAt DateTime?
|
||||||
|
failedAt DateTime?
|
||||||
|
|
||||||
|
/// ID refund di gateway (mis. Midtrans refund_id). Kosong untuk manual transfer.
|
||||||
|
externalRefundId String?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([bookingId, status])
|
||||||
|
@@index([status, createdAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RefundReason {
|
||||||
|
/// Peserta cancel booking sendiri (mengikuti refund window policy).
|
||||||
|
USER_CANCELLATION
|
||||||
|
/// Organizer membatalkan trip — peserta dapat full refund.
|
||||||
|
ORGANIZER_CANCELLED
|
||||||
|
/// Masalah saat/setelah trip (mis. itinerary tidak sesuai).
|
||||||
|
TRIP_ISSUE
|
||||||
|
/// Penyesuaian dari admin (kompensasi, koreksi nominal, dll.).
|
||||||
|
ADMIN_ADJUSTMENT
|
||||||
|
/// Hasil resolusi dispute / chargeback bank.
|
||||||
|
DISPUTE_RESOLVED
|
||||||
|
OTHER
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RefundStatus {
|
||||||
|
/// Baru dilaporkan, menunggu review admin.
|
||||||
|
PENDING
|
||||||
|
/// Admin sudah setujui, siap dieksekusi (manual transfer / gateway).
|
||||||
|
APPROVED
|
||||||
|
/// Admin tolak (alasan di `adminNote`).
|
||||||
|
REJECTED
|
||||||
|
/// (R-4) Request sudah dikirim ke gateway, menunggu callback.
|
||||||
|
PROCESSING
|
||||||
|
/// Uang sudah keluar dari kas Setrip / merchant gateway.
|
||||||
|
SUCCEEDED
|
||||||
|
/// Eksekusi gagal (alasan di `adminNote`). Record tidak dihapus.
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RefundInitiator {
|
||||||
|
USER
|
||||||
|
ORGANIZER
|
||||||
|
SYSTEM
|
||||||
|
ADMIN
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RefundReporter {
|
||||||
|
PARTICIPANT
|
||||||
|
ORGANIZER
|
||||||
|
}
|
||||||
|
|||||||
+1257
-436
File diff suppressed because it is too large
Load Diff
@@ -71,4 +71,17 @@ export const bookingRepo = {
|
|||||||
data: { status },
|
data: { status },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jumlah booking PAID/PARTIALLY_REFUNDED di trip. Dipakai untuk preview
|
||||||
|
* dampak cancel-trip (berapa peserta yang akan dapat auto-refund).
|
||||||
|
*/
|
||||||
|
async countSettledForTrip(tripId: string) {
|
||||||
|
return prisma.booking.count({
|
||||||
|
where: {
|
||||||
|
tripId,
|
||||||
|
status: { in: ["PAID", "PARTIALLY_REFUNDED"] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { Prisma } from "@/app/generated/prisma/client";
|
||||||
|
|
||||||
|
const refundListInclude = {
|
||||||
|
booking: {
|
||||||
|
include: {
|
||||||
|
trip: { select: { id: true, title: true, date: true, organizerId: true } },
|
||||||
|
user: { select: { id: true, name: true, email: true, image: true } },
|
||||||
|
payments: {
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
provider: true,
|
||||||
|
method: true,
|
||||||
|
amount: true,
|
||||||
|
status: true,
|
||||||
|
paidAt: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
payment: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
provider: true,
|
||||||
|
method: true,
|
||||||
|
amount: true,
|
||||||
|
status: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reviewedBy: { select: { id: true, name: true, email: true } },
|
||||||
|
} satisfies Prisma.RefundInclude;
|
||||||
|
|
||||||
|
export const refundRepo = {
|
||||||
|
async findById(id: string) {
|
||||||
|
return prisma.refund.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: refundListInclude,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async listByStatus(
|
||||||
|
status?: "PENDING" | "APPROVED" | "REJECTED" | "PROCESSING" | "SUCCEEDED" | "FAILED"
|
||||||
|
) {
|
||||||
|
return prisma.refund.findMany({
|
||||||
|
where: status ? { status } : undefined,
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
include: refundListInclude,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async listByBooking(bookingId: string) {
|
||||||
|
return prisma.refund.findMany({
|
||||||
|
where: { bookingId },
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Total nominal yang sudah SUCCEEDED untuk satu booking. Dipakai service untuk
|
||||||
|
* validasi `SUM(SUCCEEDED) + new.amount <= payment.amount`. */
|
||||||
|
async sumSucceededAmount(bookingId: string, tx?: Prisma.TransactionClient): Promise<number> {
|
||||||
|
const client = tx ?? prisma;
|
||||||
|
const agg = await client.refund.aggregate({
|
||||||
|
where: { bookingId, status: "SUCCEEDED" },
|
||||||
|
_sum: { amount: true },
|
||||||
|
});
|
||||||
|
return agg._sum.amount ?? 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Pending + approved + processing — refund yang "in-flight" (belum settled).
|
||||||
|
* Dipakai untuk cek apakah booking masih punya refund aktif. */
|
||||||
|
async hasActiveRefund(bookingId: string, tx?: Prisma.TransactionClient): Promise<boolean> {
|
||||||
|
const client = tx ?? prisma;
|
||||||
|
const count = await client.refund.count({
|
||||||
|
where: {
|
||||||
|
bookingId,
|
||||||
|
status: { in: ["PENDING", "APPROVED", "PROCESSING"] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return count > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
async create(
|
||||||
|
data: Pick<
|
||||||
|
Prisma.RefundUncheckedCreateInput,
|
||||||
|
| "bookingId"
|
||||||
|
| "paymentId"
|
||||||
|
| "amount"
|
||||||
|
| "reason"
|
||||||
|
| "reportedBy"
|
||||||
|
| "reportNote"
|
||||||
|
| "initiatedBy"
|
||||||
|
| "idempotencyKey"
|
||||||
|
>,
|
||||||
|
tx?: Prisma.TransactionClient
|
||||||
|
) {
|
||||||
|
const client = tx ?? prisma;
|
||||||
|
return client.refund.create({ data });
|
||||||
|
},
|
||||||
|
|
||||||
|
async update(
|
||||||
|
id: string,
|
||||||
|
data: Prisma.RefundUncheckedUpdateInput,
|
||||||
|
tx?: Prisma.TransactionClient
|
||||||
|
) {
|
||||||
|
const client = tx ?? prisma;
|
||||||
|
return client.refund.update({ where: { id }, data });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RefundWithRelations = Awaited<ReturnType<typeof refundRepo.findById>>;
|
||||||
@@ -23,4 +23,24 @@ export const reviewRepo = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Semua review untuk trip yang di-host organizer ini, terbaru di depan.
|
||||||
|
* Dipakai di profil publik organizer untuk menampilkan testimoni.
|
||||||
|
*/
|
||||||
|
async findByOrganizer(organizerId: string, limit = 20) {
|
||||||
|
return prisma.tripReview.findMany({
|
||||||
|
where: { trip: { organizerId } },
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
take: limit,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
rating: true,
|
||||||
|
comment: true,
|
||||||
|
createdAt: true,
|
||||||
|
user: { select: { id: true, name: true, image: true } },
|
||||||
|
trip: { select: { id: true, title: true, destination: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -211,4 +211,39 @@ export const tripRepo = {
|
|||||||
async updateStatus(id: string, status: "OPEN" | "FULL" | "CLOSED" | "COMPLETED") {
|
async updateStatus(id: string, status: "OPEN" | "FULL" | "CLOSED" | "COMPLETED") {
|
||||||
return prisma.trip.update({ where: { id }, data: { status } });
|
return prisma.trip.update({ where: { id }, data: { status } });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk transisi trip yang sudah lewat `cutoff` (start of today UTC) dari
|
||||||
|
* status OPEN/FULL ke COMPLETED. Idempotent — second run tidak akan match
|
||||||
|
* apa-apa karena status sudah berubah.
|
||||||
|
*
|
||||||
|
* Returns daftar id yang ter-update untuk telemetri/log.
|
||||||
|
*/
|
||||||
|
async bulkCompletePastTrips(cutoff: Date) {
|
||||||
|
const trips = await prisma.trip.findMany({
|
||||||
|
where: {
|
||||||
|
status: { in: ["OPEN", "FULL"] },
|
||||||
|
OR: [
|
||||||
|
{ endDate: { lt: cutoff } },
|
||||||
|
{ AND: [{ endDate: null }, { date: { lt: cutoff } }] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (trips.length === 0) {
|
||||||
|
return { count: 0, ids: [] as string[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = trips.map((t) => t.id);
|
||||||
|
const result = await prisma.trip.updateMany({
|
||||||
|
where: {
|
||||||
|
id: { in: ids },
|
||||||
|
status: { in: ["OPEN", "FULL"] },
|
||||||
|
},
|
||||||
|
data: { status: "COMPLETED" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { count: result.count, ids };
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,502 @@
|
|||||||
|
import { randomBytes } from "crypto";
|
||||||
|
import { Prisma } from "@/app/generated/prisma/client";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { refundRepo } from "@/server/repositories/refund.repo";
|
||||||
|
import { calculateRefundAmount, daysUntilDeparture } from "@/lib/refund-policy";
|
||||||
|
import { isTripDepartureDayPast } from "@/lib/trip-dates";
|
||||||
|
|
||||||
|
const SERIAL_TX_ATTEMPTS = 6;
|
||||||
|
|
||||||
|
function isSerializationConflict(err: unknown): boolean {
|
||||||
|
return (
|
||||||
|
typeof err === "object" &&
|
||||||
|
err !== null &&
|
||||||
|
"code" in err &&
|
||||||
|
(err as { code: string }).code === "P2034"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runSerializable<T>(fn: (tx: Prisma.TransactionClient) => Promise<T>): Promise<T> {
|
||||||
|
let lastErr: unknown;
|
||||||
|
for (let attempt = 0; attempt < SERIAL_TX_ATTEMPTS; attempt++) {
|
||||||
|
try {
|
||||||
|
return await prisma.$transaction(fn, {
|
||||||
|
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
|
||||||
|
maxWait: 5000,
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
lastErr = e;
|
||||||
|
if (isSerializationConflict(e) && attempt < SERIAL_TX_ATTEMPTS - 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastErr instanceof Error
|
||||||
|
? lastErr
|
||||||
|
: new Error("Gagal memproses refund. Coba lagi sebentar.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function newIdempotencyKey(): string {
|
||||||
|
return `refund_${randomBytes(16).toString("hex")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestRefundInput = {
|
||||||
|
bookingId: string;
|
||||||
|
reason:
|
||||||
|
| "USER_CANCELLATION"
|
||||||
|
| "ORGANIZER_CANCELLED"
|
||||||
|
| "TRIP_ISSUE"
|
||||||
|
| "ADMIN_ADJUSTMENT"
|
||||||
|
| "DISPUTE_RESOLVED"
|
||||||
|
| "OTHER";
|
||||||
|
reportedBy: "PARTICIPANT" | "ORGANIZER";
|
||||||
|
reportNote: string;
|
||||||
|
/** Nominal refund (IDR). Kalau tidak diisi → service akan pakai sisa
|
||||||
|
* refundable amount (payment.amount - sudah-di-refund). */
|
||||||
|
amount?: number;
|
||||||
|
/** Admin yang memasukkan laporan ke sistem. */
|
||||||
|
initiatedByAdminId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refundService = {
|
||||||
|
/**
|
||||||
|
* Admin mencatat laporan refund dari peserta atau organizer ke sistem.
|
||||||
|
* Status awal: PENDING. Belum mengubah Booking/Payment status.
|
||||||
|
*
|
||||||
|
* Idempotency: kalau booking masih punya refund PENDING/APPROVED/PROCESSING,
|
||||||
|
* tolak — admin harus selesaikan yang lama dulu (reject atau succeeded).
|
||||||
|
*/
|
||||||
|
async requestRefund(input: RequestRefundInput) {
|
||||||
|
return runSerializable(async (tx) => {
|
||||||
|
const booking = await tx.booking.findUnique({
|
||||||
|
where: { id: input.bookingId },
|
||||||
|
include: {
|
||||||
|
payments: {
|
||||||
|
where: { status: "PAID" },
|
||||||
|
orderBy: { paidAt: "desc" },
|
||||||
|
take: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!booking) {
|
||||||
|
throw new Error("Booking tidak ditemukan");
|
||||||
|
}
|
||||||
|
if (booking.amount <= 0) {
|
||||||
|
throw new Error("Booking gratis — tidak ada nominal untuk di-refund");
|
||||||
|
}
|
||||||
|
if (booking.status === "CANCELLED" || booking.status === "EXPIRED") {
|
||||||
|
throw new Error(
|
||||||
|
"Booking sudah dibatalkan/expired — tidak ada pembayaran untuk di-refund"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (booking.status === "REFUNDED") {
|
||||||
|
throw new Error("Booking sudah refund penuh");
|
||||||
|
}
|
||||||
|
|
||||||
|
const paidPayment = booking.payments[0];
|
||||||
|
if (!paidPayment) {
|
||||||
|
throw new Error(
|
||||||
|
"Tidak ada Payment dengan status PAID di booking ini — tidak bisa di-refund"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasActive = await refundRepo.hasActiveRefund(input.bookingId, tx);
|
||||||
|
if (hasActive) {
|
||||||
|
throw new Error(
|
||||||
|
"Booking ini masih punya refund yang sedang diproses. Selesaikan dulu sebelum membuat yang baru."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const alreadyRefunded = await refundRepo.sumSucceededAmount(input.bookingId, tx);
|
||||||
|
const remaining = paidPayment.amount - alreadyRefunded;
|
||||||
|
if (remaining <= 0) {
|
||||||
|
throw new Error("Seluruh nominal sudah di-refund");
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = input.amount ?? remaining;
|
||||||
|
if (!Number.isInteger(amount) || amount <= 0) {
|
||||||
|
throw new Error("Nominal refund harus bilangan bulat positif");
|
||||||
|
}
|
||||||
|
if (amount > remaining) {
|
||||||
|
throw new Error(
|
||||||
|
`Nominal refund (Rp ${amount.toLocaleString("id-ID")}) melebihi sisa yang bisa di-refund (Rp ${remaining.toLocaleString("id-ID")})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return refundRepo.create(
|
||||||
|
{
|
||||||
|
bookingId: input.bookingId,
|
||||||
|
paymentId: paidPayment.id,
|
||||||
|
amount,
|
||||||
|
reason: input.reason,
|
||||||
|
reportedBy: input.reportedBy,
|
||||||
|
reportNote: input.reportNote,
|
||||||
|
initiatedBy: "ADMIN",
|
||||||
|
idempotencyKey: newIdempotencyKey(),
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** PENDING → APPROVED. Boleh menambah catatan admin (opsional). */
|
||||||
|
async approveRefund(input: { refundId: string; adminId: string; adminNote?: string }) {
|
||||||
|
return runSerializable(async (tx) => {
|
||||||
|
const refund = await tx.refund.findUnique({ where: { id: input.refundId } });
|
||||||
|
if (!refund) {
|
||||||
|
throw new Error("Refund tidak ditemukan");
|
||||||
|
}
|
||||||
|
if (refund.status !== "PENDING") {
|
||||||
|
throw new Error("Hanya refund berstatus PENDING yang bisa disetujui");
|
||||||
|
}
|
||||||
|
return refundRepo.update(
|
||||||
|
input.refundId,
|
||||||
|
{
|
||||||
|
status: "APPROVED",
|
||||||
|
reviewedById: input.adminId,
|
||||||
|
reviewedAt: new Date(),
|
||||||
|
adminNote: input.adminNote ?? refund.adminNote,
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** PENDING → REJECTED. Alasan wajib supaya audit trail jelas. */
|
||||||
|
async rejectRefund(input: { refundId: string; adminId: string; adminNote: string }) {
|
||||||
|
if (!input.adminNote.trim()) {
|
||||||
|
throw new Error("Alasan tolak wajib diisi");
|
||||||
|
}
|
||||||
|
return runSerializable(async (tx) => {
|
||||||
|
const refund = await tx.refund.findUnique({ where: { id: input.refundId } });
|
||||||
|
if (!refund) {
|
||||||
|
throw new Error("Refund tidak ditemukan");
|
||||||
|
}
|
||||||
|
if (refund.status !== "PENDING") {
|
||||||
|
throw new Error("Hanya refund berstatus PENDING yang bisa ditolak");
|
||||||
|
}
|
||||||
|
return refundRepo.update(
|
||||||
|
input.refundId,
|
||||||
|
{
|
||||||
|
status: "REJECTED",
|
||||||
|
reviewedById: input.adminId,
|
||||||
|
reviewedAt: new Date(),
|
||||||
|
adminNote: input.adminNote.trim(),
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APPROVED → SUCCEEDED untuk manual transfer (admin sudah transfer manual
|
||||||
|
* ke rekening peserta). adminNote diharapkan berisi referensi transfer.
|
||||||
|
*
|
||||||
|
* Side effects:
|
||||||
|
* - Update Payment.status → REFUNDED hanya saat full refund.
|
||||||
|
* - Update Booking.status → REFUNDED (full) atau PARTIALLY_REFUNDED (partial).
|
||||||
|
* - Untuk USER_CANCELLATION: bebaskan slot — set TripParticipant → CANCELLED
|
||||||
|
* dan re-open Trip (FULL → OPEN) kalau peserta aktif < maxParticipants.
|
||||||
|
* Untuk ORGANIZER_CANCELLED slot tidak perlu dibebaskan (trip sudah CLOSED).
|
||||||
|
*/
|
||||||
|
async markSucceededManual(input: {
|
||||||
|
refundId: string;
|
||||||
|
adminId: string;
|
||||||
|
adminNote: string;
|
||||||
|
}) {
|
||||||
|
if (!input.adminNote.trim()) {
|
||||||
|
throw new Error("Catatan/referensi transfer wajib diisi");
|
||||||
|
}
|
||||||
|
return runSerializable(async (tx) => {
|
||||||
|
const refund = await tx.refund.findUnique({
|
||||||
|
where: { id: input.refundId },
|
||||||
|
});
|
||||||
|
if (!refund) {
|
||||||
|
throw new Error("Refund tidak ditemukan");
|
||||||
|
}
|
||||||
|
if (refund.status !== "APPROVED") {
|
||||||
|
throw new Error(
|
||||||
|
"Hanya refund APPROVED yang bisa ditandai SUCCEEDED. Setujui dulu."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
await refundRepo.update(
|
||||||
|
input.refundId,
|
||||||
|
{
|
||||||
|
status: "SUCCEEDED",
|
||||||
|
succeededAt: now,
|
||||||
|
reviewedById: input.adminId,
|
||||||
|
reviewedAt: now,
|
||||||
|
adminNote: input.adminNote.trim(),
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalRefunded = await refundRepo.sumSucceededAmount(
|
||||||
|
refund.bookingId,
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (refund.paymentId) {
|
||||||
|
const payment = await tx.payment.findUnique({
|
||||||
|
where: { id: refund.paymentId },
|
||||||
|
});
|
||||||
|
if (payment && totalRefunded >= payment.amount) {
|
||||||
|
await tx.payment.update({
|
||||||
|
where: { id: payment.id },
|
||||||
|
data: { status: "REFUNDED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const booking = await tx.booking.findUnique({
|
||||||
|
where: { id: refund.bookingId },
|
||||||
|
include: {
|
||||||
|
trip: { select: { id: true, status: true, maxParticipants: true } },
|
||||||
|
payments: {
|
||||||
|
where: { status: { in: ["PAID", "REFUNDED"] } },
|
||||||
|
orderBy: { paidAt: "desc" },
|
||||||
|
take: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!booking) {
|
||||||
|
throw new Error("Booking tidak ditemukan saat menutup refund");
|
||||||
|
}
|
||||||
|
const paid = booking.payments[0];
|
||||||
|
if (paid) {
|
||||||
|
const nextStatus =
|
||||||
|
totalRefunded >= paid.amount ? "REFUNDED" : "PARTIALLY_REFUNDED";
|
||||||
|
if (booking.status !== nextStatus) {
|
||||||
|
await tx.booking.update({
|
||||||
|
where: { id: booking.id },
|
||||||
|
data: { status: nextStatus },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slot release untuk user cancellation. Organizer cancel di-handle
|
||||||
|
// closeTrip (participant + trip sudah di-CANCELLED/CLOSED di sana).
|
||||||
|
if (refund.reason === "USER_CANCELLATION") {
|
||||||
|
await tx.tripParticipant.updateMany({
|
||||||
|
where: {
|
||||||
|
id: booking.participantId,
|
||||||
|
status: { not: "CANCELLED" },
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: "CANCELLED",
|
||||||
|
markedPaidAt: null,
|
||||||
|
paymentConfirmedAt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (booking.trip.status === "FULL") {
|
||||||
|
const remaining = await tx.tripParticipant.count({
|
||||||
|
where: { tripId: booking.tripId, status: { not: "CANCELLED" } },
|
||||||
|
});
|
||||||
|
if (remaining < booking.trip.maxParticipants) {
|
||||||
|
await tx.trip.update({
|
||||||
|
where: { id: booking.tripId },
|
||||||
|
data: { status: "OPEN" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true as const };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Peserta cancel booking sendiri. Hitung refund pakai policy default
|
||||||
|
* (lib/refund-policy.ts) — hardcoded MVP, akan jadi data-driven di R-5.
|
||||||
|
*
|
||||||
|
* Behaviour:
|
||||||
|
* - Kalau hasil hitung = 0 (di luar window): cancel participant + booking
|
||||||
|
* langsung, tanpa Refund row (uang tidak balik).
|
||||||
|
* - Kalau hasil hitung > 0: buat Refund PENDING (initiatedBy=USER,
|
||||||
|
* reportedBy=PARTICIPANT, reason=USER_CANCELLATION). Participant + booking
|
||||||
|
* TETAP CONFIRMED/PAID sampai admin mark SUCCEEDED — slot baru bebas saat
|
||||||
|
* refund tuntas. Cegah double-request via hasActiveRefund.
|
||||||
|
*/
|
||||||
|
async requestUserCancellation(input: {
|
||||||
|
bookingId: string;
|
||||||
|
userId: string;
|
||||||
|
}) {
|
||||||
|
return runSerializable(async (tx) => {
|
||||||
|
const booking = await tx.booking.findUnique({
|
||||||
|
where: { id: input.bookingId },
|
||||||
|
include: {
|
||||||
|
trip: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
date: true,
|
||||||
|
status: true,
|
||||||
|
maxParticipants: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
payments: {
|
||||||
|
where: { status: "PAID" },
|
||||||
|
orderBy: { paidAt: "desc" },
|
||||||
|
take: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!booking) {
|
||||||
|
throw new Error("Booking tidak ditemukan");
|
||||||
|
}
|
||||||
|
if (booking.userId !== input.userId) {
|
||||||
|
throw new Error("Booking ini bukan milikmu");
|
||||||
|
}
|
||||||
|
if (booking.status !== "PAID") {
|
||||||
|
throw new Error(
|
||||||
|
"Hanya booking PAID yang bisa cancel dengan refund. Booking yang belum lunas bisa cancel dari tombol 'Batal Ikut'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isTripDepartureDayPast(booking.trip.date)) {
|
||||||
|
throw new Error(
|
||||||
|
"Trip sudah lewat tanggal berangkat — pembatalan ditutup"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const paid = booking.payments[0];
|
||||||
|
if (!paid) {
|
||||||
|
throw new Error(
|
||||||
|
"Tidak ada Payment dengan status PAID di booking ini"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = daysUntilDeparture(booking.trip.date);
|
||||||
|
const refundAmount = calculateRefundAmount(paid.amount, days);
|
||||||
|
|
||||||
|
if (refundAmount === 0) {
|
||||||
|
await tx.tripParticipant.update({
|
||||||
|
where: { id: booking.participantId },
|
||||||
|
data: {
|
||||||
|
status: "CANCELLED",
|
||||||
|
markedPaidAt: null,
|
||||||
|
paymentConfirmedAt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await tx.booking.update({
|
||||||
|
where: { id: booking.id },
|
||||||
|
data: { status: "CANCELLED" },
|
||||||
|
});
|
||||||
|
if (booking.trip.status === "FULL") {
|
||||||
|
const remaining = await tx.tripParticipant.count({
|
||||||
|
where: { tripId: booking.tripId, status: { not: "CANCELLED" } },
|
||||||
|
});
|
||||||
|
if (remaining < booking.trip.maxParticipants) {
|
||||||
|
await tx.trip.update({
|
||||||
|
where: { id: booking.tripId },
|
||||||
|
data: { status: "OPEN" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind: "CANCELLED_NO_REFUND" as const,
|
||||||
|
days,
|
||||||
|
refundAmount: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasActive = await refundRepo.hasActiveRefund(input.bookingId, tx);
|
||||||
|
if (hasActive) {
|
||||||
|
throw new Error(
|
||||||
|
"Booking ini sudah punya refund yang sedang diproses"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const percentage = Math.floor((refundAmount * 100) / paid.amount);
|
||||||
|
const refund = await refundRepo.create(
|
||||||
|
{
|
||||||
|
bookingId: booking.id,
|
||||||
|
paymentId: paid.id,
|
||||||
|
amount: refundAmount,
|
||||||
|
reason: "USER_CANCELLATION",
|
||||||
|
reportedBy: "PARTICIPANT",
|
||||||
|
reportNote: `Self-service cancel oleh peserta — H-${days} dari tanggal berangkat (refund ${percentage}%).`,
|
||||||
|
initiatedBy: "USER",
|
||||||
|
idempotencyKey: newIdempotencyKey(),
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: "REFUND_PENDING" as const,
|
||||||
|
refundId: refund.id,
|
||||||
|
days,
|
||||||
|
refundAmount,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dipanggil dari tripService.closeTrip (organizer cancel trip) dengan tx
|
||||||
|
* yang sama. Buat Refund auto-approved untuk satu booking PAID. Tidak
|
||||||
|
* mengecek hasActiveRefund (caller harus filter dulu) supaya batch closeTrip
|
||||||
|
* idempotent dengan retry-safe.
|
||||||
|
*
|
||||||
|
* Refund langsung APPROVED — policy jelas (organizer cancel = 100% refund),
|
||||||
|
* tapi eksekusi (SUCCEEDED) tetap manual oleh admin.
|
||||||
|
*/
|
||||||
|
async createSystemRefundForClosedTrip(
|
||||||
|
tx: Prisma.TransactionClient,
|
||||||
|
input: {
|
||||||
|
bookingId: string;
|
||||||
|
paymentId: string;
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const now = new Date();
|
||||||
|
return tx.refund.create({
|
||||||
|
data: {
|
||||||
|
bookingId: input.bookingId,
|
||||||
|
paymentId: input.paymentId,
|
||||||
|
amount: input.amount,
|
||||||
|
reason: "ORGANIZER_CANCELLED",
|
||||||
|
reportedBy: "ORGANIZER",
|
||||||
|
reportNote: "Organizer membatalkan trip — auto-create oleh SYSTEM.",
|
||||||
|
initiatedBy: "SYSTEM",
|
||||||
|
idempotencyKey: newIdempotencyKey(),
|
||||||
|
status: "APPROVED",
|
||||||
|
reviewedAt: now,
|
||||||
|
adminNote: "Auto-approved (SYSTEM): organizer cancel = full refund.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APPROVED/PROCESSING → FAILED. Catatan wajib (alasan gagal).
|
||||||
|
* Tidak mengubah Booking/Payment — uang belum keluar.
|
||||||
|
*/
|
||||||
|
async markFailed(input: { refundId: string; adminId: string; adminNote: string }) {
|
||||||
|
if (!input.adminNote.trim()) {
|
||||||
|
throw new Error("Alasan gagal wajib diisi");
|
||||||
|
}
|
||||||
|
return runSerializable(async (tx) => {
|
||||||
|
const refund = await tx.refund.findUnique({ where: { id: input.refundId } });
|
||||||
|
if (!refund) {
|
||||||
|
throw new Error("Refund tidak ditemukan");
|
||||||
|
}
|
||||||
|
if (refund.status !== "APPROVED" && refund.status !== "PROCESSING") {
|
||||||
|
throw new Error(
|
||||||
|
"Hanya refund APPROVED atau PROCESSING yang bisa ditandai FAILED"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return refundRepo.update(
|
||||||
|
input.refundId,
|
||||||
|
{
|
||||||
|
status: "FAILED",
|
||||||
|
failedAt: new Date(),
|
||||||
|
reviewedById: input.adminId,
|
||||||
|
reviewedAt: new Date(),
|
||||||
|
adminNote: input.adminNote.trim(),
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -3,6 +3,10 @@ import { participantRepo } from "@/server/repositories/participant.repo";
|
|||||||
import { reviewRepo } from "@/server/repositories/review.repo";
|
import { reviewRepo } from "@/server/repositories/review.repo";
|
||||||
import { isPastTripLastDayForReview } from "@/lib/trip-dates";
|
import { isPastTripLastDayForReview } from "@/lib/trip-dates";
|
||||||
|
|
||||||
|
export type OrganizerReviewItem = Awaited<
|
||||||
|
ReturnType<typeof reviewRepo.findByOrganizer>
|
||||||
|
>[number];
|
||||||
|
|
||||||
export const reviewService = {
|
export const reviewService = {
|
||||||
async upsertReview(
|
async upsertReview(
|
||||||
tripId: string,
|
tripId: string,
|
||||||
@@ -41,4 +45,11 @@ export const reviewService = {
|
|||||||
comment: input.comment ?? null,
|
comment: input.comment ?? null,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getReviewsByOrganizer(
|
||||||
|
organizerId: string,
|
||||||
|
limit?: number
|
||||||
|
): Promise<OrganizerReviewItem[]> {
|
||||||
|
return reviewRepo.findByOrganizer(organizerId, limit);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { tripRepo, type TripFilters } from "@/server/repositories/trip.repo";
|
|||||||
import { participantRepo } from "@/server/repositories/participant.repo";
|
import { participantRepo } from "@/server/repositories/participant.repo";
|
||||||
import { bookingRepo } from "@/server/repositories/booking.repo";
|
import { bookingRepo } from "@/server/repositories/booking.repo";
|
||||||
import { bookingService } from "@/server/services/booking.service";
|
import { bookingService } from "@/server/services/booking.service";
|
||||||
|
import { refundService } from "@/server/services/refund.service";
|
||||||
import { LIMITS } from "@/lib/limits";
|
import { LIMITS } from "@/lib/limits";
|
||||||
import { utcStartOfDay, isTripDepartureDayPast } from "@/lib/trip-dates";
|
import { utcStartOfDay, isTripDepartureDayPast } from "@/lib/trip-dates";
|
||||||
import { isFreeTrip } from "@/lib/trip-pricing";
|
import { isFreeTrip } from "@/lib/trip-pricing";
|
||||||
@@ -253,6 +254,19 @@ export const tripService = {
|
|||||||
throw new Error("Kamu tidak terdaftar di trip ini");
|
throw new Error("Kamu tidak terdaftar di trip ini");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Safety: kalau booking sudah PAID, paksa lewat refund flow supaya tidak
|
||||||
|
// ada uang menggantung tanpa Refund record.
|
||||||
|
const existingBooking = await bookingRepo.findByTripAndUser(tripId, userId);
|
||||||
|
if (
|
||||||
|
existingBooking &&
|
||||||
|
(existingBooking.status === "PAID" ||
|
||||||
|
existingBooking.status === "PARTIALLY_REFUNDED")
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Booking kamu sudah lunas — pakai tombol 'Cancel & Request Refund' supaya uang bisa dikembalikan."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await prisma.$transaction(async (tx) => {
|
const result = await prisma.$transaction(async (tx) => {
|
||||||
const cancelled = await tx.tripParticipant.update({
|
const cancelled = await tx.tripParticipant.update({
|
||||||
where: { tripId_userId: { tripId, userId } },
|
where: { tripId_userId: { tripId, userId } },
|
||||||
@@ -385,6 +399,20 @@ export const tripService = {
|
|||||||
return bookingService.markPaidManual(booking.id, userId);
|
return bookingService.markPaidManual(booking.id, userId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-complete trip yang sudah lewat. Dipakai cron harian.
|
||||||
|
*
|
||||||
|
* Cutoff = start of today UTC. Trip dengan endDate < cutoff (atau, kalau
|
||||||
|
* endDate null, date < cutoff) di-set status COMPLETED — selama statusnya
|
||||||
|
* masih OPEN/FULL. CLOSED trip tidak disentuh (organizer eksplisit batalkan).
|
||||||
|
*
|
||||||
|
* Idempotent: dua kali run di hari sama, run kedua nge-match 0 row.
|
||||||
|
*/
|
||||||
|
async autoCompletePastTrips() {
|
||||||
|
const cutoff = utcStartOfDay(new Date());
|
||||||
|
return tripRepo.bulkCompletePastTrips(cutoff);
|
||||||
|
},
|
||||||
|
|
||||||
async confirmParticipantPayment(
|
async confirmParticipantPayment(
|
||||||
tripId: string,
|
tripId: string,
|
||||||
participantId: string,
|
participantId: string,
|
||||||
@@ -408,4 +436,164 @@ export const tripService = {
|
|||||||
|
|
||||||
return bookingService.confirmPaidManual(booking.id, organizerId);
|
return bookingService.confirmPaidManual(booking.id, organizerId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Organizer batalkan trip (Trip.status = CLOSED). Atomic dalam satu
|
||||||
|
* serializable transaction:
|
||||||
|
* - Set Trip.status = CLOSED.
|
||||||
|
* - Untuk setiap peserta aktif:
|
||||||
|
* - Booking PAID → buat Refund ORGANIZER_CANCELLED (auto-approved, full
|
||||||
|
* amount). Booking tetap PAID sampai admin mark SUCCEEDED — jejak
|
||||||
|
* finansial harus terjaga.
|
||||||
|
* - Booking PENDING/AWAITING_PAY → set CANCELLED langsung (uang belum
|
||||||
|
* masuk, tidak ada refund).
|
||||||
|
* - Booking PARTIALLY_REFUNDED / dengan refund aktif → di-skip (admin
|
||||||
|
* handle manual supaya tidak double-refund).
|
||||||
|
* - Semua TripParticipant aktif → CANCELLED.
|
||||||
|
*
|
||||||
|
* Idempotent: trip yang sudah CLOSED/COMPLETED akan ditolak supaya tidak
|
||||||
|
* dobel-buat refund.
|
||||||
|
*/
|
||||||
|
async closeTrip(tripId: string, organizerId: string) {
|
||||||
|
let lastErr: unknown;
|
||||||
|
for (let attempt = 0; attempt < SERIAL_TX_ATTEMPTS; attempt++) {
|
||||||
|
try {
|
||||||
|
return await prisma.$transaction(
|
||||||
|
async (tx) => {
|
||||||
|
const trip = await tx.trip.findUnique({
|
||||||
|
where: { id: tripId },
|
||||||
|
select: { id: true, status: true, organizerId: true, date: true },
|
||||||
|
});
|
||||||
|
if (!trip) {
|
||||||
|
throw new Error("Trip tidak ditemukan");
|
||||||
|
}
|
||||||
|
if (trip.organizerId !== organizerId) {
|
||||||
|
throw new Error(
|
||||||
|
"Hanya organizer trip ini yang bisa membatalkan trip"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (trip.status === "CLOSED") {
|
||||||
|
throw new Error("Trip sudah dibatalkan");
|
||||||
|
}
|
||||||
|
if (trip.status === "COMPLETED") {
|
||||||
|
throw new Error(
|
||||||
|
"Trip sudah selesai (COMPLETED) — tidak bisa dibatalkan"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isTripDepartureDayPast(trip.date)) {
|
||||||
|
throw new Error(
|
||||||
|
"Tanggal berangkat sudah lewat — gunakan flow pelaporan biasa ke admin"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookings = await tx.booking.findMany({
|
||||||
|
where: { tripId },
|
||||||
|
include: {
|
||||||
|
payments: {
|
||||||
|
where: { status: "PAID" },
|
||||||
|
orderBy: { paidAt: "desc" },
|
||||||
|
take: 1,
|
||||||
|
},
|
||||||
|
refunds: {
|
||||||
|
where: {
|
||||||
|
status: { in: ["PENDING", "APPROVED", "PROCESSING"] },
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const refundsCreated: string[] = [];
|
||||||
|
const cancelledBookings: string[] = [];
|
||||||
|
const skippedBookings: string[] = [];
|
||||||
|
|
||||||
|
for (const b of bookings) {
|
||||||
|
if (b.status === "CANCELLED" || b.status === "EXPIRED") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (b.status === "REFUNDED") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (b.refunds.length > 0) {
|
||||||
|
// Sudah ada refund aktif (mis. user request cancel). Admin
|
||||||
|
// handle manual supaya tidak konflik dengan refund existing.
|
||||||
|
skippedBookings.push(b.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.status === "PAID" || b.status === "PARTIALLY_REFUNDED") {
|
||||||
|
const paid = b.payments[0];
|
||||||
|
if (!paid) {
|
||||||
|
// Payment tidak konsisten dgn booking status — skip + flag.
|
||||||
|
skippedBookings.push(b.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Untuk PARTIALLY_REFUNDED, hitung sisa refundable.
|
||||||
|
const alreadyRefunded = await tx.refund.aggregate({
|
||||||
|
where: { bookingId: b.id, status: "SUCCEEDED" },
|
||||||
|
_sum: { amount: true },
|
||||||
|
});
|
||||||
|
const remaining = paid.amount - (alreadyRefunded._sum.amount ?? 0);
|
||||||
|
if (remaining <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const refund = await refundService.createSystemRefundForClosedTrip(
|
||||||
|
tx,
|
||||||
|
{
|
||||||
|
bookingId: b.id,
|
||||||
|
paymentId: paid.id,
|
||||||
|
amount: remaining,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
refundsCreated.push(refund.id);
|
||||||
|
} else {
|
||||||
|
// PENDING / AWAITING_PAY → uang belum masuk → langsung CANCELLED.
|
||||||
|
await tx.booking.update({
|
||||||
|
where: { id: b.id },
|
||||||
|
data: { status: "CANCELLED" },
|
||||||
|
});
|
||||||
|
cancelledBookings.push(b.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semua participant aktif → CANCELLED (apapun status booking-nya).
|
||||||
|
await tx.tripParticipant.updateMany({
|
||||||
|
where: { tripId, status: { not: "CANCELLED" } },
|
||||||
|
data: {
|
||||||
|
status: "CANCELLED",
|
||||||
|
markedPaidAt: null,
|
||||||
|
paymentConfirmedAt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.trip.update({
|
||||||
|
where: { id: tripId },
|
||||||
|
data: { status: "CLOSED" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true as const,
|
||||||
|
refundsCreated,
|
||||||
|
cancelledBookings,
|
||||||
|
skippedBookings,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
|
||||||
|
maxWait: 5000,
|
||||||
|
timeout: 15000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
lastErr = e;
|
||||||
|
if (isSerializationConflict(e) && attempt < SERIAL_TX_ATTEMPTS - 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastErr instanceof Error
|
||||||
|
? lastErr
|
||||||
|
: new Error("Gagal membatalkan trip. Coba lagi sebentar.");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,38 +1,124 @@
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { TRIP_LEADER_MIN_TRIPS } from "@/lib/trust";
|
import {
|
||||||
|
COMPLETION_RATE_MIN_SAMPLE,
|
||||||
|
TRIP_LEADER_MIN_TRIPS,
|
||||||
|
} from "@/lib/trust";
|
||||||
|
|
||||||
|
export type RatingBreakdown = Record<1 | 2 | 3 | 4 | 5, number>;
|
||||||
|
|
||||||
export type OrganizerTrust = {
|
export type OrganizerTrust = {
|
||||||
isVerified: boolean;
|
isVerified: boolean;
|
||||||
tripsCreated: number;
|
tripsCreated: number;
|
||||||
|
/// Trip yang sudah lewat tanggal selesai-nya AND tidak dibatalkan (status != CLOSED).
|
||||||
|
/// Dihitung on-the-fly dari endDate (fallback ke date kalau endDate null) — tidak
|
||||||
|
/// bergantung pada Trip.status = COMPLETED yang saat ini belum pernah di-set.
|
||||||
|
tripsCompleted: number;
|
||||||
|
/// Trip dengan status = CLOSED (dibatalkan organizer).
|
||||||
|
tripsCancelled: number;
|
||||||
|
/// Akumulasi peserta CONFIRMED di seluruh trip yang sudah selesai.
|
||||||
|
totalParticipantsServed: number;
|
||||||
|
/// tripsCompleted / (tripsCompleted + tripsCancelled). Null kalau sample
|
||||||
|
/// < COMPLETION_RATE_MIN_SAMPLE — mencegah angka menyesatkan untuk
|
||||||
|
/// organizer yang masih sedikit history-nya.
|
||||||
|
completionRate: number | null;
|
||||||
avgRating: number | null;
|
avgRating: number | null;
|
||||||
reviewCount: number;
|
reviewCount: number;
|
||||||
|
ratingBreakdown: RatingBreakdown;
|
||||||
isTripLeader: boolean;
|
isTripLeader: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const trustService = {
|
export const trustService = {
|
||||||
async getOrganizerTrust(organizerId: string): Promise<OrganizerTrust> {
|
async getOrganizerTrust(organizerId: string): Promise<OrganizerTrust> {
|
||||||
const [tripsCreated, reviewAgg, organizerVerification] = await Promise.all([
|
const now = new Date();
|
||||||
|
|
||||||
|
// Filter "trip yang sudah lewat": endDate < now, atau (endDate null AND date < now).
|
||||||
|
// Trip multi-hari pakai endDate; trip 1 hari biasanya endDate null jadi fallback ke date.
|
||||||
|
const pastTripFilter = {
|
||||||
|
OR: [
|
||||||
|
{ endDate: { lt: now } },
|
||||||
|
{ AND: [{ endDate: null }, { date: { lt: now } }] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const [
|
||||||
|
tripsCreated,
|
||||||
|
tripsCompleted,
|
||||||
|
tripsCancelled,
|
||||||
|
totalParticipantsServed,
|
||||||
|
reviewAgg,
|
||||||
|
ratingGroups,
|
||||||
|
organizerVerification,
|
||||||
|
] = await Promise.all([
|
||||||
prisma.trip.count({ where: { organizerId } }),
|
prisma.trip.count({ where: { organizerId } }),
|
||||||
prisma.tripReview.aggregate({
|
prisma.trip.count({
|
||||||
where: {
|
where: {
|
||||||
trip: { organizerId },
|
organizerId,
|
||||||
|
status: { not: "CLOSED" },
|
||||||
|
...pastTripFilter,
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
|
prisma.trip.count({
|
||||||
|
where: { organizerId, status: "CLOSED" },
|
||||||
|
}),
|
||||||
|
prisma.tripParticipant.count({
|
||||||
|
where: {
|
||||||
|
status: "CONFIRMED",
|
||||||
|
trip: {
|
||||||
|
organizerId,
|
||||||
|
status: { not: "CLOSED" },
|
||||||
|
...pastTripFilter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.tripReview.aggregate({
|
||||||
|
where: { trip: { organizerId } },
|
||||||
_avg: { rating: true },
|
_avg: { rating: true },
|
||||||
_count: { _all: true },
|
_count: { _all: true },
|
||||||
}),
|
}),
|
||||||
|
prisma.tripReview.groupBy({
|
||||||
|
by: ["rating"],
|
||||||
|
where: { trip: { organizerId } },
|
||||||
|
_count: { _all: true },
|
||||||
|
}),
|
||||||
prisma.organizerVerification.findUnique({
|
prisma.organizerVerification.findUnique({
|
||||||
where: { userId: organizerId },
|
where: { userId: organizerId },
|
||||||
select: { status: true },
|
select: { status: true },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const ratingBreakdown: RatingBreakdown = {
|
||||||
|
1: 0,
|
||||||
|
2: 0,
|
||||||
|
3: 0,
|
||||||
|
4: 0,
|
||||||
|
5: 0,
|
||||||
|
};
|
||||||
|
for (const row of ratingGroups) {
|
||||||
|
const r = row.rating;
|
||||||
|
if (r >= 1 && r <= 5) {
|
||||||
|
ratingBreakdown[r as 1 | 2 | 3 | 4 | 5] = row._count._all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const completionSample = tripsCompleted + tripsCancelled;
|
||||||
|
const completionRate =
|
||||||
|
completionSample >= COMPLETION_RATE_MIN_SAMPLE
|
||||||
|
? tripsCompleted / completionSample
|
||||||
|
: null;
|
||||||
|
|
||||||
const avg = reviewAgg._avg.rating;
|
const avg = reviewAgg._avg.rating;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isVerified: organizerVerification?.status === "APPROVED",
|
isVerified: organizerVerification?.status === "APPROVED",
|
||||||
tripsCreated,
|
tripsCreated,
|
||||||
|
tripsCompleted,
|
||||||
|
tripsCancelled,
|
||||||
|
totalParticipantsServed,
|
||||||
|
completionRate,
|
||||||
avgRating:
|
avgRating:
|
||||||
avg != null ? Math.round(Number(avg) * 10) / 10 : null,
|
avg != null ? Math.round(Number(avg) * 10) / 10 : null,
|
||||||
reviewCount: reviewAgg._count._all,
|
reviewCount: reviewAgg._count._all,
|
||||||
|
ratingBreakdown,
|
||||||
isTripLeader: tripsCreated >= TRIP_LEADER_MIN_TRIPS,
|
isTripLeader: tripsCreated >= TRIP_LEADER_MIN_TRIPS,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user