Sixième Étoile — Documentation

Shadow Calculation

Segments A / B / C (approach, service, return), estimated end time, availability overlap detection, round-trip modes, and how positioning costs feed the margin model.

The shadow calculation produces a full operational loop for every trip — including deadhead legs the client never pays for. It is the foundation for internal cost analysis, driver availability detection, and dispatch optimisation.

The Three Segments

SegmentNameRouteBilled to client?
AApproachBase → PickupNo (internal cost only)
BServicePickup → DropoffYes
CReturnDropoff → BaseNo (internal cost only)

calculateShadowSegments() assembles these three SegmentAnalysis objects inside TripAnalysis.segments. Each segment carries its own CostBreakdown (fuel, tolls, wear, driver time).

When no vehicle has been assigned yet (quote creation stage), segments A and C may be absent or estimated — the approach distance depends on which base the eventually-assigned vehicle departs from.

Routing source

TripAnalysis.routingSource records where segment distances came from:

ValueMeaning
GOOGLE_APIGoogle Routes API called for real distances
HAVERSINE_ESTIMATEStraight-line distance × correction factor
VEHICLE_SELECTIONDistances computed during vehicle dispatch selection

Time Analysis

calculateTimeAnalysis() takes the raw Google Routes duration and applies three sequential adjustments, all recorded in TimeAnalysis.

1. Vehicle speed adjustment

Predictive pricing now resolves speed from the vehicle category and its linked license rule:

  1. Vehicle.averageSpeedKmh, when a concrete vehicle is assigned.
  2. VehicleCategory.averageSpeedKmh, when the quote only knows the category.
  3. OrganizationLicenseRule.cappedAverageSpeedKmh, resolved from VehicleCategory.defaultLicenseCategoryId.
  4. Route-provider duration or Haversine fallback when no cap is configured.

This applies to both LIGHT and HEAVY categories. Missing rules surface a configuration warning and fall back to documented last-resort defaults rather than skipping compliance.

2. Traffic adjustment (pickup time)

The engine evaluates the pickup hour against a set of traffic rules. Only the first matching rule applies:

Rule nameHours (local)Adjustment
RUSH_HOUR_MORNING07:00 – 09:00+15 %
RUSH_HOUR_EVENING17:00 – 19:00+15 %
NIGHT22:00 – 06:00−10 %

The percentage is applied to the base Google duration (before vehicle adjustment), then the resulting minutes are added to the running total.

3. RSE mandatory breaks and staffing profile

French RSE regulation (Art. 561-2) limits continuous driving. The engine computes mandatory breaks and then resolves a staffing profile from the trip shape (TRANSFER, ROUND_TRIP, MULTI_STOP, MAD, SEJOUR):

breakCount = floor(drivingMinutes / 270)     // 4.5 h max continuous
totalBreakMinutes = breakCount × 45          // 45 min per break

Constants:

ConstantValue
Max continuous driving270 min (4.5 h)
Break duration45 min
Regulation referenceRSE Art. 561-2

mandatoryBreaks is null when the resolved rule does not trigger a break. LIGHT vehicles are no longer skipped by category alone; they use the linked license rule and, when configured, a bounded light-vehicle derogation.


Estimated End Time

calculateEstimatedEndAt(pickupAt, tripAnalysis) computes the mission end timestamp:

estimatedEndAt = pickupAt + totalDurationMinutes

Special case — MULTI_DAY compliance plan:

estimatedEndAt = pickupAt + (daysRequired × 24 × 60)

This timestamp is used by the dispatch engine to detect driver availability overlap: a driver cannot be assigned to a new mission whose window overlaps [scheduledAt, estimatedEndAt] of an existing confirmed mission.


Positioning Costs

calculatePositioningCosts() breaks out the segments A and C as billable items for margin analysis.

Approach fee (Segment A)

When a vehicle is selected, the approach cost equals segments.approach.cost.total — the full internal cost of driving from the base to the pickup point.

When no vehicle has been assigned yet, approachFee.cost = 0 and the reason records that the cost will be computed at dispatch.

Empty return (Segment C)

When a vehicle is selected, the empty return cost is:

adjustedCost = segments.return.cost.total × (emptyReturnCostPercent / 100)

emptyReturnCostPercent is configured in OrganizationPricingSettings (default 100 %). Setting it to 50 % means only half the return operating cost is counted against margin.

When no vehicle is selected, emptyReturn.cost = 0 — the engine cannot estimate the return distance without knowing the vehicle's base.

Neither the approach fee nor the empty return is added to the client-facing price. They are internal cost items used solely for profitability analysis and ProfitabilityIndicator calculation.


Round-Trip Segments

When a quote is marked as a round trip, calculateRoundTripSegments() replaces the simple ×2 shortcut with accurate per-segment pricing.

The six segments for a round trip:

SegmentRoutePresent in
ABase → PickupBoth modes
BPickup → Dropoff (outbound)Both modes
CDropoff → Base (empty return)RETURN_BETWEEN_LEGS only
DBase → Pickup (repositioning)RETURN_BETWEEN_LEGS only
EDropoff → Pickup (return service)Both modes
FPickup → Base (final empty return)Both modes

Round-trip mode selection

ModeConditionCost
WAIT_ON_SITENo waiting time, or waitingTimeMinutes < thresholdA + B + E + F ≈ 2× single leg
RETURN_BETWEEN_LEGSwaitingTimeMinutes ≥ threshold (default 120 min)A + B + C + D + E + F

waitOnSiteThresholdMinutes defaults to 120 minutes and can be overridden per call.

Round-trip cockpit behavior built on this analysis:

  • waitingTimeMinutes participates in recalculation, so changing the wait immediately updates the segment breakdown.
  • Dense-zone and long-wait detection can suggest switching the quote to DISPO / MàD when a continuous availability product is more coherent.
  • Multi-day round trips show a proposal to either convert to a STAY package or split into two independent transfer lines. The proposal is non-blocking.
  • sameDayRequired removes overnight-return options from staffing selection; returnBaseId lets the operator choose a different deadhead return base for the predictive calculation.

TripAnalysis Structure

interface TripAnalysis {
  segments: {
    approach: SegmentAnalysis | null;   // Segment A
    service: SegmentAnalysis;           // Segment B
    return: SegmentAnalysis | null;     // Segment C
  };
  totalDistanceKm: number;
  totalDurationMinutes: number;
  totalInternalCost: number;
  costBreakdown: CostBreakdown;         // Combined A + B + C
  routingSource: "GOOGLE_API" | "HAVERSINE_ESTIMATE" | "VEHICLE_SELECTION";
  vehicleSelection: VehicleSelectionInfo | undefined;
  compliancePlan: CompliancePlan | undefined;
  staffingPlan?: StaffingPlanSnapshot;
  sejourRSEAccounting?: SejourRSEAccountingResult;
  calculatedAt: string;                 // ISO 8601
}

Each SegmentAnalysis contains distanceKm, durationMinutes, cost (a CostBreakdown), and isEstimated.


Relationship to the Cost Model

The shadow calculation feeds directly into the cost model. See Cost Model for:

  • How CostBreakdown is built (fuel, tolls, wear, driver).
  • Fuel resolution chain (vehicle → category → organization → default).
  • Toll resolution (Google Routes → TollGuru → tollCostPerKm estimate).
  • RSE compliance staffing costs and how they inflate totalInternalCost.

See also

Was this page helpful?

On this page