Skip to content

Module — Catalogue produit (Hébergement + Sièges/Cabines + Activités + Suppléments)

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

Rôle

Catalogue des éléments composables d'un voyage : hôtels (Accommodation), types de chambres (RoomType), options (vue mer, balcon), decks/cabines de bateau (croisières), plans de sièges (cars), véhicules, activités optionnelles, suppléments globaux.

Emplacement

  • Pages : src/Web/Pages/Accommodations/, Activities/, GlobalSupplements/, Resources/, Vehicles/
  • Services : AccommodationService, ActivityService, ActivityPriceService, DeckService, GlobalSupplementService, RoomPriceService, RoomTypeQuotaService, VehicleService, VehicleTypeService
  • Vue : deck-configurator/, deck-preview/, passenger-seats/, accommodation-occupancy/, accommodation-costs/, activity-costs/

Sous-domaines & entités

Hébergement (hôtels)

  • Accommodation — hôtel/logement.
    • Seuils d'âge : ChildMin/ChildMax, JuniorMin/JuniorMax, AdultMin. Convention [Min, Max) — borne haute exclusive (cf. popover du formulaire : "Enfant de 2 à 4 ans, junior de 4 à 18..." → le Max d'une tranche = Min de la suivante). Utilisés pour : (1) le pricing Seaside (BookingService.GetCostPerPassenger, branches Seaside) et le meal plan ; (2) le filtrage des passagers dans les dropdowns d'assignation aux chambres côté front Vue (booking/steps/accommodation-activities/Passenger.vue, cost.js, store.js) — y compris pour les voyages catalogue. Conséquence : un trou entre ChildMax et JuniorMin (ou entre JuniorMax et AdultMin) fait disparaître les passagers de cet âge de toutes les dropdowns côté front, en plus de casser le pricing côté back.
    • Adresse, téléphone, dispo, photos.
    • VoucherAddress (string, nullable, multi-ligne) : bloc d'adresse libre rendu sur le voucher seaside (Application/TemplatesPdf/SeasideVoucher.liquid) à la place du composé Address + Zip + Locality. Fallback transparent : si vide → l'ancien rendu structuré est utilisé. Édité via le formulaire Accommodation (textarea sous "E-Mail rooming"). Les autres écrans (Occupancy, _Occupancy partial, etc.) continuent d'utiliser Address/Zip/Locality.
    • Champs MealPlan dédiés : BreakfastPriceChild/Junior/Adult, idem pour HalfBoard, FullBoard, AllInclusive, ProgramBoard.
  • AccommodationCategory — catégorie d'hôtel (utilisée pour les regroupements Seaside : Costa Brava hôtel X et Y → même rotation).
  • AccommodationAttribute, AccommodationPicture, Attribute — métadonnées et photos.

Types de chambre

  • RoomType — type (Single / Double / Suite…). Lien Category (RoomTypeCategory).
  • RoomTypeCategory — catégorie de chambre (groupement marketing).
  • RoomTypeOption — option (vue mer, balcon, terrasse).
  • RoomTypeQuota — contingent négocié manuellement avec l'hôtel pour une occurrence (combien de chambres dispo).
  • Room — chambre concrète (numéro, capacité).
  • RoomPrice — prix par chambre/option.
  • OccurrenceRoomPrice + OccurrenceRoomTypeQuota + OccurrencePrice : versions overridées par occurrence (cf. module occurrence-capacity).

Croisières (bateau)

  • Deck, Cabin, CabinType — pour les voyages croisière en bateau uniquement (flag isCroisitour dans certains discounts).
  • SPA Vue : deck-configurator (édition du plan) + deck-preview (visu).

