Skip to content

Module — Occurrences & Capacité

Synchronisé avec le code. Mettre à jour à chaque changement structurel.

Rôle

Une Occurrence est un départ daté d'un Travel : prix par tier, capacités, statut, rattachement comptable BC (NavNo). Le service TravelOccupancyAndBookingStateService calcule l'occupation et l'état "bookable" d'une occurrence depuis les bookings actifs.

Emplacement

  • Pages : src/Web/Pages/Occurrences/
  • Service : src/Application/Services/Entities/OccurrenceService.cs, src/Application/Services/TravelOccupancyAndBookingStateService.cs
  • Vue : src/Web/Frontend/Vue/src/occurrence-assign-resources/
  • Export Excel Helvetic : src/Web/Pages/Occurrences/HelveticListPassengers.cshtml.cs

Entités principales

  • Occurrence — instance datée. Champs clés :
    • Start, End : dates du voyage.
    • Status : Published / Cancelled.
    • SellingPrice, SellingPriceJunior, SellingPriceChild (round-trip) + variantes OneWay.
    • Capacity, CapacityMax : limites globales.
    • BreakEvenPoint : seuil de rentabilité (indicatif).
    • ExpectedPassengers : prévisionnel ([À CONFIRMER] usage exact).
    • NavNo : lien NAV/BC, et source du NAV_TravelID facturé pour tous les types de voyage (seul le balnéaire retombe sur SeasideDate.NavNo aller si l'occurrence n'en a pas). Cf. domain/business-rules.md > NavNo — source du NAV_TravelID facturé.
    • Note, FileOrder, FileActivityCertificate, FileReturnPlan : documents officiels uploadés.
    • MinutesForADayOfWork : minutes de travail standard pour calcul coût chauffeur.
    • EffectiveNumberOfDays (NotMapped) : nb jours effectifs.
    • BookingState (NotMapped) : Bookable/Done/Full/TooLate/Cancelled calculé.
  • TravelOccurrence : jointure N-N avec Travel (cf. travel-catalog).
  • OccurrenceCost + OccurrenceCostVariousCost : compta analytique par départ (charges réelles). À titre indicatif uniquement, données pas publiées plus loin (module en pause) — ⚠ sauf OccurrenceCost.VatType, qui n'est pas indicatif du tout : c'est le régime TVA appliqué à la facturation du départ (BookingService, groupe TVA + comptes BC). Cf. domain/business-rules.md, section « TVA non renseignée ».
  • OccurrencePrice : prix custom par chambre/option pour le voyage (Catalog).
  • OccurrenceRoomPrice : prix custom par chambre/option pour Seaside.
  • OccurrenceRoomTypeQuota : contingent par type de chambre (cf. product-catalog).
  • OccurrenceRoomTypeCabin : contingent cabines pour les croisières.
  • OccurrenceVehicle : véhicules assignés à l'occurrence.
  • OccurrenceResource + OccurrenceResourceSeat + OccurrenceResourceAccommodation : assignations chauffeur/hôtesse/musicien à l'occurrence (avec siège dans le car et chambre à l'hôtel).
  • OccurrenceLine : surcharge des lignes de chargement/dépose pour cette occurrence (override des TravelLines).
  • OccurrenceBlockedSeat : sièges bloqués (réservés à l'équipe ou inutilisables).
  • OccurrenceCancellationTask + OccurrenceCancellationTaskValue : checklist d'annulation prévue mais pas utilisée.
  • OccurrenceDocument : documents attachés.
  • OneDayTravelDriveOccurrences : pour les courses d'un jour, lien Drive ↔ Occurrence avec quota par drive.

Règles métier spécifiques

  • OccurrenceStatus : binaire, Published ou Cancelled. Pas d'état "Draft" ou "Archived".
  • Calcul d'occupation (cf. business-rules.md > Calcul d'occupation) : par défaut les occurrences passées sont skip-pées. Pour les inclure (page archive, stats) → CalculateOccupancyAndSetBookingState(..., includeAllOccurrences: true).
  • BookingState est posé pour toutes les occurrences listées :
    • Cancelled si Status = Cancelled
    • Done si Start < now
    • sinon calculé via remplissage : Bookable, TooLate, Full
  • Travel.BookingState (NotMapped) est agrégé depuis les occurrences par DetermineTravelBookingState (cf. TravelOccupancyAndBookingStateService), dans cet ordre :
    • Bookable si au moins une occurrence est Bookable
    • sinon TooLate si au moins une est TooLate
    • sinon Cancelled si toutes les occurrences sont annulées OU si toutes les occurrences encore à venir (End >= now) sont annulées (plus aucun départ futur vendable)
    • sinon Full
    • ⚠ Ne produit jamais Done : un voyage 100% passé non annulé reste Full. La condition Cancelled a été ajoutée en mai 2026 (avant, un voyage entièrement annulé tombait à tort dans Full).
  • MainTravelOccurrence sur Occurrence : helper qui retourne Travels.FirstOrDefault(p => p.IsPriority) ou à défaut le premier TravelOccurrence.
  • SettingGeneral : porté par chaque occurrence pour overrider des paramètres généraux (à confirmer).
  • Occurrence.Billed : booléen indiquant que toutes les factures du départ sont émises.
  • Baseline whitelist Published || DraftInternalBookable dans OccurrenceService.SearchAsync : appliqué sur le Main Travel dès la construction initiale du IQueryable, sans override. Tout nouveau TravelStatus reste non-listé tant qu'il n'est pas explicitement ajouté à ce filtre. Détail dans domain/business-rules.md > Travel Draft vs DraftInternalBookable vs Published.

