Skip to content

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 (flag ViewData["isPdf"], saut de page par véhicule) et fusionnée en tête du PDF « Télécharger » de Listing.OnGetExport. Colonnes : n°, Passagers, Date de naissance, Téléphone, Hôtel, Excursions. ⚠ Homonyme de Occurrences/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 de SeasideRoomings (bookings → BookingAccommodation → types de chambre → clients, via AccommodationOccupancyRoomDto) mais scopée à la rotation via BookingService.GetBySeasideDate, et regroupée par BookingAccommodation.Accommodation (une rotation = plusieurs Occurrences = plusieurs hôtels). Dans le PDF fusionné ce segment est rendu en paysage avec le header de SeasideRoomings (tableau 13 colonnes). Export PDF autonome aussi dispo via Rooms.OnGetExport (/SeasideDates/Rooms/Export?seasideDateId=…, bouton « Export PDF »), même rendu paysage + header SeasideRoomings.
  • Service : src/Application/Services/Entities/SeasideDateService.cs
  • Logique de prix Seaside : BookingService.GetCostPerPassenger lignes ~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'une Occurrence. 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 flags Travel.FirstNightInTheBus / Travel.LastNightInTheBus pour 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 par TravelOccupancyAndBookingStateService à partir des SeasideDate correspondants.
  • 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) et VehicleCapacityReturn (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 : seuils accommodation.ChildMin/ChildMax/JuniorMin/JuniorMax/AdultMin.
  • Seaside dans les LoadingTables : flag LoadingTable.Seaside = true. Logique dans LoadingTableService : la ligne IsMainTravel reçoit le chauffeur du voyage, et depuis mai 2026 cette info est aussi recopiée sur editables[0] (offset resourceCount ajusté).
  • 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 *Effective et le calcul des dates sont centralisés dans Application/Library/SeasideStay (Effective(seasideDateFlag, travelFlag) = SeasideDate.FirstNightInTheBus ?? Travel.FirstNightInTheBus ; HotelArrival/HotelDeparture), utilisé par CreateUpdate.OnGetTravel, PdfGeneratorService, BookingService.GetCostPerPassenger, Occurrences/Lists et les 2 listes de chambres (via SeasideDateService.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 quand BookingAccommodation.UseAsTwin = true (même règle d'affichage que Confirmation.liquid / Balance.liquid).
  • Voyages sans hôtel (Seaside avec AccommodationId == null) : possible (vol sec ou voyage vers location indépendante). Bloc spécial dans Confirmation.liquid / Balance.liquid qui 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, branche billed == 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) et SeasideDate (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 sur TravelCategoryId + Date), pas via Occurrence.Vehicles.
  • SeasideDate.NoVehiclePlan peut être true si 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 via BookingPassenger.Rooms → BookingPassengerRoom.BookingAccommodation.Accommodation.Name (non annulée), plus précis que le booking quand une réservation porte plusieurs BookingAccommodation. 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, sinon Passenger.Customer.Phone, sinon Booking.Customer.Phone. ⚠ Ne pas utiliser Customer.Mobile : le champ est [NotMapped] et vaut toujours null en lecture depuis la base.
    • Il s'agit de l'hôtel de séjour (BookingAccommodation), à ne pas confondre avec BookingPassenger.DropOffAccommodation (hôtel de dépose au débarquement).
    • Aucune modification de BookingService.GetBySeasideDate nécessaire : Accommodations/Accommodation et Passenger/Customer y sont déjà en Include. Les navigations BookingPassenger.Booking et BookingPassengerRoom.BookingAccommodation sont 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-word pour éviter tout débordement.
  • ⚠ Check-in/check-out des listes de chambres = calcul live, PAS le snapshot BookingAccommodation (corrigé juillet 2026) : BookingAccommodation.CheckIn/CheckOut est un instantané figé écrit une seule fois par le front Vue à la saisie (BookingAccommodation.vue > addRoomTypeCategoryBookingService.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 flags FirstNightInTheBus/LastNightInTheBus changent 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 via SeasideStay). 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ésormais SeasideDateService.GetHotelStaysAsync(bookings) (occurrence ± nuit-bus effective via SeasideStay), appelé une fois avant la boucle → dict bookingId → (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/Index et SeasideDates/Rooms (extraction AccommodationOccupancyRoomDto) regroupent les chambres par label de RoomType (a.Name == roomName), comme le rooming classique Accommodations/Occupancy. Avant juin 2026 ils groupaient par RoomType.CategoryId : comme plusieurs RoomType peuvent partager la même RoomTypeCategory (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 champ RoomTypeCategoryId du 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 + PendingWebOrMobile en hold (txid OU CreatedAt >= PendingHoldCutoff(), 20 min) — cf. business-rules > Hold des réservations web. Depuis juin 2026, GetSeatOccupancy aligne 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 passagers OneWay + RoundTrip, VehicleOccupancyReturn (retour) compte Return + RoundTrip. Vaut pour les Seaside avec ET sans hôtel. ⚠ Avant juin 2026, un court-circuit AccommodationId != null faisait 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 projection BookingData (ActivePassengerCountAller/Retour) et dans CalculateSeasideVehicleOccupancy (b.IsSeaside ? directionnel : total). NB : sans incidence sur le cas courant où tout le monde est en RoundTrip (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 sur Occurrence.Start.Date (aller) / Occurrence.End.Date (retour), mais ne filtre PAS le TripType — un booking dont l'occurrence a Start = date de l'aller contient aussi ses passagers Return-only. Chaque consommateur qui liste des passagers doit donc exclure le sens opposé : sur l'aller retirer TripType.Return, sur le retour retirer TripType.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 » de Listing.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 ajoutant p.TripType != excludedTripType au filtre passagers.
  • Propagation SeasideDate → Occurrence (aller uniquement) : SeasideDateService.ApplyToOccurrences (appelé sur Create/Update) recopie NavNo, NoVehiclePlan et Vehicles sur les Occurrences matchées par o.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 SeasideDate IsReturn = true a Date = date de retour (= o.End, jamais o.Start) : ApplyToOccurrences retourne donc immédiatement si IsReturn, 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 sur Occurrence. Le NavNo Seaside doit normalement venir de la SeasideDate (Planification > Balnéaires) et descend via ApplyToOccurrences. Mais certaines occurrences (ex. vol sec) saisissent leur NavNo directement dans le step Occurrences du travel-design (Occurrences.vue, visible si type === Seaside). Pour éviter que la resauvegarde d'une SeasideDate n'écrase ce NavNo manuel, cocher la case « Saisie manuelle » (navNoOverride = true) : ApplyToOccurrences ne réécrit alors plus occurrence.NavNo (mais continue de propager NoVehiclePlan/Vehicles). Défaut false (l'existant reste synchronisé depuis la rotation).

Conventions locales

  • LoadingTable.Seaside = true est requis pour les tableaux balnéaires — pas un flag dérivé.
  • Pour les Seaside, LoadingTable.SearchDate = date de la rotation (matche SeasideDate.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