Sièges (cars)

  • Seat, SeatType — siège dans un véhicule.
  • OccurrenceBlockedSeat — sièges bloqués sur une occurrence.
  • BookingPassengerSeat — assignment passager↔siège (avec VehicleIdx pour Seaside multi-véhicules).
  • OccurrenceResourceSeat — siège réservé aux ressources (chauffeur/hôtesse).
  • Page d'assignation : Pages/PassengerSeats/Assign (SPA Vue passenger-seats). Le filtre « par trajet » du panneau passagers charge les trajets effectifs via le handler FilterDrivesTravelListService.GetOccurrenceEffectiveDrives (overrides OccurrenceLines + occurrence.Drives inclus), puis matche chaque passager par appartenance de son LoadingStop aux stops du trajet (fallback nom exact Place+Localité, sinon option « Trajet indéterminé »). Ne jamais matcher via loadingStop.driveId : ce champ n'est plus rempli depuis la migration DriveStops M:N (cf. lines-stops.md).
    • Placement en cascade des accompagnants (updateSeat) : déposer un passager sur un siège place automatiquement les autres passagers de la même réservation sur les sièges suivants. Depuis août 2026, la cascade est limitée aux passagers ayant le même lieu de prise en charge que celui déposé (sameLoadingStop, même tolérance que findPassengerDriveIds : id, sinon Place+Localité, deux passagers sans arrêt restant groupés). Motivation (demande Léa D. / Caroline B.) : une réservation peut embarquer à plusieurs arrêts — donc potentiellement dans des cars différents — et glisser une personne embarquait silencieusement dans ce car quelqu'un qui part d'ailleurs, sans que le vendeur le voie. Les accompagnants d'un autre arrêt restent dans la liste des non-placés, à positionner explicitement (ils n'apparaissent d'ailleurs pas quand le filtre « par trajet » est actif).

Véhicules

  • Vehicle — entité véhicule (avec VehicleType + Capacity + plan de sièges via Decks/Cabins/Seats).
  • VehicleType — catégorie générique (ex: "car 44 places").
  • Distinct de Resource avec Type=Vehicle qui désigne le véhicule physique (ex: "MAN 238 acheté 2020-01-08"). Cf. resources-visual-planning.md.
  • OccurrenceVehicle — assignment d'un véhicule à une occurrence.
  • TravelVehicle + TravelDayVehicle + TravelDayVehicleJourneyStep : véhicules planifiés sur un voyage / jour.

Activités

  • Activity — activité optionnelle (visite, dégustation, excursion).
  • ActivityPrice — grille de prix.
  • TravelActivity + TravelDayActivity : activités attachées à un voyage / un jour.
  • BookingPassengerActivity : choix du passager.
  • Activity.PricePerActivityPricePer : Person ou Group.
  • Activity.IsOneShot : prix forfaitaire vs récurrent.

Suppléments

  • Supplement — ligne ad-hoc d'un booking.
  • GlobalSupplement — supplément global (taxe touristique, supplément carburant…). Réplique automatique sur les bookings via :
    • GlobalSupplementOccurrence (pour les voyages catalogue/oneday).
    • GlobalSupplementSeasideDate (pour les balnéaires).
  • BookingGlobalSupplement — trace par booking de l'application d'un GlobalSupplement.
  • GlobalSupplement.IsFlatRatePerPerson : true = forfait par personne, false = au montant.

