diff --git a/docs/booking-flow.md b/docs/booking-flow.md new file mode 100644 index 0000000..5a2e097 --- /dev/null +++ b/docs/booking-flow.md @@ -0,0 +1,196 @@ +# Booking Flow Reference + +Last updated: 2026-04-13 + +This document traces the booking create and edit flows from controller entry point through to session/DTO state. It is intended as orientation for developers unfamiliar with the flow, not as a substitute for reading the code. + +--- + +## Shared Concepts + +### `BookingDto` + +The central data object for both flows. Holds room selections, participants, payment method, and travel reference. Lives in the HTTP session between requests. Has two modes: `MODE_CREATE` and `MODE_EDIT`. + +### `BookingSessionService` + +Owns all reads and writes to session-stored `BookingDto` instances. Also manages the baseline room snapshot used for change detection in the create flow. + +### `BookingCreateContextFactory` + +Assembles the view model (`BookingCreateContext`) passed to create-flow templates. Handles room grouping (by-pax vs. by-room) and wires summary sidebar data. + +--- + +## Create Flow + +``` +Step 1 (room selection) + → Step 2 (participants) + → Step 2 sub-form (per-participant, HTMX) + → Step 3 (payment + API pre-validation) + → Step 4 (confirmation + final submission) + → Success +``` + +### Entry / Bootstrap + +`BookingCreateTrait::getOrCreateBookingCreateDto()` is called by every step controller. If no DTO exists in session, it calls `BookingService::startFreshBooking()` which loads travel data and initialises a fresh `BookingDto`. + +### Step 1 — Room Selection + +**Controller:** `Create/Step1Controller` +**Route:** `GET|POST /bookings/create/rooms` + +1. Load or create DTO via `getOrCreateBookingCreateDto()` +2. `BookingSessionService::getOrCreateBaselineSnapshot()` — saves the current room selection as a comparison point for change detection +3. Render `BookingCreateStep1Type` form +4. On submit: + - If room selection changed from baseline: `BookingDto::resetParticipantAssignments()` — clears stale assignments + - `BookingService::updateBookingStatusFromRoomSelection()` — may switch booking to inquiry mode if capacity constraints require it + - `BookingSessionService::saveBookingDto()`, advance `currentStep` to 2 + - `BookingSessionService::clearBaselineSnapshot()` + - Redirect to Step 2 + +HTMX refresh (`/bookings/create/refresh`): processes the form without validation and returns updated room selection + summary sidebar blocks. + +### Step 2 — Participants (Card Overview) + +**Controller:** `Create/Step2Controller` +**Route:** `GET|POST /bookings/create/participants` + +1. Load DTO from session (or redirect to Step 1 if missing) +2. `TravelDataService::enrichWithFreshAvailabilities()` — refreshes room availability +3. `ensureCorrectNumberOfParticipants()` — syncs the `BookingDto::participants` array length with the room selections (e.g., 2× double room → 4 participants). Preserves existing participant data for slots that still exist. +4. `ParticipantPrepopulationService::shouldPrepopulateApplicant()` + `prepopulateApplicantFromUser()` — prefills participant[0] from user profile on first visit +5. `RoomAssignmentService::validateAndResetInvalidAssignments()` — clears any room assignments that no longer match the current room selection (handles back-navigation from Step 2 → Step 1 → Step 2) +6. `RoomAssignmentService::assignRoomsIfNeeded()` — auto-assigns participants to rooms +7. `BookingService::preselectDefaultServices()` — selects mandatory and auto-book services for all participants +8. `BookingService::applyCreateBookingStatusRules()` — re-evaluates booking status (book / inquiry / not-bookable) +9. `BookingSessionService::saveBookingDto()` +10. On form submit (all participants valid): advance to Step 3, redirect + +### Step 2 — Individual Participant Form (HTMX) + +**Controller:** `Create/Step2ParticipantController` +**Routes:** `GET|POST /bookings/create/participants/{index}` and `/participants/{index}/refresh` + +Each participant card lazy-loads its form via HTMX. The controller: + +1. Verifies the participant slot exists +2. Refreshes availability +3. Handles dummy data fill (`ParticipantPrepopulationService::isDummyDataFillRequested()` / `fillDummyParticipant()`) +4. On submit: re-applies status rules, saves to session, redirects to card overview +5. `ParticipantFormSupportService` is used for form creation options and notification draining + +### Step 3 — Payment + API Pre-Validation + +**Controller:** `Create/Step3Controller` +**Route:** `GET|POST /bookings/create/payment` + +1. Load DTO from session +2. `BookingService::applyCreateBookingStatusRules()` +3. On submit: + - `ApiClient::createBookingInquiry()` — submits booking to API for validation without committing it + - If inquiry fails with "nicht möglich" + "Anfrage" in message: auto-switch to inquiry mode, advance to Step 4 + - If inquiry fails otherwise: show error, stay on step + - Price verification: `BookingPriceCalculatorService::calculateGrandTotal()` is compared against the API response total (accounting for promotional/goodwill voucher discounts and API-applied group discounts). Mismatch triggers `BookingPriceMismatchDiagnosticsService` for detailed logging. + - On success: advance `currentStep` to 4, redirect + +### Step 4 — Confirmation + Final Submission + +**Controller:** `Create/Step4Controller` +**Route:** `GET|POST /bookings/create/confirmation` + +1. Load DTO from session +2. Newsletter subscription check for opt-in display +3. On submit: + - `BookingService::applyCreateBookingStatusRules()` + - `ApiClient::createBooking()` — final booking submission (already validated in Step 3) + - Newsletter double-opt-in if selected + - On success: store booking number + total in flash (for analytics), clear travel data cache, `BookingSessionService::clearBookingDto()`, redirect to success page + +--- + +## Edit Flow + +``` +Entry (start) + → Load (session or API + draft restore) + → Participant sub-forms (HTMX, per-participant) + → Submission +``` + +### Entry + +**Controller:** `Edit/IndexController::start()` +**Route:** `GET /bookings/{id}/edit/start` + +Clears any existing session DTO for edit mode and invalidates the API booking cache, then redirects to the edit page. This ensures a clean load from API on every intentional re-entry. + +### Load + +**Controller:** `Edit/IndexController::index()` +**Route:** `GET|POST /bookings/{id}/edit` + +Loading is delegated entirely to `BookingEditDataLoaderService::loadFormData()`: + +**If session DTO exists and matches the requested booking ID:** +- `TravelDataService::enrichWithFreshAvailabilities()` — refreshes room availability +- `TravelDataService::getMutabilityData()` + `patchMutability()` — applies cached mutability state to DTO + +**If no session DTO (first load or after `start`):** + +`initializeFromApi()`: +1. `fetchBookingData()` — `ApiClient::getBooking()` with 5-minute cache (tagged by user ID for bulk invalidation) +2. `TravelDataService::getTravelData()` — loads travel data (XML or snapshot) +3. `TravelDataService::getAvailabilityData(forceRefresh: true)` + `patchAvailabilities()` — force-refreshes availability at edit start, populates cache for subsequent loads within the session +4. `TravelDataService::getMutabilityData(cached: true)` + `patchMutability()` — applies which service categories are currently editable +5. `BookingDataProcessor::createBookingDtoFromBooking()` — hydrates `BookingDto` from API booking entity +6. Agency code resolved via `AgencyLoader` (used by field state conditions) +7. `BookingFingerprintService::generateFingerprint()` — sets `originalFingerprint` on DTO **before** draft is applied, so dirty detection compares against original API data +8. `BookingEditDraftService::findDraft()` + `applyDraftToDto()` — if draft exists, merges user edits on top of fresh API data. Sets `draftWasRestored = true` if successful. +9. `BookingSessionService::saveBookingDto()` + +Back in the controller: shows "draft restored" flash if applicable, fetches booking data for surcharge/status display, renders the card overview with `BookingEditContextFactory::createOverviewContext()`. + +### Individual Participant Form (HTMX) + +**Controller:** `Edit/ParticipantController` +**Routes:** `GET|POST /bookings/{id}/edit/participants/{index}` and `/{index}/refresh` + +Per-participant forms are lazy-loaded, matching the create flow pattern. + +1. `BookingEditContextFactory::prepareBookingDto()` — refreshes availabilities on the DTO travel object +2. `ParticipantFormSupportService` — form creation, notification draining +3. On submit: `BookingSessionService::saveBookingDto()` + `BookingEditDraftService::saveDraft()` — persists participant edits to both session and draft + +### Submission + +**Service:** `BookingEditSubmitService::handleSubmission()` +Called from `Edit/IndexController` on valid form submit. + +1. Invalidate API cache, fetch fresh booking data +2. Force-refresh mutability data (`TravelDataService::getMutabilityData(forceRefresh: true)`) and patch DTO travel +3. `BookingEditSubmitGuardService::reconcileImmutableCategories()` — reverts any DTO fields that have become immutable since the session was loaded. Shows flash if anything was reverted. +4. `ApiClient::updateBooking()` — submit +5. On success: invalidate cache again, `BookingSessionService::clearBookingDto()`, `BookingEditDraftService::deleteDraft()` + +### Other Edit Routes + +| Route | Action | +|-------|--------| +| `GET /bookings/{id}/edit/reload` | Shows confirmation modal | +| `POST /bookings/{id}/edit/reload` | Clears session + **deletes draft**, reloads from API | +| `GET /bookings/{id}/edit/cancel` | Clears session, **preserves draft**, redirects to booking list | + +The distinction between reload and cancel is intentional: cancel keeps the draft so the user can return to their changes later; reload discards everything. + +--- + +## Key Invariants + +- `currentStep` on the DTO gates forward navigation in create mode; controllers redirect to the current step if a higher step is requested directly. +- The original fingerprint is set before draft application in edit mode. Dirty detection (`BookingFingerprintService::isDirty()`) always compares the live session DTO against the original API state, not against the draft. +- Mutability is always patched from fresh or cached data before rendering. The DTO travel object is the authoritative source of which fields are editable at render time. +- Drafts survive cancel but not reload, and are deleted on successful submission.