10 KiB
Booking Flow Reference
Last updated: 2026-07-15
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.
BookingSessionManager
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
AbstractBookingCreateController::loadBookingCreateDto() is called by every step controller and delegates to BookingSessionManager::getOrCreateBookingCreateDto(). If no DTO exists in session, BookingConfigurator::startFreshBooking() loads travel data and initialises a fresh BookingDto.
Step 1 — Room Selection
Controller: Create/Step1Controller
Route: GET|POST /bookings/create/rooms
- Load or create DTO via
getOrCreateBookingCreateDto() BookingSessionManager::getOrCreateBaselineSnapshot()— saves the current room selection as a comparison point for change detection- Render
BookingCreateStep1Typeform - On submit:
- If room selection changed from baseline:
BookingDto::resetParticipantAssignments()— clears stale assignments BookingConfigurator::updateBookingStatusFromRoomSelection()— may switch booking to inquiry mode if capacity constraints require itBookingSessionManager::saveBookingDto(), advancecurrentStepto 2BookingSessionManager::clearBaselineSnapshot()- Redirect to Step 2
- If room selection changed from baseline:
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
- Load DTO from session (or redirect to Step 1 if missing)
TravelDataProvider::enrichWithFreshAvailabilities()— refreshes room availabilityensureCorrectNumberOfParticipants()— syncs theBookingDto::participantsarray length with the room selections (e.g., 2× double room → 4 participants). Preserves existing participant data for slots that still exist.ParticipantDataPrefiller::shouldPrefillApplicant()+prefillApplicantFromUser()— prefills participant[0] from user profile on first visitRoomAssigner::validateAndResetInvalidAssignments()— clears any room assignments that no longer match the current room selection (handles back-navigation from Step 2 → Step 1 → Step 2)RoomAssigner::assignRoomsIfNeeded()— auto-assigns participants to roomsBookingConfigurator::preselectDefaultServices()— selects mandatory and auto-book services for all participantsBookingConfigurator::applyCreateBookingStatusRules()— re-evaluates booking status (book / inquiry / not-bookable)BookingSessionManager::saveBookingDto()- 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:
- Verifies the participant slot exists
- Refreshes availability
- Handles dummy data fill (
ParticipantDataPrefiller::isDummyDataFillRequested()/fillDummyParticipant()) - On submit: re-applies status rules, saves to session, redirects to card overview
ParticipantFormSupportis used for form creation options and notification draining
Step 3 — Payment + API Pre-Validation
Controller: Create/Step3Controller
Route: GET|POST /bookings/create/payment
- Load DTO from session
BookingConfigurator::applyCreateBookingStatusRules()- 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:
BookingPriceCalculator::calculateGrandTotal()is compared against the API response total (accounting for promotional/goodwill voucher discounts and API-applied group discounts). Mismatch triggersBookingPriceMismatchAnalyzerfor detailed logging. - On success: advance
currentStepto 4, redirect
Step 4 — Confirmation + Final Submission
Controller: Create/Step4Controller
Route: GET|POST /bookings/create/confirmation
- Load DTO from session
- Newsletter subscription check for opt-in display
- On submit:
BookingConfigurator::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,
BookingSessionManager::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 BookingEditDataLoader::loadFormData():
If session DTO exists and matches the requested booking ID:
TravelDataProvider::enrichWithFreshAvailabilities()— refreshes room availabilityTravelDataProvider::getMutabilityData()+patchMutability()— applies cached mutability state to DTO
If no session DTO (first load or after start):
initializeFromApi():
fetchBookingData()—ApiClient::getBooking()with 5-minute cache (tagged by user ID for bulk invalidation)TravelDataProvider::getTravelData()— loads travel data (XML or snapshot)TravelDataProvider::getAvailabilityData(forceRefresh: true)+patchAvailabilities()— force-refreshes availability at edit start, populates cache for subsequent loads within the sessionTravelDataProvider::getMutabilityData(cached: true)+patchMutability()— applies which service categories are currently editableBookingDataProcessor::createBookingDtoFromBooking()— hydratesBookingDtofrom API booking entity- Agency code resolved via
AgencyLoader(used by field state conditions) BookingChangeTracker::generateFingerprint()— setsoriginalFingerprinton DTO before draft is applied, so dirty detection compares against original API dataBookingEditDraftManager::findDraft()+applyDraftToDto()— if draft exists, merges user edits on top of fresh API data. SetsdraftWasRestored = trueif successful.BookingSessionManager::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.
BookingEditContextFactory::prepareBookingDto()— refreshes availabilities on the DTO travel objectParticipantFormSupport— form creation, notification draining- On submit:
BookingSessionManager::saveBookingDto()+BookingEditDraftManager::saveDraft()— persists participant edits to both session and draft
Submission
Service: BookingEditSubmitter::handleSubmission()
Called from Edit/IndexController on valid form submit.
- Invalidate API cache, fetch fresh booking data
- Force-refresh mutability data (
TravelDataProvider::getMutabilityData(forceRefresh: true)) and patch DTO travel BookingEditSubmitGuard::reconcileImmutableCategories()— reverts any DTO fields that have become immutable since the session was loaded. Shows flash if anything was reverted.ApiClient::updateBooking()— submit- On success: invalidate cache again,
BookingSessionManager::clearBookingDto(),BookingEditDraftManager::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
currentStepon 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 (
BookingChangeTracker::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.