Files
setrip/prisma/schema.prisma
T

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
}