331 lines
9.6 KiB
Plaintext
331 lines
9.6 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?
|
|
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")
|
|
|
|
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?
|
|
|
|
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?
|
|
/// Itinerary hari per hari (teks bebas, bullet OK)
|
|
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)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
organizerId String
|
|
organizer User @relation(fields: [organizerId], references: [id])
|
|
|
|
participants TripParticipant[]
|
|
images TripImage[]
|
|
reviews TripReview[]
|
|
bookings Booking[]
|
|
|
|
@@index([category, status, date])
|
|
@@index([vibe, status, date])
|
|
}
|
|
|
|
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[]
|
|
|
|
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
|
|
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?
|
|
|
|
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
|
|
}
|