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..." → leMaxd'une tranche =Minde 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 entreChildMaxetJuniorMin(ou entreJuniorMaxetAdultMin) 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'utiliserAddress/Zip/Locality.- Champs MealPlan dédiés :
BreakfastPriceChild/Junior/Adult, idem pourHalfBoard,FullBoard,AllInclusive,ProgramBoard.
- Seuils d'âge :
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…). LienCategory(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. moduleoccurrence-capacity).
Croisières (bateau)
Deck,Cabin,CabinType— pour les voyages croisière en bateau uniquement (flagisCroisitourdans 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 (avecVehicleIdxpour Seaside multi-véhicules).OccurrenceResourceSeat— siège réservé aux ressources (chauffeur/hôtesse).- Page d'assignation :
Pages/PassengerSeats/Assign(SPA Vuepassenger-seats). Le filtre « par trajet » du panneau passagers charge les trajets effectifs via le handlerFilterDrives→TravelListService.GetOccurrenceEffectiveDrives(overridesOccurrenceLines+occurrence.Drivesinclus), puis matche chaque passager par appartenance de sonLoadingStopaux stops du trajet (fallback nom exact Place+Localité, sinon option « Trajet indéterminé »). Ne jamais matcher vialoadingStop.driveId: ce champ n'est plus rempli depuis la migrationDriveStopsM: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 quefindPassengerDriveIds: 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).
- Placement en cascade des accompagnants (
Véhicules
Vehicle— entité véhicule (avecVehicleType+Capacity+ plan de sièges viaDecks/Cabins/Seats).VehicleType— catégorie générique (ex: "car 44 places").- ⚠ Distinct de
Resourceavec 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.PricePer∈ActivityPricePer:PersonouGroup.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é parBookingService.GetCostPerPassenger(Seaside avec accommodation) et parPdfGeneratorService.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.Mapremplace intégralementTravelActivitiesetTravelDay.TravelDayActivitiesà chaque enregistrement du voyage (TravelService.cs:1138et:843). Enlever une excursion du voyage supprime donc les liens catalogue, mais pas lesBookingPassengerActivitydéjà posés : l'excursion reste dansBooking.FinalAmount(GetCostPerPassengerlit_db.Activitiesdirectement, sans vérifier le rattachement au voyage) et dans la ligne « Excursions fac. » des factures. Deux écrans mentaient :AccommodationsAndActivities.vueconstruisait sa liste uniquement depuis le catalogue du voyage → aucune option à cocher, carte entière masquée parv-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.jsignorait ces excursions dans son total (getActivitiesCostPerPassengerne trouvait pas l'activité) → l'écran affichait moins que ce que le serveur facturait. Corrigé paraddBookedActivitiesMissingFromTravel().BookingActivitiesDtotransporte désormaisName/Price/PricePer/IsOneShot: sans catalogue, le front n'a aucune autre source pour le libellé et le prix.- Garde-fou côté voyage :
TravelService.ValidateRemovedActivitiesStillBookedrefuse 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éservationDraft/Confirmed/Billed(ValidatingBookingStatus.HasImpactOnOccurrenceOccupancy). Comparaison état DB (AsNoTracking) vs modèle posté, donc aprèsMap. 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) litPassengerActivitiesen 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é surAccommodation(BreakfastPriceChild/Junior/Adult, etc.).
Points d'attention / pièges
Vehicle(entité) ≠Resourceavec 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 renseignerChildMin/ChildMax/JuniorMin/JuniorMax/AdultMinsinon le calcul de prix passager part en vrille. ActivityPricepeut avoir des plages/dates : vérifierActivityPriceRangeDto.- 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.GetActivitiesCostPerPassengercalcule 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

