fixing platform admin
This commit is contained in:
@@ -0,0 +1,980 @@
|
||||
# Prisma Schema — RentalDriveGo (Complete)
|
||||
|
||||
> Stripe has been removed. Payments use AmanPay and PayPal.
|
||||
|
||||
```prisma
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// B2B — COMPANY SIDE
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
model Company {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
slug String @unique
|
||||
email String @unique
|
||||
phone String?
|
||||
address Json?
|
||||
status CompanyStatus @default(PENDING)
|
||||
|
||||
// RentalDriveGo billing — company pays subscription via AmanPay or PayPal
|
||||
// No third-party customer ID needed — we manage payment references ourselves
|
||||
subscriptionPaymentRef String? // last AmanPay/PayPal transaction ref
|
||||
|
||||
// Public API key
|
||||
apiKey String @unique @default(cuid())
|
||||
|
||||
subscription Subscription?
|
||||
brand BrandSettings?
|
||||
employees Employee[]
|
||||
vehicles Vehicle[]
|
||||
offers Offer[]
|
||||
reservations Reservation[]
|
||||
customers Customer[]
|
||||
rentalPayments RentalPayment[]
|
||||
subscriptionInvoices SubscriptionInvoice[]
|
||||
contractSettings ContractSettings?
|
||||
accountingSettings AccountingSettings?
|
||||
notifications Notification[] @relation("CompanyNotifications")
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("companies")
|
||||
}
|
||||
|
||||
enum CompanyStatus {
|
||||
PENDING
|
||||
TRIALING
|
||||
ACTIVE
|
||||
PAST_DUE
|
||||
SUSPENDED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
// ─── Subscription ─────────────────────────────────────────────
|
||||
|
||||
model Subscription {
|
||||
id String @id @default(cuid())
|
||||
companyId String @unique
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
|
||||
plan Plan @default(STARTER)
|
||||
billingPeriod BillingPeriod @default(MONTHLY)
|
||||
status SubscriptionStatus @default(TRIALING)
|
||||
currency String @default("MAD") // MAD, USD, EUR
|
||||
|
||||
trialStartAt DateTime?
|
||||
trialEndAt DateTime?
|
||||
currentPeriodStart DateTime?
|
||||
currentPeriodEnd DateTime?
|
||||
cancelledAt DateTime?
|
||||
cancelAtPeriodEnd Boolean @default(false)
|
||||
|
||||
invoices SubscriptionInvoice[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("subscriptions")
|
||||
}
|
||||
|
||||
enum Plan { STARTER GROWTH PRO }
|
||||
enum BillingPeriod { MONTHLY ANNUAL }
|
||||
enum SubscriptionStatus { TRIALING ACTIVE PAST_DUE CANCELLED UNPAID }
|
||||
|
||||
// ─── Subscription Invoice ──────────────────────────────────────
|
||||
// Records each subscription payment (AmanPay or PayPal)
|
||||
|
||||
model SubscriptionInvoice {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
subscriptionId String
|
||||
subscription Subscription @relation(fields: [subscriptionId], references: [id])
|
||||
|
||||
amount Int // in smallest unit (centimes for MAD)
|
||||
currency String @default("MAD")
|
||||
status InvoiceStatus
|
||||
|
||||
// Payment provider reference — exactly one is set
|
||||
amanpayTransactionId String? @unique
|
||||
paypalCaptureId String? @unique
|
||||
paymentProvider PaymentProvider @default(AMANPAY)
|
||||
|
||||
paidAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([companyId])
|
||||
@@map("subscription_invoices")
|
||||
}
|
||||
|
||||
enum InvoiceStatus { PENDING PAID FAILED REFUNDED }
|
||||
enum FuelPolicyType { FULL_TO_FULL FULL_TO_EMPTY SAME_TO_SAME PREPAID FREE }
|
||||
enum PaymentProvider { AMANPAY PAYPAL }
|
||||
|
||||
// ─── Brand Settings ───────────────────────────────────────────
|
||||
|
||||
model BrandSettings {
|
||||
id String @id @default(cuid())
|
||||
companyId String @unique
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
|
||||
displayName String
|
||||
tagline String?
|
||||
logoUrl String?
|
||||
faviconUrl String?
|
||||
heroImageUrl String?
|
||||
|
||||
primaryColor String @default("#1A56DB")
|
||||
accentColor String @default("#F59E0B")
|
||||
|
||||
subdomain String @unique
|
||||
customDomain String? @unique
|
||||
customDomainVerified Boolean @default(false)
|
||||
customDomainAddedAt DateTime?
|
||||
|
||||
// Public contact info
|
||||
publicEmail String?
|
||||
publicPhone String?
|
||||
publicAddress String?
|
||||
publicCity String?
|
||||
publicCountry String?
|
||||
websiteUrl String?
|
||||
whatsappNumber String?
|
||||
facebookUrl String?
|
||||
instagramUrl String?
|
||||
|
||||
// Locale + currency for public site
|
||||
defaultLocale String @default("en")
|
||||
defaultCurrency String @default("MAD")
|
||||
|
||||
// Company's own payment accounts (for receiving rental payments)
|
||||
amanpayMerchantId String? // company's AmanPay merchant ID
|
||||
amanpaySecretKey String? // encrypted at rest
|
||||
paypalEmail String? // company's PayPal business email
|
||||
paypalMerchantId String? // optional PayPal merchant ID
|
||||
|
||||
paymentMethodsEnabled PaymentProvider[] // which methods they have set up
|
||||
|
||||
// Marketplace
|
||||
isListedOnMarketplace Boolean @default(true)
|
||||
marketplaceRating Float?
|
||||
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("brand_settings")
|
||||
}
|
||||
|
||||
// ─── Employee ─────────────────────────────────────────────────
|
||||
|
||||
model Employee {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
|
||||
clerkUserId String @unique
|
||||
firstName String
|
||||
lastName String
|
||||
email String
|
||||
phone String?
|
||||
role EmployeeRole @default(AGENT)
|
||||
isActive Boolean @default(true)
|
||||
|
||||
notifications Notification[] @relation("EmployeeNotifications")
|
||||
notificationPreferences NotificationPreference[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([companyId])
|
||||
@@map("employees")
|
||||
}
|
||||
|
||||
enum EmployeeRole { OWNER MANAGER AGENT }
|
||||
|
||||
// ─── Vehicle ──────────────────────────────────────────────────
|
||||
// Strictly company-scoped. Photos uploaded by the company appear
|
||||
// on their public site AND on the global marketplace.
|
||||
|
||||
model Vehicle {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
|
||||
make String
|
||||
model String
|
||||
year Int
|
||||
color String
|
||||
licensePlate String
|
||||
vin String?
|
||||
category VehicleCategory @default(ECONOMY)
|
||||
seats Int @default(5)
|
||||
transmission Transmission @default(AUTOMATIC)
|
||||
fuelType FuelType @default(GASOLINE)
|
||||
features String[] // ["AC", "GPS", "Bluetooth", "Baby seat"]
|
||||
|
||||
dailyRate Int // in smallest currency unit
|
||||
status VehicleStatus @default(AVAILABLE)
|
||||
|
||||
// Photos uploaded via Cloudinary — these URLs are shown on the marketplace
|
||||
photos String[] // Cloudinary HTTPS URLs, first photo is the "cover"
|
||||
|
||||
mileage Int?
|
||||
notes String?
|
||||
isPublished Boolean @default(true) // appears on public site + marketplace
|
||||
|
||||
reservations Reservation[]
|
||||
maintenance MaintenanceLog[]
|
||||
offerVehicles OfferVehicle[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([companyId])
|
||||
@@index([companyId, status])
|
||||
@@index([companyId, isPublished])
|
||||
@@map("vehicles")
|
||||
}
|
||||
|
||||
enum VehicleStatus { AVAILABLE RENTED MAINTENANCE OUT_OF_SERVICE }
|
||||
enum VehicleCategory { ECONOMY COMPACT MIDSIZE FULLSIZE SUV LUXURY VAN TRUCK }
|
||||
enum Transmission { AUTOMATIC MANUAL }
|
||||
enum FuelType { GASOLINE DIESEL ELECTRIC HYBRID }
|
||||
|
||||
// ─── Offer ────────────────────────────────────────────────────
|
||||
|
||||
model Offer {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
|
||||
title String
|
||||
description String?
|
||||
termsAndConds String?
|
||||
imageUrl String? // offer banner (Cloudinary)
|
||||
|
||||
type OfferType
|
||||
discountValue Int
|
||||
specialRate Int?
|
||||
|
||||
appliesToAll Boolean @default(true)
|
||||
categories VehicleCategory[]
|
||||
minRentalDays Int?
|
||||
maxRentalDays Int?
|
||||
|
||||
promoCode String? @unique
|
||||
maxRedemptions Int?
|
||||
redemptionCount Int @default(0)
|
||||
|
||||
validFrom DateTime
|
||||
validUntil DateTime
|
||||
isActive Boolean @default(true)
|
||||
isPublic Boolean @default(true) // shown on global marketplace
|
||||
isFeatured Boolean @default(false) // featured carousel (GROWTH+ plan)
|
||||
|
||||
vehicles OfferVehicle[]
|
||||
reservations Reservation[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([companyId])
|
||||
@@index([isPublic, isActive, validUntil])
|
||||
@@map("offers")
|
||||
}
|
||||
|
||||
enum OfferType { PERCENTAGE FIXED_AMOUNT FREE_DAY SPECIAL_RATE }
|
||||
|
||||
model OfferVehicle {
|
||||
offerId String
|
||||
offer Offer @relation(fields: [offerId], references: [id], onDelete: Cascade)
|
||||
vehicleId String
|
||||
vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([offerId, vehicleId])
|
||||
@@map("offer_vehicles")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// B2C — RENTER SIDE
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
model Renter {
|
||||
id String @id @default(cuid())
|
||||
firstName String
|
||||
lastName String
|
||||
email String @unique
|
||||
phone String?
|
||||
passwordHash String
|
||||
driverLicense String?
|
||||
dateOfBirth DateTime?
|
||||
nationality String?
|
||||
preferredLocale String @default("en")
|
||||
preferredCurrency String @default("MAD")
|
||||
fcmToken String?
|
||||
|
||||
emailVerified Boolean @default(false)
|
||||
phoneVerified Boolean @default(false)
|
||||
isActive Boolean @default(true)
|
||||
|
||||
reservations Reservation[] @relation("RenterReservations")
|
||||
savedCompanies RenterSavedCompany[]
|
||||
reviews Review[]
|
||||
notifications Notification[] @relation("RenterNotifications")
|
||||
notificationPreferences NotificationPreference[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("renters")
|
||||
}
|
||||
|
||||
model RenterSavedCompany {
|
||||
renterId String
|
||||
renter Renter @relation(fields: [renterId], references: [id], onDelete: Cascade)
|
||||
companyId String
|
||||
savedAt DateTime @default(now())
|
||||
|
||||
@@id([renterId, companyId])
|
||||
@@map("renter_saved_companies")
|
||||
}
|
||||
|
||||
// Customer = company-scoped CRM record. Auto-created when someone books with a company.
|
||||
model Customer {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
renterId String? // linked to global Renter (if booked as logged-in renter)
|
||||
|
||||
firstName String
|
||||
lastName String
|
||||
email String
|
||||
phone String?
|
||||
driverLicense String?
|
||||
dateOfBirth DateTime?
|
||||
nationality String?
|
||||
address Json?
|
||||
notes String?
|
||||
flagged Boolean @default(false)
|
||||
flagReason String?
|
||||
|
||||
// ── Driver license details & validation ───────────────────
|
||||
licenseExpiry DateTime?
|
||||
licenseIssuedAt DateTime?
|
||||
licenseCountry String?
|
||||
licenseNumber String?
|
||||
licenseCategory String? // "B", "A", "BE", etc.
|
||||
licenseExpired Boolean @default(false)
|
||||
licenseExpiringSoon Boolean @default(false) // <3 months validity
|
||||
licenseValidationStatus LicenseStatus @default(PENDING)
|
||||
licenseApprovedBy String? // Employee name
|
||||
licenseApprovedAt DateTime?
|
||||
licenseApprovalNote String?
|
||||
|
||||
reservations Reservation[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([companyId, email])
|
||||
@@index([companyId])
|
||||
@@map("customers")
|
||||
}
|
||||
|
||||
// ─── Reservation ──────────────────────────────────────────────
|
||||
|
||||
model Reservation {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
|
||||
vehicleId String
|
||||
vehicle Vehicle @relation(fields: [vehicleId], references: [id])
|
||||
|
||||
customerId String
|
||||
customer Customer @relation(fields: [customerId], references: [id])
|
||||
|
||||
renterId String? // global renter account
|
||||
renter Renter? @relation("RenterReservations", fields: [renterId], references: [id])
|
||||
|
||||
offerId String?
|
||||
offer Offer? @relation(fields: [offerId], references: [id])
|
||||
promoCodeUsed String?
|
||||
|
||||
// Where the booking originated
|
||||
source BookingSource @default(DASHBOARD)
|
||||
// For marketplace bookings: renter selected vehicle on explore, was redirected here
|
||||
marketplaceRef String? // vehicle ID + dates encoded from marketplace redirect
|
||||
|
||||
status ReservationStatus @default(DRAFT)
|
||||
|
||||
startDate DateTime
|
||||
endDate DateTime
|
||||
pickupLocation String?
|
||||
returnLocation String?
|
||||
|
||||
dailyRate Int
|
||||
discountAmount Int @default(0)
|
||||
totalDays Int
|
||||
totalAmount Int
|
||||
depositAmount Int @default(0)
|
||||
|
||||
checkedInAt DateTime?
|
||||
checkedOutAt DateTime?
|
||||
checkInMileage Int?
|
||||
checkOutMileage Int?
|
||||
|
||||
notes String?
|
||||
cancelReason String?
|
||||
cancelledBy CancelledBy?
|
||||
|
||||
// Document reference numbers — assigned once, stored permanently
|
||||
// PDFs are NEVER stored — generated fresh from DB data on every request
|
||||
contractNumber String? @unique // e.g. "CNT-2024-00042"
|
||||
invoiceNumber String? @unique // e.g. "INV-2024-00042"
|
||||
|
||||
// Additional check-in data needed for contract
|
||||
// check-in fuel moved to DamageReport.fuelLevel — kept for backward compat
|
||||
checkInFuelLevel String?
|
||||
checkOutFuelLevel String?
|
||||
|
||||
// ── Insurance, additional driver, pricing, damage ─────────
|
||||
insurances ReservationInsurance[]
|
||||
insuranceTotal Int @default(0)
|
||||
additionalDrivers AdditionalDriver[]
|
||||
additionalDriverTotal Int @default(0)
|
||||
pricingRulesApplied Json? // [{ruleId, name, type, amount}] snapshot at booking
|
||||
pricingRulesTotal Int @default(0)
|
||||
damageReports DamageReport[]
|
||||
|
||||
rentalPayments RentalPayment[]
|
||||
inspections DamageInspection[]
|
||||
review Review?
|
||||
|
||||
// Damage charge (added by employee after checkout inspection)
|
||||
damageChargeAmount Int?
|
||||
damageChargeNote String?
|
||||
|
||||
// Extras (GPS, baby seat, insurance, extra driver, etc.) — JSON array
|
||||
extras Json? // ReservationExtra[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([companyId])
|
||||
@@index([companyId, status])
|
||||
@@index([vehicleId, startDate, endDate])
|
||||
@@index([renterId])
|
||||
@@map("reservations")
|
||||
}
|
||||
|
||||
enum ReservationStatus { DRAFT CONFIRMED ACTIVE COMPLETED CANCELLED NO_SHOW }
|
||||
enum BookingSource { DASHBOARD PUBLIC_SITE MARKETPLACE API }
|
||||
enum CancelledBy { COMPANY RENTER SYSTEM }
|
||||
enum LicenseStatus { PENDING VALID EXPIRING APPROVED DENIED EXPIRED }
|
||||
enum InsuranceType { CDW SCDW THEFT THIRD_PARTY FULL BASIC ROADSIDE PERSONAL CUSTOM }
|
||||
enum InsuranceChargeType { PER_DAY PER_RENTAL PERCENTAGE_OF_RENTAL }
|
||||
enum DamageReportType { CHECKIN CHECKOUT }
|
||||
enum PricingRuleType { SURCHARGE DISCOUNT }
|
||||
enum PricingCondition { AGE_LESS_THAN AGE_GREATER_THAN LICENSE_YEARS_LESS_THAN LICENSE_YEARS_GREATER_THAN }
|
||||
enum PriceAdjustmentType { PERCENTAGE FLAT_PER_DAY FLAT_TOTAL }
|
||||
enum AdditionalDriverCharge { FREE PER_DAY FLAT }
|
||||
enum AdminRole { SUPER_ADMIN ADMIN SUPPORT FINANCE VIEWER }
|
||||
|
||||
// ─── Rental Payment ───────────────────────────────────────────
|
||||
// Payment from renter to company. No Stripe — uses AmanPay or PayPal.
|
||||
|
||||
model RentalPayment {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
reservationId String
|
||||
reservation Reservation @relation(fields: [reservationId], references: [id])
|
||||
|
||||
amount Int // in smallest unit
|
||||
currency String @default("MAD")
|
||||
status PaymentStatus @default(PENDING)
|
||||
type PaymentType @default(CHARGE)
|
||||
paymentProvider PaymentProvider
|
||||
|
||||
// Exactly one of these is set:
|
||||
amanpayTransactionId String? @unique
|
||||
paypalCaptureId String? @unique
|
||||
|
||||
paymentMethod String? // "card_national", "card_international", "cash", "ewallet", "paypal"
|
||||
paidAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([companyId])
|
||||
@@index([reservationId])
|
||||
@@map("rental_payments")
|
||||
}
|
||||
|
||||
enum PaymentStatus { PENDING SUCCEEDED FAILED REFUNDED PARTIALLY_REFUNDED }
|
||||
enum PaymentType { CHARGE DEPOSIT REFUND }
|
||||
|
||||
// ─── Review ───────────────────────────────────────────────────
|
||||
|
||||
model Review {
|
||||
id String @id @default(cuid())
|
||||
reservationId String @unique
|
||||
reservation Reservation @relation(fields: [reservationId], references: [id])
|
||||
renterId String
|
||||
renter Renter @relation(fields: [renterId], references: [id])
|
||||
companyId String
|
||||
|
||||
overallRating Int
|
||||
vehicleRating Int?
|
||||
serviceRating Int?
|
||||
comment String?
|
||||
isPublished Boolean @default(true)
|
||||
companyReply String?
|
||||
companyRepliedAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([companyId])
|
||||
@@map("reviews")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// NOTIFICATIONS
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
model Notification {
|
||||
id String @id @default(cuid())
|
||||
|
||||
companyId String?
|
||||
company Company? @relation("CompanyNotifications", fields: [companyId], references: [id], onDelete: Cascade)
|
||||
employeeId String?
|
||||
employee Employee? @relation("EmployeeNotifications", fields: [employeeId], references: [id], onDelete: Cascade)
|
||||
renterId String?
|
||||
renter Renter? @relation("RenterNotifications", fields: [renterId], references: [id], onDelete: Cascade)
|
||||
|
||||
type NotificationType
|
||||
title String
|
||||
body String
|
||||
data Json?
|
||||
|
||||
channel NotificationChannel
|
||||
status NotificationStatus @default(PENDING)
|
||||
sentAt DateTime?
|
||||
failReason String?
|
||||
readAt DateTime?
|
||||
providerMessageId String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([companyId])
|
||||
@@index([renterId])
|
||||
@@map("notifications")
|
||||
}
|
||||
|
||||
enum NotificationType {
|
||||
NEW_BOOKING BOOKING_CANCELLED PAYMENT_RECEIVED PAYMENT_FAILED
|
||||
SUBSCRIPTION_TRIAL_ENDING SUBSCRIPTION_SUSPENDED
|
||||
VEHICLE_MAINTENANCE_DUE OFFER_EXPIRING NEW_REVIEW_RECEIVED
|
||||
BOOKING_CONFIRMED PICKUP_REMINDER_24H PICKUP_REMINDER_2H
|
||||
VEHICLE_READY RETURN_REMINDER BOOKING_CANCELLED_BY_COMPANY
|
||||
REFUND_PROCESSED NEW_OFFER_FROM_SAVED_COMPANY REVIEW_REQUEST
|
||||
}
|
||||
|
||||
enum NotificationChannel { EMAIL SMS WHATSAPP IN_APP PUSH }
|
||||
enum NotificationStatus { PENDING SENT DELIVERED FAILED READ }
|
||||
|
||||
model NotificationPreference {
|
||||
id String @id @default(cuid())
|
||||
employeeId String?
|
||||
employee Employee? @relation(fields: [employeeId], references: [id], onDelete: Cascade)
|
||||
renterId String?
|
||||
renter Renter? @relation(fields: [renterId], references: [id], onDelete: Cascade)
|
||||
notificationType NotificationType
|
||||
channel NotificationChannel
|
||||
enabled Boolean @default(true)
|
||||
|
||||
@@unique([employeeId, notificationType, channel])
|
||||
@@unique([renterId, notificationType, channel])
|
||||
@@map("notification_preferences")
|
||||
}
|
||||
|
||||
// ─── Maintenance Log ──────────────────────────────────────────
|
||||
|
||||
model MaintenanceLog {
|
||||
id String @id @default(cuid())
|
||||
vehicleId String
|
||||
vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
|
||||
|
||||
type String
|
||||
description String?
|
||||
cost Int?
|
||||
mileage Int?
|
||||
performedAt DateTime
|
||||
nextDueAt DateTime?
|
||||
nextDueMileage Int?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([vehicleId])
|
||||
@@map("maintenance_logs")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// DOCUMENTS — Rental Contracts & Customer Invoices
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// PDFs are NEVER stored. All fields here are DATA, not files.
|
||||
// contractNumber and invoiceNumber are assigned once and immutable.
|
||||
|
||||
// ─── Contract Settings ────────────────────────────────────────
|
||||
// Per-company contract customization. One record per company.
|
||||
// Auto-created with defaults if not yet configured.
|
||||
|
||||
model ContractSettings {
|
||||
id String @id @default(cuid())
|
||||
companyId String @unique
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
|
||||
// Legal identity (may differ from brand display name)
|
||||
legalName String?
|
||||
registrationNumber String? // RC / SIRET / ICE
|
||||
taxId String? // TVA / ICE number
|
||||
|
||||
// Contract text (company writes their own)
|
||||
terms String @default("")
|
||||
fuelPolicy String @default("The vehicle must be returned with the same fuel level as at pickup.")
|
||||
depositPolicy String @default("The security deposit is refundable within 7 days after return, subject to vehicle inspection.")
|
||||
lateFeePolicy String @default("Late returns will incur a charge of 1 additional day rate per hour of delay.")
|
||||
damagePolicy String @default("The renter is liable for any damage not covered by insurance.")
|
||||
additionalClauses String[] @default([])
|
||||
|
||||
// Contract options
|
||||
signatureRequired Boolean @default(true)
|
||||
contractFooterNote String?
|
||||
|
||||
// Invoice options
|
||||
invoiceFooterNote String?
|
||||
showTax Boolean @default(false)
|
||||
taxRate Float? // 0.20 = 20%
|
||||
taxLabel String? // "TVA" | "VAT" | "Tax"
|
||||
|
||||
// Sequential numbering — incremented atomically via DB transaction
|
||||
contractPrefix String @default("CNT") // CNT-2024-00001
|
||||
invoicePrefix String @default("INV") // INV-2024-00001
|
||||
|
||||
// Structured fuel policy
|
||||
fuelPolicyType FuelPolicyType @default(FULL_TO_FULL)
|
||||
fuelPolicyNote String?
|
||||
fuelChargePerLiter Int? // in cents per liter
|
||||
fuelShortfallFee Int? // flat fee in cents if not returned full
|
||||
lateFeePerHour Int? // in cents per hour of late return
|
||||
|
||||
// Multiple tax lines as JSON: [{ label, rate, appliesToCategories? }]
|
||||
taxLines Json?
|
||||
lastContractSeq Int @default(0) // NEVER reset via API
|
||||
lastInvoiceSeq Int @default(0) // NEVER reset via API
|
||||
|
||||
// Additional driver pricing
|
||||
additionalDriverCharge AdditionalDriverCharge @default(FREE)
|
||||
additionalDriverDailyRate Int @default(0) // in smallest unit
|
||||
additionalDriverFlatRate Int @default(0)
|
||||
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("contract_settings")
|
||||
}
|
||||
|
||||
// ─── Insurance Policy ─────────────────────────────────────────
|
||||
|
||||
model InsurancePolicy {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
|
||||
name String
|
||||
description String?
|
||||
type InsuranceType
|
||||
chargeType InsuranceChargeType
|
||||
chargeValue Int // per-day rate, flat fee, or percentage (0-100)
|
||||
isRequired Boolean @default(false)
|
||||
isActive Boolean @default(true)
|
||||
sortOrder Int @default(0)
|
||||
|
||||
reservations ReservationInsurance[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([companyId])
|
||||
@@index([companyId, isActive])
|
||||
@@map("insurance_policies")
|
||||
}
|
||||
|
||||
model ReservationInsurance {
|
||||
id String @id @default(cuid())
|
||||
reservationId String
|
||||
reservation Reservation @relation(fields: [reservationId], references: [id], onDelete: Cascade)
|
||||
insurancePolicyId String
|
||||
insurancePolicy InsurancePolicy @relation(fields: [insurancePolicyId], references: [id])
|
||||
|
||||
// Snapshot at booking time
|
||||
policyName String
|
||||
policyType InsuranceType
|
||||
chargeType InsuranceChargeType
|
||||
chargeValue Int
|
||||
totalCharge Int
|
||||
|
||||
@@unique([reservationId, insurancePolicyId])
|
||||
@@map("reservation_insurances")
|
||||
}
|
||||
|
||||
// ─── Additional Driver ────────────────────────────────────────
|
||||
|
||||
model AdditionalDriver {
|
||||
id String @id @default(cuid())
|
||||
reservationId String
|
||||
reservation Reservation @relation(fields: [reservationId], references: [id], onDelete: Cascade)
|
||||
companyId String
|
||||
|
||||
firstName String
|
||||
lastName String
|
||||
email String?
|
||||
phone String?
|
||||
driverLicense String
|
||||
licenseExpiry DateTime?
|
||||
licenseIssuedAt DateTime?
|
||||
dateOfBirth DateTime?
|
||||
nationality String?
|
||||
|
||||
chargeType AdditionalDriverCharge @default(FREE)
|
||||
chargeValue Int @default(0)
|
||||
totalCharge Int @default(0)
|
||||
|
||||
licenseExpired Boolean @default(false)
|
||||
licenseExpiringSoon Boolean @default(false)
|
||||
requiresApproval Boolean @default(false)
|
||||
approvedBy String?
|
||||
approvedAt DateTime?
|
||||
approvalNote String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([reservationId])
|
||||
@@index([companyId])
|
||||
@@map("additional_drivers")
|
||||
}
|
||||
|
||||
// ─── Damage Report ────────────────────────────────────────────
|
||||
|
||||
model DamageReport {
|
||||
id String @id @default(cuid())
|
||||
reservationId String
|
||||
reservation Reservation @relation(fields: [reservationId], references: [id], onDelete: Cascade)
|
||||
companyId String
|
||||
type DamageReportType
|
||||
|
||||
damages Json // DamageZone[] — see advanced-features.md
|
||||
photos String[] // Cloudinary URLs (optional damage photos)
|
||||
fuelLevel FuelLevel @default(FULL)
|
||||
mileage Int?
|
||||
inspectedAt DateTime @default(now())
|
||||
inspectedBy String?
|
||||
customerSignedAt DateTime?
|
||||
customerName String?
|
||||
|
||||
@@unique([reservationId, type])
|
||||
@@index([companyId])
|
||||
@@map("damage_reports")
|
||||
}
|
||||
|
||||
// ─── Pricing Rule ─────────────────────────────────────────────
|
||||
|
||||
model PricingRule {
|
||||
id String @id @default(cuid())
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
|
||||
name String
|
||||
type PricingRuleType
|
||||
condition PricingCondition
|
||||
conditionValue Int // age or years threshold
|
||||
adjustmentType PriceAdjustmentType
|
||||
adjustmentValue Int // percentage (0-100) or flat amount in smallest unit
|
||||
isActive Boolean @default(true)
|
||||
description String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([companyId])
|
||||
@@map("pricing_rules")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// DAMAGE INSPECTION SYSTEM
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
model DamageInspection {
|
||||
id String @id @default(cuid())
|
||||
reservationId String
|
||||
reservation Reservation @relation(fields: [reservationId], references: [id], onDelete: Cascade)
|
||||
companyId String
|
||||
|
||||
type InspectionType
|
||||
mileage Int?
|
||||
fuelLevel FuelLevel
|
||||
fuelCharge Int? // in cents (calculated at checkout)
|
||||
generalCondition String?
|
||||
employeeNotes String?
|
||||
customerAgreed Boolean @default(false)
|
||||
inspectedAt DateTime @default(now())
|
||||
inspectedBy String? // employeeId
|
||||
|
||||
damagePoints DamagePoint[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([reservationId, type])
|
||||
@@index([reservationId])
|
||||
@@index([companyId])
|
||||
@@map("damage_inspections")
|
||||
}
|
||||
|
||||
enum InspectionType { CHECKIN CHECKOUT }
|
||||
|
||||
enum FuelLevel {
|
||||
FULL SEVEN_EIGHTHS THREE_QUARTERS FIVE_EIGHTHS
|
||||
HALF THREE_EIGHTHS QUARTER ONE_EIGHTH EMPTY
|
||||
}
|
||||
|
||||
model DamagePoint {
|
||||
id String @id @default(cuid())
|
||||
inspectionId String
|
||||
inspection DamageInspection @relation(fields: [inspectionId], references: [id], onDelete: Cascade)
|
||||
|
||||
viewType DiagramView
|
||||
x Float // 0-100 percentage of diagram width
|
||||
y Float // 0-100 percentage of diagram height
|
||||
|
||||
damageType DamageType
|
||||
severity DamageSeverity @default(MINOR)
|
||||
description String?
|
||||
isPreExisting Boolean @default(false)
|
||||
|
||||
@@index([inspectionId])
|
||||
@@map("damage_points")
|
||||
}
|
||||
|
||||
enum DiagramView { TOP FRONT REAR LEFT RIGHT }
|
||||
enum DamageType { SCRATCH DENT CRACK CHIP MISSING STAIN OTHER }
|
||||
enum DamageSeverity { MINOR MODERATE MAJOR }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// ACCOUNTING SETTINGS
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
model AccountingSettings {
|
||||
id String @id @default(cuid())
|
||||
companyId String @unique
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
|
||||
reportingPeriod ReportingPeriod @default(MONTHLY)
|
||||
fiscalYearStart Int @default(1) // 1=Jan
|
||||
currency String @default("MAD")
|
||||
|
||||
accountantEmail String?
|
||||
accountantName String?
|
||||
autoSendReport Boolean @default(false)
|
||||
reportFormat ReportFormat @default(PDF)
|
||||
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("accounting_settings")
|
||||
}
|
||||
|
||||
enum ReportingPeriod { WEEKLY MONTHLY QUARTERLY ANNUAL }
|
||||
enum ReportFormat { PDF CSV BOTH }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// ADMIN PANEL
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
model AdminUser {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
firstName String
|
||||
lastName String
|
||||
passwordHash String
|
||||
role AdminRole @default(SUPPORT)
|
||||
isActive Boolean @default(true)
|
||||
|
||||
totpSecret String?
|
||||
totpEnabled Boolean @default(false)
|
||||
|
||||
lastLoginAt DateTime?
|
||||
lastLoginIp String?
|
||||
|
||||
auditLogs AuditLog[]
|
||||
permissions AdminPermission[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("admin_users")
|
||||
}
|
||||
|
||||
enum AdminRole { SUPER_ADMIN ADMIN SUPPORT FINANCE VIEWER }
|
||||
|
||||
model AdminPermission {
|
||||
id String @id @default(cuid())
|
||||
adminUserId String
|
||||
adminUser AdminUser @relation(fields: [adminUserId], references: [id], onDelete: Cascade)
|
||||
resource AdminResource
|
||||
actions AdminAction[]
|
||||
|
||||
@@unique([adminUserId, resource])
|
||||
@@map("admin_permissions")
|
||||
}
|
||||
|
||||
enum AdminResource {
|
||||
COMPANIES COMPANY_EMPLOYEES SUBSCRIPTIONS PAYMENTS
|
||||
OFFERS RENTERS ADMIN_USERS AUDIT_LOGS PLATFORM_METRICS
|
||||
}
|
||||
|
||||
enum AdminAction { READ CREATE UPDATE DELETE SUSPEND IMPERSONATE }
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
adminUserId String
|
||||
adminUser AdminUser @relation(fields: [adminUserId], references: [id])
|
||||
|
||||
action String
|
||||
resource String
|
||||
resourceId String?
|
||||
companyId String?
|
||||
|
||||
before Json?
|
||||
after Json?
|
||||
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
note String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([adminUserId])
|
||||
@@index([companyId])
|
||||
@@index([action])
|
||||
@@map("audit_logs")
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user