From 045fdea92325718a04e3234167bb6733eb6c5581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Tue, 4 Aug 2026 16:43:06 +0200 Subject: [PATCH] chore: cleanup documents --- docs/body-dimensions-transition-plan.md | 122 -- docs/booking-flow.md | 196 --- docs/technical-documentation.md | 1509 ----------------------- 3 files changed, 1827 deletions(-) delete mode 100644 docs/body-dimensions-transition-plan.md delete mode 100644 docs/booking-flow.md delete mode 100644 docs/technical-documentation.md diff --git a/docs/body-dimensions-transition-plan.md b/docs/body-dimensions-transition-plan.md deleted file mode 100644 index 5d5fed5..0000000 --- a/docs/body-dimensions-transition-plan.md +++ /dev/null @@ -1,122 +0,0 @@ -# Body Dimensions Transition Plan (Stakeholder Draft) - -> **STATUS (verified 2026-07-15): NOT IMPLEMENTED.** `src/Form/BodyDimensionsType.php` still declares `height`, `shoeSize`, and `weight` as `IntegerType` fields, and `src/Validator/Constraints/ParticipantValidator.php` has no regex/legacy-format handling — only numeric range checks. This plan has not been acted on. - -## Goal - -Prevent edit-form failures for existing bookings while we transition from legacy body-dimension values to a better long-term field model. - -This plan introduces a temporary compatibility phase and keeps sunsetting manual and straightforward. - -## Current Problem - -- Existing bookings may contain legacy values such as `-148`, `149-157`, or `195+`. -- Integer-only form fields can fail when these legacy strings are loaded. -- Result: participant edit forms can crash before submit. - -## Proposed Transition Strategy - -Use **text input fields** for body dimensions during transition, with explicit server-side format validation. - -### Key Decisions - -- No gating by booking create date. -- One unified behavior for all bookings. -- Accept both legacy and numeric formats during transition. -- Keep manual sunsetting later (no automated migration switch). - -## Functional Scope - -Affected fields: - -- `height` -- `weight` -- `shoeSize` - -Affected components: - -- `src/Form/BodyDimensionsType.php` -- `src/Validator/Constraints/ParticipantValidator.php` -- `src/Form/BookingParticipantType.php` (option passthrough already in place) -- `config/services.yaml` (ranges remain as validation/config source) - -## Validation Rules During Transition - -### Accepted formats - -- Numeric value: `^\d+$` (e.g. `176`) -- Legacy lower/open/range tokens: - - `^-\d+$` (e.g. `-148`) - - `^\d+\+$` (e.g. `195+`) - - `^\d+\s*-\s*\d+$` (e.g. `149-157`) - -### Requiredness - -- Keep existing rule: body dimensions required when rentals are selected. - -### Range checks - -- Keep current numeric range checks based on `body_dimension_ranges`. -- Apply range checks only to plain numeric values in transition phase. - -## UX/Behavior Expectations - -- Existing legacy values render without form initialization errors. -- New numeric entries are accepted and validated. -- Invalid free text (e.g. `abc`, `17x`) shows clear validation errors. -- HTMX refresh behavior remains unchanged. - -## Implementation Steps - -1. **Form type change** - - In `BodyDimensionsType`, replace integer fields with text fields for all three body dimensions. - - Keep labels/help text; keep placeholders based on configured ranges. - -2. **Validator extension** - - Add format validation for all three fields in `ParticipantValidator`. - - Keep existing required-when-rentals logic. - - Keep existing range validation for numeric values. - -3. **Message tuning** - - Add one clear message for invalid format. - - Retain existing range message for numeric out-of-range values. - -4. **Testing and QA** - - Validate old bookings with legacy values open/edit successfully. - - Validate numeric path (valid and invalid range). - - Validate invalid format handling. - - Validate rental-required behavior. - -## Acceptance Criteria - -- No 500 error when editing participants with legacy body-dimension values. -- Legacy values can be loaded and submitted in transition phase. -- Numeric values are accepted and range-validated. -- Invalid text is rejected with a user-facing validation message. -- Existing participant edit/refresh flows continue to work. - -## Risks and Mitigations - -- **Risk:** Free-text field semantics are less strict than integer fields. - - **Mitigation:** strict server-side regex + range checks. - -- **Risk:** Inconsistent data representations during transition. - - **Mitigation:** explicit acceptance policy and manual sunset plan. - -## Effort Estimate - -- Implementation: 0.5 day -- Validation/message tuning: 0.25 day -- QA/manual testing: 0.5 day -- **Total:** ~1 to 1.5 days - -## Manual Sunset Plan (Later) - -When stakeholders approve end of transition: - -1. Replace text fields with final semantic field type(s). -2. Remove legacy format acceptance from validator. -3. Keep only final numeric/range validation behavior. -4. Remove transition-specific tests and copy. - -Expected cleanup effort: ~0.25 to 0.5 day. diff --git a/docs/booking-flow.md b/docs/booking-flow.md deleted file mode 100644 index 0d350c5..0000000 --- a/docs/booking-flow.md +++ /dev/null @@ -1,196 +0,0 @@ -# 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` - -1. Load or create DTO via `getOrCreateBookingCreateDto()` -2. `BookingSessionManager::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 - - `BookingConfigurator::updateBookingStatusFromRoomSelection()` — may switch booking to inquiry mode if capacity constraints require it - - `BookingSessionManager::saveBookingDto()`, advance `currentStep` to 2 - - `BookingSessionManager::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. `TravelDataProvider::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. `ParticipantDataPrefiller::shouldPrefillApplicant()` + `prefillApplicantFromUser()` — prefills participant[0] from user profile on first visit -5. `RoomAssigner::validateAndResetInvalidAssignments()` — clears any room assignments that no longer match the current room selection (handles back-navigation from Step 2 → Step 1 → Step 2) -6. `RoomAssigner::assignRoomsIfNeeded()` — auto-assigns participants to rooms -7. `BookingConfigurator::preselectDefaultServices()` — selects mandatory and auto-book services for all participants -8. `BookingConfigurator::applyCreateBookingStatusRules()` — re-evaluates booking status (book / inquiry / not-bookable) -9. `BookingSessionManager::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 (`ParticipantDataPrefiller::isDummyDataFillRequested()` / `fillDummyParticipant()`) -4. On submit: re-applies status rules, saves to session, redirects to card overview -5. `ParticipantFormSupport` 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. `BookingConfigurator::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: `BookingPriceCalculator::calculateGrandTotal()` is compared against the API response total (accounting for promotional/goodwill voucher discounts and API-applied group discounts). Mismatch triggers `BookingPriceMismatchAnalyzer` 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: - - `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 availability -- `TravelDataProvider::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. `TravelDataProvider::getTravelData()` — loads travel data (XML or snapshot) -3. `TravelDataProvider::getAvailabilityData(forceRefresh: true)` + `patchAvailabilities()` — force-refreshes availability at edit start, populates cache for subsequent loads within the session -4. `TravelDataProvider::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. `BookingChangeTracker::generateFingerprint()` — sets `originalFingerprint` on DTO **before** draft is applied, so dirty detection compares against original API data -8. `BookingEditDraftManager::findDraft()` + `applyDraftToDto()` — if draft exists, merges user edits on top of fresh API data. Sets `draftWasRestored = true` if successful. -9. `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. - -1. `BookingEditContextFactory::prepareBookingDto()` — refreshes availabilities on the DTO travel object -2. `ParticipantFormSupport` — form creation, notification draining -3. 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. - -1. Invalidate API cache, fetch fresh booking data -2. Force-refresh mutability data (`TravelDataProvider::getMutabilityData(forceRefresh: true)`) and patch DTO travel -3. `BookingEditSubmitGuard::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, `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 - -- `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 (`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. diff --git a/docs/technical-documentation.md b/docs/technical-documentation.md deleted file mode 100644 index 91aae2a..0000000 --- a/docs/technical-documentation.md +++ /dev/null @@ -1,1509 +0,0 @@ -# Technical Documentation - -## Customer Portal for Ski Travel Company - -This document provides comprehensive technical documentation for experienced PHP and Symfony developers working with this codebase. It covers the architecture, components, and implementation details necessary to navigate and extend the application. - ---- - -## Table of Contents - -1. [Architecture Overview](#1-architecture-overview) -2. [Directory Structure & Namespaces](#2-directory-structure--namespaces) -3. [BusProNet API Integration](#3-buspro-net-api-integration) -4. [Controllers](#4-controllers) -5. [Service Layer](#5-service-layer) -6. [Form System](#6-form-system) -7. [Security](#7-security) -8. [Database & Entities](#8-database--entities) -9. [Frontend Integration](#9-frontend-integration) -10. [CLI Commands](#10-cli-commands) -11. [Logging & Debugging](#11-logging--debugging) -12. [Validation](#12-validation) -13. [BusProNet API Quirks & Workarounds](#13-buspro-net-api-quirks--workarounds) - ---- - -## 1. Architecture Overview - -### 1.1 System Overview - -This is a Symfony 6.4 web application serving as a customer portal for a ski travel company. The application integrates with the BusProNet API to manage customer data, bookings, and travel information. - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ User Interface │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ -│ │ Browser │ │ HTMX │ │ Stimulus │ │ TailwindCSS │ │ -│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └─────────────────────┘ │ -└─────────┼────────────────┼────────────────┼─────────────────────────────────┘ - │ │ │ - ▼ ▼ ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Symfony Application │ -│ ┌─────────────────────────────────────────────────────────────────────────┐│ -│ │ Controllers ││ -│ │ Security │ Booking/Create │ Booking/Edit │ Account │ API │ Admin ││ -│ └─────────────────────────────────────────────────────────────────────────┘│ -│ ┌─────────────────────────────────────────────────────────────────────────┐│ -│ │ Service Layer ││ -│ │ BookingService │ TravelDataService │ PricingCalculators │ Insurance ││ -│ └─────────────────────────────────────────────────────────────────────────┘│ -│ ┌─────────────────────────────────────────────────────────────────────────┐│ -│ │ Form System ││ -│ │ Form Types │ Field Handlers │ Conditions │ State Providers │ DTOs ││ -│ └─────────────────────────────────────────────────────────────────────────┘│ -│ ┌─────────────────────────────────────────────────────────────────────────┐│ -│ │ BusProNet Integration ││ -│ │ ApiClient │ XmlParsers │ DataProcessors │ XmlLoaders │ Models ││ -│ └─────────────────────────────────────────────────────────────────────────┘│ -└─────────────────────────────────────────────────────────────────────────────┘ - │ │ - ▼ ▼ -┌─────────────────────────┐ ┌─────────────────────────────────┐ -│ Database │ │ BusProNet API │ -│ User │ LogEntry │ Draft│ │ Socket-based XML Protocol │ -└─────────────────────────┘ └─────────────────────────────────┘ -``` - -### 1.2 Key Application Flows - -#### Multi-Step Booking Creation - -``` -Step 1 (Room Selection) - │ - ▼ -Step 2 (Participant Details + Services) - │ - ▼ -Step 3 (Payment - API Inquiry Validation) - │ - ▼ -Step 4 (Confirmation - API Booking Submission) - │ - ▼ -Success Page -``` - -#### Booking Edit Flow - -``` -Load Booking from API - │ - ├─ Apply Saved Draft (if exists) - │ - ▼ -Card-based Participant Editing - │ - ├─ Auto-save Draft on Each Participant - │ - ▼ -Submit Update to API - │ - ▼ -Delete Draft on Success -``` - -### 1.3 Core Technologies - -| Component | Technology | -|-----------|------------| -| Framework | Symfony 6.4 | -| PHP Version | 8.1+ | -| Database | Doctrine ORM | -| Frontend | Stimulus, HTMX, TailwindCSS | -| Build | Webpack Encore | -| API Communication | Socket-based XML | -| Caching | Symfony Cache (TagAware) | - ---- - -## 2. Directory Structure & Namespaces - -### 2.1 Source Directory (`src/`) - -``` -src/ -├── BusProNet/ # BusProNet API integration layer -│ ├── ApiClient.php # Main API client -│ ├── Constants.php # Service tokens and constants -│ ├── DataProcessor/ # DTO ↔ API payload transformation -│ ├── DataProvider/ # Data providers (countries) -│ ├── Exception/ # API-specific exceptions -│ ├── Form/ # Form choice loaders -│ ├── Model/ # Data models (41 classes) -│ ├── Traits/ # Shared traits -│ ├── Utility/ # Helper utilities -│ ├── XmlLoader/ # Cached XML file loaders -│ └── XmlParser/ # XML response parsers (29 classes) -├── Command/ # CLI commands (15 commands) -├── Controller/ # HTTP controllers -│ ├── Account/ # Account management -│ ├── Admin/ # EasyAdmin controllers -│ ├── Api/ # OAuth2-protected JSON endpoints -│ ├── Booking/ # Booking workflows -│ │ ├── Create/ # Multi-step creation -│ │ ├── Edit/ # Booking editing -│ │ └── Traits/ # Shared controller logic -│ ├── RegistrationController.php -│ ├── ResetPasswordController.php -│ └── SecurityController.php -├── Email/ # Transactional email builders -├── Entity/ # Doctrine entities -│ ├── User.php -│ ├── LogEntry.php -│ └── BookingEditDraft.php -├── EventListener/ # Symfony event listeners -├── Exception/ # Application exceptions -├── Form/ # Form system -│ ├── Model/ # DTOs (BookingDto, ParticipantDto, etc.) -│ ├── Service/ # Field handlers & conditions -│ │ └── Condition/ # 25 condition classes -│ ├── DataTransformer/ # Form data transformers -│ └── Extension/ # Form extensions (XSS protection) -├── Htmx/ # HTMX utilities -├── Logger/ # Custom Monolog handlers/processors -├── Menu/ # Navigation menu builders -├── Message/ # Messenger message classes -├── MessageHandler/ # Messenger message handlers -├── Model/ # Application models -├── Repository/ # Doctrine repositories -├── Security/ # Authentication & authorization -│ └── Voter/ # Custom voters -├── Service/ # Business logic services (43 services) -├── Twig/ # Template extensions -└── Validator/ # Custom validation constraints - └── Constraints/ # Constraint classes -``` - -### 2.2 Configuration (`config/`) - -``` -config/ -├── packages/ # Bundle configurations -│ ├── security.yaml # Firewalls, access control -│ ├── league_oauth2_server.yaml -│ └── monolog.yaml # Logging configuration -├── routes/ # Route definitions -├── services.yaml # Service definitions, BPN config -├── secret/ # RSA keys for encryption -└── bundles.php # Enabled bundles -``` - -### 2.3 Assets (`assets/`) - -``` -assets/ -├── app.js # Main entry point -├── bootstrap.js # Stimulus initialization -├── loading.js # HTMX loading indicator -├── controllers/ # Stimulus controllers (14) -├── styles/ # TailwindCSS styles -│ ├── app.css # Main stylesheet -│ └── components/ # Component styles -├── images/ # Static images -├── fonts/ # Web fonts (Lato) -└── favicon/ # Favicon files -``` - ---- - -## 3. BusProNet API Integration - -### 3.1 Architecture - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ Application Layer │ -└─────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ ApiClient │ -│ src/BusProNet/ApiClient.php │ -│ - Socket connection management with load balancing │ -│ - MD5 authentication (username + password + date + type) │ -│ - Automatic retry on busy server │ -│ - XML debugging dumps │ -└─────────────────────────────────────────────────────────────────────┘ - │ │ - ▼ ▼ -┌──────────────────────────────┐ ┌──────────────────────────────────┐ -│ DataProcessor/ │ │ XmlParser/ │ -│ - BookingDataProcessor │ │ - ApiResponseParser (router) │ -│ - BookingPayloadBuilder │ │ - TravelParser, BookingParser │ -│ - ParticipantServiceProcessor│ │ - PersonalDataParser, etc. │ -└──────────────────────────────┘ └──────────────────────────────────┘ - (DTO → API payload) (XML → Model objects) -``` - -### 3.2 ApiClient - -**Location:** `src/BusProNet/ApiClient.php` - -#### Connection & Authentication - -```php -// Load balancing across multiple ports -$this->selectedPort = $this->config['bpn_api_ports'][array_rand($this->config['bpn_api_ports'])]; - -// MD5-based authentication key -private function createKey(string $username, string $password, string $type): string -{ - $date = (new \DateTimeImmutable())->format('Ymd'); - return md5($username.$password.$date.$type); -} -``` - -#### Request Types - -| Constant | Value | Purpose | -|----------|-------|---------| -| `TYPE_CUSTOMER_DATA` | `KUNDENKONTO` | Customer account operations | -| `TYPE_BOOKING` | `BUCHUNG` | Create new bookings | -| `TYPE_BOOKING_UPDATE` | `BUCHUNGAENDERUNG` | Modify existing bookings | -| `TYPE_PRODUCT_DATA` | `PRODUKTDATEN` | Travel/product details | -| `TYPE_AVAILABILITY` | `VERFUEGBARKEIT` | Service availability | -| `TYPE_PURCHASE_VOUCHER` | `GUTSCHEINPRUEFUNGEINLOESUNG` | Validate purchase vouchers | -| `TYPE_PROMO_VOUCHER` | `AKTIONSGUTSCHEIN` | Validate promotional codes | - -#### Key Methods - -| Method | Return Type | Purpose | -|--------|-------------|---------| -| `getPersonalData(email, password)` | `PersonalData\|Notification` | Authenticate and fetch profile | -| `getBookings(email, password)` | `BaseData\|Notification` | List customer bookings | -| `getBooking(email, password, id)` | `Booking\|Notification` | Single booking details | -| `getTravelData(travelId, hotelId)` | `Travel\|Notification` | Travel package details | -| `createBookingInquiry(BookingDto)` | `BookingResponse\|Notification` | Validate booking (phase 1) | -| `createBooking(BookingDto)` | `BookingResponse\|Notification` | Submit booking (phase 2) | -| `updateBooking(BookingDto)` | `BookingUpdate\|Notification` | Modify existing booking | - -#### Two-Phase Booking Process - -1. **Inquiry Phase** (`createBookingInquiry`): Validates data and returns pricing without creating a booking -2. **Booking Phase** (`createBooking`): Creates the actual booking after inquiry validation - -#### Timeout Configuration - -| Setting | Default | Purpose | -|---------|---------|---------| -| `connection_timeout` | 5s | Socket connect | -| `stream_timeout` | 30s | Read/write operations | -| `total_timeout` | 45s | Entire operation including retries | -| `busy_retry_attempts` | 3 | Retry count for busy server | -| `busy_retry_delay` | 1s | Delay between retries | - -### 3.3 Response Parsing - -**Router:** `src/BusProNet/XmlParser/ApiResponseParser.php` - -Routes XML responses to specialized parsers based on response type and sub-type. - -#### Parser Classes - -| Parser | Output Model | Purpose | -|--------|--------------|---------| -| `TravelParser` | `Travel` | Travel packages with services, rooms, pickups | -| `BookingParser` | `Booking` | Complete booking with participants | -| `PersonalDataParser` | `PersonalData` | Customer profile and address | -| `RoomsParser` | `Room[]` | Hotel room information | -| `ServicesParser` | `Service[]` | Additional services and transportation | -| `InsuranceParser` | `Insurance[]` | Insurance options | -| `PickupsParser` | `Pickup[]` | Pickup/dropoff locations | -| `PurchaseVoucherParser` | `PurchaseVoucher` | Voucher validation results | -| `PromoVoucherParser` | `PromoVoucher` | Promo code validation results | - -### 3.4 Data Models - -**Location:** `src/BusProNet/Model/` - -#### Travel - -```php -class Travel -{ - public ?int $id = null; - public ?int $hotelId = null; - public ?\DateTimeImmutable $dateFrom = null; - public ?\DateTimeImmutable $dateTo = null; - public array $additionalServices = []; - public array $transportationServices = []; - public array $rooms = []; - public array $pickups = []; - public array $dropOffs = []; - public array $insurances = []; - - public function getAdditionalServicesBySubTypes(mixed $subTypes): array; - public function getTransportationServicesByDirection(string $direction): array; - public function getAvailableRooms(): array; - public function requiresInquiryBooking(): bool; -} -``` - -#### Service Token Constants - -| Token | Meaning | -|-------|---------| -| `KUR` | Courses (ski lessons) | -| `SPA` | Ski passes | -| `SON` | Additional services | -| `VPF` | Board/catering | -| `VER`-`VE8` | Equipment rentals | -| `PAR` | Parking | -| `RRV`, `PAK` | Insurance types | -| `LVS` | Rental insurance | - -### 3.5 Data Processors - -**Location:** `src/BusProNet/DataProcessor/` - -| Class | Purpose | -|-------|---------| -| `BookingDataProcessor` | Orchestrates booking data transformation | -| `BookingPayloadBuilder` | Constructs nested XML payload structure | -| `ParticipantServiceProcessor` | Maps participant service selections | -| `ServiceMappingCollector` | Collects service IDs by type | -| `PersonalDataSynchronizer` | Syncs personal data between applicant/participants | -| `PickupPlanningTransformer` | Transforms pickup planning data | - -### 3.6 XML Loaders - -**Location:** `src/BusProNet/XmlLoader/` - -Load data from pre-cached XML files for performance: - -| Loader | Purpose | -|--------|---------| -| `TravelLoader` | Travel packages | -| `HotelLoader` | Hotel details | -| `InsuranceLoader` | Insurance options | -| `PickupLoader` | Pickup locations | -| `AgencyLoader` | Agency information | - ---- - -## 4. Controllers - -### 4.1 Authentication Controllers - -#### SecurityController (`/`) - -- `GET /` - Login page with booking flow detection -- `GET /logout` - Logout endpoint - -Detects OAuth2 authorization requests and booking flow context from session. - -#### RegistrationController (`/registration`) - -- `POST /registration` - Handle customer registration via BusProNet API - -#### ResetPasswordController (`/reset-password`) - -- `POST /reset-password` - Password reset request via BusProNet API - -### 4.2 Booking Creation Flow - -#### Create/IndexController - -- `GET /bookings/create` - Entry point with HTMX loading -- `POST /bookings/create/init` - Initialize fresh booking session -- `POST /bookings/cancel` - Cancel and redirect to return URL -- `GET /bookings/create/error` - Error display page - -Validates query parameters (dateId, hotelId, agency), resolves agency codes, creates fresh `BookingDto`. - -#### Create/Step1Controller (Room Selection) - -- `GET /bookings/create/rooms` - Room selection form -- `POST /bookings/create/refresh` - HTMX refresh for sidebar - -Validates step access, detects room changes, updates booking status for inquiry mode. - -#### Create/Step2Controller (Participant Details) - -- `GET /bookings/create/participants` - Participant cards overview -- `GET/POST /bookings/create/participants/{index}` - Edit single participant -- `POST /bookings/create/participants/{index}/refresh` - HTMX form refresh - -Card-based UI for scalability (50+ participants). Auto-assigns rooms, preselects mandatory services, saves drafts. - -#### Create/Step3Controller (Payment Validation) - -- `GET /bookings/create/payment` - Payment form -- `POST /bookings/create/payment/refresh` - HTMX refresh - -Calls `createBookingInquiry()` for validation, compares calculated vs. API prices. - -#### Create/Step4Controller (Confirmation) - -- `GET /bookings/create/confirmation` - Final confirmation - -Calls `createBooking()` for submission, clears caches, stores booking number. - -#### Create/SuccessController - -- `GET /bookings/create/success` - Success page with booking number - -### 4.3 Booking Management - -#### Booking/IndexController - -- `GET /bookings` - List user's bookings - -#### Booking/DownloadController - -- `GET /bookings/{id}/documents` - Download booking documents -- `GET /bookings/{id}/invoice` - Download invoice/statement - -#### Booking/Edit/IndexController - -- `GET /bookings/{id}/edit/start` - Clear cache, redirect to edit -- `GET /bookings/{id}/edit` - Edit participant cards -- `GET/POST /bookings/{id}/edit/participants/{index}` - Edit single participant -- `POST /bookings/{id}/edit/reload` - Reload from API (discard changes) -- `POST /bookings/{id}/edit/cancel` - Cancel edit (preserve draft) - -Similar card-based UI as Step 2. Auto-saves drafts, uses fingerprinting for change detection. - -### 4.4 Account Controllers - -#### Account/IndexController - -- `GET /account` - Account dashboard (requires `ROLE_USER`) - -#### Account/PersonalDataController - -- `GET /personal-data` - View/edit personal data -- `POST /personal-data/newsletter` - Toggle newsletter subscription - -### 4.5 API Controllers (OAuth2 Protected) - -All under `/api` prefix, require OAuth2 authentication. - -| Controller | Routes | Purpose | -|------------|--------|---------| -| `UserinfoController` | `GET /api/userinfo` | User profile (scope-filtered) | -| `ProductController` | `GET /api/products` | Product listing | -| `TravelController` | `GET /api/travels/*` | Travel data endpoints | -| `HotelController` | `GET /api/hotels/*` | Hotel information | -| `PickupController` | `GET/POST /api/pickups*` | Pickup locations and planning | -| `CountryController` | `GET /api/countries` | Country list | -| `CrmAttributeController` | `GET /api/crm-attributes` | User CRM attributes | -| `ContactFormController` | `POST /api/contactform` | Contact form submission | -| `LastUpdateController` | `GET /api/last-update` | Data sync timestamp | - -### 4.6 Admin Controllers (EasyAdmin) - -| Controller | Entity | Purpose | -|------------|--------|---------| -| `DashboardController` | - | Admin panel home | -| `UserCrudController` | `User` | User management (read-only) | -| `BookingEditDraftCrudController` | `BookingEditDraft` | Draft management + export | -| `LogEntryCrudController` | `LogEntry` | Activity logging | -| `XmlDumpController` | - | API request debugging | - -### 4.7 Shared Traits - -| Trait | Purpose | -|-------|---------| -| `BookingCreateTrait` | Step validation, redirects, error handling | -| `BookingExceptionHandlerTrait` | Safe DTO retrieval with error messages | -| `ParticipantCardFlowTrait` | Card UI, form creation, notifications | - ---- - -## 5. Service Layer - -### 5.1 Booking Services - -#### BookingService - -**Location:** `src/Service/BookingService.php` - -Orchestrates booking workflow, session management, participant calculations. - -| Method | Purpose | -|--------|---------| -| `startFreshBooking()` | Initialize new booking | -| `getOrCreateBookingCreateDto()` | Session DTO management | -| `ensureCorrectNumberOfParticipants()` | Sync participant count | -| `preselectMandatoryServices()` | Pre-select required services | -| `updateBookingStatusFromRoomSelection()` | Update inquiry status | -| `getParticipantsCount()` | Calculate from room selections | -| `groupRoomsBySelectionType()` | Group by "by_pax" or "by_room" | - -#### BookingEditDataLoaderService - -Loads booking data for edit mode with automatic draft restoration. - -| Method | Purpose | -|--------|---------| -| `loadFormData()` | Load or initialize for editing | -| `initializeFromApi()` | Fresh load with draft application | -| `fetchBookingData()` | Cached API fetch (5-min TTL) | -| `invalidateUserBookingCaches()` | Bulk cache invalidation | - -#### BookingEditDraftService - -Draft persistence for edit sessions. - -| Method | Purpose | -|--------|---------| -| `findDraft()` | Retrieve existing draft | -| `saveDraft()` | Create or update draft | -| `deleteDraft()` | Remove after submission | -| `applyDraftToDto()` | Merge draft onto API data | - -### 5.2 Pricing Calculators - -#### BookingPriceCalculatorService - -Facade for all pricing calculations. - -| Method | Purpose | -|--------|---------| -| `getPricingBreakdown()` | Complete breakdown with surcharges | -| `calculateGrandTotal()` | Total booking price | -| `calculateRoomPricing()` | Room costs only | -| `calculateServicePricing()` | Service costs only | -| `calculateIndividualParticipantPrice()` | Per-participant total | - -#### Supporting Calculators - -| Class | Purpose | -|-------|---------| -| `RoomPricingCalculator` | Room cost calculations | -| `ServicePricingCalculator` | Service aggregation by subtype | -| `ParticipantPricingCalculator` | Per-participant with caching | - -### 5.3 Travel Data Services - -#### TravelDataService - -Unified interface for travel data from XML files and API. - -| Method | Purpose | -|--------|---------| -| `getTravelData()` | Primary method with source selection | -| `getTravelDataFromXml()` | Load from cached XML | -| `getTravelDataFromApi()` | Load from live API | -| `getMutabilityData()` | Mutable services flags (12h cache) | -| `getAvailabilityData()` | Service availability (10-min cache) | -| `enrichWithFreshAvailabilities()` | Ensure current data | - -#### InsuranceService - -Consolidated insurance operations with eligibility filtering. - -| Method | Purpose | -|--------|---------| -| `getSelectableInsurances()` | Non-complementary options | -| `getEligibleInsurances()` | Filtered by participant criteria | -| `filterByType()` | Group by subType + familyInsurance | -| `reassignInsuranceForPriceChange()` | Find compatible insurance | -| `batchAssignInsuranceToParticipants()` | Bulk assignment | - -#### VoucherValidationService - -Voucher validation with API caching (15-min TTL). - -| Method | Purpose | -|--------|---------| -| `validatePurchaseVoucher()` | Validate redemption codes | -| `validatePromoVoucher()` | Validate promo codes with context | - -### 5.4 Supporting Services - -| Service | Purpose | -|---------|---------| -| `ParticipantEligibilityService` | Check booking eligibility by skipass availability | -| `ServiceAvailabilityCalculator` | Track dynamic availability within session | -| `RoomAssignmentService` | Automatic room assignment | -| `BookingSummaryDataService` | Consolidate sidebar display data | -| `ParticipantCardDataService` | Extract card display data | -| `BookingFingerprintService` | SHA-256 change detection | -| `BookingExportService` | Excel export from drafts | -| `CmsDataService` | CMS data retrieval (hotel images) | -| `XmlDumpService` | API debugging file management | - ---- - -## 6. Form System - -### 6.1 Architecture Overview - -The form system implements sophisticated conditional field visibility with dynamic state management. - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ Form Submission │ -└─────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ ParticipantFieldHandlerRegistry │ -│ - Topological sort by dependencies │ -│ - Execute handlers in correct order │ -│ - Sync DTO back to submitted data │ -└─────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ Field Handlers (20+) │ -│ DateOfBirth → SkiPass → Rentals → Insurance → ... │ -└─────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ Field State Providers │ -│ CreateFieldStateProvider │ EditFieldStateProvider │ -│ - Evaluate conditions for each field │ -│ - Determine: hidden, static_text, readonly, disabled, required │ -└─────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ Conditions (22+) │ -│ CompositeCondition (AND/OR/NOT) │ -│ AgeRangeCondition │ SkiPassSelectionCondition │ ... │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -### 6.2 Form Types - -#### Booking Flow Forms - -| Form Type | Purpose | -|-----------|---------| -| `BookingCreateStep1Type` | Room selection | -| `BookingCreateStep2Type` | Embeds `BookingParticipantType` | -| `BookingCreateStep3Type` | Additional participants | -| `BookingCreateStep4Type` | Confirmation/payment | -| `BookingEditType` | Edit mode root form | -| `BookingEditParticipantType` | Edit individual participants | -| `BookingParticipantType` | Base participant form (create + edit) | - -#### Supporting Forms - -| Form Type | Purpose | -|-----------|---------| -| `AddressType` | Street, postal code, city, country | -| `BodyDimensionsType` | Height, weight, shoe size | -| `RoomAssignmentType` | Room selection | -| `PersonalDataType` | Customer profile | -| `PaymentType` | Payment information | -| `RegistrationType` | User registration | -| `BankAccountType` | Bank details | - -### 6.3 DTOs - -#### ParticipantDto - -```php -class ParticipantDto -{ - // Personal Data - public ?string $firstName = null; - public ?string $lastName = null; - public ?string $gender = null; - public ?\DateTimeImmutable $dateOfBirth = null; - public ?Address $address = null; - - // Service Selections - public ?int $assignedRoomId = null; - public array $courses = []; - public array $additionalServices = []; - public ?Service $skiPass = null; - public array $rentals = []; - public ?Service $transportationOutbound = null; - public ?Service $transportationInbound = null; - public ?Pickup $pickup = null; - - // Insurance - public ?Insurance $insurance = null; - public bool $bulkInsuranceBooking = false; - - // Vouchers - public ?string $purchaseVoucherCode = null; - public ?string $promoVoucherCode = null; -} -``` - -#### BookingDto - -```php -class BookingDto -{ - public const MODE_CREATE = 'create'; - public const MODE_EDIT = 'edit'; - - public string $mode = self::MODE_CREATE; - public ?Travel $travel = null; - public ?Booking $booking = null; - public array $participants = []; - public array $selectedRooms = []; - public ?int $agencyId = null; - public ?string $paymentMethod = null; -} -``` - -### 6.4 Field Handler System - -#### Handler Registry - -**Location:** `src/Form/Service/ParticipantFieldHandlerRegistry.php` - -Uses topological sort (Kahn's algorithm) to execute handlers in dependency order. - -```php -interface ParticipantFieldHandlerInterface -{ - public function getFieldName(): string; - public function getDependencies(): array; - public function shouldProcess(...): bool; - public function processField(...): void; - public function getFieldStateModifications(...): array; -} -``` - -#### Handler Implementations - -| Handler | Field | Dependencies | -|---------|-------|--------------| -| `ParticipantDateOfBirthFieldHandler` | dateOfBirth | None (first) | -| `ParticipantSkiPassFieldHandler` | skiPass | dateOfBirth | -| `ParticipantRentalsFieldHandler` | rentals | dateOfBirth, skiPass | -| `ParticipantCoursesFieldHandler` | courses | dateOfBirth | -| `ParticipantBoardFieldHandler` | board | dateOfBirth | -| `ParticipantTransportationOutboundFieldHandler` | transportationOutbound | dateOfBirth | -| `ParticipantTransportationInboundFieldHandler` | transportationInbound | dateOfBirth | -| `ParticipantInsuranceFieldHandler` | insurance | All price-affecting fields | -| `ParticipantBulkInsuranceFieldHandler` | bulkInsuranceBooking | None | -| `ParticipantPickupFieldHandler` | pickup | transportation | -| `ParticipantParkingFieldHandler` | parking | transportationOutbound | -| `ParticipantPromoVoucherFieldHandler` | promoVoucherCode | None | -| `ParticipantPurchaseVoucherFieldHandler` | purchaseVoucherCode | None | - -### 6.5 Condition System - -#### CompositeCondition - -```php -CompositeCondition::and(condition1, condition2); -CompositeCondition::or(condition1, condition2); -CompositeCondition::not(condition); -``` - -#### Core Conditions - -| Condition | Purpose | -|-----------|---------| -| `FirstParticipantCondition` | Is applicant (index 0) | -| `MultipleParticipantsCondition` | 2+ participants | -| `DateOfBirthProvidedCondition` | Date of birth is set | -| `AgeRangeCondition(min, max)` | Age in range | -| `BabyAgeCondition` | Age 0-2 | -| `SkiPassSelectionCondition` | Ski pass selected | -| `RentalSelectionCondition` | Rentals selected | -| `RoomSelectionCondition(['codes'])` | Specific room type | -| `ServiceSubTypeCondition::equals(field, type)` | Service type match | -| `BookingEligibilityCondition` | Has eligible services | -| `BulkInsuranceBookingCondition` | Bulk insurance active | -| `PersonalDataMutabilityCondition` | BPN mutability flag | -| `BookingModeCondition(mode)` | Create or edit mode | - -### 6.6 Field State Providers - -| State | Behavior | -|-------|----------| -| `hidden` | Field excluded from form | -| `static_text` | Rendered as read-only text | -| `readonly` | Visible but not editable | -| `disabled` | User cannot interact | -| `required` | Field is mandatory | - -#### Example: Insurance Field Visibility (Create Mode) - -```php -// Hidden if: -// - Date of birth not provided OR -// - Bulk insurance active for dependents OR -// - Participant ineligible (no skipasses available) -CompositeCondition::or( - CompositeCondition::not(new DateOfBirthProvidedCondition()), - new BulkInsuranceBookingCondition(), - new BookingEligibilityCondition() -) -``` - ---- - -## 7. Security - -### 7.1 Authentication - -#### BpnAuthenticator - -**Location:** `src/Security/BpnAuthenticator.php` - -Custom authenticator that validates credentials against BusProNet API. - -``` -Login Form → MD5 Hash Password → ApiClient::getPersonalData() - │ - ├─ Valid → Create/Update User Entity - │ Get CRM Attributes (roles, hotel codes) - │ Encrypt password with RSA - │ Store in database - │ - └─ Invalid → CustomUserMessageAuthenticationException -``` - -#### User Entity Password Handling - -- Plain password hashed with MD5 for API authentication -- MD5 hash encrypted with RSA before database storage -- Decrypted when needed for subsequent API calls - -### 7.2 Encryption - -**Location:** `src/Security/Crypt.php` - -Uses `spatie/crypto` library with RSA asymmetric encryption. - -| Method | Purpose | -|--------|---------| -| `encrypt(string)` | Encrypt with private key | -| `decrypt(string)` | Decrypt with public key | -| `sign(string)` | Create digital signature | -| `verify(string, signature)` | Verify signature | - -Keys stored in `config/secret/` directory. - -### 7.3 Authorization - -#### BookingVoter - -**Location:** `src/Security/Voter/BookingVoter.php` - -This voter currently exists as legacy authorization logic and is not actively invoked by booking controllers. - -Current booking routes (`/bookings/*`) are guarded with `ROLE_USER`, while final document access checks are enforced by BusProNet. - -### 7.4 OAuth2 - -Configured via League OAuth2 Server for API endpoints. - -- **Access Token TTL:** 10 minutes -- **Scopes:** `email`, `id`, `profile`, `roles`, `api` -- **Grants:** Authorization code, client credentials - -#### API Protection - -```yaml -# config/packages/security.yaml -api: - pattern: ^/api - security: true - stateless: true - oauth2: true -``` - -### 7.5 Access Control - -| Path | Role | -|------|------| -| `^/authorize` | `IS_AUTHENTICATED_REMEMBERED` | -| `^/admin` | `ROLE_ADMIN` | -| `^/` | `PUBLIC_ACCESS` | - ---- - -## 8. Database & Entities - -### 8.1 User Entity - -**Location:** `src/Entity/User.php` - -| Property | Type | Purpose | -|----------|------|---------| -| `email` | string (unique) | User identifier | -| `password` | string (nullable) | RSA-encrypted API password | -| `personId` | int (nullable) | BusProNet person ID | -| `addressId` | int (nullable) | BusProNet address ID | -| `roles` | array (JSON) | CRM roles | -| `hotelCodes` | array (JSON) | Associated hotels | -| `lastLoginAt` | DateTimeImmutable | Audit timestamp | - -### 8.2 LogEntry Entity - -**Location:** `src/Entity/LogEntry.php` - -| Property | Type | Purpose | -|----------|------|---------| -| `channel` | string | Log channel (core, bpn, auth) | -| `message` | string | Log message | -| `context` | array (JSON) | Contextual data | -| `extra` | array (JSON) | Request ID, URI, user info | -| `createdAt` | DateTimeImmutable | Timestamp | - -### 8.3 BookingEditDraft Entity - -**Location:** `src/Entity/BookingEditDraft.php` - -| Property | Type | Purpose | -|----------|------|---------| -| `user` | User (ManyToOne) | Draft owner | -| `bookingId` | int | BusProNet booking ID | -| `bookingNumber` | int (nullable) | User-facing number (vorgang) | -| `travelDate` | DateTimeImmutable | For cleanup queries | -| `dateId` | int (nullable) | Travel date ID | -| `hotelId` | int (nullable) | Hotel ID | -| `formData` | array (JSON) | Saved form values | -| `createdAt` | DateTimeImmutable | Creation timestamp | -| `updatedAt` | DateTimeImmutable | Last update | - -**Unique constraint:** `(user_id, booking_id)` - -### 8.4 Repositories - -#### LogEntryRepository - -```php -public function deleteOlderThan(DateTimeImmutable $threshold): int; -``` - -#### BookingEditDraftRepository - -```php -public function findByUserAndBooking(User $user, int $bookingId): ?BookingEditDraft; -public function deleteByUserAndBooking(User $user, int $bookingId): void; -public function deleteExpiredDrafts(): int; -``` - ---- - -## 9. Frontend Integration - -### 9.1 Stimulus Controllers - -**Location:** `assets/controllers/` - -| Controller | Purpose | -|------------|---------| -| `modal_controller` | Modal dialog management | -| `toggle_controller` | Collapsible sections with state persistence | -| `checkbox_toggle_controller` | Container visibility based on checkboxes | -| `select_toggle_controller` | Container visibility based on select values | -| `form_collection_controller` | Dynamic add/remove form fields | -| `step_input_controller` | Numeric spinner input | -| `birthday_controller` | Date of birth validation/completion event | -| `toast_controller` | Toast notifications via Toastify | -| `tooltip_controller` | Tooltips via Tippy.js | -| `backbutton_controller` | Browser back navigation | -| `data_layer_controller` | Pushes analytics events to the data layer | -| `mobilenav_controller` | Mobile navigation menu | -| `password_reveal_controller` | Show/hide password input | -| `sidebar_menu_controller` | Sidebar menu state | - -### 9.2 HTMX Integration - -#### HxRedirectResponse - -```php -// src/Htmx/HxRedirectResponse.php -class HxRedirectResponse extends Response -{ - public function __construct(string $url) - { - parent::__construct('', Response::HTTP_OK, ['HX-Redirect' => $url]); - } -} -``` - -Triggers full-page navigation without CORS issues. - -#### HxTrait - -```php -// Out-of-band swap for multiple sections -protected function htmxOobResponse( - string $templateName, - array $blockNames, - array $context = [], - ?string $pushUrl = null -): Response; - -// Intelligent redirect for HTMX and regular requests -protected function htmxRedirect(Request $request, string $url): Response; -``` - -#### HTMX Configuration - -```javascript -// assets/app.js -htmx.config.includeIndicatorStyles = false; -htmx.config.historyEnabled = true; -htmx.config.historyCacheSize = 10; -htmx.config.allowScriptTags = false; -htmx.config.withCredentials = true; -htmx.config.timeout = 50000; -``` - -### 9.3 Asset Pipeline - -#### Webpack Encore Configuration - -```javascript -Encore - .setOutputPath('public/build/') - .setPublicPath('/build') - .addEntry('app', './assets/app.js') - .enableStimulusBridge('./assets/controllers.json') - .enablePostCssLoader() - .enableVersioning(Encore.isProduction()) -``` - -#### TailwindCSS - -```css -/* assets/styles/app.css */ -@import "tailwindcss/base"; -@import "_base.css"; -@import "tailwindcss/components"; -@import "_components.css"; -@import "tailwindcss/utilities"; -``` - -### 9.4 NPM Scripts - -```json -{ - "dev": "encore dev", - "watch": "encore dev --watch", - "build": "encore production --progress" -} -``` - ---- - -## 10. CLI Commands - -### 10.1 BusProNet API Commands - -#### app:bpn:fetch-travel - -```bash -php bin/console app:bpn:fetch-travel [--filename|-f ] [--dry-run] -``` - -Fetches travel data from BusProNet API and saves to XML export directory. - -#### app:bpn:replay - -```bash -php bin/console app:bpn:replay [--dry-run|-d] [--output|-o ] -``` - -Replays stored XML requests against the API for debugging. - -#### app:bpn:xml-anonymize - -```bash -php bin/console app:bpn:xml-anonymize [] -``` - -Anonymizes a single BPN XML dump or all XML dumps in a folder. Request/response -pairs are processed together so the anonymized identities stay consistent across -both files. - -#### app:bpn:xml-sync - -```bash -php bin/console app:bpn:xml-sync [--force|-f] [--dry-run] -``` - -Synchronizes XML export files from remote SFTP. Invalidates caches on completion. - -#### app:bpn:refresh-travel-snapshot - -```bash -php bin/console app:bpn:refresh-travel-snapshot -``` - -Refreshes active travel snapshots using extended availability data. - -#### app:bpn:xml-cache-invalidate - -```bash -php bin/console app:bpn:xml-cache-invalidate -``` - -Invalidates BPN XML caches when local `uebertragung.info` changes. - -### 10.2 Maintenance Commands - -#### app:draft:cleanup - -```bash -php bin/console app:draft:cleanup [--dry-run] -``` - -Deletes booking edit drafts for past travels. - -#### app:draft:inspect - -```bash -php bin/console app:draft:inspect [--days|-d ] [--booking-id|-b ] [--delete] -``` - -Inspects and optionally deletes drafts. - -#### app:cleanup:log-entries - -```bash -php bin/console app:cleanup:log-entries [--retention|-r ] -``` - -Removes log entries older than retention period (default: 6 months). - -#### app:cleanup:xml-dumps - -```bash -php bin/console app:cleanup:xml-dumps -``` - -Removes XML debug dumps older than 3 days. - -#### app:cleanup:newsletter-opt-in-requests - -```bash -php bin/console app:cleanup:newsletter-opt-in-requests -``` - -Removes expired pending newsletter double opt-in requests. - -- Request TTL is controlled by `NEWSLETTER_CONFIRMATION_TTL_HOURS` (default: 1 hour). -- Expired pending requests are removed immediately when they are encountered (new request / confirmation attempt). -- Scheduled cleanup removes all currently expired pending requests (`expires_at <= now`) as a safety net. -- Confirmed requests are converted into durable per-list newsletter consent records and then deleted. - -#### app:mailjet:newsletter-webhook - -```bash -php bin/console app:mailjet:newsletter-webhook register -php bin/console app:mailjet:newsletter-webhook remove 123 -php bin/console app:mailjet:newsletter-webhook deactivate -``` - -Creates or deletes the Mailjet `unsub` callback for `POST /webhooks/mailjet/newsletter`. - -- Uses `APP_BASE_URL` by default and appends the webhook path. -- The command prepends `mailjet:@` to the URL before registering it with Mailjet. -- Configure the plain password in `MAILJET_WEBHOOK_BASIC_PASSWORD`; Symfony uses it for the webhook firewall. -- Pass `--url` to target a different full webhook URL. -- `register` prints the API response payload so you can note the callback ID. -- `remove` and `deactivate` require the callback ID returned by Mailjet. - -#### app:db:anonymize - -```bash -php bin/console app:db:anonymize -``` - -Anonymizes personal data in the local database. - -#### app:log-entry:backfill-error-codes - -```bash -php bin/console app:log-entry:backfill-error-codes -``` - -Backfills the `error_code` column from extra data in existing log entries. - -### 10.3 Setup Commands - -#### app:crypto:generate-keys - -```bash -php bin/console app:crypto:generate-keys -``` - -Generates RSA keypair for encryption in configured path. - ---- - -## 11. Logging & Debugging - -### 11.1 Log Channels - -| Channel | Purpose | -|---------|---------| -| `core` | Core application logic | -| `bpn` | BusProNet API interactions | -| `auth` | Authentication/security events | - -### 11.2 Custom Processors - -**Location:** `src/Logger/` - -| Processor | Adds to Record | -|-----------|----------------| -| `RequestIdProcessor` | `extra['request_id']` | -| `RequestInfoProcessor` | `extra['uri']`, `extra['method']` | -| `UserDataProcessor` | `extra['user']['username']`, `extra['user']['roles']` | - -### 11.3 Database Handler - -**Location:** `src/Logger/DatabaseHandler.php` - -Persists INFO+ level logs to `LogEntry` entity. Replaces message placeholders with context values. - -### 11.4 XML Debugging - -When debug mode is enabled, API requests/responses are dumped to `var/bpn/`: - -``` -r-00m8z7k58a-8k1pvd9q_1_request.xml -r-00m8z7k58a-8k1pvd9q_1_response.xml -``` - -View via Admin Panel: Admin → Log → XML Dumps action. - ---- - -## 12. Validation - -### 12.1 Custom Constraints - -**Location:** `src/Validator/Constraints/` - -#### ParticipantValidator - -Cross-field validation for participant data. - -```php -// Requires pickup when bus transportation selected -if (($hasOutboundBus || $hasInboundBus) && null === $participant->pickup) { - $this->context->buildViolation('Bitte auswählen') - ->atPath('pickup') - ->addViolation(); -} -``` - -#### RoomSelectionValidator - -- At least one room must be selected -- Baby rooms cannot be booked standalone - -```php -// Baby room + regular room required -if ($hasBabyRoom && false === $hasRegularRoom) { - $this->context->buildViolation($constraint->onlyBabyRoomsMessage) - ->addViolation(); -} -``` - -#### PromoVoucherValidator - -Validates promo codes against BusProNet API with travel and price context. - -#### PurchaseVoucherValidator - -Validates purchase voucher codes for existence and remaining balance. - ---- - -## 13. BusProNet API Quirks & Workarounds - -This section documents known API behaviors and the solutions implemented. - -### 13.1 Server Busy - Immediate Connection Close - -**Issue:** Server may close socket immediately without data when busy. - -**Solution:** Automatic retry with configurable attempts (default: 3) and delay (default: 1s). - -**Location:** `src/BusProNet/ApiClient.php:646` (`executeWithRetry`) - -```php -private function executeWithRetry(callable $operation, ?string $type = null): mixed -{ - for ($attempt = 1; $attempt <= $maxAttempts; ++$attempt) { - try { - return $operation(); - } catch (ImmediateConnectionCloseException $e) { - if ($attempt < $maxAttempts) { - sleep($retryDelay); - } - } - } -} -``` - -### 13.2 Pickup API Limitation - -**Issue:** API only supports pickup submission via outbound (`zustiege`) section. When outbound is car but inbound is bus, pickup cannot be properly submitted. - -**Solution:** -- Pickup field shown when either direction is bus -- Pickup pricing only calculated when outbound is bus -- Summary shows "Zustieg (inkl.)" without price for inbound-only bus - -**Documentation:** `docs/pickup-api-limitation.md` - -### 13.3 Insurance Not Available in Edit Mode - -**Issue:** API does not return insurance data in booking responses. - -**Solution:** Insurance field skipped in edit mode. Original insurance preserved and passed through unchanged. - -**Location:** `src/Form/Service/ParticipantInsuranceFieldHandler.php:96-100` - -```php -public function shouldProcess(...): bool -{ - if (BookingDto::MODE_EDIT === $mode) { - return false; // Skip processing entirely in edit mode - } -} -``` - -### 13.4 Minimal Data for First Participant - -**Issue:** API may return full data only in `` but minimal/empty in ``. - -**Solution:** Copy address from applicant if first participant has empty address. - -**Location:** `src/BusProNet/DataProcessor/BookingDataProcessor.php:71-81` - -### 13.5 Automatic Customer Matching - -**Issue:** Including customer IDs prevents automatic matching by personal data. - -**Solution:** Exclude `personId` and `addressId` from create mode payload. - -**Location:** `src/BusProNet/DataProcessor/BookingPayloadBuilder.php:275,313` - -### 13.6 Personal Data Mutability Flag - -**Issue:** Per-participant `aenderungmoeglich` flag controls editability. - -**Solution:** Hide personal data fields and render as static text when not mutable. - -**Exception:** Internal agency bookings (code '0004') ignore mutability. - -**Location:** `src/Form/Service/Condition/PersonalDataMutabilityCondition.php` - -### 13.7 Synthetic "No Insurance" Option - -**Issue:** Insurance is optional but requires explicit choice for legal compliance. - -**Solution:** Inject synthetic option with ID '0' that satisfies validation but is excluded from API transmission. - -**Location:** `src/BusProNet/Model/Insurance.php:26` (constant), `:201` (`isNoInsurance()`) - -```php -public const NO_INSURANCE_ID = '0'; - -public function isNoInsurance(): bool -{ - return self::NO_INSURANCE_ID === $this->id; -} -``` - -### 13.8 Goodwill (Kulanz) Vouchers - -**Issue:** Goodwill vouchers must be treated differently from regular purchase vouchers. - -**Solution:** Detect by `type="Kulanz"`, send as `` per participant instead of aggregated. - -**Location:** `src/BusProNet/Model/PurchaseVoucher.php:42-49` - -### 13.9 API-Applied Discounts - -**Issue:** API may apply automatic discounts (group discounts) not known to local calculator. - -**Solution:** Extract ERM-type negative price items from response and account for them in price comparison. - -**Location:** `src/BusProNet/Model/BookingResponse.php:106-120` - -### 13.10 Insurance Auto-Reassignment - -**Issue:** Insurance may become ineligible when participant price changes. - -**Solution:** Automatically reassign to same type with appropriate price tier, preserving user intent. - -**Location:** `src/Form/Service/ParticipantInsuranceFieldHandler.php:115-224` - -### 13.11 Address Data Cloning - -**Issue:** Shared address references could cause unintended modifications. - -**Solution:** Clone address objects when loading bookings and creating new instances during updates. - -**Locations:** -- `src/BusProNet/DataProcessor/BookingDataProcessor.php:43-48` -- `src/BusProNet/DataProcessor/PersonalDataSynchronizer.php:60-71` - -### 13.12 Company Bookings - Missing Fields - -**Issue:** Travel agencies create bookings with company data lacking required personal fields. - -**Solution:** Fill missing mandatory fields with sensible defaults. - -**Location:** `src/BusProNet/DataProcessor/PersonalDataSynchronizer.php:91-122` - -```php -// Default values for company applicants -$applicant->firstName = 'Anmelder'; -$applicant->gender = 'D'; -$applicant->nationality = 'D'; -$applicant->dateOfBirth = new \DateTimeImmutable('-20 years'); -$applicant->mobile = '12345'; -``` - -### 13.13 Message Length Header - -**Issue:** All API responses are prefixed with 10-byte message length. - -**Solution:** Strip first 10 bytes from all socket responses. - -**Location:** `src/BusProNet/ApiClient.php:695-696` - -```php -$xml = substr($response, 10); -``` - ---- - -## Appendix: Key File Locations - -### Configuration - -| File | Purpose | -|------|---------| -| `config/services.yaml` | Service definitions, BPN config | -| `config/packages/security.yaml` | Firewalls, access control | -| `config/packages/monolog.yaml` | Logging configuration | -| `config/secret/` | RSA encryption keys | - -### Entry Points - -| File | Purpose | -|------|---------| -| `src/Controller/SecurityController.php` | Login entry | -| `src/Controller/Booking/Create/IndexController.php` | Booking creation entry | -| `src/BusProNet/ApiClient.php` | API communication | -| `assets/app.js` | Frontend entry | - -### Core Business Logic - -| File | Purpose | -|------|---------| -| `src/Service/BookingService.php` | Booking orchestration | -| `src/Service/BookingPriceCalculatorService.php` | Pricing facade | -| `src/Form/Service/ParticipantFieldHandlerRegistry.php` | Form field processing | -| `src/BusProNet/DataProcessor/BookingDataProcessor.php` | DTO ↔ API transformation |