Sixième Étoile — Documentation

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.

FieldMeaning
unitPriceUnit price excl. tax (HT)
totalPriceunitPrice × quantity, excl. tax (HT)
unitPriceTtcUnit price incl. tax (TTC) — source of truth from user input, nullable
totalTtcunitPriceTtc × quantity
vatAmounttotalTtc - totalPrice

Do not display unitPrice to end-users as the final price without adding VAT. Use totalTtc for 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 100x

Default 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:

ValueDescription
TRANSFERPoint-to-point transfer (airport, station, hotel)
EXCURSIONRound-trip with stops (city tour, day trip)
DISPOMise à disposition — hourly charter with km cap
OFF_GRIDUnstructured 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:

TableIndex columnsPurpose
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

TableUnique constraintEnforced how
organizationslugSchema @@unique
quote(organizationId, reference)Schema @@unique
orderreferenceSchema @@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_accountone default per org (active)Partial unique index via raw migration
driver_locationdriverIdSchema @@unique — one row per driver
tracking_tokentokenSchema @@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:

  1. EndCustomer.preferredLanguage (if passenger is specified)
  2. Contact.preferredLanguage (billing contact)
  3. Operator fallback (fr)

Soft-delete conventions

No model uses hard-delete by default. Soft-delete patterns:

ModelFieldNotes
DriverisActiveInactive drivers excluded from dispatch
Vehiclestatus = ARCHIVED / SOLDArchived vehicles excluded from assignments
ContactisArchivedHidden from default lists, restorable
BankAccountisActiveReferenced accounts cannot be hard-deleted (422 guard)
AgencyPortalUserstatus = INACTIVEAccess 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).

Was this page helpful?

On this page