Module — Balnéaires (Seaside)
Synchronisé avec le code. Mettre à jour à chaque changement structurel.
Rôle
Gestion des voyages balnéaires : Travel.Type = Seaside. Spécificité forte par rapport au catalogue : un client peut partir à une date X et revenir à une date Y ou Z (séjour 2 ou 3 semaines), avec potentiellement des véhicules différents pour l'aller et le retour. Plusieurs Occurrence (= différents hôtels d'une même destination) partagent une même rotation car quand ils ont le même TravelCategoryId + même Date.
Emplacement
- Pages :
src/Web/Pages/SeasideDates/,src/Web/Pages/SeasideRoomings/SeasideDates/Listing= écran « listes à télécharger » d'une rotation (passagers, plan de déroulement, plans des véhicules, liste des chambres) ; chaque entrée est une page autonome + fusionnée dans le PDF « Télécharger » (Listing.OnGetExport).SeasideDates/ListPassengers(+Partial/_listPassengers.cshtml) = liste des passagers de la rotation, groupée par véhicule. Page HTML autonome, également rendue en PDF portrait (flagViewData["isPdf"], saut de page par véhicule) et fusionnée en tête du PDF « Télécharger » deListing.OnGetExport. Colonnes : n°, Passagers, Date de naissance, Téléphone, Hôtel, Excursions. ⚠ Homonyme deOccurrences/ListPassengers(voyages catalogue) — ce sont deux rapports distincts, ne pas modifier l'un en croyant toucher l'autre.SeasideDates/Rooms(+Partial/_rooms.cshtml) = liste des chambres de la rotation, en sections par hôtel. Réutilise la logique d'extraction deSeasideRoomings(bookings →BookingAccommodation→ types de chambre → clients, viaAccommodationOccupancyRoomDto) mais scopée à la rotation viaBookingService.GetBySeasideDate, et regroupée parBookingAccommodation.Accommodation(une rotation = plusieurs Occurrences = plusieurs hôtels). Dans le PDF fusionné ce segment est rendu en paysage avec le header deSeasideRoomings(tableau 13 colonnes). Export PDF autonome aussi dispo viaRooms.OnGetExport(/SeasideDates/Rooms/Export?seasideDateId=…, bouton « Export PDF »), même rendu paysage + headerSeasideRoomings.
- Service :
src/Application/Services/Entities/SeasideDateService.cs - Logique de prix Seaside :
BookingService.GetCostPerPassengerlignes ~1536-1574 + bloc avec accommodation 1555-1574 - Templates PDF spécifiques :
src/Application/TemplatesPdf/SeasideVoucher.liquid
Entités principales
SeasideDate— rotation datée distincte d'uneOccurrence. Identifiée par (TravelCategoryId,Date,IsReturn). Champs :IsReturn:false= aller,true= retour.Quota: capacité (places dans la rotation).NavNo: lien BC.Vehicles: véhicules assignés à la rotation.Billed: flag indiquant que les bookings clients de cette rotation+destination ont été facturés.FirstNightInTheBus(bool?),LastNightInTheBus(bool?) : override des flagsTravel.FirstNightInTheBus/Travel.LastNightInTheBuspour cette rotation.null= hérite du voyage ;true/false= force la valeur. Cas typique : dernière rotation retour Costa Brava qui part le samedi matin (donc arrive le soir même, pas le lendemain) alors que toute la saison part le samedi soir avec nuit dans le bus.
Occurrence(de type Seaside) — porte ce que le client réserve. La capacité véhicule réelle est lue côtéSeasideDate.VehicleCapacity/VehicleCapacityReturn(NotMapped) sont calculés parTravelOccupancyAndBookingStateServiceà partir desSeasideDatecorrespondants.GlobalSupplementSeasideDate— supplément global appliqué sur une SeasideDate (cf.product-catalog).
Règles métier spécifiques
- Regroupement par
TravelCategoryId + Date: pour une destination Costa Brava avec hôtel X et hôtel Y, les deux Occurrences partagent la même rotation car (même véhicules / chauffeurs) si elles ont la même TravelCategory et la même Date. - Capacité aller ≠ capacité retour : le client peut prolonger son séjour ⇒
VehicleCapacity(aller) etVehicleCapacityReturn(retour) sont calculées séparément. - Calcul de prix passager Seaside :
- Sans
Accommodation(Travel.AccommodationId == null) : seuils 0-2 / 2-12 / 12-18 / 18+ → Bébé/Enfant/Junior/Adulte. - Avec
Accommodation: seuilsaccommodation.ChildMin/ChildMax/JuniorMin/JuniorMax/AdultMin.
- Sans
- Seaside dans les LoadingTables : flag
LoadingTable.Seaside = true. Logique dansLoadingTableService: la ligneIsMainTravelreçoit le chauffeur du voyage, et depuis mai 2026 cette info est aussi recopiée sureditables[0](offsetresourceCountajusté). - SeasideVoucher : imprimé par Buchard, remis au client à l'embarquement aller, à présenter à l'hôtel à l'arrivée. Date d'arrivée hôtel =
Occurrence.Start + (FirstNightInTheBusEffective ? 1 : 0). Date de départ hôtel =Occurrence.End + (LastNightInTheBusEffective ? -1 : 0). Les flags*Effectiveet le calcul des dates sont centralisés dansApplication/Library/SeasideStay(Effective(seasideDateFlag, travelFlag)=SeasideDate.FirstNightInTheBus ?? Travel.FirstNightInTheBus;HotelArrival/HotelDeparture), utilisé parCreateUpdate.OnGetTravel,PdfGeneratorService,BookingService.GetCostPerPassenger,Occurrences/Listset les 2 listes de chambres (viaSeasideDateService.GetHotelStaysAsync, batch 1 requête, cf. piège ci-dessous) — avant juin 2026 cette logique était dupliquée inline. Testé :SeasideStayTest. Le voucher affiche la mention(Twin)après le type de chambre quandBookingAccommodation.UseAsTwin = true(même règle d'affichage queConfirmation.liquid/Balance.liquid). - Voyages sans hôtel (Seaside avec
AccommodationId == null) : possible (vol sec ou voyage vers location indépendante). Bloc spécial dansConfirmation.liquid/Balance.liquidqui liste les passagers + tarif + sens du trajet (cf. business-rules). - Offres spéciales par personne sur un balnéaire sans hôtel : le bébé (< 2 ans au départ, tarif transport 0) ne compte pas dans le
Value × nb pax. C'est le seul cas d'exclusion — avec hôtel, tous les passagers comptent. Cf.domain/business-rules.md > Offres spéciales « par personne ». - « À facturer » (
/SeasideDates/Overview?billed=false) = rotations aller uniquement :SeasideDateService.SearchOverviewAsync, branchebilled == false, ajoute!sd.IsReturn. La facturation Seaside est portée par la commande aller (NavNo aller = TravelID BC, cf.ApplyToOccurrences) ; une rotation retour ne doit jamais apparaître dans la liste à facturer, sinon doublon avec l'aller correspondant.
Points d'attention / pièges
- Ne pas confondre
Occurrence(ce que le client réserve) etSeasideDate(la rotation logistique). Une réservation Seaside fait référence aux deux indirectement. - Si on cherche les véhicules d'un client Seaside : passer par
SeasideDate(joint surTravelCategoryId + Date), pas viaOccurrence.Vehicles. SeasideDate.NoVehiclePlanpeut êtretruesi la rotation n'a pas de plan de car (par ex. en avion) — adapter l'affichage.- Liste des passagers Seaside — colonnes Téléphone et Hôtel (juillet 2026,
SeasideDates/ListPassengers) :- Hôtel (
ListPassengers.GetAccommodationName) : une rotation regroupe plusieurs Occurrences = plusieurs hôtels, donc l'hôtel varie d'un passager à l'autre dans une même liste. Résolution par passager viaBookingPassenger.Rooms → BookingPassengerRoom.BookingAccommodation.Accommodation.Name(non annulée), plus précis que le booking quand une réservation porte plusieursBookingAccommodation. Fallback si le passager n'a aucune chambre : les hôtels non annulés du booking (Booking.Accommodations). Cas multi-hôtels : les noms distincts sont concaténés par,(pas de choix arbitraire du premier). Cas Seaside sans hôtel (AccommodationId == null) → cellule vide, pas d'exception. - Téléphone (
ListPassengers.GetPhone) :Passenger.Phone, sinonPassenger.Customer.Phone, sinonBooking.Customer.Phone. ⚠ Ne pas utiliserCustomer.Mobile: le champ est[NotMapped]et vaut toujoursnullen lecture depuis la base. - Il s'agit de l'hôtel de séjour (
BookingAccommodation), à ne pas confondre avecBookingPassenger.DropOffAccommodation(hôtel de dépose au débarquement). - Aucune modification de
BookingService.GetBySeasideDatenécessaire :Accommodations/AccommodationetPassenger/Customery sont déjà enInclude. Les navigationsBookingPassenger.BookingetBookingPassengerRoom.BookingAccommodationsont résolues par le relationship fixup EF (requête trackée, pas de lazy loading) — pas de N+1. - Rendu PDF : le tableau passe à 6 colonnes en portrait A4, d'où
table-layout: fixed+ largeurs en % +word-wrap: break-wordpour éviter tout débordement.
- Hôtel (
- ⚠ Check-in/check-out des listes de chambres = calcul live, PAS le snapshot
BookingAccommodation(corrigé juillet 2026) :BookingAccommodation.CheckIn/CheckOutest un instantané figé écrit une seule fois par le front Vue à la saisie (BookingAccommodation.vue > addRoomTypeCategory→BookingService.Map~:956), calculé depuis l'occurrence + nuit-dans-le-bus effective de ce jour-là. Il n'est jamais recalculé si les dates de la rotation ou les flagsFirstNightInTheBus/LastNightInTheBuschangent après coup. Les 2 listes de chambres (SeasideDates/Rooms,SeasideRoomings/Index) lisaient ce snapshot → dossiers anciens affichés avec des dates périmées, divergentes du voucher hôtel (qui, lui, calcule en direct viaSeasideStay). Cas réel (juillet 2026, Prestige Victoria / Rosas, dossiers 5640/5644 vs 6346/7747 sur la même occurrence 31.07→09.08 : snapshot 31.07→07.08 au lieu de 01.08→08.08 ; les SeasideDate avaient été éditées 8 mois après la réservation). Fix : les 2 listes appellent désormaisSeasideDateService.GetHotelStaysAsync(bookings)(occurrence ± nuit-bus effective viaSeasideStay), appelé une fois avant la boucle → dictbookingId → (arrivée, départ)en une seule requête SeasideDates (évite le 2×N d'un lookup par booking), comme le voucher. Le snapshot en base n'est pas corrigé (aucun backfill) — il n'est simplement plus lu par ces écrans. À terme, envisager de le recalculer/persister à chaque save, ou de ne plus le stocker. - Rooming balnéaire — regrouper par type de chambre, pas par catégorie :
SeasideRoomings/IndexetSeasideDates/Rooms(extractionAccommodationOccupancyRoomDto) regroupent les chambres par label deRoomType(a.Name == roomName), comme le rooming classiqueAccommodations/Occupancy. Avant juin 2026 ils groupaient parRoomType.CategoryId: comme plusieursRoomTypepeuvent partager la mêmeRoomTypeCategory(ex. « Double » et « Chill out room » tous deux en catégorie « Chambre double », distingués par leur label personnaliséRoomType.Name), la ligne fusionnait les deux chambres sous le label du premier booking rencontré — un passager en chambre double apparaissait sous « Chill out room ». Le champRoomTypeCategoryIddu DTO reste renseigné mais ne sert plus de clé de regroupement. - Le calcul d'occupation Seaside exclut les bookings annulés mais inclut les
Confirmed/Billed/Draft+PendingWebOrMobileen hold (txid OUCreatedAt >= PendingHoldCutoff(), 20 min) — cf.business-rules > Hold des réservations web. Depuis juin 2026,GetSeatOccupancyaligne aussi la branche Seaside (regroupée par date de rotation) sur ce filtre de statut (avant, elle prenait tous les bookings sans filtre de statut). - Occupation véhicule aller/retour = par
BookingPassenger.TripType(TravelOccupancyAndBookingStateService) :VehicleOccupancy(aller) compte les passagersOneWay+RoundTrip,VehicleOccupancyReturn(retour) compteReturn+RoundTrip. Vaut pour les Seaside avec ET sans hôtel. ⚠ Avant juin 2026, un court-circuitAccommodationId != nullfaisait que les balnéaires avec hôtel comptaient tous les passagers (TripType ignoré) sur les deux véhicules : un passager aller-seul gonflait à tort l'occupation du retour. Corrigé dans la projectionBookingData(ActivePassengerCountAller/Retour) et dansCalculateSeasideVehicleOccupancy(b.IsSeaside ? directionnel : total). NB : sans incidence sur le cas courant où tout le monde est enRoundTrip(compté des deux côtés) ; ne change le résultat que si des passagers sont en aller-seul ou retour-seul. - Listes de passagers Seaside — filtrer le sens par
BookingPassenger.TripType:GetBySeasideDate(BookingService) matche les bookings au niveau booking surOccurrence.Start.Date(aller) /Occurrence.End.Date(retour), mais ne filtre PAS le TripType — un booking dont l'occurrence aStart= date de l'aller contient aussi ses passagersReturn-only. Chaque consommateur qui liste des passagers doit donc exclure le sens opposé : sur l'aller retirerTripType.Return, sur le retour retirerTripType.OneWay(excludedTripType = IsReturn ? OneWay : Return, comme l'occupation véhicule ci-dessus).HelveticListPassengers(export Excel avion) le fait depuis l'origine (bp.TripType != excludedTripType).ListPassengers(liste PDF/écran, aussi fusionnée dans le PDF « Télécharger » deListing.OnGetExport) ne le faisait pas avant juillet 2026 : un passager retour-seul ressortait sur la liste aller (regroupé sous « véhicule -1 » faute de siège aller), faussant le comptage — signalé par le client sur Majorque. Corrigé en ajoutantp.TripType != excludedTripTypeau filtre passagers. - Propagation SeasideDate → Occurrence (aller uniquement) :
SeasideDateService.ApplyToOccurrences(appelé sur Create/Update) recopieNavNo,NoVehiclePlanetVehiclessur les Occurrences matchées paro.Start == SeasideDate.Date. Ce match n'est valide que pour l'aller (Occurrence.NavNo= TravelID BC de la commande aller, unique, pas de variante retour). Une SeasideDateIsReturn = trueaDate= date de retour (=o.End, jamaiso.Start) :ApplyToOccurrencesretourne donc immédiatement siIsReturn, sinon le NavNo du retour écraserait le TravelID d'une autre rotation aller dont le Start coïncide, cassant la facturation. Occurrence.NavNoOverride(saisie manuelle du NavNo) : flag bool surOccurrence. Le NavNo Seaside doit normalement venir de la SeasideDate (Planification > Balnéaires) et descend viaApplyToOccurrences. Mais certaines occurrences (ex. vol sec) saisissent leur NavNo directement dans le step Occurrences du travel-design (Occurrences.vue, visible sitype === Seaside). Pour éviter que la resauvegarde d'une SeasideDate n'écrase ce NavNo manuel, cocher la case « Saisie manuelle » (navNoOverride = true) :ApplyToOccurrencesne réécrit alors plusoccurrence.NavNo(mais continue de propagerNoVehiclePlan/Vehicles). Défautfalse(l'existant reste synchronisé depuis la rotation).
Conventions locales
LoadingTable.Seaside = trueest requis pour les tableaux balnéaires — pas un flag dérivé.- Pour les Seaside,
LoadingTable.SearchDate= date de la rotation (matcheSeasideDate.Date).
Dépendances
- →
travel-catalog(Travel + TravelCategory). - →
occurrence-capacity(Occurrence porte le voyage client). - →
loading-tables(rotation = tableau de chargement Seaside). - →
product-catalog(Accommodation seuils d'âge). - →
billing-payment(vouchers + factures).
Contributors
No contributors
Changelog
No recent changes

