Key Relationships
Foreign-key graph, multi-tenancy chain, and important nullable / optional relations.
This page documents the primary relationships between models. Understanding these links is essential before writing queries or API handlers.
Multi-tenancy: the organizationId chain
Every VTC business model carries a mandatory organizationId (FK to Organization). API handlers enforce row-level isolation by always filtering on the active organization from the authenticated session.
Organization
├── Contact (organizationId)
├── Quote (organizationId)
│ └── QuoteLine (quoteId)
├── Mission (organizationId)
│ └── MissionTransition (missionId)
│ └── MissionExpense (missionId)
├── Invoice (organizationId)
│ └── InvoiceLine (invoiceId)
│ └── InvoicePayment (invoiceId)
├── Order (organizationId)
├── Vehicle (organizationId)
├── Driver (organizationId)
├── PricingZone (organizationId)
└── … all other VTC modelsContact → commercial documents
A Contact is the anchor for all commercial activity:
Contact
├── quotes[] — all quotes for this contact
├── invoices[] — all invoices for this contact
├── orders[] — all orders (dossiers) for this contact
├── endCustomers[] — individual passengers under this partner/agency
├── partnerContract — 1:1 commercial contract (PARTNER/AGENCY only)
└── portalUsers[] — agency portal accounts (AGENCY only)An EndCustomer (individual passenger within an agency) can be attributed to a Quote via Quote.endCustomerId, bypassing the parent agency contact for personalized communication.
Quote → lines → missions
Quote
├── lines[] — ordered list of QuoteLine items
│ └── missions[] — operational Mission(s) spawned per dispatchable line
└── missions[] — all missions for this quote (convenience relation)Each QuoteLine with dispatchable = true can spawn one or more Mission records. The link is Mission.quoteLineId → QuoteLine.id.
Mission.quoteId — nullable
Mission.quoteId is nullable. There are two kinds of missions:
| Kind | quoteId | Description |
|---|---|---|
| Commercial mission | Set | Created from a QuoteLine, invoiceable |
| Internal task | null | Created directly by the operator (staff transfer, depot move, etc.) — not linked to a quote, excluded from invoice generation |
When writing queries that join Mission → Quote, always use a LEFT JOIN or include: { quote: true } — never assume quoteId is present.
Quote → Invoice
A Quote can have multiple Invoice records (e.g. deposit + balance, or supplement invoices). The link is Invoice.quoteId → Quote.id (nullable — standalone invoices exist).
Quote (status=ACCEPTED)
└── invoices[]
├── Invoice (STANDARD)
├── Invoice (SUPPLEMENT)
└── Invoice (CREDIT_NOTE)Order grouping
An Order acts as a "dossier" grouping:
Order
├── quotes[] — one or more Quote records
├── missions[] — all operational Missions for the order
└── invoices[] — all Invoice records for the orderMission.ref (e.g. ORD-2026-001-01) encodes the order reference and the mission sequence number within that order.
Fleet relations
Vehicle ──────────── VehicleCategory
│ │
└── OperatingBase └── ZoneRoute / ExcursionPackage / DispoPackage
(Method 1 pricing grids per category)A Vehicle must belong to both a VehicleCategory and an OperatingBase. The pricing engine uses the vehicle's base coordinates for deadhead distance calculation.
Driver licensing & compliance
Driver
├── driverLicenses[] — DriverLicense (M:N with LicenseCategory)
├── driverCalendarEvents[] — absences and unavailability periods
├── driverRSECounters[] — daily RSE driving/amplitude/rest counters
└── driverLocations — latest GPS position (1:1)OrganizationLicenseRule defines the RSE thresholds per LicenseCategory (max daily driving, amplitude, rest, break intervals). The compliance engine reads these dynamically — no hardcoded limits.
PartnerContract
A PartnerContract (1:1 with Contact) holds commercial grid assignments for partner/agency contacts:
PartnerContract
├── zoneRoutes[] — ZoneRoute entries via PartnerContractZoneRoute (M:N)
├── excursionPackages[] — ExcursionPackage entries (M:N)
└── dispoPackages[] — DispoPackage entries (M:N)Each junction table (PartnerContractZoneRoute, etc.) carries an optional overridePrice — when set, the partner-specific price takes precedence over the catalog price.
Agency portal
Contact (type=AGENCY)
├── portalUsers[] — AgencyPortalUser (M:1 with User)
│ ├── delegationsAsSource[] — StaffDelegation (coverage handoff FROM this user)
│ └── delegationsAsDelegate[] — StaffDelegation (coverage handoff TO this user)
└── clientDelegationsAsAgency[] — ClientDelegation (per-client ownership transfers)StaffDelegation models "while staff A is away, staff B covers all their clients". ClientDelegation models "this specific end-client is owned by this specific staff member". They are distinct models — do not confuse them.
Tracking chain
Mission
└── trackingTokens[] — TrackingToken (public magic links)
├── messages[] — TrackingMessage (customer messages)
└── notifications[] — TrackingNotification (delay alerts, rate-limited)
Mission
└── driverLocations[] — DriverLocation (current position)
└── locationHistory[] — DriverLocationHistory (breadcrumb trail)A TrackingToken expires automatically: the API checks expiresAt on every request and computes it as Mission.actualDropoffAt + Organization.gracePeriodMinutes.
Activity & audit log
Organization
└── activities[] — Activity (immutable business-level event log)
Organization
└── auditLogs[] — AuditLog (API-level developer audit — keys, webhooks)
Quote
├── statusAuditLogs[] — QuoteStatusAuditLog (status transitions)
└── quoteNotesAuditLogs[] — QuoteNotesAuditLog (notes edits)
Driver
└── complianceAuditLogs[] — ComplianceAuditLog (RSE decisions: APPROVED/BLOCKED/WARNING)MissionAuditLog was unified into Activity in Epic 74 (migration 20260420000000_activity_unification). The legacy table is preserved as mission_audit_log_legacy and is not exposed via Prisma.