generator client { provider = "prisma-client" output = "../src/generated/prisma" } datasource db { provider = "postgresql" } enum UserRole { OWNER CE_MANAGER CE_MEMBER TOURIST ADMIN RENTAL_PROVIDER } enum CarbetStatus { DRAFT PUBLISHED ARCHIVED } enum MediaType { PHOTO VIDEO } enum AvailabilityScope { PUBLIC CE_ONLY } enum AvailabilityBlockReason { NONE CE_BLOCKED WEEKEND_BLOCKED } enum BookingStatus { PENDING CONFIRMED CANCELLED COMPLETED } enum PaymentStatus { PENDING AUTHORIZED SUCCEEDED FAILED REFUNDED } enum SubscriptionStatus { TRIAL ACTIVE PAST_DUE CANCELED } enum AccessType { ROAD_AND_RIVER RIVER_ONLY } enum TransportMode { OWNER_PROVIDES SELF_ARRANGE PARTNER_PROVIDER } model Organization { id String @id @default(cuid()) name String slug String @unique description String? contactEmail String? approved Boolean @default(false) approvedAt DateTime? approvedBy String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt members User[] carbetMemberships OrganizationCarbetMembership[] rentalProviders RentalProvider[] invites OrgInviteToken[] @@index([name]) @@index([approved]) } /// Token d'invitation pour rejoindre une organisation comme CE_MEMBER. /// Le CE_MANAGER génère un lien, le destinataire s'inscrit via /inscription?invite=TOKEN. /// Pas de unique sur email pour permettre plusieurs invites pendants par destinataire. model OrgInviteToken { tokenHash String @id organizationId String email String? createdByUserId String? expiresAt DateTime usedAt DateTime? createdAt DateTime @default(now()) organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) @@index([organizationId]) @@index([expiresAt]) } /// Co-gestion des carbets côté CE. Un Carbet a toujours un ownerId (créateur initial), /// et zéro ou plusieurs orgs liées : un CE_MANAGER d'une org liée peut gérer le carbet /// en plus de l'owner. Pour un hôte individuel : aucune membership ; pour un carbet CE : /// 1 membership pour l'org du créateur. Plusieurs orgs possibles si co-publication. model OrganizationCarbetMembership { organizationId String carbetId String addedByUserId String? addedAt DateTime @default(now()) organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade) @@id([organizationId, carbetId]) @@index([carbetId]) } model User { id String @id @default(cuid()) email String @unique passwordHash String firstName String lastName String phone String? role UserRole organizationId String? avatarUrl String? isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull) carbets Carbet[] @relation("CarbetOwner") bookings Booking[] @relation("BookingTenant") reviews Review[] @relation("ReviewAuthor") subscriptions Subscription[] rentalProviders RentalProvider[] rentalBookings RentalBooking[] @relation("RentalBookingTenant") @@index([organizationId]) @@index([role]) } model Carbet { id String @id @default(cuid()) ownerId String title String slug String @unique description String river String latitude Decimal @db.Decimal(9, 6) longitude Decimal @db.Decimal(9, 6) embarkPoint String // Pirogue : obligatoire pour RIVER_ONLY, optionnelle pour ROAD_AND_RIVER // (estimation pour ceux qui veulent quand même venir en pirogue). pirogueDurationMin Int? accessType AccessType @default(ROAD_AND_RIVER) // Détails d'accès route pour ROAD_AND_RIVER (GPS, distance, type de piste). roadAccessNote String? capacity Int // 4 critères opérationnels dealbreakers (dispo en filtres + badges UI) roadAccess RoadAccess? electricity Electricity? gsmAtCarbet Boolean @default(false) gsmExitDistanceKm Decimal? @db.Decimal(4, 2) // Prix par nuit pour le carbet entier (toute capacité). En euros. nightlyPrice Decimal @db.Decimal(10, 2) @default(0) // Contraintes séjour (plugin min-stay). null = pas de contrainte. minStayNights Int? maxStayNights Int? minCapacity Int? // Contraintes saisonnières (plugin seasonality). JSON libre, schéma type : // { closedInLowWater: bool, closedSeasons: ["WET"|"DRY"|"LOW_WATER"][], note: string } seasonalConstraints Json? // Plugin pirogue-providers : qui organise le transport ? transportMode TransportMode? pirogueProviderId String? status CarbetStatus @default(DRAFT) lastBookedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt owner User @relation("CarbetOwner", fields: [ownerId], references: [id], onDelete: Restrict) pirogueProvider PirogueProvider? @relation(fields: [pirogueProviderId], references: [id], onDelete: SetNull) amenities CarbetAmenity[] media Media[] availabilities Availability[] bookings Booking[] reviews Review[] subscriptions Subscription[] organizations OrganizationCarbetMembership[] @@index([ownerId]) @@index([status]) @@index([river]) @@index([accessType]) @@index([pirogueProviderId]) } model PirogueProvider { id String @id @default(cuid()) name String contactEmail String? contactPhone String? rivers String[] @default([]) pricingNote String? description String? active Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt carbets Carbet[] @@index([active]) } model Amenity { id String @id @default(cuid()) key String @unique label String description String? createdAt DateTime @default(now()) carbets CarbetAmenity[] } model CarbetAmenity { carbetId String amenityId String carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade) amenity Amenity @relation(fields: [amenityId], references: [id], onDelete: Cascade) @@id([carbetId, amenityId]) @@index([amenityId]) } model Media { id String @id @default(cuid()) carbetId String type MediaType s3Key String s3Url String sortOrder Int @default(0) createdAt DateTime @default(now()) carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade) @@index([carbetId, sortOrder]) } model Availability { id String @id @default(cuid()) carbetId String startDate DateTime endDate DateTime scope AvailabilityScope @default(PUBLIC) blockReason AvailabilityBlockReason @default(NONE) isAvailable Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade) @@index([carbetId]) @@index([scope, blockReason]) @@index([startDate, endDate]) } model Booking { id String @id @default(cuid()) carbetId String tenantId String startDate DateTime endDate DateTime guestCount Int status BookingStatus @default(PENDING) amount Decimal @db.Decimal(10, 2) currency String @default("EUR") paymentStatus PaymentStatus @default(PENDING) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Restrict) tenant User @relation("BookingTenant", fields: [tenantId], references: [id], onDelete: Restrict) review Review? rentalBookings RentalBooking[] @@index([carbetId]) @@index([tenantId]) @@index([status, paymentStatus]) @@index([startDate, endDate]) } model Subscription { id String @id @default(cuid()) ownerId String carbetId String provider String providerSubId String? @unique status SubscriptionStatus @default(TRIAL) startedAt DateTime @default(now()) renewedAt DateTime? canceledAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt owner User @relation(fields: [ownerId], references: [id], onDelete: Restrict) carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade) @@index([ownerId]) @@index([carbetId]) @@index([status]) } model Review { id String @id @default(cuid()) bookingId String @unique carbetId String authorId String rating Int comment String? hostResponse String? hostRespondedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade) carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Restrict) author User @relation("ReviewAuthor", fields: [authorId], references: [id], onDelete: Restrict) @@index([carbetId]) @@index([authorId]) } model Plugin { key String @id name String description String category String version String @default("0.1.0") enabled Boolean @default(false) config Json @default("{}") migrationsApplied String[] @default([]) installedAt DateTime @default(now()) updatedAt DateTime @updatedAt lastEnabledAt DateTime? lastDisabledAt DateTime? @@index([category]) @@index([enabled]) } model ContentPage { slug String lang String @default("fr") title String body String // 'general' (about, faq, ...) ou 'legal' (cgv, mentions, ...) category String @default("general") published Boolean @default(true) lastEditedBy String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@id([slug, lang]) @@index([slug]) @@index([category]) @@index([published]) } model AuditLog { id String @id @default(cuid()) scope String event String target String? actorEmail String? details Json @default("{}") createdAt DateTime @default(now()) @@index([scope]) @@index([event]) @@index([actorEmail]) @@index([createdAt]) } model Setting { key String @id value Json @default("{}") updatedAt DateTime @updatedAt updatedBy String? } model Translation { key String lang String value String updatedAt DateTime @updatedAt updatedBy String? @@id([key, lang]) @@index([lang]) } model PasswordResetToken { tokenHash String @id userId String expiresAt DateTime createdAt DateTime @default(now()) @@index([userId]) @@index([expiresAt]) } model Favorite { userId String carbetId String createdAt DateTime @default(now()) @@id([userId, carbetId]) @@index([userId]) @@index([carbetId]) } enum RoadAccess { NONE DRY_SEASON_ONLY ALL_YEAR } enum Electricity { NONE SOLAR GENERATOR_READY EDF } enum RentalCategory { SLEEP NAVIGATION FISHING COOKING SAFETY } enum RentalBookingStatus { PENDING CONFIRMED HANDED_OVER RETURNED CANCELLED } model RentalProvider { id String @id @default(cuid()) name String isSystemD Boolean @default(false) managedByUserId String? /// Si renseigné, le provider appartient à une organisation (CE) ; tout CE_MANAGER /// membre de l'org peut gérer items et réservations en plus du manager nominal. organizationId String? contactEmail String? contactPhone String? rivers String[] @default([]) description String? commissionPct Decimal @db.Decimal(5, 2) @default(0) active Boolean @default(true) approved Boolean @default(false) approvedAt DateTime? approvedBy String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt manager User? @relation(fields: [managedByUserId], references: [id], onDelete: SetNull) organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull) items RentalItem[] rentalBookings RentalBooking[] payoutMarks RentalPayoutMark[] @@index([active, approved]) @@index([managedByUserId]) @@index([organizationId]) } /// Trace les reversements bancaires manuels (System D paie le provider hors plateforme). /// La période est représentée par le mois (1er du mois minuit UTC) ; unique par /// (provider, période) pour empêcher de marquer 2 fois le même mois. model RentalPayoutMark { id String @id @default(cuid()) providerId String /// 1er du mois minuit UTC — sert de clé de période. periodMonth DateTime /// Montant effectivement viré au provider, en euros. amount Decimal @db.Decimal(10, 2) /// Référence de virement (optionnelle, à coller depuis la banque). reference String? paidAt DateTime @default(now()) paidByEmail String? createdAt DateTime @default(now()) provider RentalProvider @relation(fields: [providerId], references: [id], onDelete: Cascade) @@unique([providerId, periodMonth]) @@index([periodMonth]) } model RentalItem { id String @id @default(cuid()) providerId String category RentalCategory name String description String? imageUrl String? pricePerDay Decimal @db.Decimal(8, 2) pricePerWeek Decimal? @db.Decimal(8, 2) deposit Decimal @db.Decimal(8, 2) @default(0) totalQty Int @default(1) withMotor Boolean @default(false) fuelIncluded Boolean @default(false) requiresLicense Boolean @default(false) active Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt provider RentalProvider @relation(fields: [providerId], references: [id], onDelete: Cascade) availabilities RentalItemAvailability[] lines RentalLine[] media RentalItemMedia[] @@index([providerId]) @@index([category, active]) } model RentalItemMedia { id String @id @default(cuid()) itemId String type MediaType s3Key String s3Url String sortOrder Int @default(0) createdAt DateTime @default(now()) item RentalItem @relation(fields: [itemId], references: [id], onDelete: Cascade) @@index([itemId, sortOrder]) } model RentalItemAvailability { id String @id @default(cuid()) itemId String startDate DateTime endDate DateTime qty Int reason String rentalBookingId String? createdAt DateTime @default(now()) item RentalItem @relation(fields: [itemId], references: [id], onDelete: Cascade) @@index([itemId, startDate, endDate]) @@index([rentalBookingId]) } model RentalBooking { id String @id @default(cuid()) bookingId String? tenantId String providerId String startDate DateTime endDate DateTime status RentalBookingStatus @default(PENDING) paymentStatus PaymentStatus @default(PENDING) itemsTotal Decimal @db.Decimal(10, 2) depositTotal Decimal @db.Decimal(10, 2) commissionAmount Decimal @db.Decimal(10, 2) @default(0) amount Decimal @db.Decimal(10, 2) currency String @default("EUR") stripeSessionId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt booking Booking? @relation(fields: [bookingId], references: [id], onDelete: SetNull) tenant User @relation("RentalBookingTenant", fields: [tenantId], references: [id], onDelete: Restrict) provider RentalProvider @relation(fields: [providerId], references: [id], onDelete: Restrict) lines RentalLine[] @@index([tenantId, status]) @@index([providerId, status]) @@index([bookingId]) @@index([startDate, endDate]) } model RentalLine { id String @id @default(cuid()) rentalBookingId String itemId String qty Int pricePerDay Decimal @db.Decimal(8, 2) deposit Decimal @db.Decimal(8, 2) @default(0) lineTotal Decimal @db.Decimal(10, 2) rentalBooking RentalBooking @relation(fields: [rentalBookingId], references: [id], onDelete: Cascade) item RentalItem @relation(fields: [itemId], references: [id], onDelete: Restrict) @@index([rentalBookingId]) }