Files
carmanagement/docs/project-design/schema.md
T
2026-05-26 19:28:44 -04:00

779 lines
18 KiB
Markdown

# Database Schema and Table Relationships
This document explains the current Prisma schema, how the data is partitioned, and how the main tables relate to each other.
Source of truth:
- `packages/database/prisma/schema.prisma`
## Design Principles
The schema is built around a multi-tenant SaaS model with three distinct concerns:
1. company operations for running a rental business
2. renter-facing booking and review flows
3. platform-level SaaS billing and administration
Important design choices:
- `Company` is the tenant root for nearly every business table.
- operational data and platform billing data are kept separate.
- reservation-time snapshots are stored explicitly so later changes to policies or prices do not rewrite history.
- some user concepts intentionally exist twice:
- `Renter` is a cross-company identity
- `Customer` is a company-owned CRM record
- status changes are modeled with enums rather than free-text fields where the workflow matters.
## High-Level Domain Map
```mermaid
erDiagram
Company ||--o| Subscription : has
Company ||--o| BrandSettings : has
Company ||--o| ContractSettings : has
Company ||--o| AccountingSettings : has
Company ||--o{ Employee : employs
Company ||--o{ Vehicle : owns
Company ||--o{ Offer : publishes
Company ||--o{ Customer : manages
Company ||--o{ Reservation : receives
Company ||--o{ RentalPayment : collects
Company ||--o{ InsurancePolicy : defines
Company ||--o{ PricingRule : defines
Company ||--o{ BillingAccount : bills
Company ||--o{ BillingInvoice : invoices
Company ||--o{ SubscriptionInvoice : tracks
Company ||--o{ Notification : receives
Company ||--o{ Complaint : tracks
Vehicle ||--o{ Reservation : booked_in
Vehicle ||--o{ MaintenanceLog : has
Vehicle ||--o{ VehicleCalendarBlock : blocked_by
Offer ||--o{ OfferVehicle : applies_to
Vehicle ||--o{ OfferVehicle : offered_in
Customer ||--o{ Reservation : books
Renter ||--o{ Reservation : owns
Reservation ||--o{ RentalPayment : paid_by
Reservation ||--o{ ReservationInsurance : includes
Reservation ||--o{ AdditionalDriver : adds
Reservation ||--o{ DamageInspection : inspected_by
Reservation ||--o{ DamageReport : reported_by
Reservation ||--o{ ReservationPhoto : photographed_by
Reservation ||--o| Review : reviewed_as
Reservation ||--o{ Complaint : disputed_in
InsurancePolicy ||--o{ ReservationInsurance : snapshotted_into
Renter ||--o{ Review : writes
Review ||--o{ Complaint : escalates_to
AdminUser ||--o{ AdminPermission : has
AdminUser ||--o{ AuditLog : writes
```
## Core Tenant Root
### `Company`
`Company` is the anchor row for tenant isolation.
Key fields:
- identity: `id`, `name`, `slug`, `email`, `phone`
- public/integration: `apiKey`
- SaaS state: `status`, `subscriptionPaymentRef`
- address: `address` JSON
Key relations:
- 1:1 with `Subscription`
- 1:1 with `BrandSettings`
- 1:1 with `ContractSettings`
- 1:1 with `AccountingSettings`
- 1:N with `Employee`, `Vehicle`, `Offer`, `Customer`, `Reservation`, `RentalPayment`
- 1:N with billing tables such as `BillingAccount`, `BillingInvoice`, `SubscriptionInvoice`
- 1:N with `InsurancePolicy`, `PricingRule`, `Notification`, `Complaint`
Important uniqueness:
- `slug`
- `email`
- `apiKey`
## SaaS Subscription and Billing
There are two billing layers in the schema:
1. legacy provider-oriented subscription records
2. richer billing-account and billing-invoice records for platform finance operations
### `Subscription`
One row per company.
Purpose:
- current plan
- billing period
- lifecycle state
- trial dates
- renewal windows
- suspension and cancellation timing
Relations:
- belongs to one `Company`
- has many `SubscriptionInvoice`
- has many `SubscriptionEvent`
- can be referenced by many `BillingInvoice`
- can be referenced by many `BillingEvent`
### `SubscriptionEvent`
Append-only event log for the subscription lifecycle.
Examples of what it represents:
- trial started
- checkout created
- payment received
- plan changed
- grace period extended
- subscription suspended
### `SubscriptionInvoice`
Legacy or provider-facing subscription payment records.
Purpose:
- capture AmanPay or PayPal payment references
- track amount, currency, due date, paid date, and failure/void state
- optionally bridge to the richer `BillingInvoice` model via `billingInvoiceId`
### `PaymentAttempt`
Legacy payment-attempt rows tied to `SubscriptionInvoice`.
This is narrower than the newer billing payment tables and exists for provider-level retry/failure tracking.
### `BillingAccount`
The primary finance entity for a company at the platform level.
Purpose:
- billing identity and address
- invoice terms
- default currency
- tax flags
- default payment method
- dunning pause state
- external provider customer ID
Relations:
- belongs to one `Company`
- has many `BillingPaymentMethod`
- has many `BillingInvoice`
- has many `BillingPaymentIntent`
- has many `BillingPaymentAttempt`
- has many `BillingCreditBalance`, `BillingCreditLedgerEntry`, `BillingCreditNote`, `BillingRefund`, `BillingEvent`
### `BillingPaymentMethod`
Stored billing method metadata for the company account.
Examples:
- card brand/last4/expiration
- ACH/bank/manual invoice style billing methods
One billing account can have many payment methods; one may be selected as default.
### `BillingInvoice`
The main platform invoice table.
Purpose:
- invoice numbering and sequencing
- totals and tax math
- status transitions
- billing snapshot fields such as billing name and email
- finance/admin metadata
Relations:
- belongs to one `BillingAccount`
- belongs to one `Company`
- optionally belongs to one `Subscription`
- optionally has a linked legacy `SubscriptionInvoice`
- has many `BillingInvoiceLineItem`
- has many `BillingPaymentIntent`
- has many `BillingPaymentAttempt`
- has many `BillingTaxRecord`
- has many `BillingCreditNote`
- has many `BillingRefund`
- has many `BillingEvent`
### Supporting billing tables
- `BillingInvoiceLineItem`: immutable invoice lines with type, quantity, unit amount, and optional service period.
- `BillingPaymentIntent`: payment-intent level state for a billing invoice.
- `BillingPaymentAttempt`: actual collection attempts against an invoice.
- `BillingCreditBalance`: running credit balance per billing account and currency.
- `BillingCreditLedgerEntry`: credit ledger movement history.
- `BillingCreditNote`: credit documents issued against an invoice.
- `BillingRefund`: refund records for billing invoices.
- `BillingTaxRecord`: tax breakdown rows for an invoice.
- `BillingEvent`: append-only finance/subscription/company event log.
## Branding and Company Configuration
### `BrandSettings`
One-to-one with `Company`.
Purpose:
- public display name, tagline, logo, favicon, hero image
- theme colors
- subdomain and custom domain
- public contact and social metadata
- default locale and currency
- company-owned payment provider settings for renter payments
- marketplace visibility and rating
- homepage/menu JSON configuration
Important uniqueness:
- `companyId`
- `subdomain`
- `customDomain`
### `ContractSettings`
One-to-one with `Company`.
Purpose:
- rental contract legal copy
- invoice/footer text
- fuel and late-fee policy
- tax settings
- numbering prefixes and running sequences
- additional-driver charging policy
This table is used when generating operational rental documents.
### `AccountingSettings`
One-to-one with `Company`.
Purpose:
- reporting period
- fiscal-year start month
- accounting contact
- automatic report settings
- preferred export format
### `PricingRule`
Company-owned pricing rule definitions applied to reservations.
Purpose:
- define surcharge or discount logic
- express the rule type, condition, and adjustment
These rules are copied into reservation snapshots through `Reservation.pricingRulesApplied` and `Reservation.pricingRulesTotal`.
### `InsurancePolicy`
Company-defined upsell or mandatory protection products.
Purpose:
- define policy name and type
- define flat, percent, or per-day charges
- mark required or optional coverage
These are also snapshotted into reservations.
## Staff, Fleet, and Commercial Catalog
### `Employee`
Company staff records.
Purpose:
- operational user identity
- role assignment
- preferred language
- password-reset support
- activation state
Implementation note:
- `clerkUserId` still exists as a required unique field in the schema, but Clerk is no longer an active auth dependency in this project.
- the field is currently populated with local/generated identifiers so existing schema constraints remain satisfied.
Relations:
- belongs to one `Company`
- has many `Notification`
- has many `NotificationPreference`
Important uniqueness:
- `clerkUserId`
### `Vehicle`
The core fleet entity.
Purpose:
- static vehicle information
- public listing content
- daily rate and status
- pickup/dropoff location rules
Relations:
- belongs to one `Company`
- has many `Reservation`
- has many `MaintenanceLog`
- has many `VehicleCalendarBlock`
- participates in many-to-many offers through `OfferVehicle`
Soft deletion is implemented operationally by setting:
- `status = OUT_OF_SERVICE`
- `isPublished = false`
### `MaintenanceLog`
One-to-many from `Vehicle`.
Purpose:
- maintenance type and description
- cost, mileage, performed date
- next due date or mileage
### `VehicleCalendarBlock`
One-to-many from `Vehicle`.
Purpose:
- manual or maintenance blocks on the reservation calendar
- date-range unavailability independent of reservations
### `Offer`
Company-defined promotional offers.
Purpose:
- percentage/fixed/free-day/special-rate discount logic
- validity window
- promo code
- public and featured visibility
- redemption caps
Relations:
- belongs to one `Company`
- has many `Reservation`
- many-to-many with `Vehicle` through `OfferVehicle`
### `OfferVehicle`
Join table between `Offer` and `Vehicle`.
Composite primary key:
- `(offerId, vehicleId)`
## Renter Identity, Company CRM, and Reservation Aggregate
The schema intentionally separates global renter identity from company-local customer records.
### `Renter`
Cross-company end-user identity.
Purpose:
- login identity
- app preferences and verification state
- push token
- reservation and review linkage across companies
Relations:
- has many `Reservation`
- has many `Review`
- has many `Notification`
- has many `NotificationPreference`
- has many saved companies through `RenterSavedCompany`
### `RenterSavedCompany`
Join table that stores renter bookmarks.
Composite primary key:
- `(renterId, companyId)`
### `Customer`
Company-owned CRM record for a person renting from that company.
Purpose:
- renter contact and profile data as understood by that company
- notes and flag state
- driver-license metadata
- approval and compliance state
- company-specific reservation history
Relations:
- belongs to one `Company`
- has many `Reservation`
- has many `Complaint`
Important uniqueness:
- `(companyId, email)`
### `Reservation`
This is the central transactional aggregate for rental operations.
Core relations:
- belongs to one `Company`
- belongs to one `Vehicle`
- belongs to one `Customer`
- optionally belongs to one `Renter`
- optionally belongs to one `Offer`
Core business fields:
- booking source
- date range
- pickup/return locations
- pricing fields: `dailyRate`, `discountAmount`, `totalDays`, `totalAmount`, `depositAmount`
- payment summary fields: `paymentStatus`, `paidAmount`
- lifecycle fields: `status`, `checkedInAt`, `checkedOutAt`
- cancellation fields
- contract and invoice numbering
- review token and review reminder timestamps
- extras and pricing-rule snapshots
The reservation is the parent of most rental workflow records.
### Reservation child tables
- `RentalPayment`: individual payment rows for the reservation.
- `ReservationInsurance`: reservation-time snapshots of selected insurance products.
- `AdditionalDriver`: reservation-time records for extra drivers and approval status.
- `DamageReport`: pickup/dropoff summary damage records with photos and signatures.
- `DamageInspection`: structured inspection record per reservation and inspection type.
- `DamagePoint`: individual annotated damage points tied to one inspection.
- `ReservationPhoto`: ad hoc pickup/dropoff photo attachments.
- `Review`: at most one review per reservation.
- `Complaint`: operational or post-rental dispute record, optionally linked to reservation, review, and customer.
This snapshot pattern is important. The reservation stores what was actually sold and reviewed at the time of booking, not just pointers to mutable company configuration.
## Reservation-Adjacent Tables in Detail
### `RentalPayment`
Operational payment rows for rentals, not SaaS billing.
Purpose:
- amount, currency, type, status
- AmanPay or PayPal provider references
- paid timestamp
Relations:
- belongs to one `Company`
- belongs to one `Reservation`
### `ReservationInsurance`
Snapshot bridge between `Reservation` and `InsurancePolicy`.
It copies:
- policy name
- policy type
- charge type
- charge value
- total charge
That prevents later edits to the base insurance policy from mutating historical reservation pricing.
### `AdditionalDriver`
Reservation child rows for additional drivers.
Purpose:
- identity and license details
- charge model
- approval requirement and approval outcome
- expiry flags
### `DamageReport`
One row per reservation and report type.
Composite uniqueness:
- `(reservationId, type)`
Stores:
- summary damages JSON
- photo URLs
- fuel level
- mileage
- inspection metadata
### `DamageInspection`
Structured pickup/dropoff inspection entity.
Composite uniqueness:
- `(reservationId, type)`
Stores:
- fuel and mileage
- general condition notes
- customer agreement state
- inspector identity
### `DamagePoint`
Granular diagram point rows tied to `DamageInspection`.
Stores:
- diagram view
- x/y coordinates
- damage type and severity
- free-text description
- whether the damage is pre-existing
### `ReservationPhoto`
Simple photo attachments tied to a reservation with type `PICKUP` or `DROPOFF`.
### `Review`
One review per reservation.
Important uniqueness:
- `reservationId`
Purpose:
- overall, vehicle, and service ratings
- comment
- moderation/publication flags
- company reply data
- optional feedback category
Relations:
- optionally linked to `Renter`
- can have many `Complaint`
### `Complaint`
Company-scoped issue-tracking table.
Can be linked to:
- a reservation
- a review
- a customer
This supports both operational complaints and post-review escalations.
## Notifications and Messaging
### `Notification`
A notification can target one of three audiences:
- company-level
- employee-level
- renter-level
Purpose:
- delivery type
- title/body/data payload
- channel
- locale
- send/read/failure state
- provider message ID
### `NotificationTemplate`
Reusable message content keyed by:
- template key
- channel
- locale
- version
### `NotificationPreference`
Per-recipient opt-in/out rows.
The uniqueness rules are intentionally split:
- unique per `(employeeId, notificationType, channel)`
- unique per `(renterId, notificationType, channel)`
## Admin and Platform Governance
### `AdminUser`
Platform operator account.
Purpose:
- role
- password hash
- 2FA state
- login tracking
- password reset support
Relations:
- has many `AdminPermission`
- has many `AuditLog`
### `AdminPermission`
Resource/action override rows for an admin user.
Important uniqueness:
- `(adminUserId, resource)`
### `AuditLog`
Immutable log of admin-side actions.
Stores:
- actor
- action and resource
- optional company and resource IDs
- before/after JSON
- IP, user agent, and note
## Pricing and Platform Content Configuration
### `PricingConfig`
Platform-managed plan pricing rows.
Important uniqueness:
- `(plan, billingPeriod)`
### `PlanFeature`
Platform-managed feature labels per plan, ordered by `sortOrder`.
### `PricingPromotion`
Platform-managed promotion codes for SaaS pricing.
Purpose:
- code and description
- discount type/value
- applicable plans/periods
- usage limits
- validity window
## Relationship Summary by Cardinality
### One-to-one
- `Company` -> `Subscription`
- `Company` -> `BrandSettings`
- `Company` -> `ContractSettings`
- `Company` -> `AccountingSettings`
### One-to-many
- `Company` -> `Employee`, `Vehicle`, `Offer`, `Customer`, `Reservation`, `RentalPayment`
- `Company` -> `BillingAccount`, `BillingInvoice`, `SubscriptionInvoice`, `InsurancePolicy`, `PricingRule`, `Notification`, `Complaint`
- `Vehicle` -> `Reservation`, `MaintenanceLog`, `VehicleCalendarBlock`
- `Reservation` -> `RentalPayment`, `ReservationInsurance`, `AdditionalDriver`, `DamageReport`, `DamageInspection`, `ReservationPhoto`, `Complaint`
- `BillingAccount` -> `BillingPaymentMethod`, `BillingInvoice`, `BillingPaymentIntent`, `BillingPaymentAttempt`, `BillingCreditBalance`, `BillingCreditLedgerEntry`, `BillingCreditNote`, `BillingRefund`, `BillingEvent`
### Many-to-many through join tables
- `Offer` <-> `Vehicle` through `OfferVehicle`
- `Renter` <-> `Company` through `RenterSavedCompany`
## Indexing and Uniqueness Rules Worth Remembering
Operationally important uniqueness constraints:
- `Company.slug`
- `Company.email`
- `Company.apiKey`
- `BrandSettings.subdomain`
- `BrandSettings.customDomain`
- `Employee.clerkUserId`
- `Customer(companyId, email)`
- `Reservation.contractNumber`
- `Reservation.invoiceNumber`
- `Reservation.reviewToken`
- `Review.reservationId`
- `OfferVehicle(offerId, vehicleId)`
- `RenterSavedCompany(renterId, companyId)`
- `ReservationInsurance(reservationId, insurancePolicyId)`
- `DamageReport(reservationId, type)`
- `DamageInspection(reservationId, type)`
- `PricingConfig(plan, billingPeriod)`
- `NotificationPreference` employee and renter uniqueness pairs
## Practical Reading Guide
If you need to understand the schema quickly, read it in this order:
1. `Company`
2. `Subscription`, `BillingAccount`, `BillingInvoice`
3. `Employee`, `Vehicle`, `Offer`
4. `Customer`, `Renter`
5. `Reservation` and its child tables
6. `Notification*`
7. `Admin*`, `AuditLog`, `Pricing*`
That order matches how the application is structured in the API: tenant first, operations second, finance/admin last.