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[] 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/.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[] @@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()) /// Peserta menekan "Saya sudah bayar" (pembayaran manual) markedPaidAt DateTime? /// Organizer mengonfirmasi uang sudah masuk paymentConfirmedAt DateTime? tripId String trip Trip @relation(fields: [tripId], references: [id]) userId String user User @relation(fields: [userId], references: [id]) @@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 }