From b326510b04b8bc6666442cc80cc48bd03a359238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Fri, 24 Oct 2025 12:17:42 +0200 Subject: [PATCH] chore: consolidate documentation --- docs/PROJECT_OVERVIEW.md | 498 --------------------------------------- docs/README.md | 337 -------------------------- 2 files changed, 835 deletions(-) delete mode 100644 docs/PROJECT_OVERVIEW.md delete mode 100644 docs/README.md diff --git a/docs/PROJECT_OVERVIEW.md b/docs/PROJECT_OVERVIEW.md deleted file mode 100644 index 03accd2..0000000 --- a/docs/PROJECT_OVERVIEW.md +++ /dev/null @@ -1,498 +0,0 @@ -# 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 with comprehensive pricing breakdown and submission to BPN API (`Create\Step4Controller`) - - Displays complete pricing breakdown by category (rooms, services grouped by type) - - Shows per-participant total pricing in card headers - - Displays individual service prices inline with each booked service - - Three-level pricing transparency: aggregate, participant, and service-level -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 - - `ParticipantCardDataService` - Card data generation with optional validation state -- **Validation Pattern**: - - Both controllers use validation-only forms (`BookingCreateStep2Type`, `BookingEditType`) - - Standard Symfony form flow: `handleRequest()` → `isSubmitted()` → `isValid()` - - On invalid: Cards show red border with "unvollständige oder fehlerhafte Daten" badge - - Validation state determined via `ParticipantCardDataService::getAllCardsDataWithValidation()` - - 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`, `ParticipantEditDto` (session-stored) - - `ParticipantEditDto` wrapper enables cross-participant validation (e.g., email uniqueness) - - Wrapper contains participant being edited + full booking context for validation -- **Field Handlers**: 15+ specialized handlers in `src/Form/Service/` - - Registered via service tags with dependency resolution - - Process in dependency order via `ParticipantFieldHandlerRegistry` - - `ParticipantDateOfBirthFieldHandler` handles array input from BirthdayType widget -- **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 -- **Email Uniqueness Validation**: Adult participants (16+) must have unique email addresses - - Children (under 16) exempt from validation, can share emails with adults - - Implemented via `ParticipantEditDto::validateEmailUniqueness()` callback validator - - Case-insensitive comparison with whitespace normalization - - Validation groups: `booking_create`, `booking_edit` -- **Date of Birth Field**: Uses `BirthdayType` with three text inputs (day, month, year) - - Widget: `widget: 'text'` renders separate text fields instead of dropdowns - - Input format: `input: 'datetime_immutable'` for immutable date handling - - Custom theme: `birthday_widget` block in `templates/forms.html.twig` handles rendering - - German format: Day.Month.Year with hardcoded placeholders ("Tag", "Monat", "Jahr") - - Field order explicitly set to `['day', 'month', 'year']` for German date convention - - HTMX attributes applied to parent container for form refresh on change - -### Service Layer (`src/Service/`) -- `BookingService` - Core booking workflow -- `BookingPriceCalculatorService` - Comprehensive pricing calculations - - `getPricingBreakdown()` - Complete pricing breakdown with rooms and services grouped by type - - `calculateAllParticipantIndividualPrices()` - Per-participant total pricing - - `calculateIndividualParticipantPrice()` - Single participant total (room + all services) - - `calculateServicePricing()` - Service aggregation with grouping by subtype - - Powers sidebar summary and Step 4 confirmation pricing display -- `BookingFingerprintService` - Dirty state detection for edit mode -- `TravelDataService` - API integration and caching -- `ParticipantCardDataService` - Card display data with optional validation state - - `getAllCardsData()` - Basic card data without validation - - `getAllCardsDataWithValidation()` - Card data enriched with validation state for error display -- `InsuranceService` - Consolidated insurance operations (eligibility, type filtering, reassignment) with request-scoped caching -- `RoomAssignmentService` - Automatic room assignment with intelligent conflict resolution - -### Room Reassignment with Conflict Resolution -The room assignment system allows participants to freely select any room from Step 1 selections, with automatic conflict resolution when capacity is exceeded. - -**Key Features:** -- **Flexible Selection**: All rooms from Step 1 always appear in participant dropdown, regardless of current capacity -- **Auto-Assignment**: When exactly ONE room type selected, all participants auto-assigned via `RoomAssignmentService` -- **Field Display**: Single room type hides dropdown field entirely, displays room label as read-only text -- **Conflict Resolution**: When participant selects room at capacity, system automatically unassigns minimum participants needed -- **Unassignment Priority**: Highest index participants unassigned first (keeps applicant and early participants stable) -- **User Notifications**: Unassigned participants receive warning notifications via toast system - -**Implementation:** -- `ParticipantRoomChoiceLoader`: Always includes all selected rooms (no capacity filtering) -- `ParticipantAssignedRoomFieldHandler`: Detects conflicts and resolves via auto-unassignment - - `detectRoomCapacityConflict()`: Calculates if assignment would exceed capacity - - `resolveRoomCapacityConflict()`: Unassigns minimum participants (highest index first) - - Generates notifications for unassigned participants only (not for user-initiated assignment) - - Handles missing field gracefully when single room type (field excluded from form) -- `RoomAssignmentService`: Auto-assigns when single room type selected at DTO level -- `CreateFieldStateProvider`: Hides `assignedRoomId` field when `SingleRoomTypeCondition` is true -- `BookingDto::getSingleRoomLabel()`: Returns room label for template display when single room type -- Template: Conditionally renders dropdown (multiple rooms) or read-only label (single room) - -**Example Scenario:** -- User selects 1x "Doppelzimmer" (capacity 2), 3 participants -- Participants 0 and 1 auto-assigned to "Doppelzimmer" -- Participant 2 manually selects "Doppelzimmer" (at capacity) -- System automatically unassigns Participant 1 (highest index) -- Participant 2 gets assigned, Participant 1 receives warning notification - -## 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. - -**Implementation:** -- `ParticipantDto::$notifications` - Array of notification messages (keyed by MD5 hash for deduplication) -- `ParticipantDto::addNotification($type, $message, $id = null)` - Add notification with auto-generated or custom ID -- Field handlers call `addNotification()` when making automatic changes (e.g., insurance reassignment, rental clearing) -- Controllers collect notifications via `collectParticipantNotifications()` after field processing -- Notifications sent to frontend via `HX-Trigger` response header with `showNotifications` event -- Frontend `toast_controller.js` displays notifications using toastify-js library -- Notification types: `info`, `warning`, `success` (mapped to Tailwind color classes) - -### Floating-Point Precision in Price Calculations -**Critical**: All monetary comparisons must account for floating-point arithmetic accumulation errors. - -**The Problem:** -- Prices are parsed from XML as German-formatted strings (e.g., `"1.812,70"`) -- Multiple price additions (rooms + services + insurances) accumulate tiny precision errors (`~1e-15` per operation) -- With bulk insurance and deep calculation chains, errors compound to `~1e-13` or larger -- Example: API returns `1812.7`, calculation produces `1812.6999999999998` - -**The Solution:** -- **Always round monetary values to 2 decimal places before comparison** -- Use `round($price, 2)` for cent precision (standard for EUR currency) -- Never use strict equality (`===` or `!==`) on unrounded float prices - -**Implementation Example (Step3Controller:93-94):** -```php -// CORRECT: Round to cent precision before comparison -$apiTotal = round($inquiryResponse->totalPrice ?? 0.0, 2); -$calculatedTotal = round($this->priceCalculator->calculateGrandTotal($bookingCreateDto), 2); - -if ($apiTotal !== $calculatedTotal) { - // Handle mismatch -} - -// WRONG: Direct float comparison (will fail due to precision errors) -if ($inquiryResponse->totalPrice !== $this->priceCalculator->calculateGrandTotal($bookingCreateDto)) { - // This comparison is unreliable! -} -``` - -**Important Notes:** -- Rounding eliminates precision errors smaller than 1 cent (€0.01) -- The BPN API returns prices already rounded to cent precision -- This is the industry-standard approach for financial calculations -- Only affects comparison logic - does not alter actual price calculation flow - -### 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 -- **Discount replacement logic**: Automatic correction of incompatible transportation combinations - - When inbound bus selected with discounted PKW outbound: system automatically replaces with regular PKW - - When inbound changes from bus to PKW: system automatically restores discounted PKW if available - - Form options update dynamically to show only valid PKW variant - - User notified via toast messages about automatic changes - - Implemented via `ParticipantTransportationDiscountReplacementFieldHandler` - - Prevents invalid pricing where user gets both PKW discount and bus pricing - -## 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 -- **Array input handling**: `ParticipantDateOfBirthFieldHandler` processes array `['day' => X, 'month' => Y, 'year' => Z]` from three-field widget -- **Date normalization**: Handler converts array to `DateTimeImmutable` using `sprintf()` with proper formatting - -### Bulk Insurance Booking -- Applicant enables bulk → applies to all participants -- `BulkInsuranceBookingCondition` hides dependent participant insurance fields -- Uses `InsuranceService::batchAssignInsuranceToParticipants()` for price tier matching - -### Room Assignment Field -- **Single room type**: Field completely hidden, room label displayed as read-only text -- **Multiple room types**: Dropdown shown with all selected rooms from Step 1 -- `SingleRoomTypeCondition` controls field visibility in `CreateFieldStateProvider` -- Auto-assignment by `RoomAssignmentService` happens at DTO level, independent of form field -- Field handler gracefully handles missing field when excluded from form - -## 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 - -### Adding Cross-Participant Validation -1. Add validation method to `ParticipantEditDto` (wrapper DTO) -2. Use `#[Assert\Callback]` attribute with appropriate validation groups -3. Access booking context via `$this->bookingContext` for cross-participant checks -4. Access current participant via `$this->participant` -5. Validation errors automatically displayed on participant cards via red border and badge - -### 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. Note: `InsuranceService` is stateless with no dependencies (no mocking required) -- **Insurance mutability**: In edit mode, insurances are always readonly (API limitation) -- **Notification arrays**: Use associative keys (MD5 hashes), not numeric indices - use `reset($notifications)` to get first notification - -## 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 with comprehensive pricing display and API submission - - Integrates `BookingPriceCalculatorService` for complete pricing breakdown - - Calculates per-participant individual prices via `calculateAllParticipantIndividualPrices()` - - Template displays three-level pricing: aggregate breakdown, per-participant totals, and inline service prices - - All service prices shown in blue (text-blue-700) with German formatting - - Zero prices hidden per project standards -- `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 - - Creates `ParticipantEditDto` wrapper for email uniqueness validation - - Wraps participant form type with booking context for cross-participant validation -- `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` - Final confirmation with comprehensive pricing display - - **Aggregate pricing breakdown**: Rooms and services grouped by type with totals - - **Per-participant cards**: Individual total price in header, all personal details and service selections - - **Inline service pricing**: Each service shows price in blue (€X,XX format) next to label - - **Payment summary**: Selected payment method and bank details (if debit) - - **Confirmation checkbox**: Final acceptance before API submission -- `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 - - Displays validation state with red border and "unvollständige oder fehlerhafte Daten" badge - - Requires `isValid` and `errorMessages` in cardData for validation display -- `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` (16+ handlers) -- Transportation (outbound, inbound, discount replacement), pickup, parking, ski pass, rentals, insurance, bulk insurance, etc. -- `ParticipantTransportationDiscountReplacementFieldHandler` - Automatic discount replacement for invalid transportation combinations - -## Testing - -```bash -./vendor/bin/phpunit # All tests (234 tests, 588 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 (including `ParticipantEditDto` wrapper) -- Service layer (pricing, insurance matching, room assignment, card data generation) -- Conditional field system -- Cross-participant validation (email uniqueness) -- 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 validation display**: Uses `ParticipantCardDataService::getAllCardsDataWithValidation()` to enrich cards with validation state -- **Cross-participant validation**: Implemented via `ParticipantEditDto` wrapper with callback validators -- **Email uniqueness**: Adults (16+) require unique emails; children exempt from validation -- **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) -- **Notification array structure**: Uses associative keys (MD5 hashes) for deduplication, not numeric indices -- **Floating-point price comparisons**: ALWAYS use `round($price, 2)` before comparing monetary values to avoid precision errors (see "Floating-Point Precision in Price Calculations" section) - -## References - -- **Project conventions**: `../CLAUDE.md` (root level) -- **User preferences**: `~/.claude/CLAUDE.md` -- **Documentation index**: `README.md` (this directory) diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 719c603..0000000 --- a/docs/README.md +++ /dev/null @@ -1,337 +0,0 @@ -# MyEP Next Booking System - -A sophisticated Symfony 6.4 travel booking application that integrates with Bus Pro Net (BPN) XML API for comprehensive travel management. The system handles complex multi-step booking workflows with dynamic participant forms, conditional field logic, and real-time pricing display. - -## 🚀 Quick Start - -### Prerequisites -- PHP 8.1+ -- Composer -- Node.js & npm -- DDEV (recommended for local development) - -### Installation - -```bash -# Clone the repository -git clone [repository-url] -cd myep-next-booking - -# Install dependencies -composer install -npm install - -# Start local development environment -ddev start - -# Run database migrations -bin/console doctrine:migrations:migrate - -# Generate OAuth2 keys -bin/console app:generate-keys - -# Compile assets -npm run dev -``` - -## 🏗️ Architecture Overview - -### Multi-Step Booking Flow -1. **Step 1**: Room selection with quantities and dates -2. **Step 2**: Participant details with conditional fields and service selection -3. **Step 3**: Final confirmation and submission to BPN API - -### Core Components - -#### BusProNet Integration (`src/BusProNet/`) -- **ApiClient**: XML API communication layer -- **XmlParser/**: Response parsing for travels, hotels, bookings -- **XmlLoader/**: Data loading with caching -- **DataProcessor/**: API data transformation - -#### Advanced Form System (`src/Form/Service/`) -- **Conditional Field States**: Dynamic field behavior based on participant data -- **Service Field Handlers**: Modular field processing with dependency resolution -- **Transportation Services**: Comprehensive pickup, parking, and transportation options -- **Real-time Updates**: HTMX integration for seamless UX - -#### Service Architecture (`src/Service/`) -- **BookingService**: Core booking workflow management -- **TravelDataService**: Travel data access and caching -- **BookingPriceCalculatorService**: Real-time pricing calculations - -## 🎯 Key Features - -### ✅ Implemented Features - -#### Multi-Step Booking Workflow -- Room selection with dynamic pricing display -- Participant registration with conditional fields -- Service selection (board, ski passes, courses, rentals) -- Real-time booking summary with pricing - -#### Transportation Services -- **Transportation Selection**: Bus vs. car transport options -- **Pickup Services**: Location-based pickup with conditional visibility -- **Parking Services**: Self-organized transport parking options -- **Direction Mapping**: Outbound/inbound transportation handling - -#### Advanced Form System -- **Conditional Field States**: Age-based, value-dependent field visibility -- **Dynamic Field Options**: Context-aware choice generation -- **HTMX Integration**: Real-time form updates without page refresh -- **XSS Protection**: Built-in security measures - -#### Pricing & Display -- **Inline Pricing**: Service costs displayed in form options -- **Real-time Calculations**: Live pricing updates via HTMX -- **Smart Formatting**: Zero-price services handled gracefully -- **Unified Summary**: Integrated booking and pricing display - -#### Service Integration -- **Field Handler Registry**: Modular field processing system -- **Service Registration**: Automatic field handler discovery -- **Dependency Resolution**: Smart field interdependency handling - -### 🔄 Ongoing Development -- Age-based field constraints -- Enhanced pricing features (discounts, taxes) -- Advanced booking management -- Extended BPN API integration - -## 🛠️ Development - -### Commands - -```bash -# Development workflow -npm run dev # Development build -npm run watch # Watch mode -npm run build # Production build - -# Testing -bin/phpunit # All tests -./vendor/bin/phpunit tests/Service/ # Specific directory -./vendor/bin/phpunit tests/BusProNet/ # API integration tests - -# Code quality -./vendor/bin/php-cs-fixer fix # Fix code style - -# Cache & debugging -bin/console cache:clear # Clear cache -bin/console debug:router # Debug routes -bin/console debug:container # Debug services - -# Custom commands -bin/console app:cleanup-xml-dumps # Clean XML dump files -bin/console app:generate-keys # Generate OAuth2 keys -``` - -### Development Standards - -#### Code Style -- PSR-12 compliance with `declare(strict_types=1)` -- PHP 8+ features (typed properties, constructor promotion, match expressions) -- Explicit comparisons and Yoda conditions -- Immutable DateTime objects (DateTimeImmutable, CarbonImmutable) - -#### Architecture Patterns -- Service layer for business logic -- DTO pattern for type-safe form data -- Registry pattern for configurable components -- Field handler pattern for complex form processing -- Trait-based code reuse - -## 🏛️ Technical Stack - -### Backend -- **Framework**: Symfony 6.4 LTS -- **PHP**: 8.1+ -- **Database**: MariaDB with Doctrine ORM -- **API Integration**: Custom XML client for BPN API -- **Authentication**: OAuth2 Server Bundle - -### Frontend -- **JavaScript**: Stimulus (Hotwired) controllers -- **Dynamic Updates**: HTMX for seamless interactions -- **Styling**: TailwindCSS -- **Build Tool**: Webpack Encore -- **Templating**: Twig - -### Development & Operations -- **Local Environment**: DDEV (PHP 8.2, MariaDB 10.11) -- **File Operations**: Flysystem with SFTP support -- **Date Handling**: Carbon for advanced date/time manipulation -- **Logging**: Monolog with multiple channels -- **Testing**: PHPUnit with Symfony bridge - -## 📋 Form System Architecture - -### Field Handler System -The application uses a sophisticated field handler system for processing complex participant forms: - -```php -// Example field handler -class ParticipantTransportationOutboundFieldHandler extends AbstractParticipantFieldHandler -{ - public function processField(/* ... */): void - { - // Complex field processing with conditional logic - } - - public function getDependencies(): array - { - return ['assignedRoomId', 'dateOfBirth']; // Field dependencies - } -} -``` - -### Conditional Field States -Fields can have dynamic states based on conditions: - -```php -// Age-based field state -$this->fieldStateConditions['advancedServices'] = [ - 'hidden' => new AgeRangeCondition(null, 15), // Hide for under 15 - 'required' => FieldValueCondition::equals('roomType', 'suite'), -]; -``` - -### Available Field Handlers -- **Transportation**: Outbound/inbound transport selection -- **Pickup Services**: Location-based pickup options -- **Parking**: Self-organized transport parking -- **Accommodation**: Board, room assignment -- **Activities**: Ski passes, courses, rentals -- **Personal Data**: Age-aware field processing - -## 💰 Pricing System - -### Real-time Pricing Display -- **Inline Pricing**: Costs shown in form options -- **Live Updates**: HTMX-powered real-time calculations -- **Smart Formatting**: Zero-price services handled elegantly -- **Unified Summary**: Integrated pricing in booking summary - -### Service Integration -```php -// Pricing calculation example -$serviceTotal = $this->bookingService->calculateServiceTotal($bookingDto); -$roomTotal = $this->bookingService->calculateRoomTotal($bookingDto); -$grandTotal = $serviceTotal + $roomTotal; -``` - -## 🔄 BusProNet API Integration - -### XML Communication -- **Request Building**: Dynamic XML generation for BPN API -- **Response Parsing**: Structured XML parsing with validation -- **Data Caching**: Intelligent caching for performance -- **Error Handling**: Comprehensive error management - -### Data Flow -1. Form submission triggers API request building -2. XML sent to BPN API endpoints -3. Response parsed and validated -4. Data transformed for application use -5. Results cached for performance - -## 🧪 Testing Strategy - -### Test Coverage -- **Unit Tests**: Service layer and business logic -- **Integration Tests**: API communication and data processing -- **Form Tests**: Field handler and validation logic -- **XML Tests**: API response parsing with fixtures - -### Running Tests -```bash -# All tests -./vendor/bin/phpunit - -# Specific test suites -./vendor/bin/phpunit tests/BusProNet/ # API integration -./vendor/bin/phpunit tests/Service/ # Service layer -./vendor/bin/phpunit tests/Form/ # Form processing -``` - -## 📚 Documentation - -### Available Documentation -- **[PROJECT_OVERVIEW.md](PROJECT_OVERVIEW.md)**: Comprehensive architecture and implementation guide -- **[CLAUDE.md](../CLAUDE.md)**: Development guidelines for AI assistance (root level) - -### Architecture Documentation -Each major system component has detailed documentation covering: -- Implementation patterns -- Usage examples -- Extension guidelines -- Testing strategies - -## 🔒 Security & Configuration - -### Environment Setup -- BPN API credentials in `.env.local` -- OAuth2 encryption keys via custom command -- SFTP configuration for deployment -- Logging channels for monitoring - -### Security Features -- XSS protection in form processing -- OAuth2 authentication -- Secure API communication -- Input validation and sanitization - -## 🚀 Deployment - -### Production Requirements -- PHP 8.1+ with required extensions -- MariaDB 10.11+ -- Web server (Apache/Nginx) -- SFTP access for file operations -- BPN API credentials - -### Deployment Steps -1. Install dependencies (`composer install --no-dev`) -2. Generate OAuth2 keys (`bin/console app:generate-keys`) -3. Run database migrations (`bin/console doctrine:migrations:migrate`) -4. Build production assets (`npm run build`) -5. Configure environment variables -6. Set up SFTP access for XML exports - -## 🤝 Contributing - -### Development Workflow -1. Follow PSR-12 coding standards -2. Use type declarations consistently -3. Write comprehensive tests for new features -4. Update documentation for architectural changes -5. Use the field handler pattern for form extensions - -### Key Patterns -- **Service Layer**: Business logic separation -- **DTO Pattern**: Type-safe data transfer -- **Registry Pattern**: Component discovery -- **Field Handlers**: Modular form processing - -## 📞 Support - -### Logging Channels -- **app**: General application logs -- **bpn**: BusProNet API interactions -- **security**: Authentication/authorization -- **db**: Database-related logs - -### Debugging -- Use `bin/console debug:router` for route inspection -- Use `bin/console debug:container` for service inspection -- Check logs in `var/log/` for troubleshooting -- Use DDEV for consistent development environment - ---- - -**Version**: 2.0 -**Symfony**: 6.4 LTS -**PHP**: 8.1+ -**Status**: Production Ready -**Last Updated**: 2025-01-XX \ No newline at end of file