Constraints & Non-obvious Semantics
Fields with non-obvious semantics, storage formats, key indexes, and database-level constraints.
This page flags fields that have non-obvious semantics, non-standard storage formats, or constraints that are easy to get wrong.
Critical: pricing field semantics
QuoteLine.unitPrice and QuoteLine.totalPrice are always HT
QuoteLine.unitPrice and QuoteLine.totalPrice are HT (hors taxes / excl. VAT) values — never TTC.
| Field | Meaning |
|---|---|
unitPrice | Unit price excl. tax (HT) |
totalPrice | unitPrice × quantity, excl. tax (HT) |
unitPriceTtc | Unit price incl. tax (TTC) — source of truth from user input, nullable |
totalTtc | unitPriceTtc × quantity |
vatAmount | totalTtc - totalPrice |
Do not display
unitPriceto end-users as the final price without adding VAT. UsetotalTtcfor customer-facing displays.
QuoteLine.vatRate — decimal percent, not a ratio
vatRate is stored as a decimal percentage. 10.00 means 10 %, not 0.10.
Correct: totalTtc = totalPrice × (1 + vatRate / 100)
Incorrect: totalTtc = totalPrice × (1 + vatRate) ← off by 100xDefault value: 10.00 (the French transport VAT rate). This default comes from OrganizationPricingSettings.defaultVatRate.
The same convention applies to InvoiceLine.vatRate, ZoneRoute.vatRate, ExcursionPackage.vatRate, DispoPackage.vatRate, and PartnerContractZoneRoute.vatRate.
Grid prices — HT or TTC depends on priceMode
ZoneRoute.fixedPrice, ExcursionPackage.price, and DispoPackage.basePrice may be stored as either HT or TTC. Check the priceMode field (HT / TTC) before using the value.
if (priceMode === 'TTC') {
htPrice = fixedPrice / (1 + vatRate / 100)
} else {
htPrice = fixedPrice
}PartnerContractZoneRoute.overridePrice is always HT (no priceMode on override).
TripType enum
Quote.tripType has exactly four values:
| Value | Description |
|---|---|
TRANSFER | Point-to-point transfer (airport, station, hotel) |
EXCURSION | Round-trip with stops (city tour, day trip) |
DISPO | Mise à disposition — hourly charter with km cap |
OFF_GRID | Unstructured bespoke request — no fixed pricing grid |
dropoffAddress is nullable for DISPO and OFF_GRID. vehicleCategoryId is nullable for OFF_GRID.
Mission.quoteId — nullable by design
Mission.quoteId is nullable. Internal tasks (operator-created missions not tied to a commercial quote) leave this field null. These missions have isInternal = true and are excluded from invoice generation.
Never assume quoteId is present in a Mission query. Always use a left-join or optional include:
// Prisma — safe
const mission = await prisma.mission.findUnique({
where: { id },
include: { quote: true } // quote may be null
})
// Guard before accessing quote fields
if (mission.quote) {
// use mission.quote.finalPrice, etc.
}Quote.tripAnalysis JSON shape
Quote.tripAnalysis stores the shadow calculation output from the pricing engine. Typical shape:
{
"segments": [
{
"type": "APPROACH" | "SERVICE" | "RETURN",
"distanceKm": 12.5,
"durationSeconds": 1800,
"costEur": 18.75
}
],
"totalDistanceKm": 37.2,
"totalDurationSeconds": 5400,
"costBreakdown": {
"fuel": 12.50,
"tolls": 4.20,
"driverCost": 18.00,
"wear": 3.72
},
"staffingPlan": {
"mode": "SIMPLE" | "DOUBLE_EQUIPAGE" | "RELAIS",
"additionalCost": 0
},
"alternativeCalculation": {
"mode": "DISPO",
"estimatedPrice": 220.00
}
}This field is read-only from the frontend — it is produced by packages/api/src/services/pricing-engine.ts and must not be hand-crafted.
Key database indexes
Performance-critical indexes defined in the schema:
| Table | Index columns | Purpose |
|---|---|---|
quote | (organizationId) | Org-scoped list queries |
quote | (organizationId, status) | Quote list filter by status |
quote | (organizationId, createdAt) | Default sort |
quote | (contactId) | Contact quote history |
quote | (pickupAt) | Calendar / Gantt queries |
quote | (source, awaitingOperatorPricing) | Agency inbox |
mission | (driverId, status) | Driver app "my active missions" |
mission | (status, startAt) | Dispatch cockpit active missions |
contact | (organizationId, isArchived) | Default list (archived filter) |
driver_location | (missionId) | Tracking: driver for mission |
tracking_token | (token) | Public token lookup |
invoice | (organizationId, number) | Unique constraint |
Unique constraints
| Table | Unique constraint | Enforced how |
|---|---|---|
organization | slug | Schema @@unique |
quote | (organizationId, reference) | Schema @@unique |
order | reference | Schema @@unique |
invoice | (organizationId, number) | Schema @@unique |
vehicle_category | (organizationId, code) | Schema @@unique |
pricing_zone | (organizationId, code) | Schema @@unique |
license_category | (organizationId, code) | Schema @@unique |
bank_account | one default per org (active) | Partial unique index via raw migration |
driver_location | driverId | Schema @@unique — one row per driver |
tracking_token | token | Schema @@unique |
Document counters and references
Sequential document references (QT-2026-001, ORD-2026-001, INV-2026-001) are generated using the DocumentCounter model:
DocumentCounter (organizationId, type, year) → lastNumber (atomic increment)Types: DEV (devis/quote), RES (reservation/order), MIS (mission), INV (invoice). The upsert+increment pattern guarantees uniqueness without race conditions.
Language field conventions
Language fields across all models store an ISO 639-1 two-letter code (fr, en). Forward-compatible values (es, pt, de, it) are planned. A CHECK constraint is enforced at the DB level via migration on Contact.preferredLanguage and EndCustomer.preferredLanguage.
Resolution order for PDF and email language:
EndCustomer.preferredLanguage(if passenger is specified)Contact.preferredLanguage(billing contact)- Operator fallback (
fr)
Soft-delete conventions
No model uses hard-delete by default. Soft-delete patterns:
| Model | Field | Notes |
|---|---|---|
Driver | isActive | Inactive drivers excluded from dispatch |
Vehicle | status = ARCHIVED / SOLD | Archived vehicles excluded from assignments |
Contact | isArchived | Hidden from default lists, restorable |
BankAccount | isActive | Referenced accounts cannot be hard-deleted (422 guard) |
AgencyPortalUser | status = INACTIVE | Access revoked, record preserved |
The onDelete cascades defined in the schema handle hard-deletes of parent records (e.g., deleting an Organization cascades to all its child records).