628 lines
20 KiB
Plaintext
628 lines
20 KiB
Plaintext
generator client {
|
|
provider = "prisma-client"
|
|
output = "../app/generated/prisma"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
name String
|
|
email String @unique
|
|
/// Hash bcrypt. Null untuk user yang sign-in via OAuth (mis. Google).
|
|
password String?
|
|
image String?
|
|
/// Diisi PrismaAdapter NextAuth saat email diverifikasi provider OAuth (Google selalu sudah verified).
|
|
emailVerified DateTime?
|
|
/// Apakah user telah menyetujui Syarat & Ketentuan dan Kebijakan Privasi
|
|
acceptedTermsAndPrivacy Boolean @default(false)
|
|
/// Waktu user menyetujui Syarat & Ketentuan dan Kebijakan Privasi
|
|
acceptedAt DateTime?
|
|
/// Suspended user diblokir sign-in dan aksi mutatif. Set oleh admin via panel.
|
|
suspended Boolean @default(false)
|
|
suspendedAt DateTime?
|
|
suspendedReason String?
|
|
suspendedById String?
|
|
suspendedBy User? @relation("UserSuspendedBy", fields: [suspendedById], references: [id], onDelete: SetNull)
|
|
suspendedUsers User[] @relation("UserSuspendedBy")
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
accounts Account[]
|
|
trips Trip[]
|
|
participations TripParticipant[]
|
|
tripReviews TripReview[]
|
|
bookings Booking[]
|
|
|
|
organizerVerification OrganizerVerification? @relation("OrganizerVerificationOwner")
|
|
reviewedVerifications OrganizerVerification[] @relation("OrganizerVerificationReviewer")
|
|
manualOverrideVerifications OrganizerVerification[] @relation("OrganizerVerificationManualOverride")
|
|
|
|
reviewedRefunds Refund[] @relation("RefundReviewer")
|
|
|
|
/// Payout yang diterima user ini sebagai organizer (escrow trip selesai).
|
|
payouts Payout[] @relation("PayoutOrganizer")
|
|
/// Payout yang ditandai admin sebagai PAID/CANCELLED oleh user ini.
|
|
processedPayouts Payout[] @relation("PayoutProcessor")
|
|
|
|
/// Trip yang dibatalkan admin ini lewat panel admin (intervensi).
|
|
adminCancelledTrips Trip[] @relation("TripCancelledByAdmin")
|
|
|
|
/// Audit log polymorphic — semua aksi admin yang dilakukan oleh user ini.
|
|
adminActionLogs AdminActionLog[] @relation("AdminActionLogAdmin")
|
|
|
|
profile UserProfile?
|
|
}
|
|
|
|
/// Profil sosial publik. Berisi info yang user pilih untuk dibagikan ke peserta lain
|
|
/// (bio, kota, minat, vibe). Tidak menyimpan data sensitif — KYC tetap di OrganizerVerification.
|
|
model UserProfile {
|
|
id String @id @default(cuid())
|
|
userId String @unique
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
/// Bio singkat, teks bebas
|
|
bio String?
|
|
/// Kota domisili (teks bebas, mis. "Bandung", "Jakarta Selatan")
|
|
city String?
|
|
/// Tag minat aktivitas (mis. ["hiking", "fotografi", "yoga"])
|
|
interests String[] @default([])
|
|
/// Username Instagram (tanpa @, opsional)
|
|
instagram String?
|
|
/// Gaya jalan / energi user — dipakai untuk matching teman dengan ritme serupa.
|
|
vibe Vibe?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
enum Vibe {
|
|
CHILL
|
|
BALANCED
|
|
HARDCORE
|
|
}
|
|
|
|
/// Tabel link akun OAuth pihak ketiga (Google, dst). Diisi oleh PrismaAdapter NextAuth.
|
|
/// Session tidak pakai DB — kita pakai JWT, jadi Session/VerificationToken tidak perlu.
|
|
model Account {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
type String
|
|
provider String
|
|
providerAccountId String
|
|
refresh_token String?
|
|
access_token String?
|
|
expires_at Int?
|
|
token_type String?
|
|
scope String?
|
|
id_token String?
|
|
session_state String?
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([provider, providerAccountId])
|
|
}
|
|
|
|
model OrganizerVerification {
|
|
id String @id @default(cuid())
|
|
userId String @unique
|
|
user User @relation("OrganizerVerificationOwner", fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
/// Nama lengkap sesuai KTP
|
|
fullName String
|
|
/// NIK terenkripsi (AES-256-GCM, base64). Plaintext tidak disimpan.
|
|
nikEncrypted String
|
|
/// HMAC-SHA256(NIK + pepper) untuk uniqueness lookup tanpa membuka plaintext.
|
|
nikHash String @unique
|
|
birthDate DateTime
|
|
address String
|
|
|
|
/// Storage key foto KTP (mis. `ktp/<id>.jpg`). File disimpan terenkripsi di luar /public.
|
|
ktpImageKey String
|
|
/// Storage key foto liveness — user memegang kertas bertuliskan "SETRIP".
|
|
/// (Sebelumnya: selfie memegang KTP. Diganti supaya user tidak perlu memajang KTP dua kali.)
|
|
livenessKey String
|
|
|
|
bankName String
|
|
bankAccountNumber String
|
|
bankAccountName String
|
|
|
|
status VerificationStatus @default(PENDING)
|
|
rejectionReason String?
|
|
reviewedAt DateTime?
|
|
reviewedById String?
|
|
reviewedBy User? @relation("OrganizerVerificationReviewer", fields: [reviewedById], references: [id])
|
|
verifiedAt DateTime?
|
|
|
|
/// Phase 2: admin minta organizer upload ulang field tertentu. Saat true,
|
|
/// organizer page /verify menampilkan banner kuning + highlight field
|
|
/// yang diminta. Auto-clear saat organizer submit ulang.
|
|
reuploadRequested Boolean @default(false)
|
|
reuploadFields String[] @default([])
|
|
reuploadNote String?
|
|
|
|
/// Phase 3: jumlah submission ulang + arsip rejection sebelumnya
|
|
/// (di-append saat REJECTED baru, supaya history tidak ke-overwrite).
|
|
submissionCount Int @default(1)
|
|
previousRejections Json?
|
|
|
|
/// Phase 4: admin verify manual tanpa upload KYC (mis. partner trusted).
|
|
isManualOverride Boolean @default(false)
|
|
manualOverrideById String?
|
|
manualOverrideBy User? @relation("OrganizerVerificationManualOverride", fields: [manualOverrideById], references: [id], onDelete: SetNull)
|
|
manualOverrideNote String?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
enum VerificationStatus {
|
|
PENDING
|
|
APPROVED
|
|
REJECTED
|
|
}
|
|
|
|
model Trip {
|
|
id String @id @default(cuid())
|
|
title String
|
|
description String?
|
|
/// Kategori aktivitas.
|
|
category ActivityCategory @default(HIKING)
|
|
/// Destinasi/subjek aktivitas — interpretasinya tergantung kategori (gunung untuk HIKING, spot untuk DIVING, venue untuk CONCERT, tema untuk WORKSHOP, dst).
|
|
destination String
|
|
location String
|
|
/// Titik kumpul / meeting point (teks bebas)
|
|
meetingPoint String?
|
|
/// @deprecated — itinerary lama bentuk teks bebas, backward-compat untuk data
|
|
/// lama. Trip baru pakai `itineraryItems` (struktural per hari + jam).
|
|
itinerary String?
|
|
/// Yang termasuk harga (teks bebas)
|
|
whatsIncluded String?
|
|
/// Yang tidak termasuk (teks bebas)
|
|
whatsExcluded String?
|
|
date DateTime
|
|
endDate DateTime?
|
|
maxParticipants Int
|
|
price Int
|
|
/// Ritme/energi trip — dipakai untuk matching dengan vibe user.
|
|
vibe Vibe?
|
|
status TripStatus @default(OPEN)
|
|
/// Admin yang membatalkan trip via panel admin (intervensi). NULL kalau
|
|
/// organizer yang cancel sendiri atau trip tidak dibatalkan.
|
|
cancelledByAdminId String?
|
|
cancelledByAdmin User? @relation("TripCancelledByAdmin", fields: [cancelledByAdminId], references: [id], onDelete: SetNull)
|
|
/// Alasan admin membatalkan trip — wajib diisi saat admin cancel.
|
|
cancelledReason String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
organizerId String
|
|
organizer User @relation(fields: [organizerId], references: [id])
|
|
|
|
participants TripParticipant[]
|
|
images TripImage[]
|
|
reviews TripReview[]
|
|
bookings Booking[]
|
|
payouts Payout[]
|
|
itineraryItems TripItineraryItem[]
|
|
|
|
@@index([category, status, date])
|
|
@@index([vibe, status, date])
|
|
}
|
|
|
|
/// Itinerary item terstruktur per hari + jam. Satu Trip punya banyak item;
|
|
/// dikelompokkan per `day` lalu diurutkan `order`. Format jam: "HH:mm" 24-jam.
|
|
model TripItineraryItem {
|
|
id String @id @default(cuid())
|
|
tripId String
|
|
trip Trip @relation(fields: [tripId], references: [id], onDelete: Cascade)
|
|
|
|
/// Hari ke-berapa, mulai dari 1.
|
|
day Int
|
|
/// Waktu mulai aktivitas, format "HH:mm".
|
|
startTime String
|
|
/// Waktu selesai (opsional), format "HH:mm".
|
|
endTime String?
|
|
/// Deskripsi aktivitas singkat.
|
|
activity String
|
|
/// Urutan dalam hari, untuk preserve order saat render.
|
|
order Int @default(0)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([tripId, day, order])
|
|
}
|
|
|
|
model TripReview {
|
|
id String @id @default(cuid())
|
|
rating Int
|
|
comment String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
tripId String
|
|
trip Trip @relation(fields: [tripId], references: [id], onDelete: Cascade)
|
|
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([tripId, userId])
|
|
}
|
|
|
|
model TripImage {
|
|
id String @id @default(cuid())
|
|
url String
|
|
caption String?
|
|
order Int @default(0)
|
|
|
|
tripId String
|
|
trip Trip @relation(fields: [tripId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
model TripParticipant {
|
|
id String @id @default(cuid())
|
|
status ParticipantStatus @default(PENDING)
|
|
createdAt DateTime @default(now())
|
|
/// @deprecated — sumber kebenaran pindah ke Booking/Payment. Tetap di-update
|
|
/// untuk backward-compat selama transisi UI lama. Akan dihapus PR berikutnya.
|
|
markedPaidAt DateTime?
|
|
/// @deprecated — sumber kebenaran pindah ke Booking/Payment. Tetap di-update
|
|
/// untuk backward-compat selama transisi UI lama. Akan dihapus PR berikutnya.
|
|
paymentConfirmedAt DateTime?
|
|
|
|
tripId String
|
|
trip Trip @relation(fields: [tripId], references: [id])
|
|
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id])
|
|
|
|
booking Booking?
|
|
|
|
@@unique([tripId, userId])
|
|
}
|
|
|
|
enum TripStatus {
|
|
OPEN
|
|
FULL
|
|
CLOSED
|
|
COMPLETED
|
|
}
|
|
|
|
enum ActivityCategory {
|
|
HIKING
|
|
CAMPING
|
|
SNORKELING
|
|
DIVING
|
|
ISLAND_HOPPING
|
|
CITY_TRIP
|
|
CULINARY
|
|
CONCERT
|
|
WORKSHOP
|
|
RETREAT
|
|
}
|
|
|
|
enum ParticipantStatus {
|
|
PENDING
|
|
CONFIRMED
|
|
CANCELLED
|
|
}
|
|
|
|
/// Booking 1-1 ke TripParticipant. Lifecycle ikut peserta:
|
|
/// - join → Booking PENDING (menunggu approve organizer)
|
|
/// - organizer confirm → AWAITING_PAY (paid trip) atau PAID (free trip)
|
|
/// - peserta + organizer rampungkan pembayaran → PAID
|
|
/// - cancel/reject → CANCELLED
|
|
/// `amount` adalah snapshot harga saat booking dibuat — protect dari perubahan trip.price.
|
|
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], onDelete: Cascade)
|
|
|
|
amount Int
|
|
currency String @default("IDR")
|
|
status BookingStatus @default(PENDING)
|
|
|
|
payments Payment[]
|
|
refunds Refund[]
|
|
payout Payout?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
/// Konsistensi: 1-1 ke participant via participantId, dan participant unique
|
|
/// per (tripId, userId). Constraint ini eksplisit + jadi index untuk query
|
|
/// `findByTripAndUser`.
|
|
@@unique([tripId, userId])
|
|
@@index([tripId, status])
|
|
}
|
|
|
|
enum BookingStatus {
|
|
PENDING
|
|
AWAITING_PAY
|
|
PAID
|
|
CANCELLED
|
|
REFUNDED
|
|
PARTIALLY_REFUNDED
|
|
EXPIRED
|
|
}
|
|
|
|
/// Satu attempt pembayaran. Satu Booking bisa punya banyak Payment kalau retry
|
|
/// (di Phase MIDTRANS nanti). Untuk MANUAL biasanya cukup 1 Payment.
|
|
model Payment {
|
|
id String @id @default(cuid())
|
|
bookingId String
|
|
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
|
|
|
|
provider PaymentProvider
|
|
/// order_id eksternal (unik per attempt). Format MANUAL: `manual-<bookingId>`.
|
|
/// Format MIDTRANS nanti: `midtrans-<bookingId>-<retry>`.
|
|
externalOrderId String @unique
|
|
/// transaction_id dari gateway. Kosong untuk MANUAL atau sebelum first callback.
|
|
externalTxId String?
|
|
/// Metode konkret: bca_va, gopay, qris, manual_transfer, dst.
|
|
method String?
|
|
amount Int
|
|
status PaymentStatus @default(PENDING)
|
|
|
|
/// Snapshot mentah callback gateway (untuk audit & dispute).
|
|
rawCallback Json?
|
|
/// Snap token Midtrans / redirect URL.
|
|
snapToken String?
|
|
/// Kapan attempt ini kadaluarsa (Midtrans default 24 jam).
|
|
expiresAt DateTime?
|
|
|
|
paidAt DateTime?
|
|
failedAt DateTime?
|
|
rejectionReason String?
|
|
|
|
refunds Refund[]
|
|
|
|
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
|
|
}
|
|
|
|
/// 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])
|
|
}
|
|
|
|
/// Log polymorphic untuk admin actions lintas entity. Append-only — kalau
|
|
/// admin dihapus, `adminId` di-set NULL tapi `adminEmail` snapshot tetap.
|
|
/// Dipakai untuk compliance & investigasi (siapa approve/reject/cancel/
|
|
/// suspend, kapan, dengan payload apa).
|
|
model AdminActionLog {
|
|
id String @id @default(cuid())
|
|
adminId String?
|
|
admin User? @relation("AdminActionLogAdmin", fields: [adminId], references: [id], onDelete: SetNull)
|
|
/// Snapshot email admin saat action dijalankan — tetap ada meski admin dihapus.
|
|
adminEmail String
|
|
/// Nama aksi dalam SCREAMING_SNAKE, mis. `REFUND_APPROVE`, `TRIP_CANCEL`, `USER_SUSPEND`.
|
|
action String
|
|
/// Tipe entity yang di-target: `Refund` / `Payout` / `Trip` / `User` / `Verification` / `Payment`.
|
|
entityType String
|
|
entityId String
|
|
/// Payload bebas (input parameter, hasil, dst) untuk konteks investigasi.
|
|
payload Json?
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([adminId, createdAt(sort: Desc)])
|
|
@@index([entityType, entityId])
|
|
@@index([createdAt(sort: Desc)])
|
|
}
|
|
|
|
/// Log per cron run untuk observability admin. Append-only.
|
|
/// `runCron(jobName, fn)` di `lib/cron-runner.ts` otomatis create row RUNNING
|
|
/// → update SUCCESS/FAILED setelah selesai. Dipakai admin di `/admin/system`.
|
|
model CronRun {
|
|
id String @id @default(cuid())
|
|
jobName String
|
|
startedAt DateTime @default(now())
|
|
finishedAt DateTime?
|
|
status CronRunStatus @default(RUNNING)
|
|
errorMessage String?
|
|
/// Snapshot ringkas hasil run (mis. `{ completed: 5, ids: [...] }`).
|
|
payload Json?
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([jobName, startedAt(sort: Desc)])
|
|
@@index([startedAt(sort: Desc)])
|
|
}
|
|
|
|
enum CronRunStatus {
|
|
RUNNING
|
|
SUCCESS
|
|
FAILED
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
/// Escrow payout ke organizer. Uang peserta ditahan sejak Booking → PAID sampai
|
|
/// trip selesai + buffer beberapa hari, baru di-release untuk ditransfer admin.
|
|
///
|
|
/// State machine:
|
|
/// HELD → diciptakan saat booking PAID, heldUntil = endDate/date + 3 hari
|
|
/// RELEASED → cron flip setelah heldUntil lewat + trip COMPLETED
|
|
/// PAID → admin sudah transfer manual ke rekening organizer
|
|
/// CANCELLED → booking di-refund / trip dibatalkan; payout tidak jadi
|
|
///
|
|
/// Audit: 1-1 dengan Booking (unique). Refund SUCCEEDED mengurangi amount
|
|
/// (partial) atau membatalkan payout (full).
|
|
model Payout {
|
|
id String @id @default(cuid())
|
|
bookingId String @unique
|
|
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Restrict)
|
|
|
|
tripId String
|
|
trip Trip @relation(fields: [tripId], references: [id])
|
|
|
|
organizerId String
|
|
organizer User @relation("PayoutOrganizer", fields: [organizerId], references: [id])
|
|
|
|
/// Nominal yg organizer terima (IDR integer). Default = booking.amount saat
|
|
/// payout dibuat. Refund SUCCEEDED memotong nilai ini supaya total payout +
|
|
/// total refund = uang yang dibayar peserta.
|
|
amount Int
|
|
currency String @default("IDR")
|
|
|
|
status PayoutStatus @default(HELD)
|
|
|
|
/// Tanggal payout boleh di-release ke organizer
|
|
/// (= trip.endDate ?? trip.date + buffer days).
|
|
heldUntil DateTime
|
|
releasedAt DateTime?
|
|
paidAt DateTime?
|
|
cancelledAt DateTime?
|
|
|
|
/// Snapshot bank info organizer dari OrganizerVerification saat payout dibuat.
|
|
/// Disimpan inline supaya audit-friendly walau organizer ganti bank nanti.
|
|
bankName String?
|
|
bankAccountNumber String?
|
|
bankAccountName String?
|
|
|
|
/// Catatan admin: referensi transfer manual, alasan cancel, dst.
|
|
adminNote String?
|
|
|
|
/// Admin yang menandai PAID/CANCELLED.
|
|
processedById String?
|
|
processedBy User? @relation("PayoutProcessor", fields: [processedById], references: [id])
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([organizerId, status])
|
|
@@index([status, heldUntil])
|
|
}
|
|
|
|
enum PayoutStatus {
|
|
/// Menunggu trip selesai + buffer beberapa hari sebelum boleh ditransfer.
|
|
HELD
|
|
/// Buffer lewat & trip COMPLETED, siap di-transfer admin ke rekening organizer.
|
|
RELEASED
|
|
/// Admin sudah transfer ke rekening organizer.
|
|
PAID
|
|
/// Booking di-refund penuh / trip dibatalkan — uang tidak jadi ke organizer.
|
|
CANCELLED
|
|
}
|