# MyEP Next Booking - Project Overview **Symfony 6.4 travel booking application** integrating with Bus Pro Net (BPN) XML API. ## Core Architecture ### Multi-Step Booking Flow 1. **Step 1**: Room selection and dates (`Create\Step1Controller`) 2. **Step 2**: Participant details with card-based UI (`Create\Step2Controller`) 3. **Step 3**: Payment method selection (`Create\Step3Controller`) 4. **Step 4**: Final confirmation and submission to BPN API (`Create\Step4Controller`) 5. **Edit Flow**: Similar card-based UI for existing bookings (`Edit\IndexController`) ### Card-Based UI Pattern (Production) - **Overview**: Grid of participant cards with lazy-loaded individual forms - **Performance**: Handles 50+ participants efficiently via HTMX - **Controllers**: - `Create\Step2Controller` - create flow with validation before step 3 - `Edit\IndexController` - edit flow with validation before API submission - **Shared Logic**: - `ParticipantCardFlowTrait` - Card rendering and form handling - `ParticipantValidationTrait` - Validation error extraction for card indicators - **Validation Pattern**: - Both controllers use validation-only forms (`BookingCreateStep2Type`, `BookingEditType`) - Standard Symfony form flow: `handleRequest()` → `isSubmitted()` → `isValid()` - On invalid: Extract error indices, display error banner, highlight cards, disable submit button - On valid: Proceed to next step (create) or submit to API (edit) ## Key Architectural Layers ### BusProNet Integration (`src/BusProNet/`) - `ApiClient` - XML API communication - `XmlParser/` - Response parsers (travels, hotels, bookings) - `XmlLoader/` - Data loaders with caching - `DataProcessor/` - Transform API data to DTOs ### Form System (`src/Form/`) - **DTOs**: `BookingCreateDto`, `ParticipantDto` (session-stored) - **Field Handlers**: 15+ specialized handlers in `src/Form/Service/` - Registered via service tags with dependency resolution - Process in dependency order via `ParticipantFieldHandlerRegistry` - **Conditional Fields**: Universal condition system (`FieldConditionInterface`) - Age-based, field-dependent, service-specific conditions - Applied via `CreateFieldStateProvider` / `EditFieldStateProvider` - **HTMX Integration**: Real-time updates for dynamic fields ### Service Layer (`src/Service/`) - `BookingService` - Core booking workflow - `BookingPriceCalculatorService` - Real-time pricing - `BookingFingerprintService` - Dirty state detection for edit mode - `TravelDataService` - API integration and caching - `ParticipantCardDataService` - Card display data - `InsuranceMatchingService` - Insurance eligibility and auto-reassignment - `RoomAssignmentService` - Automatic room assignment ## Critical Patterns ### Field Handler Pattern ```php // Handlers process in dependency order (topological sort) // Example: insurance handler depends on ALL price-affecting fields $this->fieldHandlerRegistry->processFieldsForParticipant( $participantData, $bookingDto, $index ); ``` **Important**: Field handlers use "sync pattern" - only sync fields present in original submission to avoid validation errors. ### HTMX Block-Based Rendering ```php // All HTMX swaps target #main-content with innerHTML // OOB swaps for sidebar: #booking-summary return $this->htmxOobResponse( 'booking/_participant_form.html.twig', ['participant_form', 'booking_summary'], $templateData ); ``` ### Service Enrichment Pattern Services from BPN API may lack complete data (especially prices). Always enrich from travel data: ```php // BookingDataProcessor::enrichParticipantServicesFromTravel() // Looks up each service in travel data and replaces with full version ``` ### Notification System Field handlers generate notifications (auto-changes) → collected by controller → sent via HX-Trigger → displayed as toasts. ### Dirty State Detection (Edit Mode) Fingerprint-based change detection to warn users about unsaved modifications: - **BookingFingerprintService** generates SHA-256 hash of all mutable booking data - Original fingerprint stored in `BookingDto::$originalFingerprint` on API load - `isDirty()` compares current state with original to detect changes - Yellow warning banner displays when changes detected - Update button conditionally shown only when dirty - Fingerprint persists in session, survives page refreshes - Resets on submission, "Änderungen verwerfen", or "Zurück" actions **Unavailable Services Handling:** - Services with `available <= 0` included in edit mode (not filtered out) - `ParticipantFieldOptionsProvider::shouldMakeServiceReadonly()` implements intelligent readonly logic: - **Create mode**: Uses standard availability calculator (filters out unavailable services) - **Edit mode**: Services participants already have remain editable even if now fully booked - **Edit mode**: Unavailable services participant doesn't have are marked readonly - Prevents fingerprint false positives when services become fully booked during editing session - Ensures service IDs remain consistent in form submissions for accurate dirty detection ## Key Service Dependencies ### Field Handler Execution Order Critical for correct pricing and auto-reassignment: 1. Age-dependent fields (dateOfBirth) 2. Price-affecting services (skiPass, rentals, courses, board, transportation, pickup, parking) 3. **Insurance handler LAST** (depends on all price-affecting fields) 4. Bulk insurance handler (applicant only) ### Insurance System - **3-pass parsing**: Referenced IDs → Individual insurances → Packages with family detection - **Auto-reassignment**: Maintains insurance type when price tier changes - **Age constraints**: Absolute age (at travel date) vs birth year - **Hydration**: `TravelDataService::hydrateInsurancePackageRelationships()` rebuilds package relationships after cache deserialization - **ID Type**: Insurance IDs are strings (not integers) - ensure all test fixtures use string IDs - **Mutability**: Insurances are always readonly in edit mode (API limitation) - see `InsuranceMutabilityCondition` ### Transportation Services - **Unified pickup field**: Single field for both directions (BPN API limitation) - **Conditional visibility**: Pickup vs parking fields mutually exclusive - **Direction mapping**: `DirectionMapper` translates API ↔ internal codes ## Important Field Dependencies ### Ski Pass → Rentals → Insurance → Body Dimensions - Rentals filtered by ski pass duration (exact date matching) - Rental insurance only shown when rentals selected - Body dimensions only shown when rentals selected - All cleared automatically when dependencies removed ### Date of Birth → Age-Dependent Services - Courses, additional services, board, insurance hidden until DOB provided - Age evaluated at travel start date, not current date - Dual constraint types: absolute_age, birth_year, mixed ### Bulk Insurance Booking - Applicant enables bulk → applies to all participants - `BulkInsuranceBookingCondition` hides dependent participant insurance fields - Uses `InsuranceMatchingService::batchAssignInsuranceToParticipants()` for price tier matching ## Data Flow ### Create Flow 1. Load/create DTO from session 2. Enrich with fresh API availability data 3. Auto-assign rooms, preselect mandatory services 4. Render cards → user edits participant → field handlers process → save to session 5. Validation check before step 3 6. Final submission to BPN API ### Edit Flow 1. Load booking from BPN API on first visit 2. Generate fingerprint of initial state for dirty detection 3. Store in session with `MODE_EDIT` 4. Apply mutability constraints via `EditFieldStateProvider` 5. Same card-based UI as create flow 6. Handle canceled participants (status 'S') 7. Display warning banner when unsaved changes detected 8. **Validation on submission**: Form wraps cards, validates all participants before API call 9. Submit changes back to BPN API (only if validation passes) 10. Staleness warnings after 5 minutes 11. **Session cleanup**: "Zurück" button clears session via `app_booking_edit_cancel` action ## Common Development Tasks ### Adding a New Field Handler 1. Create handler class extending `AbstractParticipantFieldHandler` 2. Implement `shouldProcess()`, `process()`, `getDependencies()` 3. Register in `services.yaml` with `participant.field_handler` tag 4. Add field options to `ParticipantFieldOptionsProvider` 5. Add conditional logic to `CreateFieldStateProvider` if needed 6. Update template with HTMX refresh triggers ### Adding a New Conditional Field 1. Create condition class implementing `FieldConditionInterface` 2. Register in `CreateFieldStateProvider::registerFieldStateConditions()` 3. Use composite conditions for complex logic (AND/OR/NOT) ### Debugging Field Handler Issues - Check execution order in `ParticipantFieldHandlerRegistry` (topological sort) - Verify `shouldProcess()` logic for mode awareness - Ensure dependencies declared correctly - Check sync pattern: only sync fields in original submission ### Writing Tests - **Insurance IDs**: Always use strings, not integers (e.g., `'100'` not `100`) - **Room properties**: Use `$label` property, not `$name` - **Participant names**: Index 0 expects "Anmelder:in", others expect "Teilnehmer:in N" (1-based) - **Mock dependencies**: Ensure all constructor dependencies have mocks (especially new ones like `InsuranceLoader`, `InsuranceTypeFilterService`) - **Insurance mutability**: In edit mode, insurances are always readonly (API limitation) ## File Locations ### Key Design Patterns **Service Availability in Edit Mode:** Edit mode requires special handling of unavailable services to prevent fingerprint false positives: ```php // ParticipantFieldOptionsProvider - intelligent availability filtering $bookingDto->travel->getAdditionalServicesBySubTypes( Constants::TOKEN_COURSES, BookingDto::MODE_CREATE === $bookingDto->getMode() // Only filter in create mode ) // shouldMakeServiceReadonly() - context-aware readonly logic // In edit mode: service readonly ONLY if unavailable AND participant doesn't have it private function shouldMakeServiceReadonly(Service $service, BookingDto $bookingDto, int $participantIndex, string $fieldName): bool { if (BookingDto::MODE_CREATE === $bookingDto->getMode()) { return $this->isServiceUnavailableForParticipant($service, $bookingDto, $participantIndex); } // Edit mode: allow keeping services participant already has if (null !== $service->available && $service->available > 0) { return false; } $participantHasService = match ($fieldName) { 'courses' => $this->hasServiceById($participant->courses, $service->id), 'additionalServices' => $this->hasServiceById($participant->additionalServices, $service->id), // ... other field types }; return !$participantHasService; // Readonly only if participant doesn't have it } ``` **Session Lifecycle Management:** Edit mode session requires proper cleanup to prevent dirty state persistence: - **Entry**: `IndexController::loadFormData()` initializes from API with original fingerprint - **Exit (save)**: `IndexController::index()` clears session on successful API update - **Exit (discard)**: `IndexController::reloadFromApi()` clears session and reloads from API - **Exit (cancel)**: `IndexController::cancelEdit()` clears session when user clicks "Zurück" - **Validation**: Dirty state persists across page refreshes until explicit action taken ### Controllers **Create Namespace** (`src/Controller/Booking/Create/`): - `IndexController.php` - Booking session initialization and error handling - `Step1Controller.php` - Room selection and dates - `Step2Controller.php` - Participant details with card-based UI - `Step3Controller.php` - Payment method selection - `Step4Controller.php` - Final confirmation and API submission - `SuccessController.php` - Success page after booking completion **Edit Namespace** (`src/Controller/Booking/Edit/`): - `IndexController.php` - Edit flow with card-based UI, validation, and session management - `index()` - Main edit view with dirty state detection - `editParticipant()` - Individual participant form editing - `refreshParticipantForm()` - HTMX refresh without validation - `reloadFromApi()` - Discard changes and reload from API - `cancelEdit()` - Clean session exit to bookings list **Root Booking Namespace** (`src/Controller/Booking/`): - `IndexController.php` - Bookings list - `DownloadController.php` - Booking document downloads **Shared Traits** (`src/Controller/Booking/Traits/`): - `ParticipantCardFlowTrait` - Card rendering, form creation, summary calculation - `ParticipantValidationTrait` - Validation error extraction for card indicators - `BookingCreateTrait` - Create flow helpers - `BookingDataTrait` - API data fetching - `BookingExceptionHandlerTrait` - Error handling ### Templates **Create Flow**: - `templates/booking/create/step_1.html.twig` - Room selection - `templates/booking/create/step_2.html.twig` - Participant cards - `templates/booking/create/step_3.html.twig` - Payment method - `templates/booking/create/step_4.html.twig` - Confirmation - `templates/booking/create/success.html.twig` - Success page - `templates/booking/create/error.html.twig` - Error page **Edit Flow**: - `templates/booking/edit/index.html.twig` - Edit with participant cards **Shared Components**: - `templates/booking/_participant_card.html.twig` - Individual participant card - `templates/booking/_participant_form.html.twig` - Participant edit form - `templates/booking/_summary.html.twig` - Pricing summary sidebar ### Services - `src/Service/BookingService.php` - Core workflow and session management - `src/Service/BookingFingerprintService.php` - Dirty state detection via SHA-256 fingerprinting - `src/Service/ParticipantCardDataService.php` - Card data generation - `src/Form/Service/ParticipantFieldHandlerRegistry.php` - Handler orchestration - `src/Form/Service/ParticipantFieldOptionsProvider.php` - Field configuration with mode-aware availability - `src/Form/Service/CreateFieldStateProvider.php` - Conditional field states - `src/Form/Service/EditFieldStateProvider.php` - Edit mode mutability constraints ### Field Handlers - `src/Form/Service/Participant*FieldHandler.php` (15+ handlers) - Transportation, pickup, parking, ski pass, rentals, insurance, bulk insurance, etc. ## Testing ```bash ./vendor/bin/phpunit # All tests (182 tests, 465 assertions) ./vendor/bin/phpunit tests/Service/ # Service layer ./vendor/bin/phpunit tests/BusProNet/ # API integration ./vendor/bin/phpunit tests/Form/ # Form processing and field handlers /opt/homebrew/bin/php-cs-fixer fix --rules=@Symfony # Code style (Symfony ruleset) ``` **Test Coverage Areas:** - BusProNet data loaders and processors - XML parsers (travels, hotels, bookings, insurances) - Form DTOs and field handlers - Service layer (pricing, insurance matching, room assignment) - Conditional field system - Utility classes ## Development Environment ```bash ddev start # Start DDEV ddev composer install # Install dependencies ddev exec bin/console cache:clear # Clear cache ddev logs # Read PHP error logs ddev exec "php -r 'opcache_reset()';" # Clear opcache after code changes ``` ## Important Notes - **Room prices are per person** - **Room model uses `$label` property** (not `$name`) - ensure test fixtures use correct property - **Zero prices display without suffix** (e.g., "Vollpension" not "Vollpension (€0,00)") - **All services sorted by price** (cheapest first) via `SortByPriceTrait` - **Field sync pattern critical**: Only sync fields in original submission to avoid "extra fields" errors - **Insurance handler requires mode awareness**: Skips processing in edit mode (API doesn't return insurance data) - **Insurance IDs are strings**: All insurance IDs must be strings, not integers (type safety) - **Clear opcache after code changes** affecting hydration or serialization - **HTMX targeting consistency**: All swaps target `#main-content` with `innerHTML`, sidebar via OOB swap - **Validation pattern**: Both create and edit flows use validation-only forms that wrap card UI for standard Symfony form handling - **Card error indicators**: `ParticipantValidationTrait::extractParticipantErrorIndices()` parses form errors to highlight invalid participant cards - **Edit mode service availability**: Services with `available <= 0` remain visible and editable for participants who already have them (prevents fingerprint false positives) - **Session cleanup on exit**: All exit paths from edit mode (save, discard, cancel) properly clear session to reset dirty state - **Participant naming convention**: Index 0 is "Anmelder:in", others are "Teilnehmer:in N" (1-based, not 0-based) ## References - **Project conventions**: `../CLAUDE.md` (root level) - **User preferences**: `~/.claude/CLAUDE.md` - **Documentation index**: `README.md` (this directory)