Règles métier spécifiques

  • Travel.Accommodation singulier = hôtel principal (Seaside). Travel.Accommodations collection = catalogue avec plusieurs hôtels.
  • RoomTypeQuota est saisi manuellement (négocié avec hôtel par occurrence).
  • Tier d'âge depuis Accommodation : utilisé par BookingService.GetCostPerPassenger (Seaside avec accommodation) et par PdfGeneratorService.GetPriceTier (label PDF).
  • Activity prix :
    • PricePer = Person + IsOneShot = false → multiplier par jours.
    • PricePer = Person + IsOneShot = true → forfait par personne.
    • PricePer = Group → un seul prix pour le groupe.
  • Excursion retirée du voyage alors qu'elle est déjà réservée (Asana 1217785611307289, résas 10082 / 13270 août 2026) : TravelService.Map remplace intégralement TravelActivities et TravelDay.TravelDayActivities à chaque enregistrement du voyage (TravelService.cs:1138 et :843). Enlever une excursion du voyage supprime donc les liens catalogue, mais pas les BookingPassengerActivity déjà posés : l'excursion reste dans Booking.FinalAmount (GetCostPerPassenger lit _db.Activities directement, sans vérifier le rattachement au voyage) et dans la ligne « Excursions fac. » des factures. Deux écrans mentaient :
    • AccommodationsAndActivities.vue construisait sa liste uniquement depuis le catalogue du voyage → aucune option à cocher, carte entière masquée par v-if="activities.length > 0" : l'excursion devenait invisible sur la réservation. Corrigé — les excursions portées par la réservation sont réinjectées dans la liste (removedFromTravel: true), avec un avertissement sous le select ; elles restent sélectionnables uniquement sur la ligne qui les porte déjà, et le bouton « Ajouter une excursion facultative » ne dépend plus que des excursions encore au catalogue.
    • booking/cost.js ignorait ces excursions dans son total (getActivitiesCostPerPassenger ne trouvait pas l'activité) → l'écran affichait moins que ce que le serveur facturait. Corrigé par addBookedActivitiesMissingFromTravel().
    • BookingActivitiesDto transporte désormais Name / Price / PricePer / IsOneShot : sans catalogue, le front n'a aucune autre source pour le libellé et le prix.
    • Garde-fou côté voyage : TravelService.ValidateRemovedActivitiesStillBooked refuse l'enregistrement d'un voyage qui retire une excursion encore réservée sur un départ à venir (Occurrence.Start >= aujourd'hui) par une réservation Draft / Confirmed / Billed (ValidatingBookingStatus.HasImpactOnOccurrenceOccupancy). Comparaison état DB (AsNoTracking) vs modèle posté, donc après Map. Collections nulles = graphe non chargé ⇒ on ne conclut rien. Message nommant l'excursion et les n° de réservation.
    • Écrans non concernés : la liste des excursions facultatives d'un départ (Pages/Occurrences/ListOptionalActivities + export Excel) lit PassengerActivities en direct, elle a toujours montré les passagers.
    • Passif au 26.08.2026 : 54 réservations portent une activité orpheline (15 couples voyage × activité), dont des activités à prix négatif (« Réduction de 50.- pp par demi-tarif »). Le garde-fou est préventif : il n'assainit pas l'existant.
  • MealPlan : Breakfast, HalfBoard, FullBoard, AllInclusive, ProgramBoard (cf. enum). Prix MealPlan stocké sur Accommodation (BreakfastPriceChild/Junior/Adult, etc.).

Points d'attention / pièges

  • Vehicle (entité) ≠ Resource avec Type=Vehicle. Le premier est le modèle/configuration, le second est l'instance physique liée à VP.
  • Decks/Cabins sont uniquement utilisés pour les croisières — ne pas les confondre avec sièges car.
  • Quand on crée un Travel.Accommodation (Seaside avec hôtel), bien renseigner ChildMin/ChildMax/JuniorMin/JuniorMax/AdultMin sinon le calcul de prix passager part en vrille.
  • ActivityPrice peut avoir des plages/dates : vérifier ActivityPriceRangeDto.
  • Pour le rendu PDF : le bloc "passagers seaside sans accommodation" affiche tarif (Adulte/Junior/Enfant/Bébé) + sens du trajet, calculé depuis le tier d'âge dynamique (cf. PdfGeneratorService.GetPriceTier).

Conventions locales

  • Le SPA deck-configurator édite Decks + Cabins + Seats en drag-and-drop. Sauvegarde via PageModel handler.
  • BookingService.GetActivitiesCostPerPassenger calcule les coûts d'activité.

Dépendances

  • travel-catalog, seaside (consommateurs des hôtels/chambres/sièges).
  • occurrence-capacity (override prix/quotas).
  • booking (sélections passager).
  • billing-payment (lignes facturées).

Contributors

No contributors

Changelog

No recent changes