Points d'attention / pièges

  • OccurrenceCost indicatif, sauf VatType : ne pas baser de calcul de prix client sur les autres champs. Le prix vendu = Occurrence.SellingPrice/Junior/Child (+ variantes OneWay) + ajustements activities/insurance/supplements via BookingService. VatType fait exception : il pilote la facturation réelle, un départ vendable ne peut pas le laisser à « - » (cf. domain/business-rules.md).
  • Helvetic salutation codes : si on modifie GetSalutationCode dans HelveticListPassengers.cshtml.cs, ça impacte aussi les exports Seaside via BuildHelveticWorksheet. Ne pas dupliquer la logique ailleurs.
  • o.NumberOfPassengers est [NotMapped] sur Occurrence et est rempli par TravelOccupancyAndBookingStateService. Ne pas espérer le voir directement depuis _db.Occurrences.
  • HasImpactOnOccurrenceOccupancyExpr : utiliser cette expression (pas la IsActive collection) dans les IQueryable.Where(...) pour que EF traduise correctement le SQL. Inclut désormais les PendingWebOrMobile en hold (txid OU CreatedAt >= PendingHoldCutoff(), 20 min) — cf. domain/business-rules.md > Hold des réservations web. Les filtres inline équivalents (... || (b.Status == PendingWebOrMobile && (b.TransactionId != null || b.CreatedAt >= ValidatingBookingStatus.PendingHoldCutoff())))) doivent rester alignés sur cette règle.
  • La capacité opposable à une réservation n'est pas Occurrence.CapacityMax : ce champ ne pilote que l'occupation affichée et le BookingState (Full). BookingService.Validate ne le lit jamais — il contrôle les sièges (voyages avec plan de car), les quotas de types de chambre, et pour les courses d'un jour les quotas de lignes de chargement (OneDayTravelDriveOccurrences.Quota). Un dépassement de CapacityMax reste donc possible si les quotas de lignes totalisent plus que la capacité. Cf. domain/business-rules.md > Quota de ligne de chargement (OneDay) (surbooking Europa-Park du 22.08.2026).
  • Compteurs de passagers divergents entre écrans (Plan vs ListPassengers vs Index Occurrences vs tableau de chargement) : voir le tableau récap dans domain/business-rules.md > Compteurs de passagers. Plan filtre par sens (TripType), ListPassengers + Index ne filtrent pas, le tableau de chargement filtre aussi par sens.
  • Passagers sans LoadingStopId : depuis le fix mai 2026, TravelListService.PopulateDriveWithPassengers les rattache au drive virtuel "Trajet indéterminé" sous un stop "Lieu non assigné" (au lieu de les éjecter silencieusement). LoadingTableService ne fait pas encore ce fallback — un même booking mal saisi peut donner des compteurs différents entre Plan et tableau de chargement.
  • Occurrence.ResourceAccommodations = tous les hôtels du départ : la collection n'est pas scopée à un hébergement, et elle conserve les chambres des hôtels retirés du voyage depuis (orphelines invisibles dans Resources/Assign). Tout écran par hébergement doit filtrer sur ra.Room.RoomType.AccommodationId. Détail dans resources-visual-planning.md > Points d'attention.
  • GetTravelVehicleSeatOccupancy : ne jamais laisser bookings en IQueryable (correctif 11.08.2026, mesuré 5 872 ms → 141 ms). La boucle par siège cherchait le BookingPassenger occupant via bookings.SelectMany(b => b.Passengers).FirstOrDefault(...) alors que bookings était une requête non matérialisée portant quatre Include de collections : la requête était rejouée pour chaque siège réservé (90 passagers ⇒ 90 exécutions). La liste est désormais matérialisée une fois et les passagers indexés sur la même clé (vehicleIdx, numéro de siège) que le dictionnaire bookedSeats construit juste au-dessus, avec la même règle « le premier gagne » — résultat identique. ⚠ Méthode partagée : plan de véhicule, attribution des sièges, chambres balnéaires, dossier chauffeur. Trouvé en instrumentant l'export d'un dossier chauffeur, où elle pesait 35 % du temps total.
  • Occupation par drive (OneDay) & stops partagés : CalculateDriveOccupancy indexe les drives par StopId. Depuis le passage de DriveStop en many-to-many, un même stop peut appartenir à plusieurs drives. Le BookingPassenger ne porte que son LoadingStop.Id (pas de DriveId), donc on ne peut pas savoir ici sur quel drive il est. Règle : le passager est rattaché à UN seul drive (le premier, déterministe via GroupBy(StopId).First()) → l'occupation totale reste égale au nombre de passagers. ⚠ Compter sur tous les drives desservant le stop sur-gonflerait l'occupation. Avant ce fix, le ToDictionary(StopId → Drive) crashait (ArgumentException: same key) dès qu'un stop était partagé (ex. page Reports/Management).

Conventions locales

  • L'export Excel Helvetic charge seulement les BookingPassenger non annulés des bookings actifs (statuts dans HasImpactOnOccurrenceOccupancy + PendingWebOrMobile avec txid ou en hold CreatedAt >= PendingHoldCutoff()).
  • OccurrenceFilterMode enum guide certaines requêtes (à explorer si besoin).

Dépendances

  • travel-catalog (Travel parent).
  • booking (consomme Occurrence + capacité).
  • seaside (Seaside-spécifique pour les rotations multiples).
  • loading-tables (un tableau peut grouper plusieurs occurrences).
  • resources-visual-planning (assignment chauffeurs).

Contributors

No contributors

Changelog

No recent changes