diff --git a/docs/ADDRESS_FIELDS_IMPLEMENTATION.md b/docs/ADDRESS_FIELDS_IMPLEMENTATION.md deleted file mode 100644 index b391441..0000000 --- a/docs/ADDRESS_FIELDS_IMPLEMENTATION.md +++ /dev/null @@ -1,190 +0,0 @@ -# Address Fields Implementation - -**Date:** 2025-10-06 -**Status:** ✅ TESTED SUCCESSFULLY (2025-10-06) - -## Overview - -Added participant address collection to the booking create flow. Address is mandatory for the applicant (first participant) and optional for others. - -## Implementation - -### 1. Models - -**ParticipantDto** (`src/Form/Model/ParticipantDto.php`): -- Added `Address $address` property -- Made email field required (`@Assert\NotBlank`) -- Added constructor to initialize `Address` object -- Removed `@Assert\NotNull` constraint (validation handled by ParticipantValidator) - -**Address** (`src/BusProNet/Model/Address.php`): -- Already had `toPayload()` method for XML generation -- Properties: street, postCode, city, district, country -- No changes needed (existing model worked perfectly) - -### 2. Forms - -**AddressType** (`src/Form/AddressType.php`) - NEW: -```php -class AddressType extends AbstractType -{ - public function buildForm(FormBuilderInterface $builder, array $options): void - { - $builder - ->add('street', TextType::class, [...]) - ->add('postCode', TextType::class, [...]) - ->add('city', TextType::class, [...]) - ->add('country', CountryType::class, [ - 'property' => 'country', - 'preferred_choices' => ['DE', 'AT', 'CH'], - ]); - } -} -``` - -**BookingParticipantType** (`src/Form/BookingParticipantType.php`): -- Added address field after mobile field -- `required => 0 === $participantIndex` (mandatory for applicant) -- Added 'address' to `$baseFields` array for rebuild handling - -### 3. Validation - -**ParticipantValidator** (`src/Validator/Constraints/ParticipantValidator.php`): -- Added `assertApplicantAddressValid()` method -- Validates street, postCode, city, country for applicant only -- Uses `$participant->isApplicant()` to check if validation should run -- Error messages: "Bitte angeben" for each missing field - -### 4. Payload Generation - -**BookingDataProcessor** (`src/BusProNet/DataProcessor/BookingDataProcessor.php`): - -**Applicant section** (lines 451-454): -```php -// Add address for applicant -if (null !== $firstParticipant->address) { - $payload['anmelder']['anschrift'] = $firstParticipant->address->toPayload(); -} -``` - -**Participant list** (lines 481-488): -```php -// Add address (always include structure, even if empty) -$participantData['anschrift'] = $participant->address?->toPayload() ?? [ - 'strasse' => null, - 'plz' => null, - 'ort' => null, - 'ortsteil' => null, - 'land' => null, -]; -``` - -**XML Output:** -```xml - - - Gablonzer Straße 32 - 53359 - Rheinbach - - D - - - - - - - Gablonzer Straße 32 - 53359 - Rheinbach - - D - - - -``` - -## Bug Fixes (Same Session) - -### 1. Room Quantity Fix -**Issue:** Room `@anzahl` was set to participant count instead of room quantity -**Fix:** Use `roomSelections[].quantity` from step 1 (lines 576-603) -```php -$roomQuantities = []; -foreach ($bookingDto->roomSelections as $selection) { - if ($selection->quantity > 0) { - $roomQuantities[$selection->roomId] = $selection->quantity; - } -} -$quantity = $roomQuantities[$roomId] ?? 1; -``` - -### 2. Insurance Selection for Dependent Participants -**Issue:** `ParticipantBulkInsuranceFieldHandler` cleared independent selections on every form submit -**Fix:** Added `wasBulkInsurancePreviouslyEnabled()` check (lines 90, 144-176) -```php -if (false === $isBulkEnabled && $this->wasBulkInsurancePreviouslyEnabled($bookingDto)) { - $this->clearDependentParticipantsInsurance($bookingDto); -} -``` - -## Files Modified - -1. `src/Form/Model/ParticipantDto.php` - Added address property, constructor, required email -2. `src/Form/AddressType.php` - NEW form type -3. `src/Form/BookingParticipantType.php` - Added address field -4. `src/Validator/Constraints/ParticipantValidator.php` - Added address validation -5. `src/BusProNet/DataProcessor/BookingDataProcessor.php` - Address in payload, room quantity fix, `isApplicant()` usage -6. `src/Form/Service/ParticipantBulkInsuranceFieldHandler.php` - Insurance clearing fix - -## Testing - -### Manual Testing Checklist: -- [x] Applicant address required (all fields) - ✅ PASSED -- [x] Dependent participant address optional - ✅ PASSED -- [x] Country dropdown shows DE, AT, CH first - ✅ PASSED -- [x] Address appears in XML for applicant - ✅ PASSED -- [x] Address appears in XML for all participants (empty if not provided) - ✅ PASSED -- [x] Email is mandatory for all participants - ✅ PASSED -- [x] Insurance selection works for dependent participants - ✅ PASSED -- [x] Room quantity matches selection from step 1 - ✅ PASSED - -### Expected XML: -- Applicant: Full address in `` -- All participants: Address structure in `` (may be empty) -- Room quantity: Matches quantity selected in step 1, not participant count - -## Notes - -- Address object initialized in ParticipantDto constructor -- Uses existing `Address::toPayload()` method for XML generation -- CountryType provides nationality dropdown with German country codes -- Validation runs via existing ParticipantValidator constraint -- Template integration happens automatically via Symfony form system - ---- - -**Implementation Status:** ✅ TESTED SUCCESSFULLY -**Code Quality:** ✅ PHP-CS-Fixer validated -**Test Date:** 2025-10-06 - -## Test Results - -**Test Date:** 2025-10-06 -**Environment:** DDEV sandbox with BusProNet API - -**Validated Features:** -- ✅ Applicant address validation (all fields mandatory) -- ✅ Dependent participant address optional -- ✅ Country dropdown with preferred choices (DE, AT, CH) -- ✅ Address correctly included in XML payload for applicant -- ✅ Address structure included for all participants (empty nodes for optional) -- ✅ Email mandatory validation working for all participants -- ✅ Mobile mandatory validation working for applicant only - -**Validated Bug Fixes:** -- ✅ Room quantity using correct value from step 1 selections -- ✅ Insurance selection working correctly for dependent participants -- ✅ Pickup locations included with correct quantities - -**Result:** All address field requirements working correctly in production-like environment. diff --git a/docs/AGE_BASED_FIELDS_PLAN.md b/docs/AGE_BASED_FIELDS_PLAN.md deleted file mode 100644 index 88be080..0000000 --- a/docs/AGE_BASED_FIELDS_PLAN.md +++ /dev/null @@ -1,525 +0,0 @@ -# Age-Based Field Visibility with Enhanced UX Implementation Plan - -## Overview -Extend the existing conditional field system to hide fields when date of birth is not provided and filter options based on age groups, with comprehensive UX improvements to guide users through the progressive disclosure process. - -## Architecture Analysis -**Existing Infrastructure to Leverage:** -- `AgeRangeCondition` already handles age calculations and returns `false` when `dateOfBirth` is null -- Field state providers can hide fields using `'hidden'` state conditions -- `CompositeCondition` allows complex condition combinations -- Field options providers support dynamic choice filtering -- Form events already trigger rebuilds when form data changes - -## Implementation Strategy: Hybrid Approach with Enhanced UX - -### 1. Create New Condition Classes - -**A. DateOfBirthProvidedCondition** -- Simple condition that checks if participant has `dateOfBirth` set -- Returns `true` only when date of birth exists -- Used to show/hide entire fields - -```php -class DateOfBirthProvidedCondition implements FieldConditionInterface -{ - public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool - { - $participant = $bookingDto->getParticipant($participantIndex); - return null !== $participant && null !== $participant->dateOfBirth; - } - - public function getDependentFields(): array - { - return ['dateOfBirth']; - } -} -``` - -**B. Extend AgeRangeCondition Usage** -- Leverage existing age calculation logic -- Create specific age range instances for different field requirements - -### 2. Extend CreateFieldStateProvider - -**Add Field Visibility Rules:** -```php -protected function registerFieldStateConditions(): void -{ - // Existing rental condition logic... - - // Hide age-restricted fields when no date of birth provided - $this->fieldStateConditions['courses'] = [ - 'hidden' => CompositeCondition::not(new DateOfBirthProvidedCondition()), - ]; - - $this->fieldStateConditions['additionalServices'] = [ - 'hidden' => CompositeCondition::not(new DateOfBirthProvidedCondition()), - ]; - - // Hide fields for specific age groups (example: alcohol services) - $this->fieldStateConditions['alcoholicBeverages'] = [ - 'hidden' => CompositeCondition::or( - CompositeCondition::not(new DateOfBirthProvidedCondition()), - CompositeCondition::not(new AgeRangeCondition(18, null)) - ), - ]; - - // Youth-specific fields (example: under 16 only) - $this->fieldStateConditions['youthActivities'] = [ - 'hidden' => CompositeCondition::or( - CompositeCondition::not(new DateOfBirthProvidedCondition()), - CompositeCondition::not(new AgeRangeCondition(null, 15)) - ), - ]; -} -``` - -### 3. Enhance ParticipantFieldOptionsProvider - -**Add Age-Aware Filtering:** -```php -protected function registerFieldOptionProviders(): void -{ - // Existing providers... - - // Enhanced courses provider with age filtering - $this->fieldOptionProviders['courses'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ - 'label' => 'Kurse', - 'multiple' => true, - 'expanded' => true, - 'required' => false, - 'choices' => $this->filterChoicesByAge( - $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES), - $bookingDto, - $participantIndex - ), - 'choice_label' => 'label', - ]; -} - -private function filterChoicesByAge(array $services, BookingDtoInterface $bookingDto, int $participantIndex): array -{ - $participant = $bookingDto->getParticipant($participantIndex); - - if (null === $participant || null === $participant->dateOfBirth) { - return []; // No services available without age - } - - $age = $this->calculateAge($participant->dateOfBirth); - - return array_filter($services, function ($service) use ($age) { - return $this->isServiceAvailableForAge($service, $age); - }); -} - -private function isServiceAvailableForAge($service, int $age): bool -{ - // Check service age restrictions (could be metadata on Service model) - if (isset($service->minAge) && $age < $service->minAge) { - return false; - } - - if (isset($service->maxAge) && $age > $service->maxAge) { - return false; - } - - return true; -} -``` - -### 4. UX Enhancement System - -**A. Age Requirement Hints Provider** -```php -class ParticipantFieldHintsProvider -{ - public function getFieldHints(BookingDtoInterface $bookingDto, int $participantIndex): array - { - $hints = []; - $participant = $bookingDto->getParticipant($participantIndex); - - if (null === $participant || null === $participant->dateOfBirth) { - $hints[] = [ - 'type' => 'info', - 'message' => 'Geben Sie Ihr Geburtsdatum an, um alle verfügbaren Optionen zu sehen.', - 'icon' => 'birthday-cake', - 'fields_affected' => $this->getAgeRestrictedFields(), - 'action' => 'focus_date_field' - ]; - } else { - $age = $this->calculateAge($participant->dateOfBirth); - $restrictedFields = $this->getRestrictedFieldsForAge($age); - - if (!empty($restrictedFields)) { - $hints[] = [ - 'type' => 'warning', - 'message' => sprintf('Einige Optionen sind für Ihr Alter (%d Jahre) nicht verfügbar.', $age), - 'icon' => 'info-circle', - 'fields_affected' => $restrictedFields - ]; - } - } - - return $hints; - } -} -``` - -**B. Field Placeholder System** -```php -// Template helper for placeholder cards -public function renderFieldPlaceholder(string $fieldName, array $hint): string -{ - return sprintf(' -
-
- -

%s verfügbar nach Altersangabe

-

%s

- -
-
- ', $fieldName, $hint['icon'], ucfirst($fieldName), $hint['message']); -} -``` - -### 5. Smart Form Guidance - -**A. Enhanced Date of Birth Field** -```php -// Add to BookingCreateParticipantType -->add('dateOfBirth', BirthdayType::class, [ - 'label' => 'Geburtsdatum', - 'html5' => true, - 'widget' => 'single_text', - 'input' => 'datetime_immutable', - 'attr' => [ - 'class' => 'age-dependent-trigger', - 'data-controller' => 'age-dependent-fields', - 'data-age-dependent-fields-participant-index-value' => $participantIndex, - 'placeholder' => 'TT.MM.JJJJ - Für altersgerechte Optionen erforderlich' - ], - 'help' => 'Benötigt für die Anzeige aller verfügbaren Kurse und Services' -]) -``` - -**B. Field Counter Display** -```javascript -// Stimulus controller: age-dependent-fields -connect() { - this.updateFieldCounter(); - this.showPlaceholders(); -} - -updateFieldCounter() { - const hiddenFieldCount = this.getHiddenFieldCount(); - if (hiddenFieldCount > 0) { - this.showFieldCounter(hiddenFieldCount); - } -} - -showFieldCounter(count) { - const counter = this.element.querySelector('.field-counter'); - if (counter) { - counter.textContent = `👤 ${count} weitere Optionen verfügbar nach Altersangabe`; - counter.classList.add('visible'); - } -} -``` - -### 6. HTMX Integration Enhancements - -**A. Smooth Transitions** -```html - -
-
- -
-
-``` - -```javascript -// Stimulus controller: field-transitions -showField(fieldElement) { - fieldElement.style.opacity = '0'; - fieldElement.style.transform = 'translateY(-10px)'; - - // Animate in - fieldElement.addEventListener('transitionend', () => { - fieldElement.classList.add('field-visible'); - }, { once: true }); - - requestAnimationFrame(() => { - fieldElement.style.opacity = '1'; - fieldElement.style.transform = 'translateY(0)'; - }); -} -``` - -### 7. Update hasValidFieldOptions Method - -**Enhanced Validation Logic:** -```php -private function hasValidFieldOptions(array $fieldOptions): bool -{ - // Existing validation... - - // Check if choices array exists and is not empty - if (false === empty($fieldOptions['choices'])) { - return true; - } - - // Check if choice_loader exists and is not null - if (isset($fieldOptions['choice_loader'])) { - return true; - } - - // NEW: Check if field has age restrictions that prevent display - if (isset($fieldOptions['age_restricted']) && true === $fieldOptions['age_restricted']) { - return false; // Field should not be added if age-restricted - } - - // Check for other valid choice sources - if (isset($fieldOptions['choice_list'])) { - return true; - } - - return false; -} -``` - -### 8. Template Enhancements - -**A. Add UX Components to Form Template** -```twig -{# templates/booking/create/step2_participant.html.twig #} - -
- -
-

Persönliche Daten

- - -
- {{ form_row(form.dateOfBirth) }} -
-
- - - {{ form_row(form.firstName) }} - {{ form_row(form.lastName) }} - -
- - -
- - -
- -
- - -
-
- -

Kurse

-

Verfügbar nach Angabe des Geburtsdatums

- -
-
- - -
- -
-
-
-``` - -## Implementation Status - -### ✅ COMPLETED PHASE 1: XML Parsing & Age Constraints -1. **✅ Extended Service Model** - Added age constraint properties (`ageFrom`, `ageTo`, `birthYearFrom`, `birthYearTo`, `ageConstraintType`) -2. **✅ Created AgeConstraintResult DTO** - Structured data for parsed age constraints -3. **✅ Created AgeConstraintParserInterface** - Extensible parser contract for future constraint types -4. **✅ Implemented BirthYearConstraintParser** - Parses `JG:YYYY-YYYY` format constraints from XML -5. **✅ Created AgeConstraintParserRegistry** - Manages multiple parsers, handles semicolon-separated constraints -6. **✅ Enhanced TravelParser** - Integrated age constraint parsing for both absolute age and extensible formats -7. **✅ Successfully tested XML parsing** - Confirmed correct extraction of age constraints from real BPN data - -### ✅ COMPLETED PHASE 2: Form Integration & Age-Based Filtering -8. **✅ Created DateOfBirthProvidedCondition** - Checks if participant has provided birth date -9. **✅ Created ServiceAgeEvaluator** - Evaluates service availability for participant age -10. **✅ Enhanced CreateFieldStateProvider** - Added field visibility rules to hide age-dependent fields -11. **✅ Enhanced ParticipantFieldOptionsProvider** - Integrated age-aware service filtering -12. **✅ Enhanced hasValidFieldOptions method** - Handles empty choices from age filtering - -### ✅ RESOLVED: Age Filtering Implementation Complete -**Resolution:** Age-based service filtering is now working correctly - services with age constraints are properly filtered based on participant birth date. - -### ✅ COMPLETED PHASE 3: Form Field Management & Room-Based Fields -13. **✅ Age filtering system** - Age-based service filtering working correctly -14. **✅ Service configuration** - ServiceAgeEvaluator instantiated directly (no DI needed) -15. **✅ Skipass field implementation** - Added single-selection skipass field with age/date validation -16. **✅ Service selection bug fixes** - Fixed form submission issues with service ID/object handling -17. **✅ Room-based conditional field system** - Added remarksRoom field for specific room types -18. **✅ Field visibility improvements** - Fields no longer just hidden but excluded from form entirely -19. **✅ Dynamic field type system** - Simplified field configuration with type mapping -20. **✅ Room code normalization** - Case-insensitive room code matching (mbz/MBZ/Mbz) - -### 📋 REMAINING IMPLEMENTATION STEPS -21. **⏳ Field placeholder system** - Add explanatory content for missing age -22. **⏳ Enhanced date of birth field** - Smart guidance styling -23. **⏳ HTMX transitions** - Smooth field additions -24. **⏳ Stimulus controllers** - (`age-dependent-fields`, `field-transitions`, `participant-hints`) -25. **⏳ Template enhancements** - UX improvements and placeholder system -26. **⏳ Service model validation** - Ensure all constraint types properly handled -27. **⏳ Comprehensive testing** - UX flows and edge cases - -### ✅ NEW FEATURES ADDED - -#### Skipass Field System -- **Field Type**: Single-selection radio buttons (mandatory when available) -- **Age Constraints**: Automatic filtering via existing ServiceAgeEvaluator -- **Date Validation**: Travel date range validation for service availability -- **Form Integration**: Proper ID/object handling with choice_value configuration -- **Files Modified**: - - `ParticipantFieldOptionsProvider.php` - Added skipass field provider - - `ParticipantSkiPassFieldHandler.php` - Created field handler - - `ParticipantDto.php` - Changed from array to single Service object - - `BookingEditDto.php` - Conversion logic for existing bookings - - `BookingCreateParticipantType.php` - Added to dynamic fields - - `services.yaml` - Registered handler - -#### Room-Based Conditional Fields -- **remarksRoom Field**: Textarea field for room-specific comments -- **Conditional Logic**: Only appears when room with code 'mbz' is selected -- **Case-Insensitive**: Room code matching normalized to lowercase -- **Form Integration**: Uses TextareaType with XSS protection -- **Files Created**: - - `RoomSelectionCondition.php` - Flexible room-based condition - - `ParticipantRemarksRoomFieldHandler.php` - Field processing -- **Files Modified**: - - `CreateFieldStateProvider.php` - Added room selection condition - - `ParticipantDto.php` - Added remarksRoom property - -#### Improved Form Field Management -- **Problem**: Fields were added to form but hidden with CSS -- **Solution**: Fields now excluded from form entirely when conditions not met -- **Benefits**: Cleaner HTML, better performance, no hidden field clutter -- **Logic**: Check field state conditions BEFORE adding fields to form -- **Choice Field Optimization**: Only validate choices for ChoiceType fields -- **Field Type System**: Simple field name → form type mapping - -#### Service Selection Bug Fixes & HTMX Improvements -- **HTMX Trigger Issue**: HTMX requests were not triggered for service fields - - **Root Cause**: HTMX attributes placed on container elements instead of individual form inputs - - **Solution**: Moved HTMX triggers to individual checkbox/radio inputs for expanded choice fields - - **Affected Fields**: board, skipass, courses, additionalServices, rentals - - **Result**: Real-time dynamic updates now work reliably for all service fields - -- **Field Handler Data Issue**: Field handlers were storing service IDs instead of Service objects - - **Root Cause**: Form submission processing returned IDs rather than full Service entities - - **Solution**: Updated all field handlers to retrieve and store complete Service objects - - **Impact**: Pricing calculator now has access to service price data - - **Affected Handlers**: ParticipantBoardFieldHandler, ParticipantSkipassFieldHandler, etc. - -- **Form Submission Reset Issue**: Service selections were lost on form submission - - **Root Cause**: Inconsistent choice_value configuration between service types - - **Solution**: Added 'choice_value' => 'id' to all service field providers - - **Affected Fields**: courses, additionalServices, board, rentals, skipass - - **Result**: Consistent form submission behavior across all service types - -**Legend:** ✅ Completed | 🔄 In Progress | 🔍 Investigating | ⏳ Pending - -## Key Benefits - -### Technical Benefits -- **Leverages existing architecture** - no major refactoring needed -- **Consistent patterns** - follows current field state and options provider patterns -- **Dynamic updates** - automatically works with existing HTMX integration -- **Flexible configuration** - easy to add new age-based rules -- **Performance optimized** - fields not computed when not visible - -### UX Benefits -- **Clear Expectations** - Users know what to expect after providing age -- **Progressive Disclosure** - Information revealed at the right time -- **Visual Continuity** - Placeholders maintain form layout consistency -- **Contextual Help** - Specific guidance for different age scenarios -- **Smooth Interactions** - Animated transitions for dynamic changes -- **Accessibility** - Screen reader friendly field announcements -- **Mobile Optimized** - Touch-friendly interactions and responsive design - -## UX Components to Add - -1. **Field Hint Cards** - Informational cards explaining missing fields -2. **Age Requirement Badges** - Visual indicators on age-dependent sections -3. **Progress Indicators** - Show completion status including hidden fields -4. **Interactive Placeholders** - Clickable areas that focus date field -5. **Transition Animations** - Smooth field appearance/disappearance -6. **Contextual Tooltips** - Explain specific age requirements -7. **Smart Form Validation** - Guide users to complete prerequisites first - -## Testing Scenarios - -### Functional Testing -1. **Initial Load** - Proper placeholders and guidance shown when no date of birth -2. **Progressive Disclosure** - Fields appear correctly as age is entered -3. **Age Restrictions** - Correct fields hidden/shown based on age ranges -4. **Dynamic Updates** - HTMX updates work seamlessly with field state changes -5. **Form Validation** - Proper validation with conditional field visibility -6. **Field Dependencies** - Complex conditions work correctly - -### UX Testing -1. **Visual Feedback** - Clear indication of why fields are missing -2. **Interaction Flow** - Smooth user journey through form completion -3. **Accessibility** - Screen readers announce field changes properly -4. **Mobile Experience** - Touch interactions work on mobile devices -5. **Performance** - No lag during dynamic field updates -6. **Error Handling** - Graceful handling of invalid dates and edge cases - -## Configuration Examples - -### Service Age Restrictions -```php -// Example: Add age restrictions to Service model or metadata -$alcoholicBeverages = [ - 'minAge' => 18, - 'description' => 'Nur für Erwachsene verfügbar' -]; - -$youthActivities = [ - 'maxAge' => 15, - 'description' => 'Nur für Jugendliche unter 16' -]; - -$seniorServices = [ - 'minAge' => 65, - 'description' => 'Spezielle Services für Senioren' -]; -``` - -### Field Age Requirements -```php -// Configuration for which fields require age verification -private const AGE_RESTRICTED_FIELDS = [ - 'courses' => ['requireAge' => true, 'reason' => 'Altersgerechte Kursauswahl'], - 'additionalServices' => ['requireAge' => true, 'reason' => 'Altersspezifische Services'], - 'alcoholicBeverages' => ['minAge' => 18, 'reason' => 'Jugendschutz'], - 'youthActivities' => ['maxAge' => 15, 'reason' => 'Nur für Jugendliche'], - 'seniorServices' => ['minAge' => 65, 'reason' => 'Seniorenspezifisch'], -]; -``` - -This comprehensive plan provides both robust technical implementation and excellent user experience for age-based field management, building seamlessly on the existing sophisticated form architecture. \ No newline at end of file diff --git a/docs/AGE_CONSTRAINTS_MODEL_EXTENSION_PLAN.md b/docs/AGE_CONSTRAINTS_MODEL_EXTENSION_PLAN.md deleted file mode 100644 index 07296b4..0000000 --- a/docs/AGE_CONSTRAINTS_MODEL_EXTENSION_PLAN.md +++ /dev/null @@ -1,641 +0,0 @@ -# Age Constraints Model Extension Plan - -## Current Situation Analysis - -**XML Data Contains Two Age Constraint Formats:** -1. **Absolute Age**: `614` (current age 6-14) -2. **Birth Year Ranges**: `JG:2007-2009` (birth years 2007-2009) - -**Future Considerations:** -- `hinweis_stamm` may contain additional constraint types beyond `JG:` (birth year) -- Need extensible parsing system for future constraint formats -- Maintain English naming conventions throughout - -**Current State:** -- `Service` model has `ageFrom`/`ageTo` properties but they're not populated -- `TravelParser` doesn't parse age-related XML nodes -- Form processing doesn't consider age constraints - -## Required Changes - -### 1. Extend Service Model with Extensible Age Constraints - -**Add New Properties for Flexible Age Constraints:** -```php -// Add to Service class (src/BusProNet/Model/Service.php) - -#[Groups(['api:single', 'api:list'])] -public ?int $birthYearFrom = null; - -#[Groups(['api:single', 'api:list'])] -public ?int $birthYearTo = null; - -#[Groups(['api:single', 'api:list'])] -public ?string $ageConstraintType = null; // 'absolute_age', 'birth_year', 'mixed' - -#[Groups(['api:single'])] -public ?array $ageConstraintMetadata = null; // Extensible metadata for future constraint types - -#[Groups(['api:single'])] -public ?string $rawAgeConstraintData = null; // Store original XML data for debugging/future parsing -``` - -### 2. Create Extensible Age Constraint Parser System - -**A. Create Age Constraint Parser Interface:** -```php -// src/BusProNet/XmlParser/Contract/AgeConstraintParserInterface.php -interface AgeConstraintParserInterface -{ - public function canParse(string $constraintData): bool; - public function parse(string $constraintData): AgeConstraintResult; - public function getConstraintType(): string; -} -``` - -**B. Create Age Constraint Result DTO:** -```php -// src/BusProNet/XmlParser/Model/AgeConstraintResult.php -class AgeConstraintResult -{ - public function __construct( - public readonly string $type, - public readonly ?int $ageFrom = null, - public readonly ?int $ageTo = null, - public readonly ?int $birthYearFrom = null, - public readonly ?int $birthYearTo = null, - public readonly array $metadata = [], - public readonly ?string $rawData = null - ) {} - - public function hasAgeConstraints(): bool - { - return null !== $this->ageFrom || null !== $this->ageTo; - } - - public function hasBirthYearConstraints(): bool - { - return null !== $this->birthYearFrom || null !== $this->birthYearTo; - } - - public function isEmpty(): bool - { - return !$this->hasAgeConstraints() && !$this->hasBirthYearConstraints(); - } -} -``` - -**C. Create Birth Year Constraint Parser:** -```php -// src/BusProNet/XmlParser/AgeConstraint/BirthYearConstraintParser.php -class BirthYearConstraintParser implements AgeConstraintParserInterface -{ - private const BIRTH_YEAR_PREFIX = 'JG:'; - - public function canParse(string $constraintData): bool - { - return str_starts_with($constraintData, self::BIRTH_YEAR_PREFIX); - } - - public function parse(string $constraintData): AgeConstraintResult - { - if (!$this->canParse($constraintData)) { - throw new InvalidArgumentException('Cannot parse constraint data: ' . $constraintData); - } - - $yearData = substr($constraintData, strlen(self::BIRTH_YEAR_PREFIX)); - - // Parse range format "2007-2009" - if (str_contains($yearData, '-')) { - [$fromYear, $toYear] = explode('-', $yearData, 2); - - return new AgeConstraintResult( - type: 'birth_year', - birthYearFrom: (int) trim($fromYear), - birthYearTo: (int) trim($toYear), - metadata: [ - 'range_type' => 'birth_year_range', - 'original_format' => $yearData - ], - rawData: $constraintData - ); - } - - // Parse single year format "2007" - $year = (int) trim($yearData); - return new AgeConstraintResult( - type: 'birth_year', - birthYearFrom: $year, - birthYearTo: $year, - metadata: [ - 'range_type' => 'birth_year_single', - 'original_format' => $yearData - ], - rawData: $constraintData - ); - } - - public function getConstraintType(): string - { - return 'birth_year'; - } -} -``` - -**D. Create Age Constraint Parser Registry:** -```php -// src/BusProNet/XmlParser/AgeConstraint/AgeConstraintParserRegistry.php -class AgeConstraintParserRegistry -{ - /** @var AgeConstraintParserInterface[] */ - private array $parsers = []; - - public function __construct() - { - // Register built-in parsers - $this->addParser(new BirthYearConstraintParser()); - } - - public function addParser(AgeConstraintParserInterface $parser): void - { - $this->parsers[] = $parser; - } - - public function parseConstraints(string $constraintData): AgeConstraintResult - { - // Try multiple constraint types (semicolon-separated, e.g., 'JG:2007-2009;GL:5-8') - $constraints = array_map('trim', explode(';', $constraintData)); - $results = []; - - foreach ($constraints as $constraint) { - if (empty($constraint)) { - continue; - } - - foreach ($this->parsers as $parser) { - if ($parser->canParse($constraint)) { - $results[] = $parser->parse($constraint); - break; // First matching parser wins - } - } - } - - // Merge results if multiple constraints found - return $this->mergeConstraintResults($results, $constraintData); - } - - private function mergeConstraintResults(array $results, string $rawData): AgeConstraintResult - { - if (empty($results)) { - return new AgeConstraintResult(type: 'unknown', rawData: $rawData); - } - - if (count($results) === 1) { - return $results[0]; - } - - // Merge multiple constraint results - $type = 'mixed'; - $ageFrom = null; - $ageTo = null; - $birthYearFrom = null; - $birthYearTo = null; - $metadata = ['merged_from' => []]; - - foreach ($results as $result) { - $ageFrom = $this->mergeMinValue($ageFrom, $result->ageFrom); - $ageTo = $this->mergeMaxValue($ageTo, $result->ageTo); - $birthYearFrom = $this->mergeMinValue($birthYearFrom, $result->birthYearFrom); - $birthYearTo = $this->mergeMaxValue($birthYearTo, $result->birthYearTo); - $metadata['merged_from'][] = $result->type; - } - - return new AgeConstraintResult( - type: $type, - ageFrom: $ageFrom, - ageTo: $ageTo, - birthYearFrom: $birthYearFrom, - birthYearTo: $birthYearTo, - metadata: $metadata, - rawData: $rawData - ); - } - - private function mergeMinValue(?int $current, ?int $new): ?int - { - if (null === $current) return $new; - if (null === $new) return $current; - return max($current, $new); // Most restrictive minimum - } - - private function mergeMaxValue(?int $current, ?int $new): ?int - { - if (null === $current) return $new; - if (null === $new) return $current; - return min($current, $new); // Most restrictive maximum - } -} -``` - -### 3. Enhance TravelParser with Extensible Constraint Parsing - -**Add Age Constraint Parsing to Service Methods:** -```php -// Add to TravelParser class -private AgeConstraintParserRegistry $ageConstraintRegistry; - -public function __construct() -{ - $this->ageConstraintRegistry = new AgeConstraintParserRegistry(); - // Future: inject via DI for custom parsers -} - -// Update getAdditionalServices() method: -private function parseServiceAgeConstraints(Crawler $serviceNode, Service $service): void -{ - // Parse absolute age constraints (altervon/alterbis) - $ageFrom = $this->getIntOrNullValue($serviceNode->filterXPath('.//altervon')); - $ageTo = $this->getIntOrNullValue($serviceNode->filterXPath('.//alterbis')); - - // Parse extensible constraint data (hinweis_stamm -> ageConstraintData) - $constraintData = $this->getStringOrNullValue($serviceNode->filterXPath('.//hinweis_stamm')); - - $constraintResult = null; - if (null !== $constraintData && !empty(trim($constraintData))) { - $constraintResult = $this->ageConstraintRegistry->parseConstraints($constraintData); - } - - // Apply absolute age constraints - if (null !== $ageFrom || null !== $ageTo) { - $service->ageFrom = $ageFrom; - $service->ageTo = $ageTo; - - if (null !== $constraintResult && !$constraintResult->isEmpty()) { - // Mixed constraints scenario - $service->ageConstraintType = 'mixed'; - $service->birthYearFrom = $constraintResult->birthYearFrom; - $service->birthYearTo = $constraintResult->birthYearTo; - $service->ageConstraintMetadata = array_merge( - $constraintResult->metadata, - ['has_absolute_age' => true, 'has_birth_year' => true] - ); - } else { - $service->ageConstraintType = 'absolute_age'; - } - } elseif (null !== $constraintResult && !$constraintResult->isEmpty()) { - // Only constraint data (birth year, etc.) - $service->ageConstraintType = $constraintResult->type; - $service->birthYearFrom = $constraintResult->birthYearFrom; - $service->birthYearTo = $constraintResult->birthYearTo; - $service->ageConstraintMetadata = $constraintResult->metadata; - } - - // Always store raw data for debugging/future parsing - if (null !== $constraintData) { - $service->rawAgeConstraintData = $constraintData; - } -} - -// Update getAdditionalServices() method: -public function getAdditionalServices(Crawler $node): array -{ - $additionalServices = []; - - $node->each(function (Crawler $serviceNode) use (&$additionalServices) { - $serviceId = (int) $serviceNode->attr('idbuspro'); - - $service = new Service(); - $service->source = Constants::SOURCE_TRAVEL; - $service->category = Constants::CATEGORY_ADDITIONAL; - $service->id = $serviceId; - $service->subType = $serviceNode->attr('unterart'); - $service->mandatory = $this->stringToBool($serviceNode->attr('pflicht')); - $service->dateFrom = $this->stringToDate($serviceNode->attr('termin')); - $service->dateTo = $this->stringToDate($serviceNode->attr('bis')); - $service->label = $this->getStringOrNullValue($serviceNode->filterXPath('.//text')); - $service->price = $this->stringToFloat($this->getStringOrNullValue($serviceNode->filterXPath('.//preis'))); - $service->status = $this->getStringOrNullValue($serviceNode->filterXPath('.//status')); - - // Parse age constraints - $this->parseServiceAgeConstraints($serviceNode, $service); - - $additionalServices[$serviceId] = $service; - }); - - return $additionalServices; -} -``` - -### 4. Create Extensible Age Evaluation System - -**A. Enhanced Age Evaluation Interface:** -```php -// src/Form/Service/Contract/AgeEvaluatorInterface.php -interface AgeEvaluatorInterface -{ - public function canEvaluate(Service $service): bool; - public function isServiceAvailableForParticipant(Service $service, BookingDtoInterface $bookingDto, int $participantIndex): bool; - public function getConstraintDescription(Service $service): string; -} -``` - -**B. Create Service Age Evaluator:** -```php -// src/Form/Service/AgeEvaluator/ServiceAgeEvaluator.php -class ServiceAgeEvaluator implements AgeEvaluatorInterface -{ - public function canEvaluate(Service $service): bool - { - return null !== $service->ageConstraintType; - } - - public function isServiceAvailableForParticipant(Service $service, BookingDtoInterface $bookingDto, int $participantIndex): bool - { - $participant = $bookingDto->getParticipant($participantIndex); - - if (null === $participant || null === $participant->dateOfBirth) { - return false; // Cannot evaluate without birth date - } - - return match($service->ageConstraintType) { - 'absolute_age' => $this->evaluateAbsoluteAge($service, $participant->dateOfBirth), - 'birth_year' => $this->evaluateBirthYear($service, $participant->dateOfBirth), - 'mixed' => $this->evaluateAbsoluteAge($service, $participant->dateOfBirth) - && $this->evaluateBirthYear($service, $participant->dateOfBirth), - default => true // No constraints or unknown type - }; - } - - private function evaluateAbsoluteAge(Service $service, \DateTimeImmutable $dateOfBirth): bool - { - $age = $this->calculateAge($dateOfBirth); - - if (null !== $service->ageFrom && $age < $service->ageFrom) { - return false; - } - - if (null !== $service->ageTo && $age > $service->ageTo) { - return false; - } - - return true; - } - - private function evaluateBirthYear(Service $service, \DateTimeImmutable $dateOfBirth): bool - { - $birthYear = (int) $dateOfBirth->format('Y'); - - if (null !== $service->birthYearFrom && $birthYear < $service->birthYearFrom) { - return false; - } - - if (null !== $service->birthYearTo && $birthYear > $service->birthYearTo) { - return false; - } - - return true; - } - - private function calculateAge(\DateTimeImmutable $dateOfBirth): int - { - $today = new \DateTimeImmutable(); - return (int) $dateOfBirth->diff($today)->y; - } - - public function getConstraintDescription(Service $service): string - { - return match($service->ageConstraintType) { - 'absolute_age' => $this->getAbsoluteAgeDescription($service), - 'birth_year' => $this->getBirthYearDescription($service), - 'mixed' => sprintf('%s and %s', - $this->getAbsoluteAgeDescription($service), - $this->getBirthYearDescription($service)), - default => 'No age restrictions' - }; - } - - private function getAbsoluteAgeDescription(Service $service): string - { - if (null !== $service->ageFrom && null !== $service->ageTo) { - return sprintf('Ages %d-%d', $service->ageFrom, $service->ageTo); - } - - if (null !== $service->ageFrom) { - return sprintf('Age %d+', $service->ageFrom); - } - - if (null !== $service->ageTo) { - return sprintf('Age up to %d', $service->ageTo); - } - - return ''; - } - - private function getBirthYearDescription(Service $service): string - { - if (null !== $service->birthYearFrom && null !== $service->birthYearTo) { - if ($service->birthYearFrom === $service->birthYearTo) { - return sprintf('Born in %d', $service->birthYearFrom); - } - return sprintf('Born %d-%d', $service->birthYearFrom, $service->birthYearTo); - } - - if (null !== $service->birthYearFrom) { - return sprintf('Born %d or later', $service->birthYearFrom); - } - - if (null !== $service->birthYearTo) { - return sprintf('Born up to %d', $service->birthYearTo); - } - - return ''; - } -} -``` - -### 5. Enhanced Form Field Options Provider - -**Update with Age-Aware Service Filtering:** -```php -// Add to ParticipantFieldOptionsProvider (simplified approach) -// ServiceAgeEvaluator is instantiated directly when needed - -protected function registerFieldOptionProviders(): void -{ - // Enhanced field providers with age-aware filtering - $this->fieldOptionProviders['courses'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ - 'label' => 'Kurse', - 'multiple' => true, - 'expanded' => true, - 'required' => false, - 'choices' => $this->filterServicesByAgeConstraints( - $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES), - $bookingDto, - $participantIndex - ), - 'choice_label' => 'label', - ]; - - // Similar updates for additionalServices, rentals, board, etc. -} - -private function filterServicesByAgeConstraints(array $services, BookingDtoInterface $bookingDto, int $participantIndex): array -{ - $participant = $bookingDto->getParticipant($participantIndex); - - // If no birth date provided, return empty array (handled by DateOfBirthProvidedCondition) - if (null === $participant || null === $participant->dateOfBirth) { - return []; - } - - return array_filter($services, function (Service $service) use ($bookingDto, $participantIndex) { - // No age constraints = available to all - $ageEvaluator = new ServiceAgeEvaluator(); - if (!$ageEvaluator->canEvaluate($service)) { - return true; - } - - return $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex); - }); -} -``` - -### 6. Future Extension Examples - -**A. Example: Adding Grade Level Constraints (GL:5-8)** -```php -class GradeLevelConstraintParser implements AgeConstraintParserInterface -{ - private const GRADE_PREFIX = 'GL:'; - - public function canParse(string $constraintData): bool - { - return str_starts_with($constraintData, self::GRADE_PREFIX); - } - - public function parse(string $constraintData): AgeConstraintResult - { - $gradeData = substr($constraintData, strlen(self::GRADE_PREFIX)); - - if (str_contains($gradeData, '-')) { - [$fromGrade, $toGrade] = explode('-', $gradeData, 2); - - return new AgeConstraintResult( - type: 'grade_level', - metadata: [ - 'grade_from' => (int) trim($fromGrade), - 'grade_to' => (int) trim($toGrade), - 'constraint_type' => 'grade_range' - ], - rawData: $constraintData - ); - } - - // Single grade - return new AgeConstraintResult( - type: 'grade_level', - metadata: [ - 'grade' => (int) trim($gradeData), - 'constraint_type' => 'grade_single' - ], - rawData: $constraintData - ); - } - - public function getConstraintType(): string - { - return 'grade_level'; - } -} - -// Register in registry constructor: -$this->addParser(new GradeLevelConstraintParser()); -``` - -**B. Example: Complex Mixed Constraints (JG:2007-2009;GL:5-8)** -- Registry automatically handles semicolon-separated constraints -- Merges results into mixed constraint type -- Evaluator can handle multiple constraint types - -### 7. Add Helper Methods - -**Add to AbstractParser:** -```php -protected function getIntOrNullValue(Crawler $node): ?int -{ - $value = $this->getStringOrNullValue($node); - - if (null === $value || '' === trim($value)) { - return null; - } - - return (int) $value; -} -``` - -## Implementation Order - -1. **Create extensible constraint parser system** (interfaces, registry, birth year parser) -2. **Extend Service model** with new age constraint properties -3. **Update TravelParser** with extensible constraint parsing -4. **Create age evaluator system** for service filtering -5. **Update field options provider** with age-aware filtering -6. **Add comprehensive tests** for parsing and evaluation -7. **Add helper methods** to parser base class -8. **Update documentation** with extensible patterns - -## Key Benefits - -### Technical Benefits -- **Fully extensible** - easy to add new constraint types (grade level, membership status, etc.) -- **Backward compatible** - existing absolute age constraints continue working -- **English naming** - all properties and methods use clear English names -- **Robust parsing** - handles malformed data gracefully -- **Debuggable** - stores raw constraint data for troubleshooting -- **Testable** - clear separation of parsing, evaluation, and filtering concerns - -### Future Extensibility -- **Plugin architecture** - new constraint parsers can be added via DI -- **Mixed constraints** - supports multiple constraint types per service -- **Metadata storage** - extensible metadata for complex constraint types -- **Version resilient** - unknown constraint types don't break existing functionality - -### Business Benefits -- **Accurate service filtering** - services only shown to eligible participants -- **Clear constraint communication** - descriptive messages for age restrictions -- **Flexible business rules** - supports complex eligibility scenarios - -## Edge Cases Handled - -- **Invalid constraint formats** - graceful handling with fallback to 'unknown' type -- **Mixed constraint scenarios** - services with both absolute age and birth year requirements -- **Empty/null constraint data** - treated as no constraints (available to all) -- **Future constraint types** - unknown parsers don't break existing functionality -- **Malformed date ranges** - validation and error handling in parsers -- **Single vs range values** - supports both `JG:2007` and `JG:2007-2009` formats - -## Testing Strategy - -### Unit Tests -- **Constraint parsing** for all supported formats and edge cases -- **Age evaluation** for different constraint types and participant scenarios -- **Service filtering** with mixed constraint types -- **Registry behavior** with multiple parsers and constraint merging - -### Integration Tests -- **XML parsing** with real BPN export data containing age constraints -- **Form field generation** with age-restricted services -- **HTMX updates** when birth date changes affect service availability -- **End-to-end booking flow** with age-restricted services - -## Related Completed Improvements - -**✅ Form Processing System Enhancements** (complementary to age constraints): -- **Service Field HTMX Integration**: Fixed HTMX triggers for service fields (board, skipass, courses, etc.) to enable real-time updates for age-based field filtering -- **Field Handler Data Storage**: Updated all service field handlers to store complete Service objects instead of IDs, enabling access to age constraint data -- **Service Label Formatting**: Implemented smart service label formatting with pricing integration and quantity display -- **Pricing Integration**: Service selections now properly integrate with pricing calculations, supporting age-restricted service pricing - -These improvements provide the foundation for implementing age constraint filtering once the XML parsing and model extensions described in this plan are completed. - -This plan provides a robust, extensible foundation for handling current age constraints while being prepared for future constraint types that may emerge from the XML data. \ No newline at end of file diff --git a/docs/API_VALIDATION_STAGE_PLAN.md b/docs/API_VALIDATION_STAGE_PLAN.md deleted file mode 100644 index 782cc33..0000000 --- a/docs/API_VALIDATION_STAGE_PLAN.md +++ /dev/null @@ -1,395 +0,0 @@ -# API Availability Validation Stage - Implementation Plan - -## Overview - -This document outlines the planned implementation of a final validation stage that will verify service availability against real-time BusProNet API data before booking confirmation. This enhancement will complement the existing dynamic availability system by providing authoritative validation against live data. - -## Business Context - -### Current State -- **Dynamic Availability System**: Prevents overbooking within single booking sessions using XML data -- **XML Data Limitations**: Availability data may become outdated during booking creation process -- **Session-Scoped Protection**: Current system only tracks availability within individual booking workflows - -### Business Need -- **Real-Time Validation**: Ensure final service selections are valid against current API state -- **Cross-Session Integrity**: Prevent conflicts between multiple concurrent booking sessions -- **Authoritative Source**: Use BusProNet API as single source of truth for final validation -- **User Experience**: Provide clear feedback when services become unavailable - -## Technical Architecture - -### Validation Flow - -``` -Current Flow: -XML Data → Dynamic Availability → Form Validation → Booking Submission - -Planned Flow: -XML Data → Dynamic Availability → Form Validation → API Validation → Booking Submission -``` - -### Integration Points - -#### 1. Pre-Submission Validation Hook -```php -// Planned integration in booking workflow -class BookingController -{ - public function confirmBooking(BookingCreateDto $bookingDto): Response - { - // Step 1: Standard form validation - $formErrors = $this->validateForm($bookingDto); - if (!empty($formErrors)) { - return $this->handleFormErrors($formErrors); - } - - // Step 2: API availability validation (NEW) - $apiValidation = $this->bookingValidationService->validateServiceAvailability($bookingDto); - if (!$apiValidation->isValid()) { - return $this->handleAvailabilityConflicts($apiValidation); - } - - // Step 3: Submit to BPN API - return $this->submitBooking($bookingDto); - } -} -``` - -#### 2. Validation Service Architecture -```php -interface BookingValidationServiceInterface -{ - public function validateServiceAvailability(BookingCreateDto $bookingDto): ValidationResult; - public function resolveAvailabilityConflicts(BookingCreateDto $bookingDto): ConflictResolution; - public function getAlternativeServices(Service $unavailableService): array; -} - -class BookingValidationService implements BookingValidationServiceInterface -{ - public function __construct( - private readonly BusProNetApiClient $apiClient, - private readonly ServiceAvailabilityCalculator $availabilityCalculator, - private readonly ConflictResolver $conflictResolver - ) {} -} -``` - -#### 3. Validation Result Handling -```php -class ValidationResult -{ - public function __construct( - private readonly bool $isValid, - private readonly array $conflicts = [], - private readonly array $warnings = [] - ) {} - - public function isValid(): bool; - public function getConflicts(): array; - public function hasWarnings(): bool; - public function getWarnings(): array; -} - -class AvailabilityConflict -{ - public function __construct( - private readonly Service $service, - private readonly int $requestedQuantity, - private readonly int $actualAvailability, - private readonly array $affectedParticipants - ) {} -} -``` - -## Implementation Strategy - -### Phase 1: Foundation (Week 1) -- **API Integration**: Enhance BusProNet API client with availability checking endpoints -- **Validation Models**: Create validation result and conflict data structures -- **Service Architecture**: Implement core BookingValidationService - -### Phase 2: Conflict Resolution (Week 2) -- **Conflict Detection**: Identify which services have availability issues -- **Resolution Strategies**: Implement automatic and manual conflict resolution -- **Alternative Suggestions**: Provide similar service recommendations - -### Phase 3: User Experience (Week 3) -- **Error Handling**: Graceful handling of availability conflicts -- **User Interface**: Clear messaging and resolution options -- **Progressive Enhancement**: Maintain functionality if API is unavailable - -### Phase 4: Integration & Testing (Week 4) -- **Booking Flow Integration**: Wire validation into existing booking controllers -- **Comprehensive Testing**: Test various conflict scenarios -- **Performance Optimization**: Ensure validation doesn't impact user experience - -## User Experience Design - -### Conflict Resolution Scenarios - -#### Scenario 1: Service No Longer Available -``` -User Action: Submits booking with "Advanced Ski Course" selected -API Response: Advanced Ski Course is fully booked -System Response: - - Show clear error message - - Suggest alternative courses - - Allow user to modify selection or cancel -``` - -#### Scenario 2: Reduced Availability -``` -User Action: Books 3 participants for "Equipment Rental" -API Response: Only 2 rental sets available -System Response: - - Inform user of reduced availability - - Offer options: reduce participants or find alternatives - - Update pricing accordingly -``` - -#### Scenario 3: Multiple Conflicts -``` -User Action: Complex booking with several service conflicts -API Response: Multiple services have availability issues -System Response: - - Prioritize conflicts by impact - - Provide batch resolution options - - Guide user through step-by-step resolution -``` - -### Error Messages & UI - -#### Clear Communication -```html -
-

Availability Update Required

-

Some services in your booking are no longer available:

- -
    -
  • - Advanced Ski Course - Fully booked -
    - - -
    -
  • -
- -
- - -
-
-``` - -## API Integration Details - -### BusProNet API Enhancements - -#### New Endpoint Requirements -```php -// Required API capabilities -interface BusProNetAvailabilityApi -{ - /** - * Check real-time availability for multiple services - */ - public function checkServiceAvailability(array $serviceIds, \DateTimeImmutable $travelDate): array; - - /** - * Reserve services temporarily during booking process - */ - public function reserveServices(array $selections, int $reservationMinutes = 15): ReservationResult; - - /** - * Get alternative services for unavailable selections - */ - public function findAlternativeServices(Service $unavailableService): array; -} -``` - -#### API Call Optimization -- **Batch Requests**: Check multiple services in single API call -- **Caching Strategy**: Cache availability data for short periods (1-2 minutes) -- **Timeout Handling**: Graceful degradation if API is slow/unavailable -- **Rate Limiting**: Respect API rate limits to avoid service disruption - -## Data Flow & Processing - -### Validation Pipeline - -``` -1. Booking Submission - ↓ -2. Extract Service Selections - ↓ -3. Group by Service Type - ↓ -4. API Availability Check (Batched) - ↓ -5. Compare Requested vs Available - ↓ -6. Generate Conflict Report - ↓ -7. Resolve or Present to User - ↓ -8. Continue with Booking Submission -``` - -### Performance Considerations - -#### Optimization Strategies -- **Parallel Processing**: Check different service types concurrently -- **Smart Caching**: Cache recent availability checks -- **Incremental Validation**: Only validate changed services -- **Background Refresh**: Update availability data in background - -#### Fallback Mechanisms -- **API Timeout**: Continue with booking if API unavailable (with warning) -- **Partial Validation**: Validate what's possible, warn about unvalidated services -- **Manual Override**: Allow staff to override validation in exceptional cases - -## Error Handling & Edge Cases - -### API Failure Scenarios -- **Connection Timeout**: Use cached data with warning message -- **Authentication Issues**: Log error, allow booking with notification -- **Rate Limiting**: Queue validation or use exponential backoff -- **Invalid Response**: Parse what's possible, warn about remainder - -### Data Consistency Issues -- **Service ID Mismatch**: Handle cases where XML and API have different service IDs -- **Availability Calculation Errors**: Provide conservative estimates -- **Concurrent Bookings**: Handle race conditions gracefully - -### User Experience Fallbacks -- **Progressive Enhancement**: Core booking works even if validation fails -- **Clear Status Indicators**: Show validation status to users -- **Manual Verification**: Provide staff tools for manual validation - -## Testing Strategy - -### Unit Testing -- **Validation Logic**: Test conflict detection and resolution algorithms -- **API Integration**: Mock API responses for various scenarios -- **Edge Cases**: Test timeout, error, and edge case handling - -### Integration Testing -- **End-to-End Flow**: Test complete booking workflow with validation -- **API Mocking**: Simulate various API response scenarios -- **Performance Testing**: Ensure validation doesn't slow booking process - -### Manual Testing Scenarios -1. **Happy Path**: All services available, validation passes -2. **Single Conflict**: One service unavailable, resolution works -3. **Multiple Conflicts**: Complex conflicts resolved appropriately -4. **API Failure**: Graceful degradation when API unavailable -5. **Performance**: Validation completes within acceptable timeframe - -## Security & Compliance - -### Data Protection -- **Sensitive Data**: Ensure booking data is properly encrypted during API calls -- **Logging**: Log validation events without exposing personal information -- **Audit Trail**: Maintain records of validation decisions for compliance - -### API Security -- **Authentication**: Secure API communication with proper credentials -- **Rate Limiting**: Respect API limits to maintain service availability -- **Error Handling**: Don't expose sensitive API details in user-facing errors - -## Monitoring & Observability - -### Key Metrics -- **Validation Success Rate**: Percentage of bookings passing validation -- **Conflict Rate**: How often availability conflicts occur -- **Resolution Rate**: How often conflicts are successfully resolved -- **API Performance**: Response times and error rates - -### Logging Strategy -```php -// Planned logging approach -$this->logger->info('Booking validation started', [ - 'booking_id' => $bookingDto->id, - 'service_count' => count($selectedServices), - 'participant_count' => count($bookingDto->participants) -]); - -$this->logger->warning('Availability conflict detected', [ - 'service_id' => $service->id, - 'service_name' => $service->label, - 'requested' => $requestedQuantity, - 'available' => $actualAvailability -]); -``` - -## Future Enhancements - -### Advanced Features -- **Predictive Availability**: Use historical data to predict availability issues -- **Smart Alternatives**: Machine learning-based service recommendations -- **Real-time Updates**: WebSocket integration for live availability updates -- **Mobile Optimization**: Optimized validation flow for mobile devices - -### Business Intelligence -- **Demand Analytics**: Track which services have highest conflict rates -- **Optimization Insights**: Identify opportunities to improve availability management -- **Customer Behavior**: Analyze how users respond to availability conflicts - -## Implementation Timeline - -### Sprint 1: Foundation (2 weeks) -- API client enhancements -- Core validation service -- Basic conflict detection - -### Sprint 2: User Experience (2 weeks) -- Conflict resolution UI -- Error handling and messaging -- Alternative service suggestions - -### Sprint 3: Integration (1 week) -- Booking flow integration -- Performance optimization -- Comprehensive testing - -### Sprint 4: Monitoring & Refinement (1 week) -- Logging and monitoring setup -- Performance tuning -- Documentation and training - -## Success Criteria - -### Technical Success -- ✅ API validation integrated without performance degradation -- ✅ Conflict resolution success rate > 90% -- ✅ Validation response time < 2 seconds -- ✅ Graceful handling of API failures - -### Business Success -- ✅ Reduced booking conflicts and customer complaints -- ✅ Improved booking completion rates -- ✅ Better inventory management and utilization -- ✅ Enhanced customer experience and satisfaction - -### User Experience Success -- ✅ Clear, actionable error messages -- ✅ Intuitive conflict resolution workflow -- ✅ Minimal additional steps for successful bookings -- ✅ Accessible design for all user types - ---- - -**Planning Status**: 📋 **Documented and Ready for Implementation** -**Priority**: 🔥 **High - Critical for Production Reliability** -**Estimated Effort**: 6 weeks (Foundation + UX + Integration + Testing) -**Dependencies**: Enhanced BusProNet API, existing availability system -**Risk Level**: Medium (API integration complexity) - -**Next Steps**: -1. Stakeholder review and approval -2. API specification with BusProNet team -3. Technical spike for proof of concept -4. Implementation sprint planning \ No newline at end of file diff --git a/docs/BOOKING_EDIT_MODERNIZATION.md b/docs/BOOKING_EDIT_MODERNIZATION.md deleted file mode 100644 index 685e03c..0000000 --- a/docs/BOOKING_EDIT_MODERNIZATION.md +++ /dev/null @@ -1,687 +0,0 @@ -# Booking Edit Modernization Plan - -**Status:** Complete - All Phases Done ✅ -**Last Updated:** 2025-10-04 -**Goal:** Streamline booking edit implementation to match modern booking create flow - ---- - -## Executive Summary - -The current booking edit implementation uses legacy patterns that were developed before the sophisticated field handler system was created. This document outlines the plan to modernize the edit flow by reusing the architecture from the create flow, ensuring consistency, maintainability, and feature parity. - ---- - -## Current State Analysis - -### Existing Edit Implementation Issues - -#### 1. Legacy Template Structure (`templates/booking/edit.html.twig`) -- ❌ Manual field rendering without field handler system -- ❌ No HTMX-based dynamic updates -- ❌ No real-time pricing summary -- ❌ Hardcoded field visibility logic in template -- ❌ Uses deprecated `BookingEditParticipantType` with manual PRE_SET_DATA/PRE_SUBMIT handling - -#### 2. Duplicated Form Logic -- ❌ `BookingEditParticipantType` duplicates field definitions from create flow -- ❌ Manual field state management instead of using `EditFieldStateProvider` -- ❌ Manual choice_attr configuration instead of using field handlers -- ❌ Separate form type instead of reusing `BookingCreateParticipantType` - -#### 3. Missing Features -- ❌ No HTMX form refresh for dynamic field updates -- ❌ No booking summary sidebar with real-time pricing -- ❌ No toast notifications for automatic field changes -- ❌ No insurance field integration with conditional visibility -- ❌ No rental insurance, parking, license plate fields -- ❌ No bulk insurance booking support - -#### 4. Controller Complexity (`EditController`) -- ❌ Simple form submission without HTMX refresh endpoint -- ❌ No notification collection system -- ❌ No dynamic pricing calculation per participant - -### Current Create Implementation Strengths (to Adopt) - -#### 1. Modern Form Architecture -- ✅ Field handler registry pattern with automatic dependency resolution -- ✅ Unified `BookingCreateParticipantType` with `FieldOptionsProviderInterface` -- ✅ `CreateFieldStateProvider` for conditional field states -- ✅ `ParticipantFieldHandlerRegistry` for automatic field processing - -#### 2. HTMX Integration -- ✅ Real-time form refresh endpoint (`app_booking_create_step_2_refresh`) -- ✅ Out-of-band swaps for participants form and summary -- ✅ Toast notification system for user feedback - -#### 3. Pricing & Summary -- ✅ `BookingPriceCalculatorService` for individual participant pricing -- ✅ Reusable `_summary.html.twig` partial -- ✅ Real-time price updates on field changes - -#### 4. Template Patterns -- ✅ Twig macros for consistent field rendering (`service_field`, `checkbox_field`) -- ✅ Local form theme override for fieldset wrapping -- ✅ Collapsible participant sections with state persistence - -### BookingDtoInterface Bridge -- ✅ Both `BookingCreateDto` and `BookingEditDto` implement `BookingDtoInterface` -- ✅ Enables unified field handlers and state providers -- ✅ Already supports edit context through `getSelectedRooms()` stub - -### API Submission Analysis -- ✅ `BookingDataProcessor::createUpdateRequestPayload()` handles edit submission -- ✅ Process: Reset mappings → Process services → Remove unused → Update personal data → Build payload -- ⏳ Can be adapted for create flow by implementing `createBookingRequestPayload()` method -- ✅ Same participant service processing logic applies to both create and update - ---- - -## Implementation Plan - -### Phase 1: Extend Form System for Edit Context - -#### Task 1.1: Rename and Make BookingParticipantType Context-Aware -**Status:** ✅ Complete -**Files:** -- `src/Form/BookingParticipantType.php` (renamed from BookingCreateParticipantType.php) -- `src/Form/BookingCreateStep2Type.php` (updated reference) - -**Completed Actions:** -- [x] Renamed `BookingCreateParticipantType` to `BookingParticipantType` -- [x] Added `edit_mode` boolean option to `configureOptions()` with default `false` -- [x] Injected both `CreateFieldStateProvider` and `EditFieldStateProvider` via constructor -- [x] Added property `$fieldStateProvider` to store selected provider -- [x] Select appropriate provider in `buildForm()` based on `edit_mode` option -- [x] Updated `BookingCreateStep2Type` to use new name with `edit_mode: false` - -**Implementation Notes:** -- Form type is now truly generic and works for both create and edit contexts -- Provider selection happens at runtime based on form options -- Room assignment field conditional logic will be added in Phase 4 -- Applied php-cs-fixer with @Symfony ruleset - ---- - -#### Task 1.2: Update BookingEditType to Use Modern Form Architecture -**Status:** ✅ Complete -**Files:** -- `src/Form/BookingEditType.php` -- `src/Form/BookingEditParticipantType.php` (to be removed in cleanup) - -**Completed Actions:** -- [x] Replaced `BookingEditParticipantType` entry_type with `BookingParticipantType` -- [x] Set `edit_mode: true` in entry_options -- [x] Removed manual service merging from `onPreSetData()` listener -- [x] Updated `onPreSubmit()` to use `ParticipantFieldHandlerRegistry::processFieldsAndSync()` -- [x] Removed `mergeSelectableServices()` method (handled by field options provider) -- [x] Removed unused imports (`Constants`, `Service`) -- [x] Form now rebuilds participants field in onPreSubmit for proper state updates - -**Implementation Notes:** -- Eliminated ~40 lines of duplicated service merging logic -- Field handlers now automatically manage all service field options -- Mutability conditions will be applied via `EditFieldStateProvider` -- `BookingEditParticipantType` marked for removal in post-migration cleanup -- Applied php-cs-fixer with @Symfony ruleset - ---- - -#### Task 1.3: Extend EditFieldStateProvider -**Status:** ✅ Complete (insurance skipped per requirements) -**Files:** -- `src/Form/Service/EditFieldStateProvider.php` -- `src/Form/Service/Condition/AdditionalServicesMutabilityCondition.php` (new) -- `src/Form/Service/Condition/TransportationServicesMutabilityCondition.php` (new) -- `src/Form/Service/Condition/PickupsMutabilityCondition.php` (new) - -**Completed Actions:** -- [x] Created 3 new mutability condition classes for Travel-level flags -- [x] Added readonly states for transportation fields based on `transportationServicesMutable` -- [x] Added readonly states for additional services based on `additionalServicesMutable` -- [x] Added readonly states for pickup fields based on `pickupsMutable` -- [x] Added readonly states for parking and license plate fields -- [x] Integrated conditional visibility for all service fields (matching create flow) -- [x] Added hidden states for age-dependent fields until birth date provided -- [x] Applied field dependency chain: skipass → rentals → rental insurance → body dimensions -- [~] Skipped insurance field (not available in edit mode per requirements) -- [~] Skipped bulk insurance (not available in edit mode per requirements) - -**Implementation Notes:** -- Created specialized mutability conditions that check `Travel` model properties -- Reused existing conditions: `DateOfBirthProvidedCondition`, `RentalSelectionCondition`, `SkiPassSelectionCondition`, `ServiceSubTypeCondition` -- Personal data fields readonly if applicant OR not mutable (via `MutabilityCondition`) -- Service fields have dual state: hidden until birth date + readonly based on mutability -- Field state system now fully aligned between create and edit contexts -- Applied php-cs-fixer with @Symfony ruleset to all new condition files - ---- - -### Phase 2: Modernize Edit Controller - -#### Task 2.1: Add HTMX Refresh Endpoint -**Status:** ✅ Complete -**Files:** -- `src/Controller/Booking/EditController.php` - -**Completed Actions:** -- [x] Added `use HtmxControllerTrait;` to `EditController` -- [x] Created `refreshParticipantForm()` method with route `app_booking_edit_refresh` -- [x] Fetches booking data via `BookingDataTrait::fetchBookingData()` -- [x] Creates form with `validation_groups: false` to capture state without validation -- [x] Handles request and processes via field handlers -- [x] Collects participant notifications via `collectParticipantNotifications()` helper method -- [x] Returns HTMX OOB response with `participants_form` and `booking_summary` blocks -- [x] Adds notifications to HX-Trigger header when present - -**Implementation Notes:** -- Pattern exactly matches `CreateStep2Controller::refreshParticipantForm()` -- Route: POST `/bookings/{id}/edit/refresh` with `@IsGranted('ROLE_USER')` -- Uses cached availability data via `getAvailabilityDataCached()` for performance -- Properly handles access control with `denyAccessUnlessGranted('EDIT', $bookingData)` - ---- - -#### Task 2.2: Integrate Pricing Calculation -**Status:** ✅ Complete -**Files:** -- `src/Controller/Booking/EditController.php` - -**Completed Actions:** -- [x] Injected `BookingPriceCalculatorService` and `BookingService` into constructor -- [x] Calculates participant prices using `calculateAllParticipantIndividualPrices($bookingEditDto)` -- [x] Generates summary data via `BookingService::getRoomSummaryAndParticipantCount()` -- [x] Passes `pricingData`, `participantPrices`, `assignmentCounts` to template -- [x] Groups selected rooms via `groupRoomSelectionsByType()` for summary display -- [x] Includes same data in both main action and refresh endpoint response - -**Implementation Notes:** -- Pricing calculation works seamlessly with `BookingDtoInterface` -- Room selections extracted from existing booking via `$formData->getSelectedRooms()` -- Template receives: `pricingData`, `participantCount`, `assignmentCounts`, `participantPrices`, `groupedSelectedRooms` -- Both edit() and refreshParticipantForm() calculate and pass identical pricing data - ---- - -#### Task 2.3: Enrich Availability Data -**Status:** ✅ Complete -**Files:** -- `src/Controller/Booking/EditController.php` - -**Completed Actions:** -- [x] Main action uses `getAvailabilityData()` for initial load -- [x] Refresh endpoint uses `getAvailabilityDataCached()` for performance -- [x] Patches travel data using `patchAvailabilities($travelData, $availabilities)` -- [x] Applied before form creation in both main action and refresh endpoint -- [x] Ensures service availability and pricing is current - -**Implementation Notes:** -- Availability enrichment happens before DTO creation: Load travel → Patch availability/mutability → Create DTO -- Critical for accurate service pricing and availability status -- Cached version in refresh endpoint reduces API calls during HTMX updates -- Pattern consistent with create flow implementation - ---- - -### Phase 3 & 4: Modernize Edit Template (Combined Implementation) - -#### Task 3.1: Adopt Create Template Structure -**Status:** ✅ Complete -**Files:** -- `templates/booking/edit.html.twig` (completely rewritten) - -**Completed Actions:** -- [x] Copied local form theme override from `create_step_2.html.twig` for fieldset wrapping (lines 4-22) -- [x] Imported Twig macros (`service_field`, `checkbox_field`) via `{% import _self as macros %}` (lines 24-51) -- [x] Restructured layout to grid with 2/3 form area (col-span-2) + 1/3 summary sidebar (lines 156-424) -- [x] Wrapped participant loop in `{% block participants_form %}` with `hx-swap-oob` support (lines 162-406) -- [x] Used macros for all service field rendering (lines 278-324) -- [x] Added collapsible participant sections with toggle controller and state persistence (lines 171-403) -- [x] Preserved "Reisedaten" (booking metadata) section at top (lines 58-153) - -**Implementation Notes:** -- Template now mirrors create template structure exactly -- Macros ensure consistent fieldset rendering and placeholder display -- Canceled participants (`status == 'S'`) handled with special display logic (lines 192-208) -- Age eligibility checks integrated (lines 168, 252-274) -- Applied Tailwind styling consistent with create flow - ---- - -#### Task 3.2: Integrate Summary Sidebar -**Status:** ✅ Complete -**Files:** -- `templates/booking/edit.html.twig` -- `templates/booking/_summary.html.twig` (reused) - -**Completed Actions:** -- [x] Added `{% block booking_summary %}` wrapper with `hx-swap-oob` support (lines 414-423) -- [x] Included `booking/_summary.html.twig` partial with proper variable mapping -- [x] Passed `bookingCreateDto` as `bookingEditDto` (interface compatible via `BookingDtoInterface`) -- [x] Passed `participantCount`, `groupedSelectedRooms`, `assignmentCounts` from controller -- [x] Summary updates dynamically via HTMX OOB swaps - -**Implementation Notes:** -- Summary partial works seamlessly with both DTOs via interface -- Room grouping handled by controller using `BookingService::groupRoomSelectionsByType()` -- Real-time pricing updates when services change -- Grid layout: col-span-2 for form, remaining column for sticky sidebar - ---- - -#### Task 3.3: Add HTMX Attributes -**Status:** ✅ Complete -**Files:** -- `templates/booking/edit.html.twig` - -**Completed Actions:** -- [x] Added `hx-post` to all dynamic fields pointing to `app_booking_edit_refresh` with booking ID -- [x] Added `hx-trigger="change"` to: dateOfBirth, all service fields, transportation fields, pickup fields -- [x] Added `hx-swap="none"` (updates handled via OOB swaps) -- [x] Added toast controller to page root (line 55) -- [x] All field updates trigger form refresh via HTMX - -**Implementation Notes:** -- Every service field has HTMX attributes for real-time updates -- Route includes booking ID parameter: `path('app_booking_edit_refresh', {'id': bookingData.id})` -- Toast controller listens for `showNotifications` events from HX-Trigger headers -- Pattern matches create flow exactly - ---- - -#### Task 3.4: Remove Manual Readonly Overlays -**Status:** ✅ Complete -**Files:** -- `templates/booking/edit.html.twig` - -**Completed Actions:** -- [x] Deleted all absolute positioned overlay divs for personal data mutability -- [x] Deleted all absolute positioned overlay divs for services mutability -- [x] Deleted all absolute positioned overlay divs for transportation mutability -- [x] Field state system now handles all readonly/disabled attributes automatically -- [x] No manual overlays needed - cleaner, more accessible implementation - -**Implementation Notes:** -- Field state conditions in `EditFieldStateProvider` apply readonly attributes automatically -- Personal data fields: readonly if applicant OR not mutable -- Service fields: readonly if `additionalServicesMutable == false` -- Transportation fields: readonly if `transportationServicesMutable == false` -- Pickup fields: readonly if `pickupsMutable == false` -- Much cleaner approach than z-index overlays - ---- - -#### Task 4.1: Hide Room Assignment Field -**Status:** ✅ Complete (via template exclusion) -**Files:** -- `templates/booking/edit.html.twig` - -**Completed Actions:** -- [x] Room assignment field (`assignedRoomId`) not rendered in edit template -- [x] Field simply omitted from template - no special hiding logic needed -- [x] Room assignment data preserved in DTO (not modified) - -**Implementation Notes:** -- Simplest approach: field not included in template at all -- Field handlers don't process `assignedRoomId` in edit mode -- Room data maintained in booking via API, not editable through form -- Business rule: room changes must go through customer service - ---- - -#### Task 4.2: Display Current Room Assignment -**Status:** ✅ Complete -**Files:** -- `templates/booking/edit.html.twig` (lines 236-250) - -**Completed Actions:** -- [x] Added read-only room display section in participant details -- [x] Uses `bookingData.roomForParticipant(participantData.index)` to get room info -- [x] Format: `{{ room.label }} ({{ room.individualPrice[participantData.index]|format_currency('EUR') }})` -- [x] Positioned in accommodation section alongside remarksRoom field -- [x] Shows "Keine Unterkunft zugeordnet" when no room assigned - -**Implementation Notes:** -- Read-only display with label styling (`font-semibold mb-1 block`) -- Room price displayed for transparency -- Positioned logically in form flow (after personal data, before services) -- User-friendly message when no room assigned - ---- - -### Phase 5: Update BookingDataProcessor for Create Flow - -#### Task 5.1: Implement Create Booking Method -**Status:** ⏳ Not Started -**Files:** -- `src/BusProNet/DataProcessor/BookingDataProcessor.php` - -**Actions:** -- [ ] Create `createBookingRequestPayload(BookingCreateDto $formData): array` -- [ ] Build participant array from `$formData->participants` -- [ ] Process services using existing `processParticipantServices()` method -- [ ] Build room selection payload from `$formData->roomSelections` -- [ ] Set applicant data from first participant (`$participants[0]`) -- [ ] Build complete booking payload structure -- [ ] Return array ready for API submission - -**Notes:** -- Reuses service processing logic from update flow -- Adds room selection handling (not in update flow) -- Reference: Existing `createUpdateRequestPayload()` method - ---- - -#### Task 5.2: Add ApiClient::createBooking() -**Status:** ⏳ Not Started -**Files:** -- `src/BusProNet/ApiClient.php` - -**Actions:** -- [ ] Add `createBooking(BookingCreateDto $formData, bool $debug = false): Notification|Booking` -- [ ] Use `BookingDataProcessor::createBookingRequestPayload()` -- [ ] Build request data with BPN credentials and API key -- [ ] Set `satz.@typ` to appropriate booking creation type -- [ ] Call `sendRequest()` with payload -- [ ] Return parsed response (Notification on error, Booking on success) - -**Notes:** -- Mirrors `updateBooking()` method structure -- Completes booking creation workflow -- Enables step 3 confirmation and submission - ---- - -### Phase 6: Testing & Validation - -#### Task 6.1: Test Edit Flow -**Status:** ⏳ Not Started -**Test Coverage:** - -- [ ] Manual test: Load existing booking in edit mode -- [ ] Verify HTMX refresh updates fields correctly on service changes -- [ ] Test mutability conditions prevent editing locked fields -- [ ] Validate insurance auto-reassignment works in edit context -- [ ] Confirm toast notifications appear for automatic field changes (rental clearing, insurance reassignment) -- [ ] Test bulk insurance booking if applicable to edit flow -- [ ] Verify field dependencies work (skipass → rentals → insurance → body dimensions) -- [ ] Test form submission updates booking via API -- [ ] Verify error handling displays validation messages - -**Notes:** -- Create test fixtures with various mutability states -- Test with bookings in different statuses (confirmed, option, etc.) - ---- - -#### Task 6.2: Test Room Assignment Hiding -**Status:** ⏳ Not Started -**Test Coverage:** - -- [ ] Verify room assignment field not rendered in edit mode -- [ ] Confirm current room displayed as read-only info -- [ ] Validate room data preserved in DTO after form submission -- [ ] Test that room pricing included in summary correctly - -**Notes:** -- Room changes must go through customer service -- UI should make this clear - ---- - -#### Task 6.3: Test Pricing Calculations -**Status:** ⏳ Not Started -**Test Coverage:** - -- [ ] Validate summary shows correct room totals -- [ ] Validate summary shows correct service totals grouped by type -- [ ] Confirm grand total matches booking total -- [ ] Verify individual participant prices displayed correctly -- [ ] Test service price updates reflect in real-time during HTMX refresh -- [ ] Compare calculated prices with API-returned prices - -**Notes:** -- Pricing must match BPN API calculations exactly -- Edge cases: discounts, surcharges, individual pricing - ---- - -## Key Differences: Create vs. Edit - -| Aspect | Create Flow | Edit Flow | -|--------|-------------|-----------| -| **Room Assignment** | Selectable dropdown with HTMX updates | Hidden (read-only display only) | -| **Field Mutability** | All fields editable (subject to age/conditions) | Conditional based on booking status and dates | -| **Data Source** | New `BookingCreateDto` from session | `BookingEditDto::fromBooking()` from API | -| **Validation** | Full validation on all fields | Full validation but fields may be readonly | -| **API Endpoint** | `ApiClient::createBooking()` (to be implemented) | `ApiClient::updateBooking()` (exists) | -| **DTO Population** | From user input + room selections | From API booking data via `Booking` model | -| **Field State Provider** | `CreateFieldStateProvider` | `EditFieldStateProvider` | -| **Mutability Flags** | Not applicable (all editable) | From `Travel` model (`participantDataMutable`, `additionalServicesMutable`, etc.) | -| **Booking ID** | Not yet assigned | From URL parameter (`/bookings/{id}/edit`) | -| **Cache Strategy** | Session storage | API cache (5 min TTL) | - ---- - -## Benefits - -### 1. Code Reuse -- Single participant form type for both create and edit -- Unified field handlers work in both contexts -- Shared template components (macros, summary partial) -- Reduced maintenance burden - -### 2. Consistency -- Identical UX for create and edit workflows -- Same HTMX behavior and real-time updates -- Consistent pricing display and calculations -- Unified toast notification system - -### 3. Maintainability -- Field changes propagate to both contexts automatically -- Single source of truth for field definitions -- Centralized field state logic -- Easier to add new features (apply to both flows) - -### 4. Modern UX -- Real-time form updates without full page reload -- Dynamic pricing feedback as user makes changes -- Toast notifications for automatic field changes -- Responsive summary sidebar with live totals - -### 5. Insurance Support -- Full insurance field integration in edit flow -- Auto-reassignment on price tier changes -- Conditional visibility based on date of birth -- Bulk insurance booking support (if enabled for edit) - -### 6. API Preparation -- Booking submission infrastructure ready for create flow -- `BookingDataProcessor` handles both create and update -- API client method for booking creation -- Completes end-to-end booking workflow - ---- - -## Technical Notes - -### Field Handler Compatibility -All field handlers implement `ParticipantFieldHandlerInterface` and work with `BookingDtoInterface`, making them automatically compatible with both create and edit contexts: - -- ✅ `ParticipantSkiPassFieldHandler` -- ✅ `ParticipantRentalsFieldHandler` -- ✅ `ParticipantRentalInsuranceFieldHandler` -- ✅ `ParticipantInsuranceFieldHandler` -- ✅ `ParticipantTransportationOutboundFieldHandler` -- ✅ `ParticipantTransportationInboundFieldHandler` -- ✅ `ParticipantPickupOutboundFieldHandler` -- ✅ `ParticipantPickupInboundFieldHandler` -- ✅ `ParticipantParkingFieldHandler` -- ✅ `ParticipantLicensePlateFieldHandler` -- ✅ `ParticipantBulkInsuranceFieldHandler` -- ✅ `ParticipantBoardFieldHandler` -- ✅ `ParticipantCoursesFieldHandler` -- ✅ `ParticipantAdditionalServicesFieldHandler` -- ✅ `ParticipantBodyDimensionsFieldHandler` - -### Mutability Flags Source -The edit flow relies on mutability flags from the BPN API: - -**API Endpoint:** `getMutableData($dateId)` -**Response Type:** `BaseData` with mutability items -**Application:** `TravelDataService::patchMutability($travel, $mutableData)` - -**Flags:** -- `participantDataMutable` → Personal data fields readonly if false -- `additionalServicesMutable` → Service fields readonly if false -- `transportationServicesMutable` → Transportation fields readonly if false -- `pickupsMutable` → Pickup fields readonly if false - -### Service Filtering in Edit -Edit flow needs special service filtering: - -```php -// Merge services from travel data + services from existing booking -private function mergeSelectableServices(BookingEditDto $data, string|array $subType): array -{ - $selectableServices = $data->travel->getAdditionalServicesBySubTypes($subType); - $selectableServiceIds = array_map(fn(Service $service) => $service->id, $selectableServices); - - // Add services from booking that are no longer in travel data (legacy services) - foreach ($data->booking->getAdditionalServicesByGroup($subType) as $item) { - if (!in_array($item->id, $selectableServiceIds)) { - $selectableServices[] = $item; - } - } - - return $selectableServices; -} -``` - -This ensures participants can keep legacy services that may no longer be offered. - -### Participant Status Handling -Edit template must handle canceled participants: - -```php -if ('S' === $participant->status) { - // Show canceled badge, display surcharges only, don't render form fields -} -``` - -Field handlers should skip processing for canceled participants: - -```php -if ($participant->status === 'S') { - return; // Don't process services for canceled participants -} -``` - ---- - -## Migration Checklist - -### Pre-Migration -- [ ] Backup database -- [ ] Document current edit flow behavior -- [ ] Create test bookings in various states (confirmed, option, with/without mutability) -- [ ] Review custom business rules for edit (if any) - -### During Migration -- [ ] Follow phase order (1 → 2 → 3 → 4 → 5 → 6) -- [ ] Complete all tasks in a phase before moving to next -- [ ] Test after each phase -- [ ] Commit changes per phase with descriptive messages - -### Post-Migration -- [ ] Full regression test of edit flow -- [ ] User acceptance testing -- [ ] Update documentation -- [ ] Remove deprecated `BookingEditParticipantType` file -- [ ] Clean up old template code -- [ ] Monitor production for issues - ---- - -## Open Questions - -1. **Bulk Insurance in Edit:** Should bulk insurance booking be available when editing? Or only during creation? - - **Decision:** TBD - Needs business rule clarification - -2. **Room Assignment Changes:** Should there be a way for users to request room changes (e.g., via remarks field)? - - **Decision:** TBD - May add "request change" functionality - -3. **Canceled Participant Re-activation:** Can canceled participants be re-activated through the edit form? - - **Decision:** TBD - Likely not allowed, needs customer service intervention - -4. **Legacy Service Handling:** How to display services that are no longer offered but exist in booking? - - **Current:** Service has `source: 'BOOKING'` flag, displayed with special styling - - **Action:** Maintain current approach - -5. **Insurance in Edit:** Should insurance be editable or locked after booking creation? - - **Assumption:** Editable if `additionalServicesMutable` is true - - **Action:** Follow mutability flags from API - ---- - -## Progress Tracking - -**Overall Progress:** 12/15 tasks complete (80%) - Phase 5 deferred, Phase 6 requires test data - -### Phase 1: ✅ 3/3 complete -- ✅ Task 1.1: Renamed BookingCreateParticipantType to BookingParticipantType and made it context-aware -- ✅ Task 1.2: Updated BookingEditType to use modern form architecture -- ✅ Task 1.3: Extended EditFieldStateProvider with field conditions (insurance skipped) - -### Phase 2: ✅ 3/3 complete -- ✅ Task 2.1: Added HTMX Refresh Endpoint -- ✅ Task 2.2: Integrated Pricing Calculation -- ✅ Task 2.3: Enriched Availability Data - -### Phase 3 & 4: ✅ 6/6 complete (Combined in template rewrite) -- ✅ Task 3.1: Adopted Create Template Structure (form theme, macros, grid layout) -- ✅ Task 3.2: Integrated Summary Sidebar (reuses `_summary.html.twig` partial) -- ✅ Task 3.3: Added HTMX Attributes (all service fields trigger `app_booking_edit_refresh`) -- ✅ Task 3.4: Removed Manual Readonly Overlays (field state system handles all states) -- ✅ Task 4.1: Room Assignment Field Not Rendered (not in form, handled by field state) -- ✅ Task 4.2: Display Current Room Assignment (read-only display in template lines 236-250) - -### Phase 5: ⏸️ DEFERRED -- ⏸️ Task 5.1: Implement Create Booking Method (deferred to future) -- ⏸️ Task 5.2: Add ApiClient::createBooking() (deferred to future) - -### Phase 6: ⏳ Pending Test Data -- ⏳ Task 6.1: Test Edit Flow (requires test bookings) -- ⏳ Task 6.2: Test Room Assignment Hiding (requires test bookings) -- ⏳ Task 6.3: Test Pricing Calculations (requires test bookings) - ---- - -## References - -### Key Files -- **Create Flow Controller:** `src/Controller/Booking/CreateStep2Controller.php` -- **Edit Flow Controller:** `src/Controller/Booking/EditController.php` -- **Create Form Type:** `src/Form/BookingCreateStep2Type.php` -- **Edit Form Type:** `src/Form/BookingEditType.php` -- **Participant Form Type:** `src/Form/BookingCreateParticipantType.php` -- **Legacy Edit Participant Type:** `src/Form/BookingEditParticipantType.php` (to be removed) -- **Field State Providers:** `src/Form/Service/{Create,Edit}FieldStateProvider.php` -- **Field Options Provider:** `src/Form/Service/ParticipantFieldOptionsProvider.php` -- **Field Handler Registry:** `src/Form/Service/ParticipantFieldHandlerRegistry.php` -- **Booking Data Processor:** `src/BusProNet/DataProcessor/BookingDataProcessor.php` -- **API Client:** `src/BusProNet/ApiClient.php` -- **Create Template:** `templates/booking/create_step_2.html.twig` -- **Edit Template:** `templates/booking/edit.html.twig` -- **Summary Partial:** `templates/booking/_summary.html.twig` - -### Related Documentation -- `CLAUDE.md` - Project overview and development guidelines -- Insurance Implementation (completed sessions) -- Transportation Services Implementation (completed sessions) -- Rental Insurance & Body Dimensions Implementation (completed sessions) - ---- - -**End of Document** \ No newline at end of file diff --git a/docs/BOOKING_PAYMENT_STEP.md b/docs/BOOKING_PAYMENT_STEP.md deleted file mode 100644 index 84ca83e..0000000 --- a/docs/BOOKING_PAYMENT_STEP.md +++ /dev/null @@ -1,726 +0,0 @@ -# Booking Payment Step Implementation Plan - -**Status:** ✅ COMPLETED -**Last Updated:** 2025-01-04 -**Goal:** Add payment method selection step (Step 3) to booking creation flow - -## Implementation Summary - -✅ **Completed Components:** - -1. **DTOs Created:** - - `BankAccountDto` - IBAN validation, account holder, bank name, SEPA mandate - - Payment properties added directly to `BookingCreateDto`: - - `paymentMethod` - PAYMENT_METHOD_TRANSFER (default) or PAYMENT_METHOD_DEBIT - - `bankAccount` - BankAccountDto instance (nullable) - - Payment method constants in `Constants.php` (PAYMENT_METHOD_TRANSFER, PAYMENT_METHOD_DEBIT) - -2. **Form Type:** - - `BookingCreateStep3Type` - Main step 3 form with payment method and conditional bank account fields - - **Removed** `PaymentType` wrapper (unnecessary with proper event handling) - - Dynamic field management using POST_SET_DATA and PRE_SUBMIT events - - Helper method `addBankAccountField()` to avoid duplication - -3. **Controller:** - - `CreateStep3Controller` - Main step 3 action and HTMX refresh endpoint - - Uses `getSummaryVariables()` trait method for booking summary data - - Conditional field rendering handled entirely by form events - -4. **Template:** - - `create_step_3.html.twig` - Grid layout matching steps 1 & 2 - - Payment method selection with HTMX for dynamic bank account fields - - HTMX attributes on outer wrapper div for proper field replacement - - Booking summary sidebar visible on all steps - -5. **Validation:** - - Conditional validation via `validateBankAccount()` callback in `BookingCreateDto` - - Bank account fields only validated when payment method is DEBIT - - IBAN format validation using Symfony's built-in `@Assert\Iban` - - SEPA mandate acceptance required for direct debit - -6. **Business Rules:** - - **14-day rule**: Direct debit only available if travel starts ≥14 days from now - - Debit option becomes readonly with tooltip when not available - - Authorization text from existing booking flow integrated into choice label - -## Key Implementation Details - -### Form Event Pattern -```php -// BookingCreateStep3Type.php - Conditional field rendering -$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event): void { - $bookingDto = $event->getData(); - - if (Constants::PAYMENT_METHOD_DEBIT === $bookingDto->paymentMethod) { - $this->addBankAccountField($event->getForm()); - } -}); - -$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event): void { - $data = $event->getData(); - $form = $event->getForm(); - $paymentMethod = $data['paymentMethod'] ?? null; - - if ($form->has('bankAccount')) { - $form->remove('bankAccount'); - } - - if (Constants::PAYMENT_METHOD_DEBIT === $paymentMethod) { - $this->addBankAccountField($form); - } -}); -``` - -**Key Points:** -- POST_SET_DATA: Adds field when rendering if paymentMethod is DEBIT -- PRE_SUBMIT: Removes existing field first, then adds only if DEBIT selected -- Helper method avoids duplicate field configuration -- No need to manually unset data - Symfony ignores unmapped fields - -### Conditional Validation Pattern -```php -// BookingCreateDto.php - Conditional bank account validation -#[Assert\Callback] -public function validateBankAccount(ExecutionContextInterface $context): void -{ - if (Constants::PAYMENT_METHOD_DEBIT !== $this->paymentMethod) { - return; // Skip validation for transfer - } - - // Validate only when debit is selected - if (null === $this->bankAccount) { - $context->buildViolation('Bitte geben Sie Ihre Bankverbindung an.') - ->atPath('bankAccount') - ->addViolation(); - } - // ... additional field validations -} -``` - -### 14-Day Availability Rule -```php -private function isDebitAvailable(BookingCreateDto $bookingDto): bool -{ - $travelStartDate = CarbonImmutable::instance($bookingDto->travel->dateFrom); - $now = CarbonImmutable::now(); - $daysUntilTravel = $now->diffInDays($travelStartDate, false); - - return $daysUntilTravel >= 14; -} -``` - -Applied via `choice_attr` callback to make debit option readonly when unavailable. - -### HTMX Pattern for Expanded Forms -For radio buttons (expanded choice types), HTMX attributes must be on a wrapper element: - -```twig -
- {{ form_row(form.paymentMethod) }} - - {# Conditionally rendered bank account section #} - {% if form.bankAccount is defined %} - ... - {% endif %} -
-``` - -**Why this pattern:** -- Wrapper div captures change events from child radio buttons -- Entire section (payment method + bank account fields) gets replaced -- `hx-select` extracts only `#form-payment` from full page response - ---- - -## Executive Summary - -Currently, the booking creation flow ends at Step 2 (participant details). We need to add Step 3 to collect payment method information before final submission. Users can choose between: -1. **Direct Transfer (Überweisung)** - Requires bank account details -2. **Direct Debit (Lastschrift)** - Requires bank account details + SEPA mandate - ---- - -## Requirements Analysis - -### Payment Methods - -#### 1. Direct Transfer (Überweisung) -- User will transfer money manually -- Required fields: - - Payment method selection (radio/select) -- No bank details needed (invoice will show our account for transfer) - -#### 2. Direct Debit (Lastschrift) -- Automatic withdrawal from user's account -- Required fields: - - IBAN (validated format) - - Account holder name (text) - - SEPA mandate checkbox (required agreement) -- Optional fields: - - Bank name (text) - -### Validation Requirements - -**IBAN Validation:** -- Format: Country code (2 letters) + Check digits (2 digits) + BBAN (up to 30 alphanumeric) -- German IBAN: DE + 2 digits + 18 digits (total 22 characters) -- International: Support common EU countries -- Checksum validation (mod 97 algorithm) via Symfony's `@Assert\Iban` -- Display formatted with spaces (DE12 3456 7890 1234 5678 90) - -**Account Holder Name:** -- Must match participant/applicant name or be explicitly confirmed -- Min length: 2 characters -- Max length: 70 characters (SEPA standard) -- Allowed: Letters, spaces, hyphens, apostrophes - -**Bank Name:** -- Optional but recommended -- Max length: 70 characters - -### Data Flow - -``` -Step 1 (Room Selection) - ↓ -Step 2 (Participant Details) - ↓ -Step 3 (Payment Method) ← NEW - ↓ -Final Submission to API -``` - ---- - -## Implementation Plan - -### Phase 1: Create Bank Account DTO - -#### Task 1.1: Create BankAccountDto -**Status:** ✅ Simplified - BIC omitted -**Files:** -- `src/Form/Model/BankAccountDto.php` (new) - -**Actions:** -- [ ] Create DTO class with properties: - - `?string $iban` - IBAN with spaces stripped for storage - - `?string $accountHolder` - Account holder name - - `?string $bankName` - Name of the bank (optional) - - `bool $sepaMandateAccepted = false` - SEPA mandate checkbox -- [ ] Add Symfony validation constraints: - - `@Assert\Iban()` for IBAN - - `@Assert\NotBlank()` for IBAN and account holder - - `@Assert\Length(min: 2, max: 70)` for account holder - - `@Assert\Length(max: 70)` for bank name - - `@Assert\IsTrue()` for SEPA mandate -- [ ] Add methods: - - `getFormattedIban()` - Returns IBAN with spaces for display - - `getIbanWithoutSpaces()` - Returns IBAN without spaces for storage/API - -**Notes:** -- Public properties (no constructor promotion needed for DTOs) -- BIC omitted (not required for German IBANs since 2016) -- Follow project DTO patterns - ---- - -#### Task 1.2: Create PaymentDto -**Files:** -- `src/Form/Model/PaymentDto.php` (new) - -**Actions:** -- [ ] Create DTO class with properties: - - `?string $paymentMethod` - 'transfer' or 'debit' - - `?BankAccountDto $bankAccount` - Bank account details (nullable) -- [ ] Add validation: - - `@Assert\Choice(choices: ['transfer', 'debit'])` - - `@Assert\Valid()` for bankAccount when debit selected -- [ ] Add method `requiresBankAccount(): bool` - returns true if debit -- [ ] Add getter/setter methods - -**Notes:** -- Bank account only required for direct debit -- Validation must be conditional based on payment method - ---- - -### Phase 2: Extend BookingCreateDto - -#### Task 2.1: Add Payment Property to BookingCreateDto -**Files:** -- `src/Form/Model/BookingCreateDto.php` - -**Actions:** -- [ ] Add property: `public PaymentDto $payment` -- [ ] Initialize in constructor: `$this->payment = new PaymentDto()` -- [ ] Ensure serialization works for session storage - -**Notes:** -- Payment data stored in session alongside participant data -- Must survive page navigation - ---- - -### Phase 3: Create Step 3 Form Types - -#### Task 3.1: Create BankAccountType -**Status:** ✅ Simplified - BIC omitted -**Files:** -- `src/Form/BankAccountType.php` (new) - -**Actions:** -- [ ] Create form type for `BankAccountDto` -- [ ] Add fields: - - `iban` - TextType with formatting help text (monospace font) - - `accountHolder` - TextType (required) - - `bankName` - TextType (optional) - - `sepaMandateAccepted` - CheckboxType with rich label (SEPA text) -- [ ] Add data transformer for IBAN: - - Strip spaces from IBAN on submit - - Format IBAN with spaces for display -- [ ] Add help text with IBAN format example: "DE12 3456 7890 1234 5678 90" -- [ ] Style SEPA mandate as prominent checkbox with simple legal text - -**Notes:** -- Use monospace font for IBAN field -- Add inline validation feedback -- SEPA text: Simple generic authorization text (no Creditor ID needed) - ---- - -#### Task 3.2: Create PaymentType -**Files:** -- `src/Form/PaymentType.php` (new) - -**Actions:** -- [ ] Create form type for `PaymentDto` -- [ ] Add field: `paymentMethod` - ChoiceType (expanded radios) - - Choice 1: 'transfer' → "Überweisung" - - Choice 2: 'debit' → "Lastschrift" -- [ ] Add field: `bankAccount` - BankAccountType (conditional) -- [ ] Add form events to show/hide bank account fields: - - `PRE_SET_DATA` - Add bank account if debit selected - - `PRE_SUBMIT` - Add bank account if debit submitted -- [ ] Add JavaScript/HTMX to toggle bank account section visibility - -**Notes:** -- Bank account section hidden when transfer selected -- Use Stimulus controller for toggle behavior -- Smooth transition when switching payment methods - ---- - -#### Task 3.3: Create BookingCreateStep3Type -**Files:** -- `src/Form/BookingCreateStep3Type.php` (new) - -**Actions:** -- [ ] Create form type for `BookingCreateDto` (uses payment property) -- [ ] Add field: `payment` - PaymentType -- [ ] Set data_class to `BookingCreateDto::class` -- [ ] Add validation groups: `['payment']` - -**Notes:** -- Follows same pattern as Step1Type and Step2Type -- Validates only payment-related fields - ---- - -### Phase 4: Create Step 3 Controller - -#### Task 4.1: Create CreateStep3Controller -**Files:** -- `src/Controller/Booking/CreateStep3Controller.php` (new) - -**Actions:** -- [ ] Create controller with two actions: - - `step3()` - Display form (GET) - - `processStep3()` - Process form (POST) -- [ ] Load `BookingCreateDto` from session -- [ ] Create form with `BookingCreateStep3Type` -- [ ] On valid submission: - - Update session with payment data - - Redirect to confirmation/summary page (or direct to API submission) -- [ ] Add "Back to Step 2" button -- [ ] Add route: `/booking/create/step-3` -- [ ] Add access control: `@IsGranted('IS_AUTHENTICATED_ANONYMOUSLY')` - -**Notes:** -- Session key: same as Step 1/2 (`booking_create_dto`) -- Validate that Steps 1 & 2 are complete before showing Step 3 -- Clear session on final submission - ---- - -### Phase 5: Create Step 3 Template - -#### Task 5.1: Create step_3.html.twig -**Files:** -- `templates/booking/create_step_3.html.twig` (new) - -**Actions:** -- [ ] Create template extending base layout -- [ ] Add progress indicator (Step 1 → Step 2 → **Step 3** → Confirmation) -- [ ] Add payment method selection with clear labels -- [ ] Add conditional bank account section: - - Hidden by default - - Shows when direct debit selected - - Smooth CSS transition -- [ ] Add form with: - - Payment method radios (large, clear) - - Bank account fields (conditional) - - SEPA mandate checkbox with legal text - - "Back" and "Continue" buttons -- [ ] Add Stimulus controller for payment method toggle -- [ ] Add inline validation feedback -- [ ] Add help text for IBAN format examples - -**Notes:** -- Clear visual hierarchy: payment method choice prominent -- Bank account section visually grouped -- SEPA text: "Ich ermächtige [Company] Zahlungen von meinem Konto mittels Lastschrift einzuziehen..." - ---- - -### Phase 6: Update Navigation Flow - -#### Task 6.1: Update CreateStep2Controller -**Files:** -- `src/Controller/Booking/CreateStep2Controller.php` - -**Actions:** -- [ ] Change successful submission redirect: - - FROM: Confirmation page - - TO: `app_booking_create_step_3` -- [ ] Keep session data intact (don't clear) - -**Notes:** -- Simple redirect change -- Session persistence already in place - ---- - -#### Task 6.2: Add Navigation Helpers -**Files:** -- `src/Service/BookingSessionService.php` (new, optional) - -**Actions:** -- [ ] Create service to manage booking session state (optional) -- [ ] Add methods: - - `getBookingDto(): ?BookingCreateDto` - - `saveBookingDto(BookingCreateDto $dto): void` - - `clearBookingSession(): void` - - `validateStepAccess(int $step): bool` - Check prerequisite steps -- [ ] Inject into controllers - -**Notes:** -- Optional improvement for cleaner controller code -- Can be deferred if time-constrained - ---- - -### Phase 7: Add Validation & Error Handling - -**Status:** ✅ Simplified - Using Symfony's built-in validators only - -**Notes:** -- Symfony's `@Assert\Iban` is sufficient for IBAN validation -- No custom validators needed -- German error messages configured in translation files - ---- - -### Phase 8: Add SEPA Mandate Text Management - -#### Task 8.1: Create SEPA Mandate Template -**Files:** -- `templates/booking/_sepa_mandate_text.html.twig` (new) - -**Actions:** -- [ ] Create reusable template partial with SEPA mandate text -- [ ] Include variables: - - Company name - - Creditor ID (SEPA Gläubiger-ID) - - Mandate reference (generated per booking) -- [ ] Format as legal text with proper line breaks -- [ ] Add checkbox label referencing this text - -**Notes:** -- Text must be legally compliant -- Consult legal team for exact wording -- Consider making mandate reference visible to user - ---- - -### Phase 9: Frontend Enhancements - -#### Task 9.1: Create Payment Toggle Stimulus Controller -**Files:** -- `assets/controllers/payment_toggle_controller.js` (new) - -**Actions:** -- [ ] Create Stimulus controller to toggle bank account visibility -- [ ] Listen to payment method radio change -- [ ] Show/hide bank account section with smooth transition -- [ ] Clear bank account fields when switching to transfer -- [ ] Add/remove required attributes dynamically - -**Notes:** -- Use CSS transitions for smooth UX -- Ensure accessibility (aria attributes) -- Works without JavaScript (progressive enhancement) - ---- - -#### Task 9.2: Add IBAN Formatting Helper -**Files:** -- `assets/controllers/iban_formatter_controller.js` (new) - -**Actions:** -- [ ] Create Stimulus controller for IBAN input -- [ ] Format IBAN with spaces as user types: `DE12 3456 7890 1234 5678 90` -- [ ] Strip spaces on submit -- [ ] Show character count/validation status -- [ ] Use monospace font for input - -**Notes:** -- Real-time formatting improves UX -- Visual feedback for correct format -- Consider using library: `iban-formatter` - ---- - -### Phase 10: Testing - -#### Task 10.1: Create Unit Tests -**Files:** -- `tests/Form/Model/BankAccountDtoTest.php` (new) -- `tests/Form/Model/PaymentDtoTest.php` (new) - -**Actions:** -- [ ] Test IBAN validation (valid/invalid formats) -- [ ] Test conditional validation (bank account required for debit) -- [ ] Test SEPA mandate validation -- [ ] Test account holder name validation - -**Notes:** -- Cover edge cases: empty, malformed, international IBANs -- Test both valid and invalid inputs - ---- - -#### Task 10.2: Create Integration Tests -**Files:** -- `tests/Controller/Booking/CreateStep3ControllerTest.php` (new) - -**Actions:** -- [ ] Test form display -- [ ] Test transfer selection (no bank account) -- [ ] Test debit selection (requires bank account) -- [ ] Test validation errors -- [ ] Test session persistence -- [ ] Test navigation (back to Step 2) - -**Notes:** -- Use test client to simulate user interaction -- Verify session state after each action - ---- - -### Phase 11: Update API Submission - -#### Task 11.1: Update BookingDataProcessor -**Status:** ✅ Simplified - BIC omitted from API payload -**Files:** -- `src/BusProNet/DataProcessor/BookingDataProcessor.php` - -**Actions:** -- [ ] Update `createBookingRequestPayload()` method (when implemented in Phase 5) -- [ ] Add payment data to API payload: - - Payment method (`zahlart`) - - Bank account details (if debit): - - IBAN (`iban`) - - Account holder (`kontoinhaber`) - - Bank name (`bankname`) - optional -- [ ] Map payment method to API codes: - - 'transfer' → 'UE' (or appropriate API code) - - 'debit' → 'LS' (or appropriate API code) - -**Notes:** -- BIC omitted (not required for SEPA since 2016) -- Check BPN API documentation for exact field names -- SEPA mandate reference may need to be generated - ---- - -#### Task 11.2: Store SEPA Mandate Reference -**Files:** -- `src/Service/SepaMandateService.php` (new) - -**Actions:** -- [ ] Create service to generate unique SEPA mandate references -- [ ] Format: `[PREFIX]-[BOOKING_ID]-[TIMESTAMP]` -- [ ] Store reference in booking data -- [ ] Make available for PDF invoice generation - -**Notes:** -- Mandate reference must be unique and traceable -- Consider using UUID or sequential ID - ---- - -## Database Considerations - -### SEPA Mandate Storage - -**Option 1: Store in Booking API Data** -- Mandate reference sent to BPN API -- Stored in BPN system -- Retrieved with booking data - -**Option 2: Local Database Table** -- Create `sepa_mandates` table -- Store: mandate_id, booking_id, iban, date_signed, status -- Allows local tracking and reporting - -**Recommendation:** Start with Option 1 (API storage), add Option 2 if needed for compliance/reporting - ---- - -## Security Considerations - -1. **HTTPS Required:** Bank details must be transmitted over HTTPS only -2. **Session Security:** Ensure session data encrypted -3. **Input Sanitization:** Strip/validate all bank account inputs -4. **CSRF Protection:** Ensure form has CSRF token -5. **Rate Limiting:** Prevent brute force on payment form -6. **Audit Trail:** Log payment method changes - ---- - -## UX Considerations - -1. **Clear Labels:** "Überweisung" and "Lastschrift" with explanations -2. **Visual Feedback:** Show selected payment method prominently -3. **Inline Validation:** Real-time IBAN format checking -4. **Error Messages:** Clear, actionable German error messages -5. **Help Text:** Examples of valid IBAN formats -6. **Progress Indicator:** Show user is on Step 3 of 4 -7. **Mobile Friendly:** Large touch targets for payment method selection - ---- - -## Error Handling - -### Validation Errors -- Show inline next to field -- Highlight invalid fields in red -- Provide clear guidance on how to fix - -### Session Errors -- If session expired, redirect to Step 1 with message -- Preserve as much data as possible - -### API Errors -- If payment method not accepted by API, show clear error -- Suggest alternative payment method - ---- - -## Localization - -All text in German: -- **Überweisung:** Direct transfer -- **Lastschrift:** Direct debit -- **IBAN:** Internationale Bankkontonummer -- **BIC:** Bank Identifier Code -- **Kontoinhaber:** Account holder -- **SEPA-Mandat:** SEPA mandate -- **Bankname:** Bank name - -Error messages in German with clear instructions. - ---- - -## Dependencies - -### PHP Libraries -- `symfony/validator` (already installed) -- `symfony/form` (already installed) -- Consider: `iban-validation/iban` for enhanced IBAN validation (optional) - -### JavaScript Libraries -- Stimulus (already installed) -- Consider: Custom IBAN formatter or library - -### No new major dependencies required - ---- - -## Migration Path - -1. Implement in order: Phases 1 → 2 → 3 → 4 → 5 → 6 -2. Test each phase before moving to next -3. Phases 7-11 can be done in parallel after Phase 6 -4. Deploy behind feature flag initially (optional) - ---- - -## Open Questions - -1. **SEPA Creditor ID:** What is the company's SEPA Gläubiger-ID? -2. **Payment Method Codes:** What are the exact BPN API codes for transfer/debit? -3. **Mandate Reference Format:** Any specific format requirements? -4. **Invoice Generation:** Does invoice need to show SEPA mandate reference? -5. **Default Payment Method:** Should we pre-select one method? -6. **International IBANs:** Support only German IBANs or EU-wide? - ---- - -## Success Criteria - -- [ ] User can select payment method -- [ ] Bank account fields shown only for direct debit -- [ ] IBAN validated correctly -- [ ] SEPA mandate text displayed and accepted -- [ ] Payment data stored in session -- [ ] Payment data submitted to API -- [ ] Session cleared after successful booking -- [ ] All validation works (unit + integration tests) -- [ ] Mobile-friendly UI -- [ ] Accessible (keyboard navigation, screen readers) - ---- - -## Timeline Estimate - -- **Phase 1-2:** DTOs & Validation - 2 hours -- **Phase 3:** Form Types - 2 hours -- **Phase 4:** Controller - 1 hour -- **Phase 5:** Template - 2 hours -- **Phase 6:** Navigation - 0.5 hour -- **Phase 7:** Custom Validators - 1 hour (optional) -- **Phase 8:** SEPA Text - 0.5 hour -- **Phase 9:** Frontend - 2 hours -- **Phase 10:** Testing - 2 hours -- **Phase 11:** API Integration - 1 hour - -**Total:** ~14 hours (can be reduced if some phases skipped/simplified) - ---- - -## References - -- SEPA Direct Debit Scheme: https://www.europeanpaymentscouncil.eu/ -- IBAN Validation: https://en.wikipedia.org/wiki/International_Bank_Account_Number -- Symfony Form Events: https://symfony.com/doc/current/form/events.html -- Symfony Validation: https://symfony.com/doc/current/validation.html - ---- - -**End of Document** \ No newline at end of file diff --git a/docs/BOOKING_SUBMISSION_IMPLEMENTATION.md b/docs/BOOKING_SUBMISSION_IMPLEMENTATION.md deleted file mode 100644 index 980bc7b..0000000 --- a/docs/BOOKING_SUBMISSION_IMPLEMENTATION.md +++ /dev/null @@ -1,771 +0,0 @@ -# Booking Submission Implementation Guide - -## Overview - -This document describes the complete implementation of the two-phase booking submission system for the CREATE booking flow in MyEP Next Booking. - -**Implementation Date:** 2025-10-06 -**Status:** ✅ TESTED SUCCESSFULLY (2025-10-06) -**Related Files:** See "Files Modified/Created" section below - -## Architecture - -### Two-Phase Submission Flow - -The booking submission uses a two-phase commit pattern for safety and validation: - -1. **Phase 1: Inquiry (Anfrage) - Step 3** - - Executed at the end of payment method selection (Step 3) - - Validates all booking data with BusProNet API - - Returns pricing information for validation - - Compares API total price with calculated price (exact match required) - - No permanent changes made - - Request payload: `buchungsart => 'Anfrage'` - - Response status: `möglich` indicates valid - - Blocks progression to Step 4 if validation fails or prices don't match - -2. **Phase 2: Booking (Buchung) - Step 4** - - Executed when user confirms booking on Step 4 - - Creates actual booking in BPN system (already validated in Step 3) - - Returns transaction number (Vorgangsnummer) - - Request payload: `buchungsart => 'Buchung'` - - Response status: `erfolgt` indicates success - - Fast execution (no re-validation needed) - -### Request Payload Structure - -The payload generation follows the participant-centric data structure established in the CREATE flow: - -**Key Characteristics:** -- Services grouped by ID with participant assignments -- 1-based participant indexing (API requirement) -- Comma-separated participant lists in `zuordnung` attribute -- Includes ALL service types: transportation, rooms, additional services, pickups, insurances, parking -- Participant wishes (room remarks, license plate) included in `` section -- Agency ID resolution with fallback to default agency (code '0001') -- Price validation: API total must match calculated total exactly (1:1) - -**Service Grouping Example:** -```xml - - - - -``` - -**Full XML Structure:** -```xml - - USERNAME - HASH - - Anfrage|Buchung - F - 12345 - - - Herr - Max - Mustermann - Musterstraße 1 - 12345 - Musterstadt - max@example.com - +49123456789 - - - - - Herr - Max - Mustermann - 1990-01-01 - - - - - - - - - - - - - - - - - - - - - - - - - - 2|5 - Max Mustermann - DE89370400440532013000 - - -``` - -### Response Structure - -**Success Response:** -```xml - - - möglich|erfolgt - 321530 - - - - - 1500.00 - - - - - -``` - -**Error Response:** -```xml - - - Error message here - -``` - -## Implementation Details - -### 1. Response Models - -**File:** `src/BusProNet/Model/BookingResponse.php` - -```php -status; - } - - public function isBookingSuccessful(): bool - { - return 'erfolgt' === $this->status; - } -} -``` - -**File:** `src/BusProNet/Model/PriceItem.php` - -Individual price item from response for validation against calculated prices. - -```php -` node -- Transaction number from `` node -- All price items from `` nodes -- Total price from `` node -- Payment terms from `` node - -```php -getTextOrNull($node, 'buchung') ?? ''; - $transactionNumber = $this->getTextOrNull($node, 'vorgang'); - $priceItems = $this->parsePriceItems($node); - $totalPrice = $this->getFloatOrNull($node, 'gesamtpreis'); - $paymentTerms = $this->parsePaymentTerms($node); - - return new BookingResponse( - status: $status, - transactionNumber: $transactionNumber, - priceItems: $priceItems, - totalPrice: $totalPrice, - paymentTerms: $paymentTerms - ); - } - - private function parsePriceItems(Crawler $node): array - { - $priceItems = []; - $node->filterXPath('//preise/preis')->each(function (Crawler $priceNode) use (&$priceItems): void { - $priceItems[] = new PriceItem( - position: (int) $priceNode->attr('position'), - type: $priceNode->attr('art'), - subType: $priceNode->attr('unterart'), - label: $priceNode->attr('bezeichnung'), - dateFrom: $this->parseDate($priceNode->attr('datumvon')), - dateTo: $this->parseDate($priceNode->attr('datumbis')), - quantity: (int) $priceNode->attr('anzahl'), - assignment: $priceNode->attr('zuordnung'), - unitPrice: (float) $priceNode->attr('einzelpreis'), - totalPrice: (float) $priceNode->attr('gesamtpreis'), - id: $priceNode->attr('id') ? (int) $priceNode->attr('id') : null - ); - }); - - return $priceItems; - } - - private function parsePaymentTerms(Crawler $node): ?PaymentTerms - { - $termsNode = $node->filterXPath('//zahlungsbedingungen'); - if (0 === $termsNode->count()) { - return null; - } - - $depositAmount = (float) $termsNode->filterXPath('//anzahlung')->attr('betrag'); - $depositDate = $this->parseDate($termsNode->filterXPath('//anzahlung')->attr('datum')); - $finalAmount = (float) $termsNode->filterXPath('//restzahlung')->attr('betrag'); - $finalDate = $this->parseDate($termsNode->filterXPath('//restzahlung')->attr('datum')); - - if (null === $depositDate || null === $finalDate) { - return null; - } - - return new PaymentTerms( - depositAmount: $depositAmount, - depositDate: $depositDate, - finalPaymentAmount: $finalAmount, - finalPaymentDate: $finalDate - ); - } -} -``` - -### 3. Payload Generation - -**File:** `src/BusProNet/DataProcessor/BookingDataProcessor.php` - -**Main Method:** -```php -public function createBookingRequestPayload( - BookingCreateDto $bookingDto, - string $bookingType -): array -``` - -**Helper Methods:** -- `collectServiceMappings()` - Groups services by ID -- `collectTransportationMappings()` - Groups transportation services -- `collectRoomMappings()` - Groups room assignments -- `collectPickupMappings()` - Groups pickup locations -- `collectInsuranceMappings()` - Groups insurance selections -- `addServicesFromMap()` - Generic XML structure builder - -**Critical Implementation Details:** -- Participant indexing is 1-based (API requirement) -- Services grouped by ID with comma-separated participant assignments -- Insurance included in CREATE flow (unlike UPDATE flow) -- Payment type IDs: 2 for transfer, 5 for debit - -### 4. API Client Methods - -**File:** `src/BusProNet/ApiClient.php` - -**Constants Added:** -```php -public const TYPE_BOOKING = 'BUCHUNG'; -``` - -**Payment Type Constants (in Constants.php):** -```php -public const PAYMENT_TYPE_ID_TRANSFER = 2; -public const PAYMENT_TYPE_ID_DEBIT = 5; -``` - -**Methods Added:** -```php -public function createBookingInquiry( - BookingCreateDto $bookingDto, - bool $debug = false -): Notification|BookingResponse -{ - $payload = (new BookingDataProcessor())->createBookingRequestPayload($bookingDto, 'Anfrage'); - $data = [ - 'user' => $this->config['bpn_username'], - 'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_BOOKING), - 'satz' => ['@typ' => static::TYPE_BOOKING], - ...$payload, - ]; - - return $this->sendRequest(static::TYPE_BOOKING, $data, [], $debug); -} - -public function createBooking( - BookingCreateDto $bookingDto, - bool $debug = false -): Notification|BookingResponse -{ - $payload = (new BookingDataProcessor())->createBookingRequestPayload($bookingDto, 'Buchung'); - $data = [ - 'user' => $this->config['bpn_username'], - 'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_BOOKING), - 'satz' => ['@typ' => static::TYPE_BOOKING], - ...$payload, - ]; - - return $this->sendRequest(static::TYPE_BOOKING, $data, [], $debug); -} -``` - -Both methods: -- Use `BookingDataProcessor::createBookingRequestPayload()` -- Return either `Notification` (error) or `BookingResponse` (success) -- Support debug mode for XML dumping - -### 5. Response Routing - -**File:** `src/BusProNet/XmlParser/ApiResponseParser.php` - -Added routing for `TYPE_BOOKING` responses: -```php -case ApiClient::TYPE_BOOKING: - return (new BookingResponseParser())->parse($resultNode); -``` - -Error responses still return `Notification` objects via existing error handling. - -### 6. Controller Logic - -#### Step 3: Validation with Price Check - -**File:** `src/Controller/Booking/CreateStep3Controller.php` - -**Dependencies Injected:** -- `BookingService` - Session management -- `ApiClient` - API communication -- `BookingPriceCalculatorService` - Price calculation -- `LoggerInterface` - Error logging - -**Form Submission Flow:** -```php -if ($form->isSubmitted() && $form->isValid()) { - // Call inquiry API to validate booking - $inquiryResponse = $this->apiClient->createBookingInquiry($bookingCreateDto); - - if ($inquiryResponse instanceof Notification || !$inquiryResponse->isInquiryValid()) { - // Handle validation failure - } - - // Compare API price with calculated price (exact match required) - $apiTotal = $inquiryResponse->totalPrice ?? 0.0; - $calculatedTotal = $this->priceCalculator->calculateGrandTotal($bookingCreateDto); - - if ($apiTotal !== $calculatedTotal) { - $this->logger->error('Price mismatch detected - payload incomplete', [ - 'apiTotal' => $apiTotal, - 'calculatedTotal' => $calculatedTotal, - 'difference' => abs($apiTotal - $calculatedTotal), - ]); - $this->addFlash('error', 'Ein technischer Fehler ist aufgetreten.'); - return; // Block progression to Step 4 - } - - // Proceed to Step 4 (confirmation) - $bookingCreateDto->currentStep = 4; - return $this->redirectToRoute('app_booking_create_step_4'); -} -``` - -**Price Validation Logic:** -- Exact match required: `$apiTotal !== $calculatedTotal` -- No tolerance for rounding differences -- Mismatch indicates missing service in payload -- Logs full context for debugging - -#### Step 4: Final Booking Submission - -**File:** `src/Controller/Booking/CreateStep4Controller.php` - -**Dependencies Injected:** -- `BookingService` - Session management -- `ApiClient` - API communication -- `LoggerInterface` - Error logging - -**Form Submission Flow:** -```php -if ($form->isSubmitted() && $form->isValid()) { - try { - // Submit final booking (already validated in Step 3) - $bookingResponse = $this->apiClient->createBooking($bookingCreateDto); - - if ($bookingResponse instanceof Notification) { - $this->addFlash('error', $bookingResponse->message); - return $this->render('booking/create_step_4.html.twig', [...]); - } - - if (false === $bookingResponse->isBookingSuccessful()) { - $this->addFlash('error', 'Buchung konnte nicht erstellt werden.'); - return $this->render('booking/create_step_4.html.twig', [...]); - } - - // Success: Store booking number in flash and clear session - $this->addFlash('booking_number', $bookingResponse->transactionNumber); - $this->bookingService->clearBookingCreateDto($request); - - return $this->redirectToRoute('app_booking_success'); - } catch (\Exception $e) { - $this->logger->error('Booking creation failed', [...]); - $this->addFlash('error', 'Ein technischer Fehler ist aufgetreten.'); - return $this->render('booking/create_step_4.html.twig', [...]); - } -} -``` - -**Benefits:** -- Step 3: Validates early, catches payload errors before confirmation -- Step 4: Fast execution, no validation delay -- User experience: Reduced wait time on final submission - -**Error Handling:** -- API errors: Display `Notification::message` to user -- Validation failures: Display generic error message -- Price mismatch: Log detailed context, block with generic error -- Unexpected exceptions: Log full trace and display generic error - -### 7. Service Layer - -**File:** `src/Service/BookingService.php` - -**Method Added:** -```php -/** - * Clears the booking creation DTO from the session. - * - * This method removes only the booking DTO while preserving other session data. - * Used after successful booking submission to clear the booking flow state. - */ -public function clearBookingCreateDto(Request $request): void -{ - $request->getSession()->remove(self::BOOKING_CREATE_KEY); -} -``` - -Clears only the booking DTO (not baseline snapshot) after successful submission. - -### 8. Success Page - -**Controller:** `src/Controller/Booking/BookingSuccessController.php` - -```php -getSession()->getFlashBag()->get('booking_number')[0] ?? null; - - // Redirect to homepage if no booking number (direct access or refresh) - if (null === $bookingNumber) { - return $this->redirectToRoute('app_home'); - } - - return $this->render('booking/success.html.twig', [ - 'bookingNumber' => $bookingNumber, - ]); - } -} -``` - -**Implementation Details:** -- Booking number passed via flash message (not URL parameter) -- Flash message automatically cleared after first display -- Direct access or page refresh redirects to homepage -- Clean URL: `/bookings/success` (no sensitive data in URL) -- No persistent browser history with booking numbers - -**Template:** `templates/booking/success.html.twig` - -Displays: -- Success icon (green checkmark) -- Confirmation message -- Booking number (monospace font for easy copying) -- Information about email confirmation -- Link back to homepage - -## Testing Strategy - -### Integration Testing - -**Test Scenarios:** - -1. **Successful Booking:** - - Complete all 4 steps - - Submit confirmation form - - Verify inquiry call made - - Verify booking call made - - Verify redirect to success page - - Verify session cleared - -2. **Inquiry Validation Failure:** - - Submit invalid data - - Verify inquiry returns error - - Verify booking NOT called - - Verify user sees error message - - Verify session NOT cleared - -3. **Booking Commit Failure:** - - Inquiry succeeds but booking fails - - Verify appropriate error handling - - Verify session NOT cleared - -4. **API Error Response:** - - API returns Notification - - Verify error message displayed - - Verify session NOT cleared - -### Sandbox Testing - -**Prerequisites:** -- DDEV environment running -- BPN sandbox credentials configured in `.env.local` -- Valid travel data available - -**Test Checklist:** -- [x] Single participant booking - ✅ PASSED -- [x] Multiple participants booking - ✅ PASSED -- [x] All service types selected - ✅ PASSED (transportation, rooms, services, pickups, insurances) -- [x] Insurance selection - ✅ PASSED -- [x] Both payment methods - ✅ TRANSFER TESTED (debit not tested) -- [x] Applicant address mandatory - ✅ PASSED -- [x] Dependent participant address optional - ✅ PASSED -- [x] Email mandatory for all - ✅ PASSED -- [x] Mobile mandatory for applicant - ✅ PASSED -- [x] Room quantity matches step 1 - ✅ PASSED -- [x] Pickup location included - ✅ PASSED -- [x] Two-phase submission - ✅ PASSED (inquiry → booking) -- [x] Session cleared on success - ✅ PASSED -- [x] Success page with booking number - ✅ PASSED - -## Files Modified/Created - -### Created Files: -- `src/BusProNet/Model/BookingResponse.php` -- `src/BusProNet/Model/PriceItem.php` -- `src/BusProNet/Model/PaymentTerms.php` -- `src/BusProNet/Model/Agency.php` -- `src/BusProNet/XmlParser/BookingResponseParser.php` -- `src/BusProNet/XmlParser/AgencyParser.php` -- `src/BusProNet/XmlLoader/AgencyLoader.php` -- `src/Controller/Booking/BookingSuccessController.php` -- `templates/booking/success.html.twig` -- `tests/BusProNet/XmlParser/AgencyParserTest.php` -- `docs/BOOKING_SUBMISSION_IMPLEMENTATION.md` (this file) -- `docs/BOOKING_SUBMISSION_STATUS.md` -- `docs/REFACTORING_BOOKING_DATA_PROCESSOR.md` - -### Modified Files: -- `src/BusProNet/Constants.php` - Added payment type ID constants -- `src/BusProNet/DataProcessor/BookingDataProcessor.php` - Added `createBookingRequestPayload()` with parking and wishes section -- `src/BusProNet/ApiClient.php` - Added `TYPE_BOOKING`, `TYPE_AGENCIES`, `createBookingInquiry()`, `createBooking()`, `getAgencies()` -- `src/BusProNet/XmlParser/ApiResponseParser.php` - Added BUCHUNG and AGENTUREN routing -- `src/Controller/Booking/CreateStep3Controller.php` - Added inquiry API call with price validation -- `src/Controller/Booking/CreateStep4Controller.php` - Simplified to direct booking submission (validation moved to Step 3) -- `src/Controller/Booking/CreateInitController.php` - Added agency resolution with optional query parameter -- `src/Service/BookingService.php` - Added `clearBookingCreateDto()`, agency ID parameter in `startFreshBooking()` -- `src/Form/Model/BookingCreateDto.php` - Added `agencyId` property - -## Known Limitations - -1. **Price Validation:** - - ~~Pricing data is parsed but not automatically validated against calculated prices~~ ✅ IMPLEMENTED - - Exact price match validation implemented in Step 3 - -2. **Email/Password Fields:** - - Response contains `` and `` fields that are not currently parsed - - Can be added if needed for confirmation emails - -3. **Update Flow Refactoring:** - - UPDATE flow still uses different payload structure - - Future refactoring documented in `REFACTORING_BOOKING_DATA_PROCESSOR.md` - -4. **Contact Information:** - - Phone number (mobile) is mandatory for the applicant only - - Email is mandatory for all participants - -## Future Enhancements - -1. **Price Validation:** - - ~~Compare `$bookingResponse->totalPrice` with `BookingPriceCalculatorService` result~~ ✅ IMPLEMENTED - - ~~Warn if discrepancy detected~~ ✅ BLOCKS PROGRESSION - -2. **Email Confirmation:** - - Parse email/password fields from response - - Send custom confirmation email - - Include PDF password in email - -3. **Transaction Logging:** - - Log all inquiry/booking requests with responses - - Facilitate debugging and audit trail - -4. **Retry Logic:** - - Handle transient API failures - - Implement exponential backoff - -5. **Price Item Validation:** - - Compare individual price items with selections - - Detect unexpected charges - -## Troubleshooting - -### Issue: Inquiry succeeds but booking fails - -**Symptoms:** User sees error after successful validation - -**Debugging:** -1. Check application logs for exception details -2. Enable API debug mode to dump XML -3. Verify data hasn't changed between calls -4. Check BPN API logs in admin panel - -### Issue: Session cleared prematurely - -**Symptoms:** User redirected to init page - -**Debugging:** -1. Verify `clearBookingCreateDto()` only called after successful booking -2. Check for duplicate form submissions -3. Verify error handling re-renders without clearing session - -## References - -- BusProNet API Documentation: `docs/Beschreibung XMLAnfrage.pdf` -- Example Request Payload: `scratch_113.xml` -- Example Response: `scratch_111.xml` (with pricing), `scratch_112.xml` (minimal) -- Payment Step Implementation: `docs/BOOKING_PAYMENT_STEP.md` -- Refactoring Plan: `REFACTORING_BOOKING_DATA_PROCESSOR.md` -- Implementation Status: `BOOKING_SUBMISSION_STATUS.md` - ---- - -**Implementation Status:** ✅ TESTED SUCCESSFULLY -**Code Quality:** ✅ PHP-CS-Fixer validated, syntax checked -**Test Date:** 2025-10-06 -**Next Step:** Improvements and UPDATE flow refactoring (see REFACTORING_BOOKING_DATA_PROCESSOR.md) - -## Test Results Summary - -**Test Date:** 2025-10-06 -**Environment:** DDEV sandbox with BusProNet API - -**Successful Test Booking:** -- 2 participants with complete data -- All service types: transportation, rooms, additional services, pickups, insurance, parking -- Address validation working (mandatory for applicant, optional for others) -- Contact info validation working (email for all, mobile for applicant) -- Room quantity correctly using step 1 selections -- Two-phase submission successful (inquiry → booking) -- Session cleared after success -- Success page displaying booking number -- Agency ID resolved from optional query parameter with fallback to default (code '0001') -- Participant wishes (room remarks, license plate) included in payload - -**Bugs Fixed During Testing:** -1. Room quantity using participant count → Fixed to use roomSelections[].quantity -2. Insurance selection reset on dependent participants → Fixed bulk handler clearing logic -3. Pickup quantity issues → Simplified to use only outbound pickups -4. Missing contact info for non-applicants → Removed applicant-only restriction -5. Parking service not included in payload → Added to collectServiceMappings() -6. License plate and room remarks not submitted → Added wünsche section to participant payload - -**Result:** ✅ All critical features working correctly \ No newline at end of file diff --git a/docs/BOOKING_SUBMISSION_STATUS.md b/docs/BOOKING_SUBMISSION_STATUS.md deleted file mode 100644 index f8046e8..0000000 --- a/docs/BOOKING_SUBMISSION_STATUS.md +++ /dev/null @@ -1,559 +0,0 @@ -# Booking Submission Implementation Status - -## Overview - -Implementation of two-phase booking submission for the booking creation flow. This allows users to create new bookings through inquiry validation followed by final booking commit. - -**Status:** ✅ 100% Complete - TESTED SUCCESSFULLY -**Last Updated:** 2025-10-06 -**First Successful Test Booking:** 2025-10-06 -**Related Documentation:** -- `docs/BOOKING_PAYMENT_STEP.md` - Payment step implementation -- `docs/REFACTORING_BOOKING_DATA_PROCESSOR.md` - Future refactoring plan - -## Architecture Decision - -**Participant-Centric Structure:** The CREATE flow uses a cleaner participant-centric data structure where services are attached directly to participants in the DTO, not centralized with mapping arrays. This is the new standard. - -**UPDATE Flow:** Currently uses a different structure (centralized services with mappings). Future refactoring will align it with the CREATE flow's participant-centric approach. - -## Completed Work (80%) - -### 1. Response Models ✅ - -**Created Files:** -- `src/BusProNet/Model/BookingResponse.php` -- `src/BusProNet/Model/PriceItem.php` -- `src/BusProNet/Model/PaymentTerms.php` - -**BookingResponse:** -- Represents API response from booking requests (inquiry or final) -- Properties: `status`, `transactionNumber`, `priceItems`, `totalPrice`, `paymentTerms` -- Methods: `isInquiryValid()`, `isBookingSuccessful()` -- Handles both `möglich` (inquiry valid) and `erfolgt` (booking created) statuses - -**PriceItem:** -- Represents individual price items from response -- Properties: `position`, `type`, `subType`, `label`, `dateFrom`, `dateTo`, `quantity`, `assignment`, `unitPrice`, `totalPrice`, `id` -- Used for price validation against calculated prices - -**PaymentTerms:** -- Represents payment schedule from response -- Properties: `depositAmount`, `depositDate`, `finalPaymentAmount`, `finalPaymentDate` - -### 2. Response Parser ✅ - -**File:** `src/BusProNet/XmlParser/BookingResponseParser.php` - -**Functionality:** -- Extends `AbstractParser` -- Parses BUCHUNG type responses -- Extracts booking status from `` node -- Extracts transaction number from `` node -- Parses all price items from `` nodes -- Parses total price from `` node -- Parses payment terms from `` node - -**XML Structure Handled:** -```xml - - - möglich|erfolgt - 321530 - - - - 671,78 - - - - - -``` - -### 3. Constants ✅ - -**File:** `src/BusProNet/Constants.php` - -**Added:** -- `PAYMENT_TYPE_ID_TRANSFER = 2` - Payment type ID for bank transfer -- `PAYMENT_TYPE_ID_DEBIT = 5` - Payment type ID for direct debit - -### 4. Payload Generation ✅ - -**File:** `src/BusProNet/DataProcessor/BookingDataProcessor.php` - -**New Method:** `createBookingRequestPayload(BookingCreateDto $bookingDto, string $bookingType): array` - -**Functionality:** -- Generates XML payload for new bookings (inquiry or final) -- Supports both 'Anfrage' (inquiry) and 'Buchung' (final booking) modes -- Participant-centric structure (services attached to participants) -- Includes ALL service types: board, ski passes, rentals, courses, additional services, transportation, pickups -- **INCLUDES INSURANCE** (critical difference from update flow) -- Proper room mapping via `assignedRoomId` -- Payment information with correct type IDs - -**Helper Methods:** -- `addServicesFromMap()` - Reusable helper for converting service maps to XML structure -- `collectServiceMappings()` - Groups all participant services by ID -- `collectTransportationMappings()` - Groups transportation services -- `collectRoomMappings()` - Groups room assignments -- `collectPickupMappings()` - Groups pickup selections -- `collectInsuranceMappings()` - Groups insurance selections (CREATE only!) - -**Payload Structure:** -```php -[ - 'buchungsart' => 'Anfrage|Buchung', - 'status' => 'F', - 'idreise' => $travelId, - 'anmelder' => [ - 'name' => '...', - 'vorname' => '...', - 'geschlecht' => '...', - 'nationalitaet' => '...', - 'geburtsdatum' => '...', - 'kommunikation' => ['email' => '...', 'telefonmobil' => '...'], - ], - 'teilnehmerliste' => ['teilnehmer' => [...]], - 'beförderungen' => ['beförderung' => [...]], - 'unterbringungen' => ['unterbringung' => [...]], - 'zusatzleistungen' => ['zusatzleistung' => [...]], - 'zustiege' => ['zustieg' => [...]], - 'versicherungen' => ['versicherung' => [...]], // CREATE only! - 'zahlung' => [ - '@idzahlungsart' => 2|5, - '@art' => 'EINZUG|UEBERWEISUNG', - 'bankverbindung' => [...], // if debit - ], -] -``` - -### 5. API Client Methods ✅ - -**File:** `src/BusProNet/ApiClient.php` - -**New Constant:** -- `TYPE_BOOKING = 'BUCHUNG'` - -**New Methods:** - -```php -public function createBookingInquiry( - BookingCreateDto $bookingDto, - bool $debug = false -): Notification|BookingResponse -``` -- First phase: validates booking data -- Returns pricing information -- Does not create actual booking - -```php -public function createBooking( - BookingCreateDto $bookingDto, - bool $debug = false -): Notification|BookingResponse -``` -- Second phase: creates actual booking -- Returns booking number (transaction number) -- Only called after successful inquiry - -**Both methods:** -- Use `BookingDataProcessor::createBookingRequestPayload()` -- Send request to BUCHUNG type endpoint -- Return `Notification` on error or `BookingResponse` on success -- Support debug mode for XML dumps - -### 6. Response Parser Integration ✅ - -**File:** `src/BusProNet/XmlParser/ApiResponseParser.php` - -**Updated:** -- Added case for `ApiClient::TYPE_BOOKING` -- Routes to `BookingResponseParser` -- Handles both inquiry and final booking responses - -## Remaining Work (0%) - -### 1. Controller Implementation ✅ - -**File:** `src/Controller/Booking/CreateStep4Controller.php` - -**Completed:** -- Imported `ApiClient` and injected via constructor -- Imported `LoggerInterface` and injected via constructor -- Implemented two-phase submission in form handler - -```php -if ($form->isSubmitted() && $form->isValid()) { - try { - // Phase 1: Inquiry (Validation) - $inquiryResponse = $this->apiClient->createBookingInquiry($bookingCreateDto); - - if ($inquiryResponse instanceof Notification) { - // API returned error notification - $this->addFlash('error', $inquiryResponse->message); - return $this->render(...); - } - - if (false === $inquiryResponse->isInquiryValid()) { - // Inquiry validation failed - $this->addFlash('error', 'Buchungsvalidierung fehlgeschlagen.'); - return $this->render(...); - } - - // Optional: Validate prices match our calculations - // Compare $inquiryResponse->totalPrice with calculated total - - // Phase 2: Booking (Commit) - $bookingResponse = $this->apiClient->createBooking($bookingCreateDto); - - if ($bookingResponse instanceof Notification) { - // API returned error notification - $this->addFlash('error', $bookingResponse->message); - return $this->render(...); - } - - if (false === $bookingResponse->isBookingSuccessful()) { - // Booking creation failed - $this->addFlash('error', 'Buchung konnte nicht erstellt werden.'); - return $this->render(...); - } - - // Success: Clear session and redirect - $this->bookingService->clearBookingCreateDto($request); - - return $this->redirectToRoute('app_booking_success', [ - 'bookingNumber' => $bookingResponse->transactionNumber, - ]); - - } catch (\Exception $e) { - $this->logger->error('Booking creation failed', [ - 'exception' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ]); - - $this->addFlash('error', 'Ein technischer Fehler ist aufgetreten.'); - return $this->render(...); - } -} -``` - -**Constructor Update:** -```php -public function __construct( - private readonly BookingService $bookingService, - private readonly ApiClient $apiClient, - private readonly LoggerInterface $logger, -) { -} -``` - -### 2. Service Layer ✅ - -**File:** `src/Service/BookingService.php` - -**Completed:** -```php -/** - * Clears the booking creation DTO from the session. - * - * This method removes only the booking DTO while preserving other session data. - * Used after successful booking submission to clear the booking flow state. - */ -public function clearBookingCreateDto(Request $request): void -{ - $request->getSession()->remove(self::BOOKING_CREATE_KEY); -} -``` - -### 3. Success Page ✅ - -**New Controller:** `src/Controller/Booking/BookingSuccessController.php` - -**Completed:** - -```php -render('booking/success.html.twig', [ - 'bookingNumber' => $bookingNumber, - ]); - } -} -``` - -**New Template:** `templates/booking/success.html.twig` - -```twig -{% extends 'layout.html.twig' %} - -{% block title %}Buchung erfolgreich{% endblock %} - -{% block content %} -
-
- - - -
- -

Buchung erfolgreich abgeschlossen

- -

- Ihre Buchungsnummer: {{ bookingNumber }} -

- -
-

- Sie erhalten in Kürze eine Bestätigungs-E-Mail mit allen Details zu Ihrer Buchung. -

-
- - - Zurück zur Startseite - -
-{% endblock %} -``` - -### 4. Testing ⏳ - -**Sandbox Testing Checklist:** -- [ ] Test inquiry phase with valid data -- [ ] Test inquiry phase with invalid data (validation errors) -- [ ] Test booking phase after successful inquiry -- [ ] Test booking phase failure scenarios -- [ ] Verify pricing data matches calculations -- [ ] Test with different service combinations: - - [ ] With insurance - - [ ] Without insurance - - [ ] With transportation services - - [ ] With pickup locations - - [ ] With all service types - - [ ] Minimum services only -- [ ] Test payment methods: - - [ ] Direct debit (14+ days before travel) - - [ ] Bank transfer - - [ ] Direct debit blocked (<14 days) -- [ ] Test session clearing -- [ ] Verify booking number display -- [ ] Test error handling -- [ ] Verify XML dumps in debug mode - -## Key Implementation Notes - -### Two-Phase Process - -1. **Phase 1: Inquiry (`buchungsart => 'Anfrage'`)** - - Validates all booking data - - Returns pricing information - - Response: `möglich` - - No actual booking created - -2. **Phase 2: Booking (`buchungsart => 'Buchung'`)** - - Creates actual booking - - Returns booking number - - Response: `erfolgt` - - Only proceed if Phase 1 succeeded - -### Error Handling - -**API Errors:** -- API may return `Notification` object instead of `BookingResponse` -- Check instanceof before accessing BookingResponse methods -- Display error message from notification - -**Validation Errors:** -- Check `isInquiryValid()` on inquiry response -- Check `isBookingSuccessful()` on booking response -- Display appropriate error messages - -**Network/System Errors:** -- Catch all exceptions -- Log with full trace -- Display generic error message to user -- Do NOT clear session on error (allow retry) - -### Price Validation (Optional) - -**Inquiry response includes:** -- Individual price items with quantities and assignments -- Total price from API -- Payment terms (deposit/final payment) - -**Can compare:** -- API total vs calculated total -- Individual service prices -- Participant assignments - -**Implementation:** -```php -if (abs($inquiryResponse->totalPrice - $calculatedTotal) > 0.01) { - $this->logger->warning('Price mismatch', [ - 'api_price' => $inquiryResponse->totalPrice, - 'calculated_price' => $calculatedTotal, - ]); - // Decide: continue or abort -} -``` - -### Session Management - -**Important:** -- Only clear session on successful booking -- Keep session on errors (allows retry) -- Clear using `BookingService::clearBookingCreateDto()` - -### Logging - -**Log events:** -- Inquiry submission (info level) -- Inquiry success/failure (info/error) -- Booking submission (info level) -- Booking success/failure (info/error) -- Price mismatches (warning) -- Exceptions (error with full trace) - -**Context to include:** -- Travel ID -- Participant count -- Total price -- Payment method -- Error messages -- Exception traces - -## Testing Strategy - -### Unit Tests (Future) - -**BookingResponseParser:** -- Test parsing successful inquiry response -- Test parsing successful booking response -- Test parsing price items -- Test parsing payment terms -- Test handling missing optional fields - -**BookingDataProcessor:** -- Test payload generation with all services -- Test payload generation with minimum services -- Test insurance inclusion -- Test payment methods -- Test participant mappings - -### Integration Tests (Future) - -**ApiClient:** -- Mock socket communication -- Test inquiry request format -- Test booking request format -- Test response parsing -- Test error handling - -**Controller:** -- Test two-phase submission flow -- Test error scenarios -- Test session clearing -- Test redirects - -### Manual Testing (Immediate) - -**Use sandbox environment:** -- Current ddev setup points to sandbox -- XML dumps enabled for debugging -- Test with real travel data -- Verify all email notifications - -## File Locations Summary - -**Models:** -- `src/BusProNet/Model/BookingResponse.php` -- `src/BusProNet/Model/PriceItem.php` -- `src/BusProNet/Model/PaymentTerms.php` - -**Parsers:** -- `src/BusProNet/XmlParser/BookingResponseParser.php` -- `src/BusProNet/XmlParser/ApiResponseParser.php` (updated) - -**Data Processing:** -- `src/BusProNet/DataProcessor/BookingDataProcessor.php` (enhanced) - -**API:** -- `src/BusProNet/ApiClient.php` (enhanced) -- `src/BusProNet/Constants.php` (enhanced) - -**Controllers (to be updated/created):** -- `src/Controller/Booking/CreateStep4Controller.php` (update) -- `src/Controller/Booking/BookingSuccessController.php` (create) - -**Services (to be updated):** -- `src/Service/BookingService.php` (add method) - -**Templates (to be created):** -- `templates/booking/success.html.twig` - -**Documentation:** -- `docs/BOOKING_SUBMISSION_STATUS.md` (this file) -- `docs/REFACTORING_BOOKING_DATA_PROCESSOR.md` -- `docs/BOOKING_PAYMENT_STEP.md` -- `docs/Beschreibung XMLAnfrage.pdf` (API documentation) - -## Next Steps - -1. **Immediate:** - - Implement controller logic (20 minutes) - - Add session clearing method (5 minutes) - - Create success page (10 minutes) - - Test with sandbox (30 minutes) - -2. **Short-term:** - - Price validation logic (optional) - - Enhanced error messages - - Email confirmation integration - - PDF generation - -3. **Long-term:** - - Refactor UPDATE flow to use participant-centric structure - - Comprehensive test suite - - Performance optimization - - Enhanced logging and monitoring - ---- - -**Status:** ✅ Implementation Complete - Ready for Sandbox Testing -**Estimated Time to Test:** 30-60 minutes -**Blockers:** None -**Dependencies:** All completed - -## Implementation Summary - -All coding tasks have been completed: - -1. ✅ **Response Models** - BookingResponse, PriceItem, PaymentTerms created with full pricing support -2. ✅ **Response Parser** - BookingResponseParser parses all XML response data including prices -3. ✅ **Payload Generation** - createBookingRequestPayload() with participant-centric structure and all service types -4. ✅ **API Client Methods** - createBookingInquiry() and createBooking() methods implemented -5. ✅ **Response Routing** - ApiResponseParser updated to handle BUCHUNG type -6. ✅ **Controller Logic** - Two-phase submission with comprehensive error handling in CreateStep4Controller -7. ✅ **Service Method** - clearBookingCreateDto() added to BookingService -8. ✅ **Success Page** - BookingSuccessController and success.html.twig template created -9. ✅ **Code Quality** - All files validated with PHP-CS-Fixer and syntax checking - -**Next Step:** Sandbox testing with real API calls diff --git a/docs/FIELD_STATE_SYSTEM.md b/docs/FIELD_STATE_SYSTEM.md deleted file mode 100644 index 49d5c69..0000000 --- a/docs/FIELD_STATE_SYSTEM.md +++ /dev/null @@ -1,368 +0,0 @@ -# Universal Conditional Field State System - -This system provides a flexible architecture for implementing conditional field states (readonly, disabled, hidden) based on participant data and interdependent field values. - -## Architecture Overview - -The system consists of several key components organized in a clean namespace structure: - -### Core Interfaces (`src/Form/Service/Contract/`) -1. **FieldStateProviderInterface** - Defines the contract for field state management -2. **FieldOptionsProviderInterface** - Defines the contract for field option generation - -### Abstract Base Classes (`src/Form/Service/Abstract/`) -3. **AbstractFieldStateProvider** - Common field state functionality -4. **AbstractFieldOptionsProvider** - Common field option functionality - -### Concrete Implementations (`src/Form/Service/`) -5. **CreateFieldStateProvider** - Field states for booking creation workflow -6. **EditFieldStateProvider** - Field states for booking edit workflow -7. **ParticipantFieldOptionsProvider** - Dynamic field option generation - -### Condition System (`src/Form/Service/Condition/`) -8. **FieldConditionInterface** - Defines the contract for condition evaluation -9. **Concrete Conditions** - Implement specific business logic (age ranges, field values, etc.) -10. **CompositeCondition** - Combines conditions with AND/OR/NOT logic - -### Form Integration -11. **BookingCreateParticipantType** - Uses CreateFieldStateProvider -12. **BookingEditParticipantType** - Uses EditFieldStateProvider - -## Usage Examples - -### Basic Age-Based Condition - -```php -// Make a field readonly for participants under 18 -$this->fieldStateConditions['serviceSelection'] = [ - 'readonly' => new AgeRangeCondition(null, 17), -]; -``` - -### Field Dependency Condition - -```php -// Disable field if room is not assigned -$this->fieldStateConditions['mealPreference'] = [ - 'disabled' => FieldValueCondition::empty('assignedRoomId'), -]; -``` - -### Complex Composite Condition - -```php -// Hide field for young participants OR if basic service is selected -$this->fieldStateConditions['advancedOptions'] = [ - 'hidden' => CompositeCondition::or( - new AgeRangeCondition(null, 15), - FieldValueCondition::equals('serviceType', 'basic') - ), -]; -``` - -### Multiple State Conditions - -```php -// Field with multiple conditional states -$this->fieldStateConditions['specialServices'] = [ - 'readonly' => new AgeRangeCondition(null, 17), - 'required' => FieldValueCondition::equals('roomType', 'premium'), - 'disabled' => CompositeCondition::and( - FieldValueCondition::empty('assignedRoomId'), - FieldValueCondition::notEquals('participantType', 'staff') - ), -]; -``` - -## Available Conditions - -### AgeRangeCondition -- `new AgeRangeCondition(18, null)` - At least 18 years old -- `new AgeRangeCondition(null, 17)` - Under 18 years old -- `new AgeRangeCondition(18, 65)` - Between 18 and 65 years old - -### FieldValueCondition -- `FieldValueCondition::equals('field', 'value')` - Field equals specific value -- `FieldValueCondition::notEquals('field', 'value')` - Field does not equal value -- `FieldValueCondition::in('field', ['a', 'b'])` - Field value is in array -- `FieldValueCondition::empty('field')` - Field is empty or null -- `FieldValueCondition::isNotEmpty('field')` - Field has a value - -### CompositeCondition -- `CompositeCondition::and($cond1, $cond2)` - All conditions must be true -- `CompositeCondition::or($cond1, $cond2)` - At least one condition must be true -- `CompositeCondition::not($condition)` - Inverts condition result - -## State Types - -- **readonly** - Field is visible but not editable -- **disabled** - Field interaction is disabled -- **required** - Field becomes mandatory -- **hidden** - Field is not displayed (via CSS display: none) - -## Adding New Conditions - -### For Create Workflow -To register field state conditions for the booking creation workflow, add them to the `registerFieldStateConditions()` method in `CreateFieldStateProvider`: - -```php -// src/Form/Service/CreateFieldStateProvider.php -protected function registerFieldStateConditions(): void -{ - // Age-based readonly state - $this->fieldStateConditions['assignedRoomId'] = [ - 'readonly' => new AgeRangeCondition(null, 17), - ]; - - // Field dependency - $this->fieldStateConditions['mealPreference'] = [ - 'disabled' => FieldValueCondition::empty('assignedRoomId'), - ]; - - // Complex business logic - $this->fieldStateConditions['advancedServices'] = [ - 'hidden' => CompositeCondition::or( - new AgeRangeCondition(null, 15), - FieldValueCondition::equals('membershipLevel', 'basic') - ), - 'required' => FieldValueCondition::equals('roomType', 'suite'), - ]; -} -``` - -### For Edit Workflow -To register field state conditions for the booking edit workflow, add them to the `registerFieldStateConditions()` method in `EditFieldStateProvider`: - -```php -// src/Form/Service/EditFieldStateProvider.php -protected function registerFieldStateConditions(): void -{ - // Make personal data readonly for applicants or non-mutable fields - $personalDataFields = ['firstName', 'lastName', 'dateOfBirth', 'gender', 'nationality', 'email', 'mobile']; - foreach ($personalDataFields as $field) { - $this->fieldStateConditions[$field] = [ - 'readonly' => CompositeCondition::or( - new ApplicantCondition(), - new MutabilityCondition() - ), - ]; - } -} -``` - -### Adding Field Options -To register dynamic field options, add them to the `registerFieldOptionProviders()` method in `ParticipantFieldOptionsProvider`: - -```php -// src/Form/Service/ParticipantFieldOptionsProvider.php -protected function registerFieldOptionProviders(): void -{ - $this->fieldOptionProviders['newField'] = fn(BookingDtoInterface $bookingDto, int $participantIndex) => [ - 'label' => 'New Field Label', - 'choices' => $this->generateChoicesFor($bookingDto, $participantIndex), - ]; -} -``` - -## Performance Considerations - -- Conditions use lazy evaluation and short-circuit logic -- Field state calculations are cached during form processing -- Dependency tracking prevents unnecessary re-evaluations -- Bulk state calculation optimizes multiple field updates - -## Integration with HTMX - -The system supports real-time field state updates: - -1. Field changes trigger dependency re-evaluation -2. State modifications are applied via form rebuilding -3. HTMX can update field states without full page refresh -4. Dependency tracking ensures only affected fields are updated - -### Service Field HTMX Integration Improvements - -**Critical Fix for Expanded Choice Fields**: Service fields (board, skipass, courses, etc.) use expanded choice types (checkboxes/radios) which required special HTMX trigger handling: - -**Issue Resolved**: HTMX attributes were originally placed on container elements (`row_attr`) which don't capture individual input changes for expanded choice fields. - -**Solution Implemented**: HTMX triggers must be placed directly on each individual checkbox/radio input via the `attr` configuration: - -```php -// Correct implementation for service fields -'attr' => [ - 'hx-post' => $this->urlGenerator->generate('booking_create_step_2_refresh'), - 'hx-target' => '#booking-summary', - 'hx-trigger' => 'change', -], - -// Incorrect approach (doesn't work for expanded choices) -// 'row_attr' => ['hx-post' => '...'] -``` - -**Benefits of the Fix**: -- Real-time updates now work reliably for all service field selections -- Service pricing calculations update immediately when selections change -- Booking summary reflects service changes without manual form submission -- Consistent HTMX behavior across all form field types - -**Affected Field Types**: -- Board/meal selection fields -- Ski pass selection fields -- Course selection fields -- Additional services selection fields -- Rental equipment selection fields - -This improvement ensures that the conditional field state system works seamlessly with HTMX for all field types, providing users with immediate feedback on their selections. - -### Transportation Services HTMX Integration - -Transportation services benefit significantly from the enhanced HTMX integration: - -**Service-Specific Triggers**: Transportation, pickup, and parking fields use individual input-level triggers for immediate state updates: - -```php -// Transportation field HTMX configuration -'attr' => [ - 'hx-post' => $this->urlGenerator->generate('booking_create_step_2_refresh'), - 'hx-target' => '#booking-summary', - 'hx-trigger' => 'change', -], -``` - -**Conditional Field Updates**: When users change transportation type: -- Pickup fields automatically show/hide based on bus vs. car selection -- Parking field visibility toggles for car transport -- Pricing updates reflect transportation service costs -- Booking summary updates in real-time - -**Mutual Exclusivity**: The system ensures logical field relationships: -- Car transport → Parking field visible, pickup fields hidden -- Bus transport → Pickup fields visible, parking field hidden -- No transport selected → Both pickup and parking hidden - -## Extending the System - -### Custom Conditions - -Create new condition classes implementing `FieldConditionInterface`: - -```php -// src/Form/Service/Condition/CustomBusinessRuleCondition.php -use App\Form\Service\Contract\FieldConditionInterface; - -class CustomBusinessRuleCondition implements FieldConditionInterface -{ - public function evaluate(BookingCreateDto $bookingDto, int $participantIndex, array $formData): bool - { - // Custom business logic here - return true; - } - - public function getDependentFields(): array - { - return ['fieldThatTriggersThisCondition']; - } - - public function getDescription(): string - { - return 'Custom business rule description'; - } -} -``` - -### Custom Field Options Providers - -To create more complex field option logic, extend `AbstractFieldOptionsProvider`: - -```php -// src/Form/Service/CustomFieldOptionsProvider.php -use App\Form\Service\Abstract\AbstractFieldOptionsProvider; - -class CustomFieldOptionsProvider extends AbstractFieldOptionsProvider -{ - protected function registerFieldOptionProviders(): void - { - $this->fieldOptionProviders['customField'] = fn(BookingDtoInterface $bookingDto, int $participantIndex) => [ - 'label' => 'Custom Field', - 'choices' => $this->generateCustomChoices($bookingDto, $participantIndex), - ]; - } - - private function generateCustomChoices(BookingDtoInterface $bookingDto, int $participantIndex): array - { - // Custom choice generation logic - return []; - } -} -``` - -## Current Implementation Examples - -### Body Dimensions and Rental Insurance Conditional Fields - -The current system implements sophisticated conditional field visibility for body dimensions and rental insurance: - -```php -// CreateFieldStateProvider.php -protected function registerFieldStateConditions(): void -{ - $rentalCondition = new RentalSelectionCondition(); - - // Hide body dimensions section unless rental services are selected - $this->fieldStateConditions['bodyDimensions'] = [ - 'hidden' => CompositeCondition::not($rentalCondition), - ]; - - // Hide rental insurance unless rental services are selected - $this->fieldStateConditions['rentalInsurance'] = [ - 'hidden' => CompositeCondition::not($rentalCondition), - ]; - - // Hide parking unless outbound transportation is PKW (car) - $this->fieldStateConditions['parking'] = [ - 'hidden' => CompositeCondition::not( - ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_CAR_API) - ), - ]; -} -``` - -### Rental Insurance Checkbox Implementation - -The rental insurance field demonstrates the checkbox pattern used for service selection: - -```php -// Field Options Provider -$this->fieldOptionProviders['rentalInsurance'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [ - 'label' => $this->getRentalInsuranceCheckboxLabel($rentalInsuranceServices), - 'required' => false, - 'property_path' => 'rentalInsuranceSelected', // Maps to boolean property -]; - -// Field Handler Processing -public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void -{ - $isRentalInsuranceSelected = (bool) $this->getFieldValue($submittedData, $this->getFieldName()); - - // Store boolean value for form state - $participant->rentalInsuranceSelected = $isRentalInsuranceSelected; - - // Store Service object for pricing calculations - if ($isRentalInsuranceSelected) { - $participant->rentalInsurance = $this->findRentalInsuranceService($bookingDto); - } else { - $participant->rentalInsurance = null; - } -} -``` - -### Benefits of Current Architecture - -1. **Clean Separation**: Boolean properties handle form state, Service objects handle business logic -2. **Automatic Pricing Integration**: Service objects are automatically included in pricing calculations -3. **Dynamic Visibility**: Fields appear/disappear based on related selections -4. **Consistent UX**: Checkbox pattern provides intuitive user interface -5. **Validation-Free**: Conditional visibility eliminates need for complex validation rules - -This system provides a powerful, maintainable foundation for complex conditional field behavior while maintaining clean separation of concerns and extensibility. \ No newline at end of file diff --git a/docs/FORM_PROCESSING.md b/docs/FORM_PROCESSING.md deleted file mode 100644 index 2b4a16a..0000000 --- a/docs/FORM_PROCESSING.md +++ /dev/null @@ -1,557 +0,0 @@ -# Form Processing System Documentation - -## Overview - -The MyEP Next Booking application implements a sophisticated form processing system designed to handle complex multi-step booking workflows with dynamic participant forms and conditional field logic. The system integrates with the Bus Pro Net (BPN) XML API for travel management and provides real-time field updates through HTMX integration. - -## Architecture Components - -### 1. Multi-Step Booking Flow - -The booking process consists of three main steps: - -1. **Step 1 (`CreateStep1Controller`)**: Room selection with quantities and dates -2. **Step 2 (`CreateStep2Controller`)**: Participant details with conditional fields -3. **Step 3**: Final confirmation and submission to BPN API - -### 2. Data Transfer Objects (DTOs) - -#### BookingCreateDto (`src/Form/Model/BookingCreateDto.php`) -- Main container for the entire booking process -- Contains travel data, hotel ID, room selections, and participants -- Implements `BookingDtoInterface` for polymorphic handling - -```php -class BookingCreateDto implements BookingDtoInterface -{ - public int $currentStep = 1; - public array $roomSelections = []; // RoomSelectionDto[] - public array $participants = []; // ParticipantDto[] - public Travel $travel; - public int $hotelId; -} -``` - -#### ParticipantDto (`src/Form/Model/ParticipantDto.php`) -- Individual participant data container -- Includes personal data, body dimensions, and service selections -- Body dimensions are hidden unless rental services are selected (conditional visibility) -- Rental insurance field with checkbox interface and boolean state tracking - -```php -class ParticipantDto -{ - // Personal data - public ?string $firstName = null; - public ?string $lastName = null; - public ?\DateTimeImmutable $dateOfBirth = null; - public ?string $email = null; - - // Body dimensions (hidden unless rental services are selected) - public ?string $height = null; - public ?string $weight = null; - public ?string $shoeSize = null; - - // Service selections - public ?int $assignedRoomId = null; - public array $courses = []; - public array $additionalServices = []; - public array $rentals = []; - public ?Service $rentalInsurance = null; - public bool $rentalInsuranceSelected = false; // Checkbox state - - // Transportation services - public ?Service $transportationOutbound = null; - public ?Service $transportationInbound = null; - public ?Pickup $pickupOutbound = null; - public ?Pickup $pickupInbound = null; - public bool $parking = false; - public ?Service $parkingService = null; -} -``` - -### 3. Dynamic Field Options System - -#### ParticipantFieldOptionsProvider (`src/Form/Service/ParticipantFieldOptionsProvider.php`) - -Central registry for dynamic field configurations using a provider pattern with lazy evaluation. - -**Registered Field Providers:** - -- **`assignedRoomId`**: Context-aware room selection - - Shows only available rooms for the participant - - Excludes rooms already assigned to other participants - - Respects room capacity and booking constraints - -- **`courses`**: Available courses from travel data - - Multiple selection with checkboxes - - Populated from `travel.additionalServices` with `TOKEN_COURSES` subtype - -- **`additionalServices`**: Additional services with mandatory logic - - Mandatory services are pre-selected and readonly - - Choice attributes include visual indicators for mandatory items - -- **`board`**: Board/meal options - - Multiple selection from travel data - - Populated from `TOKEN_BOARD` subtype services - -- **`rentals`**: Rental equipment options - - Date-filtered rental services - - Controls visibility of body dimensions and rental insurance fields - - Populated from `TOKEN_RENTALS` subtype services - -- **`rentalInsurance`**: Rental insurance checkbox - - Checkbox interface (similar to parking field) - - Maps to `rentalInsuranceSelected` boolean property - - Only visible when rental services are selected - - Automatically manages Service object for pricing calculations - - Populated from `TOKEN_RENTAL_INSURANCE` subtype services - -- **`parking`**: Parking service checkbox - - Boolean checkbox for self-organized transportation - - Only visible when outbound transportation is PKW (car) - - Manages both boolean state and Service object for pricing - -**Provider Pattern Implementation:** -```php -protected function registerFieldOptionProviders(): void -{ - $this->fieldOptionProviders['fieldName'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ - 'label' => 'Field Label', - 'choices' => $this->generateChoicesFor($bookingDto, $participantIndex), - // ... other Symfony form options - ]; -} -``` - -### 4. Conditional Field State System - -#### Field State Providers - -**CreateFieldStateProvider (`src/Form/Service/CreateFieldStateProvider.php`)** -- Manages field states for the booking creation workflow -- Controls conditional field visibility and state based on participant data -- Implements body dimensions and rental insurance conditional visibility - -**Field State Types:** -- `readonly`: Field is visible but not editable -- `disabled`: Field interaction is disabled -- `required`: Field becomes mandatory -- `hidden`: Field is not displayed - -**Current Implementation:** -```php -protected function registerFieldStateConditions(): void -{ - $rentalCondition = new RentalSelectionCondition(); - - // Hide body dimensions section unless rental services are selected - $this->fieldStateConditions['bodyDimensions'] = [ - 'hidden' => CompositeCondition::not($rentalCondition), - ]; - - // Hide rental insurance unless rental services are selected - $this->fieldStateConditions['rentalInsurance'] = [ - 'hidden' => CompositeCondition::not($rentalCondition), - ]; - - // Hide parking unless outbound transportation is PKW (car) - $this->fieldStateConditions['parking'] = [ - 'hidden' => CompositeCondition::not( - ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_CAR_API) - ), - ]; -} -``` - -#### Field Conditions - -**Available Condition Types:** - -1. **`RentalSelectionCondition`**: Evaluates rental service selections -2. **`AgeRangeCondition`**: Age-based conditions -3. **`FieldValueCondition`**: Field interdependency conditions (equals, in, empty, etc.) -4. **`CompositeCondition`**: Complex logic with AND/OR/NOT operators -5. **`ApplicantCondition`**: Applicant-specific conditions -6. **`MutabilityCondition`**: Mutability-based conditions - -**Condition Interface:** -```php -interface FieldConditionInterface -{ - public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool; - public function getDependentFields(): array; - public function getDescription(): string; -} -``` - -### 5. Participant Field Processing - -#### ParticipantFieldHandlerRegistry (`src/Form/Service/ParticipantFieldHandlerRegistry.php`) - -Manages field handlers in dependency order using topological sorting to ensure proper processing sequence. - -**Key Features:** -- Dependency resolution using Kahn's algorithm -- Circular dependency detection -- Support for both simple and complex handler instantiation -- Batch processing of all participants - -**Processing Flow:** -```php -public function processFieldsAndSync(array $submittedData, BookingDtoInterface $bookingDto): array -{ - // Process all field handlers to clean the DTO - $this->processFields($submittedData, $bookingDto); - - // Synchronize submitted data with the cleaned DTO state - return $this->syncSubmittedDataWithDto($submittedData, $bookingDto); -} - -public function processFields(array $submittedData, BookingDtoInterface $bookingDto): void -{ - foreach ($submittedData['participants'] as $participantIndex => $participantData) { - foreach ($this->getSortedHandlers() as $handlerName) { - $handler = $this->handlers[$handlerName]; - if ($handler->shouldProcess($participantData, $participantIndex)) { - $handler->processField($participantData, $bookingDto, $participantIndex); - } - } - } -} -``` - -**Data Synchronization:** -The registry includes a critical synchronization feature to maintain consistency between DTO state and form submitted data: - -```php -private function syncSubmittedDataWithDto(array $submittedData, BookingDtoInterface $bookingDto): array -{ - // Updates submitted data to match cleaned DTO state - // Converts DTO objects back to form-expected formats - // Ensures form rendering shows valid selections only -} -``` - -This prevents validation errors when field handlers remove invalid selections from DTOs but the original submitted data still contains those invalid choices. - -#### Field Handlers - -**AbstractParticipantFieldHandler (`src/Form/Service/Abstract/AbstractParticipantFieldHandler.php`)** - -Base class providing common functionality: -- Default dependency resolution -- Safe participant data access -- Field value extraction utilities -- Value normalization methods - -**Concrete Implementations:** - -1. **`ParticipantDateOfBirthFieldHandler`**: Processes date of birth field - - Converts submitted date strings to `DateTimeImmutable` objects - - Normalizes various date formats - - No dependencies (foundation field for age-based logic) - -2. **`ParticipantAssignedRoomFieldHandler`**: Processes room assignments - - Converts form strings to integers - - Handles empty selections as null values - - No dependencies (base field) - -3. **Service-Based Handlers** (age-aware filtering): - - **`ParticipantAdditionalServicesFieldHandler`**: Additional services filtering - - **`ParticipantCoursesFieldHandler`**: Course selections filtering - - **`ParticipantBoardFieldHandler`**: Board/meal options filtering - - **`ParticipantRentalsFieldHandler`**: Rental equipment filtering - - **`ParticipantRentalInsuranceFieldHandler`**: Rental insurance checkbox handling - - Depends on `['dateOfBirth', 'rentals']` (only visible when rentals selected) - - Processes boolean checkbox input and converts to Service object - - Manages both `rentalInsuranceSelected` (bool) and `rentalInsurance` (Service) properties - - All service handlers share these characteristics: - - Depend on `dateOfBirth` field (processed first) - - Filter selections based on age constraints (except rental insurance which uses conditional visibility) - - Instantiate `ServiceAgeEvaluator` directly when needed - - Remove invalid selections to prevent form validation errors - - **Store complete Service objects** in ParticipantDto (not just IDs) for pricing calculations - -**Key Architecture Decisions**: - -1. **Service Evaluator Instantiation**: Service handlers instantiate `ServiceAgeEvaluator` directly rather than using dependency injection because: - - `ServiceAgeEvaluator` has no dependencies itself - - Handlers are registered as simple class names in service configuration - - Avoids complex service wiring for lightweight utility classes - - Maintains clean separation between handlers and evaluator logic - -2. **Data Storage Strategy**: Service handlers store complete Service objects in ParticipantDto rather than just IDs because: - - Pricing calculations require access to service price data - - Eliminates need for additional database lookups during price calculation - - Provides immediate access to all service metadata (labels, descriptions, etc.) - - Maintains data consistency throughout the booking flow - -**Handler Interface:** -```php -interface ParticipantFieldHandlerInterface -{ - public function getFieldName(): string; - public function getDependencies(): array; - public function shouldProcess(array $submittedData, int $participantIndex): bool; - public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void; -} -``` - -### 6. Form Type Integration - -#### BookingCreateStep2Type (`src/Form/BookingCreateStep2Type.php`) - -Main form type for participant data collection with event-driven processing. - -**Form Events:** -- **`PRE_SET_DATA`**: Initial form setup with participants collection -- **`PRE_SUBMIT`**: Dynamic field updates and DTO synchronization - -**Event Processing:** -```php -public function onPreSubmit(FormEvent $event): void -{ - $form = $event->getForm(); - $submittedData = $event->getData(); - $bookingDto = $form->getData(); - - // Process field handlers and synchronize submitted data with cleaned DTO state - $cleanedSubmittedData = $this->participantFieldHandlerRegistry->processFieldsAndSync($submittedData, $bookingDto); - $event->setData($cleanedSubmittedData); - - // Rebuild the 'participants' field with the updated DTO - $this->addParticipantsField($form); -} -``` - -#### BookingCreateParticipantType (`src/Form/BookingCreateParticipantType.php`) - -Individual participant form with dynamic field management. - -**Static Fields:** -- Personal data (name, email, birth date, etc.) -- Body dimensions (embedded `BodyDimensionsType`) -- Contact information - -**Dynamic Fields:** -- Room assignment (`assignedRoomId`) -- Service selections (courses, additional services, board, rentals) - -**Dynamic State Application:** -```php -private function applyFieldStates(FormInterface $form, BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): void -{ - $allFieldStates = $this->fieldStateProvider->getAllFieldStates($bookingDto, $participantIndex, $formData); - - foreach ($allFieldStates as $fieldName => $fieldState) { - // Apply state modifications to form fields - // Handle nested body dimension fields specially - } -} -``` - -## Form Processing Pipeline - -### 1. Initial Form Rendering - -1. **Controller** creates `BookingCreateDto` with travel data -2. **Step2Type** `PRE_SET_DATA` event fires: - - Adds participants collection field - - Each participant triggers `BookingCreateParticipantType` creation -3. **ParticipantType** `PRE_SET_DATA` event fires: - - Adds dynamic fields using `FieldOptionsProvider` - - Applies initial field states using `FieldStateProvider` -4. **Form rendered** with proper field options and states - -### 2. Form Submission Processing - -1. **Form submission** received by controller -2. **Step2Type** `PRE_SUBMIT` event fires: - - `ParticipantFieldHandlerRegistry` processes all submitted data to update DTOs - - Registry synchronizes submitted data with cleaned DTO state - - Event data updated with cleaned submitted data - - Form rebuilt with updated DTO state -3. **ParticipantType** `PRE_SUBMIT` event fires: - - Field states recalculated based on submitted data - - Form fields updated with new states -4. **Validation** runs on updated DTO with cleaned data -5. **Controller** handles successful submission or re-renders with errors - -### 3. HTMX Dynamic Updates - -For real-time field updates without full form submission: - -1. **HTMX request** sent with partial form data -2. **Same pipeline** executes as form submission -3. **Partial response** returned with updated field states -4. **Frontend** updates only changed form sections - -**Service Field HTMX Integration:** - -Service fields (board, skipass, courses, etc.) use expanded choice types (checkboxes/radios) which require special HTMX trigger handling: - -- **Issue**: HTMX attributes on container elements don't capture individual input changes -- **Solution**: HTMX triggers must be placed on each individual checkbox/radio input -- **Implementation**: Field handlers ensure `hx-post`, `hx-target`, and `hx-trigger` attributes are applied to each choice input -- **Result**: Real-time updates work reliably for all service selections - -**HTMX Attribute Placement:** -```php -// Correct: Individual input triggers -'attr' => [ - 'hx-post' => $this->urlGenerator->generate('booking_create_step_2_refresh'), - 'hx-target' => '#booking-summary', - 'hx-trigger' => 'change', -], - -// Incorrect: Container-level triggers (doesn't work for expanded choices) -// 'row_attr' => ['hx-post' => '...'] -``` - -## Validation System - -### DTO-Level Validation - -**ParticipantDto Validation:** -- Symfony validation constraints on properties -- Custom callback validation for body dimensions when rentals selected - -```php -#[Assert\Callback('validateBodyDimensionsForRentals', groups: ['booking_create_step_2'])] -public function validateBodyDimensionsForRentals(ExecutionContextInterface $context): void -{ - if (!empty($this->rentals)) { - // Validate height, weight, shoeSize are provided - } -} -``` - -### Form-Level Validation - -**Validation Groups:** -- `booking_create_step_2`: Step 2 specific validations -- `booking_edit`: Edit workflow validations - -## Extension Points - -### Adding New Dynamic Fields - -1. **Register field options** in `ParticipantFieldOptionsProvider`: -```php -$this->fieldOptionProviders['newField'] = fn($bookingDto, $participantIndex) => [ - 'label' => 'New Field Label', - 'choices' => $this->generateChoicesFor($bookingDto, $participantIndex), -]; -``` - -2. **Add to dynamic fields list** in `BookingCreateParticipantType`: -```php -$dynamicFields = ['assignedRoomId', 'courses', 'newField']; // Add 'newField' -``` - -### Adding New Field Conditions - -1. **Implement condition class**: -```php -class NewCondition implements FieldConditionInterface -{ - public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool - { - // Condition logic - } -} -``` - -2. **Register in field state provider**: -```php -$this->fieldStateConditions['fieldName'] = [ - 'required' => new NewCondition(), -]; -``` - -### Adding New Field Handlers - -1. **Implement handler class**: -```php -class NewFieldHandler extends AbstractParticipantFieldHandler -{ - public function getFieldName(): string { return 'newField'; } - public function getDependencies(): array { return ['dependentField']; } - public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void - { - // Processing logic - } -} -``` - -2. **Register in service configuration** (services.yaml or through DI) - -## Integration with BPN API - -The form system is designed to prepare data for submission to the Bus Pro Net XML API: - -1. **Field handlers** transform form data into BPN-compatible format -2. **Service selections** map to BPN service IDs -3. **Room assignments** align with BPN room availability -4. **Validation rules** ensure data meets BPN requirements - -## Performance Considerations - -### Optimization Strategies - -1. **Lazy evaluation** in field option providers -2. **Caching** in field state providers -3. **Dependency sorting** cached until handlers change -4. **Minimal form rebuilding** only when necessary - -### Memory Management - -- DTOs use typed properties to minimize memory footprint -- Field handlers process data in-place where possible -- Form events only rebuild changed portions - -## Security Considerations - -### XSS Protection - -- Custom `XssCleanTransformer` applied to text inputs -- `clean_xss: true` option on relevant form fields - -### Data Validation - -- Strict type declarations throughout -- Yoda conditions for safety -- Explicit validation constraints on all user inputs - -## Testing Strategy - -### Unit Testing Focus Areas - -1. **Field option providers** with various travel data scenarios -2. **Field conditions** with different participant states -3. **Field handlers** with edge cases and dependencies -4. **Validation logic** for body dimensions and rental services - -### Integration Testing - -1. **Form submission workflows** end-to-end -2. **HTMX dynamic updates** with state changes -3. **Multi-participant scenarios** with interdependencies - -## Future Enhancements - -### Planned Features - -1. **Step 3 implementation** for booking confirmation -2. **Additional field conditions** for complex business rules -3. **Enhanced validation** for service compatibility -4. **Performance optimizations** for large participant counts - -### Architectural Improvements - -1. **Event system** for field state change notifications -2. **Caching layer** for expensive field option calculations -3. **Async processing** for complex form submissions -4. **Enhanced error handling** with user-friendly messages \ No newline at end of file diff --git a/docs/INSURANCE_EDIT_IMPLEMENTATION.md b/docs/INSURANCE_EDIT_IMPLEMENTATION.md deleted file mode 100644 index 7daf641..0000000 --- a/docs/INSURANCE_EDIT_IMPLEMENTATION.md +++ /dev/null @@ -1,693 +0,0 @@ -# Insurance Booking in Edit Flow - Implementation Plan - -## Overview - -This document outlines the implementation plan for enabling insurance booking/modification in the edit flow with time-based mutability constraints. The implementation leverages 99% of existing logic from the create flow. - -## Critical Review Updates (2025-10-07) - -**Plan Corrections:** -1. ✅ **Phase 0 Added**: Prerequisites phase completed - insurance parsing infrastructure was missing and has been implemented -2. ⚠️ **Condition Signature Corrected**: `FieldConditionInterface` uses `evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData)` NOT the old signature shown in original plan -3. ⚠️ **Phase 5 May Not Be Needed**: No hardcoded HTMX routes found in current codebase - needs verification -4. ⚠️ **Phase 7 Targets Wrong Methods**: The edit flow uses different data processor methods than assumed in original plan - -**Implementation Progress (2025-10-07):** -- ✅ **Phase 0: Prerequisites** - Booking model insurance parsing infrastructure -- ✅ **Phase 1: Mutability Condition** - Time-based readonly logic with Carbon testability -- ✅ **Phase 2: Data Population** - Insurance extraction in BookingEditDto -- ✅ **Phase 3: Helper Method** - Booking::getInsuranceForParticipant() with tests -- ⏳ **Phases 4-8: Remaining** - Field state, template, data processing, documentation - -**Total Progress:** 4 of 9 phases complete (44%) - -## Business Requirements - -### Mutability Rules - -1. **Standard Case**: Insurance editable up to 30 days before travel date -2. **Late Booking Case**: When booking date is < 30 days before travel, insurance editable up to 3 days after booking date - -### Time-Based Logic - -```php -// Standard case: Can edit if >= 30 days before travel -$daysUntilTravel = $now->diff($travelDate)->days; -$isEditable = $daysUntilTravel >= 30; - -// Late booking case: Can edit if within 3 days of booking date -$daysSinceBooking = $bookingDate->diff($now)->days; -$isEditable = $daysSinceBooking <= 3; -``` - -## Implementation Plan - -### Phase 1: Create Mutability Condition (~50 lines) - -**File**: `src/Form/Service/Condition/InsuranceMutabilityCondition.php` - -**Purpose**: Determine if insurance field should be readonly based on booking/travel dates - -**Dependencies**: -- Implements `FieldConditionInterface` -- Returns `true` if field should be readonly (locked) -- Returns `false` if field should be editable - -**Key Logic**: -```php -public function evaluate(ParticipantDto $participant, BookingDtoInterface $bookingDto): bool -{ - // Only apply to edit context - if (!$bookingDto instanceof BookingEditDto) { - return false; // Always editable in create flow - } - - $now = new DateTimeImmutable(); - $travelDate = $bookingDto->travel->dateFrom; - $bookingDate = $bookingDto->booking->bookingDate; - - // Calculate days until travel - $daysUntilTravel = $now->diff($travelDate)->days; - $isBeforeTravel = $now < $travelDate; - - // Standard case: Editable if >= 30 days before travel - if ($isBeforeTravel && $daysUntilTravel >= 30) { - return false; // Editable - } - - // Late booking case: Check if booking was made < 30 days before travel - $daysFromBookingToTravel = $bookingDate->diff($travelDate)->days; - $wasLateBooking = $daysFromBookingToTravel < 30; - - if ($wasLateBooking) { - // Editable if within 3 days of booking date - $daysSinceBooking = $bookingDate->diff($now)->days; - return $daysSinceBooking > 3; // True = readonly (past 3 days) - } - - // Default: Not editable (readonly) - return true; -} -``` - -**Testing Strategy**: -- Test standard case: 40 days before travel → editable -- Test standard case: 20 days before travel → readonly -- Test late booking: booking 15 days before travel, 2 days after booking → editable -- Test late booking: booking 15 days before travel, 5 days after booking → readonly -- Test edge case: exactly 30 days before travel → editable -- Test edge case: exactly 3 days after booking → editable - -### Phase 2: Populate Insurance Data in BookingEditDto (~10 lines) - -**File**: `src/Form/Model/BookingEditDto.php` - -**Location**: Line ~60 in `fromBooking()` method, after pickup handling - -**Changes**: -```php -// Pickup handling (currently only supports outbound pickup) -$pickup = $booking->getPickupForParticipant($index); -$participantData->pickup = $pickup; - -// Insurance - get insurance for participant -$insurance = $booking->getInsuranceForParticipant($index); -$participantData->insurance = $insurance; - -// Room assignment - extract from booking room mappings -$room = $booking->getRoomForParticipant($index); -``` - -**Note**: Requires helper method in Booking model (see Phase 3) - -### Phase 3: Add Booking Helper Method (~15 lines) - -**File**: `src/BusProNet/Model/Booking.php` - -**Location**: After `getPickupForParticipant()` method (~line 280) - -**Purpose**: Extract insurance for specific participant from booking data - -**Implementation**: -```php -/** - * Gets the insurance assigned to a specific participant. - * - * @param int $participantIndex The participant index (0-based) - * - * @return Insurance|null The assigned insurance or null if none assigned - */ -public function getInsuranceForParticipant(int $participantIndex): ?Insurance -{ - if (!isset($this->insurances) || !is_array($this->insurances)) { - return null; - } - - foreach ($this->insurances as $insurance) { - if (in_array($participantIndex, $insurance->mapping ?? [], true)) { - return $insurance; - } - } - - return null; -} -``` - -**Prerequisites**: -- Verify `Booking::$insurances` property exists and is populated by parser -- If missing, add to `BookingParser` similar to other service arrays - -### Phase 4: Update EditFieldStateProvider (~5 lines) - -**File**: `src/Form/Service/EditFieldStateProvider.php` - -**Location**: In `registerFieldStateConditions()` method after other field states - -**Changes**: -```php -// Insurance - conditionally editable based on time constraints -$this->fieldStateRegistry->registerFieldStateCondition( - 'insurance', - new InsuranceMutabilityCondition(), - [ - 'readonly' => true, - 'help' => 'Versicherungen können nicht mehr geändert werden.', - ] -); -``` - -**Notes**: -- Readonly state applied when condition returns true -- Help text informs user why field is locked -- No hidden state needed - users should see their booked insurance - -### Phase 5: Fix Hardcoded HTMX Routes (~10 lines) - -**File**: `src/Form/Service/ParticipantFieldOptionsProvider.php` - -**Location**: Line ~377 in insurance field provider - -**Current Problem**: -```php -'attr' => [ - 'data-participant-form-target' => 'insuranceInput', - 'hx-post' => '/booking/create/refresh-participant', // HARDCODED! - 'hx-trigger' => 'change', -], -``` - -**Solution**: -```php -// Add parameter to field provider callback -$this->fieldOptionProviders['insurance'] = function ( - BookingDtoInterface $bookingDto, - int $participantIndex, - array $options = [] -) { - // Determine HTMX refresh route based on context - $htmxRoute = $bookingDto instanceof BookingCreateDto - ? '/booking/create/refresh-participant' - : '/booking/edit/refresh-participant'; - - return [ - 'label' => 'Reiseversicherung', - 'placeholder' => 'Keine Versicherung', - 'choice_loader' => $this->insuranceChoiceLoaderFactory->create( - $bookingDto->travel->insurances ?? [], - $participantIndex - ), - 'attr' => [ - 'data-participant-form-target' => 'insuranceInput', - 'hx-post' => $htmxRoute, - 'hx-trigger' => 'change', - ], - ]; -}; -``` - -**Apply to All Fields**: Check and fix other fields with hardcoded routes (skiPass, rentals, courses, etc.) - -### Phase 6: Add Insurance to Template (~3 lines) - -**File**: `templates/booking/edit.html.twig` - -**Location**: After rental insurance field (~line 199) - -**Changes**: -```twig -{# Rental insurance - depends on rentals selection #} -{% if participant.rentalInsuranceSelected is defined %} - {{ form_row(participant.rentalInsuranceSelected) }} -{% endif %} - -{# Insurance - time-based mutability #} -{% if participant.insurance is defined %} - {{ form_row(participant.insurance) }} -{% endif %} -``` - -**Notes**: -- Conditional rendering maintains template stability -- Readonly state handled automatically by field state provider -- Uses existing InsuranceChoiceType with tooltip support - -### Phase 7: Process Insurance in BookingDataProcessor (~40 lines) - -**File**: `src/BusProNet/DataProcessor/BookingDataProcessor.php` - -**Location 1**: Line ~730 in `resetServiceMappings()` - add insurances to reset - -**Changes**: -```php -private function resetServiceMappings(object $bookingData): void -{ - $servicesToReset = [ - ...$bookingData->additionalServices, - ...$bookingData->transportationServices, - ...$bookingData->pickupsOutbound, - ...$bookingData->pickupsInbound, - ...$bookingData->rooms, - ...$bookingData->insurances, // ADD THIS - ]; - - foreach ($servicesToReset as $service) { - if (isset($service->mapping)) { - $service->mapping = []; - } - } -} -``` - -**Location 2**: Line ~903 in `processParticipantData()` - add insurance processing call - -**Changes**: -```php -private function processParticipantData(object $participant, object $bookingData, Travel $travel): void -{ - // ... existing code ... - - // Process insurance assignment - $this->processInsurance($participant, $bookingData); - - // ... rest of method ... -} -``` - -**Location 3**: New method after `processRoomAssignment()` - create insurance processor - -**Implementation**: -```php -/** - * Processes insurance selection for a participant. - * - * Finds the selected insurance in the available insurances and adds - * the participant index to its mapping array. - * - * @param object $participant The participant data from DTO - * @param object $bookingData The booking data object with insurances - */ -private function processInsurance(object $participant, object $bookingData): void -{ - if (!isset($participant->insurance) || null === $participant->insurance) { - return; - } - - $insuranceId = $participant->insurance->id; - - // Find the insurance in the travel's available insurances - foreach ($bookingData->insurances as $insurance) { - if ($insurance->id === $insuranceId) { - $insurance->mapping[] = $participant->index; - break; - } - } -} -``` - -**Location 4**: Line ~950 in `removeUnusedServices()` - add insurances to unused removal - -**Changes**: -```php -private function removeUnusedServices(object $bookingData): void -{ - $serviceArrays = [ - 'additionalServices' => &$bookingData->additionalServices, - 'transportationServices' => &$bookingData->transportationServices, - 'pickupsOutbound' => &$bookingData->pickupsOutbound, - 'pickupsInbound' => &$bookingData->pickupsInbound, - 'rooms' => &$bookingData->rooms, - 'insurances' => &$bookingData->insurances, // ADD THIS - ]; - - // ... existing filtering logic ... -} -``` - -**Location 5**: Line ~1007 in `buildServicePayload()` - add insurances to payload - -**Changes**: -```php -private function buildServicePayload(object $bookingData): array -{ - $allServices = [ - ...$bookingData->additionalServices, - ...$bookingData->transportationServices, - ...$bookingData->pickupsOutbound, - ...$bookingData->pickupsInbound, - ...$bookingData->insurances, // ADD THIS - ]; - - // ... existing payload building logic ... -} -``` - -**Critical Note**: Remove or update comment at line 759-762 that states: -```php -// Insurance data is only included in CREATE flow, not in UPDATE flow. -// The insurance property in the DTO is for informational display only. -``` - -This comment is **no longer accurate** after this implementation. - -### Phase 8: Update Documentation (~20 lines) - -**File**: `docs/BOOKING_EDIT_MODERNIZATION.md` - -**Location**: Add new section after "Phase 4: Room Assignment Implementation" - -**Content**: -```markdown -## Phase 5: Insurance Implementation - -**Status**: ✅ Completed - -**Date**: [Implementation date] - -### Overview -Enabled insurance booking/modification in edit flow with time-based mutability constraints. - -### Implementation Details - -1. **Mutability Condition** (`InsuranceMutabilityCondition`) - - Standard case: Editable up to 30 days before travel - - Late booking: Editable up to 3 days after booking date - - Returns true if field should be readonly - -2. **Data Population** (`BookingEditDto::fromBooking()`) - - Added insurance extraction for each participant - - Uses `Booking::getInsuranceForParticipant()` helper - -3. **Field State** (`EditFieldStateProvider`) - - Registered insurance mutability condition - - Readonly state with help text when locked - -4. **HTMX Routes** (`ParticipantFieldOptionsProvider`) - - Fixed hardcoded create routes to be context-aware - - Applied to insurance and other dynamic fields - -5. **Template** (`edit.html.twig`) - - Added insurance field rendering with conditional check - -6. **Data Processing** (`BookingDataProcessor`) - - Added insurance to service reset/removal/payload logic - - Created `processInsurance()` method for participant mapping - -### Code Reuse -- ✅ `ParticipantInsuranceFieldHandler` - 100% reused (context-agnostic) -- ✅ `InsuranceMatchingService` - 100% reused (eligibility, reassignment) -- ✅ `InsuranceChoiceType` - 100% reused (tooltips, labels) -- ✅ Field state pattern - Same as transportation/services - -### New Code -- ~50 lines: `InsuranceMutabilityCondition` -- ~25 lines: `Booking::getInsuranceForParticipant()` + DTO population -- ~40 lines: `BookingDataProcessor` insurance processing -- ~20 lines: Field state, template, route fixes - -**Total**: ~135 lines of new code, ~500 lines of reused logic - -### Testing Checklist -- [ ] Insurance editable 40 days before travel (standard case) -- [ ] Insurance readonly 20 days before travel (standard case) -- [ ] Insurance editable 2 days after late booking (late case) -- [ ] Insurance readonly 5 days after late booking (late case) -- [ ] Insurance auto-reassignment works on price changes -- [ ] Bulk insurance booking works in edit flow -- [ ] Insurance tooltips display correctly -- [ ] Insurance persists to API on edit submission -- [ ] Readonly help text displays when locked -``` - -## Code Reuse Strategy - -### Fully Reused Components (No Changes) - -1. **ParticipantInsuranceFieldHandler** (200+ lines) - - Already works with `BookingDtoInterface` - - Auto-reassignment logic for price tier changes - - Bulk insurance support for dependent participants - - User notification system - -2. **InsuranceMatchingService** (300+ lines) - - Eligibility filtering by age, price, family status - - Price tier reassignment logic - - Age constraint evaluation at travel date - -3. **ParticipantBulkInsuranceFieldHandler** (100+ lines) - - Batch insurance assignment to all participants - - Price tier adjustment per participant - -4. **InsuranceChoiceType** (80+ lines) - - Tooltip support with product info URLs - - Label formatting with pricing - -5. **InsuranceParser** (150+ lines) - - 3-pass parsing for package family detection - - Complementary insurance handling - -### New/Modified Components (~135 lines total) - -1. **InsuranceMutabilityCondition** (~50 lines) - NEW -2. **Booking::getInsuranceForParticipant()** (~15 lines) - NEW -3. **BookingEditDto insurance population** (~10 lines) - MODIFIED -4. **EditFieldStateProvider** (~5 lines) - MODIFIED -5. **ParticipantFieldOptionsProvider HTMX routes** (~10 lines) - MODIFIED -6. **edit.html.twig** (~3 lines) - MODIFIED -7. **BookingDataProcessor** (~40 lines) - MODIFIED - -## Risk Assessment - -### Low Risk -- ✅ Field handler already context-agnostic (tested in create flow) -- ✅ Mutability pattern proven with services/transportation -- ✅ Data processor pattern established with rooms -- ✅ Template conditional rendering pattern established - -### Medium Risk -- ⚠️ **Booking::$insurances property existence** - Needs verification in parser -- ⚠️ **HTMX route changes** - May affect other fields, test thoroughly -- ⚠️ **Time calculation edge cases** - Test timezone handling - -### Mitigation -- Verify insurance data populated by `BookingParser` before implementation -- Create comprehensive test cases for date calculations -- Test HTMX updates after route context-awareness changes - -## Progress Tracking - -### Phase 0: Prerequisites ✅ COMPLETED (2025-10-07) -- [x] Add `$insurances` property to `Booking` model (line 45) -- [x] Add `$mapping` and `$individualPrice` properties to `Insurance` model (lines 101-106) -- [x] Create `BookingInsurancesParser` for parsing booking insurance XML -- [x] Integrate parser into `BookingParser` constructor and parse method -- [x] Write comprehensive tests (3 tests, 20 assertions - all passing) -- [x] Apply php-cs-fixer to all modified files -- [x] Verify insurance data flow from XML → Parser → Booking model - -**Files Modified:** -- `src/BusProNet/Model/Booking.php` - Added `$insurances` property -- `src/BusProNet/Model/Insurance.php` - Added `$mapping` and `$individualPrice` properties -- `src/BusProNet/XmlParser/BookingInsurancesParser.php` - NEW parser -- `src/BusProNet/XmlParser/BookingParser.php` - Integrated insurance parsing -- `tests/BusProNet/XmlParser/BookingInsurancesParserTest.php` - NEW test file - -### Phase 1: Mutability Condition ✅ COMPLETED (2025-10-07) -- [x] Create `InsuranceMutabilityCondition.php` -- [x] **IMPORTANT**: Use correct signature: `evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool` -- [x] Implement standard case logic (30 days before travel) -- [x] Implement late booking case logic (3 days after booking) -- [x] Add comprehensive PHPDoc -- [x] Apply php-cs-fixer -- [x] Write unit tests (10 test cases, 13 assertions - all passing) -- [x] Use Carbon::setTestNow() for time-dependent tests - -**Files Created:** -- `src/Form/Service/Condition/InsuranceMutabilityCondition.php` (101 lines) -- `tests/Form/Service/Condition/InsuranceMutabilityConditionTest.php` (167 lines) - -**Implementation Details:** -- Uses `Carbon::now()->toDateTimeImmutable()` for testable time handling -- Constants: `DAYS_BEFORE_TRAVEL_THRESHOLD = 30`, `DAYS_AFTER_BOOKING_THRESHOLD = 3` -- Returns `false` (editable) in create flow -- Returns `true` (readonly) when past mutability threshold -- No field dependencies (time-based only) -- Tests use fixed "now" time (2025-01-15 12:00:00) with absolute dates for reliability - -### Phase 2: Data Population ✅ COMPLETED (2025-10-07) -- [x] Verify `Booking::$insurances` exists and is populated (DONE in Phase 0) -- [x] Add insurance extraction to `BookingEditDto::fromBooking()` -- [x] Apply php-cs-fixer - -**Files Modified:** -- `src/Form/Model/BookingEditDto.php` (lines 63-65) - Added insurance extraction - -**Implementation Details:** -- Extracts insurance for each participant using `getInsuranceForParticipant()` helper -- Populates `$participantData->insurance` property -- Positioned after pickup handling, before room assignment -- Follows same pattern as other participant data extraction - -### Phase 3: Helper Method ✅ COMPLETED (2025-10-07) -- [x] Create `Booking::getInsuranceForParticipant()` -- [x] Add PHPDoc with examples -- [x] Apply php-cs-fixer -- [x] Write unit tests (3 test cases, 14 assertions - all passing) - -**Files Created/Modified:** -- `src/BusProNet/Model/Booking.php` (lines 154-170) - NEW helper method -- `tests/BusProNet/Model/BookingTest.php` (lines 74-137) - NEW tests - -**Implementation Details:** -- Searches through `$insurances` array for participant mapping -- Returns `Insurance|null` based on participant index -- Handles empty mapping arrays with `?? []` operator -- Follows same pattern as `getPickupForParticipant()` and `getSkiPassForParticipant()` -- Tests cover: correct retrieval, no insurances, empty mapping - -### Phase 4: Field State -- [ ] Register insurance condition in `EditFieldStateProvider` -- [ ] Add appropriate help text -- [ ] Test readonly state application - -### Phase 5: HTMX Routes ⚠️ REVIEW REQUIRED -- [ ] **NOTE**: Initial review found NO hardcoded HTMX routes in current codebase -- [ ] Verify if this phase is still needed or can be skipped -- [ ] If needed: Fix insurance field route in `ParticipantFieldOptionsProvider` -- [ ] If needed: Audit and fix other hardcoded routes (skiPass, rentals, etc.) -- [ ] Test HTMX updates in both create and edit flows - -### Phase 6: Template -- [ ] Add insurance field to `edit.html.twig` -- [ ] Verify conditional rendering -- [ ] Test readonly display - -### Phase 7: Data Processing -- [ ] Add insurances to `resetServiceMappings()` -- [ ] Create `processInsurance()` method -- [ ] Add insurance to `removeUnusedServices()` -- [ ] Add insurance to `buildServicePayload()` -- [ ] Update/remove outdated comment -- [ ] Test insurance persistence to API - -### Phase 8: Documentation -- [ ] Update `BOOKING_EDIT_MODERNIZATION.md` -- [ ] Document testing checklist -- [ ] Document code reuse metrics - -## Testing Strategy - -### Unit Tests - -**InsuranceMutabilityCondition**: -```php -// Test standard case - editable -$travelDate = new DateTimeImmutable('+40 days'); -$condition = new InsuranceMutabilityCondition(); -$result = $condition->evaluate($participant, $editDto); -$this->assertFalse($result); // False = editable - -// Test standard case - readonly -$travelDate = new DateTimeImmutable('+20 days'); -$result = $condition->evaluate($participant, $editDto); -$this->assertTrue($result); // True = readonly - -// Test late booking - editable -$bookingDate = new DateTimeImmutable('-2 days'); -$travelDate = new DateTimeImmutable('+15 days'); -$result = $condition->evaluate($participant, $editDto); -$this->assertFalse($result); // Within 3 days of booking - -// Test late booking - readonly -$bookingDate = new DateTimeImmutable('-5 days'); -$travelDate = new DateTimeImmutable('+15 days'); -$result = $condition->evaluate($participant, $editDto); -$this->assertTrue($result); // Past 3 days of booking -``` - -**Booking::getInsuranceForParticipant()**: -```php -// Test insurance found for participant -$insurance = $booking->getInsuranceForParticipant(0); -$this->assertInstanceOf(Insurance::class, $insurance); - -// Test no insurance for participant -$insurance = $booking->getInsuranceForParticipant(5); -$this->assertNull($insurance); -``` - -### Integration Tests - -1. **Edit Flow E2E**: - - Load existing booking with insurance - - Verify insurance field populated correctly - - Verify readonly state when past deadline - - Verify editable state when within deadline - -2. **Data Persistence**: - - Edit insurance selection - - Submit form - - Verify API payload includes insurance data - - Verify insurance mapping correct in payload - -3. **Auto-Reassignment**: - - Edit booking, change skipass (affects price) - - Verify insurance auto-reassigns to correct tier - - Verify notification displayed to user - -## Implementation Notes - -### Date Handling -- All date calculations use `DateTimeImmutable` for immutability -- Travel date: `$bookingDto->travel->dateFrom` -- Booking date: `$bookingDto->booking->bookingDate` -- Current date: `new DateTimeImmutable()` - -### Field State System -- Condition returns `true` → field is readonly -- Condition returns `false` → field is editable -- Help text only shown when readonly -- Hidden state not used (users should see booked insurance) - -### HTMX Integration -- Context-aware route selection prevents hardcoded paths -- Maintains real-time form updates in both flows -- OOB swap targets work identically in edit flow - -### Data Flow -1. `BookingEditDto::fromBooking()` populates insurance from API data -2. `EditFieldStateProvider` applies mutability condition -3. Template renders field with readonly state if locked -4. On submit, `BookingDataProcessor` rebuilds insurance mappings -5. API receives updated insurance data in payload - -## Success Criteria - -- ✅ Insurance field visible in edit flow -- ✅ Readonly state applied based on date constraints -- ✅ Auto-reassignment works on price changes -- ✅ Bulk insurance works in edit flow -- ✅ Insurance persists to API correctly -- ✅ User notifications for automatic changes -- ✅ No code duplication from create flow -- ✅ All tests passing -- ✅ php-cs-fixer applied to all files \ No newline at end of file diff --git a/docs/LOADING_INDICATORS_IMPLEMENTATION.md b/docs/LOADING_INDICATORS_IMPLEMENTATION.md deleted file mode 100644 index 1a3b489..0000000 --- a/docs/LOADING_INDICATORS_IMPLEMENTATION.md +++ /dev/null @@ -1,285 +0,0 @@ -# Loading Indicators Implementation Plan - -## Overview - -Add loading indicators to Step 3 and Step 4 of the booking flow to provide visual feedback during slow API calls. - -**Date:** 2025-10-06 -**Status:** 📋 Planned (not yet implemented) - -## Problem Statement - -**Current User Experience:** -- Step 3: User clicks "Weiter" → 2-3 second wait (inquiry API) → No visual feedback -- Step 4: User clicks "Verbindlich buchen" → 1-2 second wait (booking API) → No visual feedback -- Users may click multiple times thinking the form didn't submit -- No indication that processing is happening - -## Solution - -Use HTMX for form submissions with built-in loading indicators. - -### Why HTMX? - -1. ✅ Already extensively used in the project (Step 2 form refreshes) -2. ✅ Built-in loading state management via `hx-indicator` -3. ✅ Better error handling (no page reload on validation errors) -4. ✅ Progressive enhancement (graceful degradation) -5. ✅ Consistent with existing architecture - -## Implementation Details - -### Step 1: Add HTMX Indicator Styles - -**File:** `assets/styles/app.css` - -Add global styles for HTMX loading indicators: - -```css -/* HTMX Loading Indicator */ -.htmx-indicator { - display: none; -} - -.htmx-request .htmx-indicator { - display: flex; -} - -.htmx-request.htmx-indicator { - display: flex; -} -``` - -### Step 2: Update Step 3 Form - -**File:** `templates/booking/create_step_3.html.twig` - -**Changes:** - -1. Add HTMX attributes to form: -```twig -{{ form_start(form, { - 'attr': { - 'novalidate': 'novalidate', - 'hx-post': path('app_booking_create_step_3'), - 'hx-swap': 'none', - 'hx-indicator': '#step3-loading' - } -}) }} -``` - -2. Add loading overlay before form close: -```twig -{# Loading indicator #} -
-
-
- - - - - Buchung wird validiert... -
-
-
- -{{ form_end(form) }} -``` - -**File:** `src/Controller/Booking/CreateStep3Controller.php` - -**Changes:** - -Add HTMX detection and response handling: - -```php -public function step3(Request $request): Response -{ - // ... existing validation logic ... - - if ($form->isSubmitted() && $form->isValid()) { - try { - // ... existing inquiry + price validation logic ... - - // Validation successful - proceed to confirmation step - $bookingCreateDto->currentStep = 4; - $this->bookingService->saveBookingCreateDto($request, $bookingCreateDto); - - // Handle HTMX requests - if ($request->headers->get('HX-Request')) { - return new Response('', 200, [ - 'HX-Redirect' => $this->generateUrl('app_booking_create_step_4') - ]); - } - - return $this->redirectToRoute('app_booking_create_step_4'); - } catch (\Exception $e) { - // ... existing error handling ... - } - } - - return $this->render('booking/create_step_3.html.twig', [ - 'bookingCreateDto' => $bookingCreateDto, - 'form' => $form->createView(), - ...$this->getSummaryVariables($bookingCreateDto), - ]); -} -``` - -### Step 3: Update Step 4 Form - -**File:** `templates/booking/create_step_4.html.twig` - -**Changes:** - -1. Add HTMX attributes to form (find `form_start`): -```twig -{{ form_start(form, { - 'attr': { - 'novalidate': 'novalidate', - 'hx-post': path('app_booking_create_step_4'), - 'hx-swap': 'none', - 'hx-indicator': '#step4-loading' - } -}) }} -``` - -2. Add loading overlay before submit button: -```twig -{# Loading indicator #} -
-
-
- - - - - Buchung wird durchgeführt... -
-
-
- -{{ form_end(form) }} -``` - -**File:** `src/Controller/Booking/CreateStep4Controller.php` - -**Changes:** - -Add HTMX response handling: - -```php -public function step4(Request $request): Response -{ - // ... existing code ... - - if ($form->isSubmitted() && $form->isValid()) { - try { - // Submit final booking (already validated in Step 3) - $bookingResponse = $this->apiClient->createBooking($bookingCreateDto); - - // ... existing error handling ... - - // Success: Store booking number in flash and clear session - $this->addFlash('booking_number', $bookingResponse->transactionNumber); - $this->bookingService->clearBookingCreateDto($request); - - // Handle HTMX requests - if ($request->headers->get('HX-Request')) { - return new Response('', 200, [ - 'HX-Redirect' => $this->generateUrl('app_booking_success') - ]); - } - - return $this->redirectToRoute('app_booking_success'); - } catch (\Exception $e) { - // ... existing error handling ... - } - } - - return $this->render('booking/create_step_4.html.twig', [ - 'bookingCreateDto' => $bookingCreateDto, - 'form' => $form->createView(), - ...$this->getSummaryVariables($bookingCreateDto), - ]); -} -``` - -## Alternative: Stimulus-Only Approach - -If HTMX is not desired, use the existing `loading_controller.js`: - -**Template:** -```twig -
- {{ form_start(form, {'attr': {'data-action': 'submit->loading#toggle'}}) }} - - - - - - - {{ form_end(form) }} -
-``` - -**Pros:** Simpler, no controller changes -**Cons:** -- Loading indicator persists if server returns error -- Page reload happens anyway -- No error handling improvement - -## Files to Modify - -### Templates: -1. `templates/booking/create_step_3.html.twig` - Add HTMX attributes + loading indicator -2. `templates/booking/create_step_4.html.twig` - Add HTMX attributes + loading indicator - -### Controllers: -3. `src/Controller/Booking/CreateStep3Controller.php` - Add HTMX response handling -4. `src/Controller/Booking/CreateStep4Controller.php` - Add HTMX response handling - -### Styles: -5. `assets/styles/app.css` - Add `.htmx-indicator` styles (if not already present) - -## Benefits - -**User Experience:** -- ✅ Clear visual feedback during API calls -- ✅ Prevents duplicate submissions (button disabled during request) -- ✅ Professional loading experience -- ✅ Reduced user confusion and frustration - -**Technical:** -- ✅ Better error handling (no page reload on validation errors) -- ✅ Consistent with existing HTMX usage in Step 2 -- ✅ Progressive enhancement (works without JavaScript) -- ✅ Flash messages still work via HX-Redirect - -## Testing Checklist - -- [ ] Step 3: Loading indicator shows during inquiry API call -- [ ] Step 3: Form disabled during submission -- [ ] Step 3: Success redirects to Step 4 -- [ ] Step 3: Validation errors show inline without reload -- [ ] Step 4: Loading indicator shows during booking API call -- [ ] Step 4: Form disabled during submission -- [ ] Step 4: Success redirects to success page with flash message -- [ ] Step 4: Errors show inline without reload -- [ ] Works without JavaScript (graceful degradation) -- [ ] No duplicate submissions possible - -## Implementation Priority - -**High Priority** - Significantly improves UX during slow API operations - -## Notes - -- HTMX already included in project dependencies -- Loading indicators match existing design system -- Compatible with all existing validation logic -- No changes to backend business logic required \ No newline at end of file diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 3705c5c..0000000 --- a/docs/README.md +++ /dev/null @@ -1,251 +0,0 @@ -# MyEP Next Booking - Documentation Index - -This directory contains comprehensive documentation for the MyEP Next Booking system architecture, implementation guides, and development workflows. - -## 📋 Documentation Overview - -### Core Architecture Documentation - -#### [FIELD_STATE_SYSTEM.md](FIELD_STATE_SYSTEM.md) -**Universal Conditional Field State System** -- Comprehensive guide to the conditional field architecture -- Field state providers, conditions, and composite logic -- HTMX integration for real-time field updates -- Examples for age-based, value-dependent, and complex conditions -- **Status**: ✅ Current and complete - -#### [FORM_PROCESSING.md](FORM_PROCESSING.md) -**Advanced Form Processing Architecture** -- Field handler system with dependency resolution -- DTO pattern implementation for type-safe data flow -- Service registration and field option providers -- HTMX dynamic updates and form validation -- **Status**: ✅ Current with recent HTMX fixes - -#### [PRICING_DISPLAY_IMPLEMENTATION.md](PRICING_DISPLAY_IMPLEMENTATION.md) -**Real-time Pricing System** -- Inline pricing in form options with smart formatting -- Unified booking summary with integrated pricing display -- Service pricing calculations and HTMX integration -- Implementation details and UX improvements -- **Status**: ✅ Implementation completed successfully - -#### [SERVICE_AVAILABILITY_SYSTEM.md](SERVICE_AVAILABILITY_SYSTEM.md) -**Dynamic Service Availability System** -- Prevents overbooking within single booking sessions -- Real-time availability tracking across all participants -- Dynamic service filtering based on capacity limits -- Seamless HTMX integration for instant updates -- **Status**: ✅ Implementation completed successfully - -### Feature Implementation Guides - -#### [AGE_BASED_FIELDS_PLAN.md](AGE_BASED_FIELDS_PLAN.md) -**Age-Based Field Constraints System** -- Age range conditions for field visibility/behavior -- Service filtering based on participant age -- Dynamic field state management -- Implementation roadmap and examples -- **Status**: ✅ Fully implemented (see CLAUDE.md) - -#### [AGE_CONSTRAINTS_MODEL_EXTENSION_PLAN.md](AGE_CONSTRAINTS_MODEL_EXTENSION_PLAN.md) -**Extended Age Constraint Model** -- Advanced age-based business logic -- Service availability constraints -- Field interdependency handling -- Data model extensions -- **Status**: ✅ Fully implemented (see CLAUDE.md) - -#### [API_VALIDATION_STAGE_PLAN.md](API_VALIDATION_STAGE_PLAN.md) -**API Availability Validation Stage** -- Real-time availability validation via BusProNet API -- Final validation before booking confirmation -- Cross-session availability tracking -- **Status**: 📋 Planned future enhancement - -### System Status - -#### [SYSTEM_STATUS_2025.md](SYSTEM_STATUS_2025.md) -**Current System Status & Feature Tracking** -- Comprehensive overview of implemented features -- Known issues and technical debt -- System architecture status -- **Status**: ✅ Updated regularly - -## 🏗️ System Architecture Overview - -### Multi-Step Booking Flow -1. **Step 1**: Room selection with dynamic pricing -2. **Step 2**: Participant details with conditional fields -3. **Step 3**: Confirmation and BPN API submission - -### Core Components - -#### Form System Architecture -- **Field Handlers**: Modular field processing with dependency chains -- **Conditional States**: Dynamic field behavior (readonly, disabled, hidden, required) -- **Service Integration**: Real-time updates via HTMX -- **Pricing Display**: Inline costs and unified summary - -#### BusProNet Integration -- **XML API Client**: Request/response handling -- **Data Processing**: API response transformation -- **Caching Layer**: Performance optimization -- **Error Handling**: Comprehensive error management - -#### Service Layer -- **Booking Management**: Core workflow orchestration -- **Travel Data Services**: API data access and caching -- **Pricing Calculations**: Real-time cost computation -- **Field Options**: Dynamic choice generation - -## 🎯 Feature Status Matrix - -| Feature | Status | -|---------|--------| -| **Multi-Step Booking** | ✅ Complete | -| **Room Selection** | ✅ Complete | -| **Service Selection** | ✅ Complete | -| **Pricing Display** | ✅ Complete | -| **Transportation Services** | ✅ Complete | -| **Conditional Field States** | ✅ Complete | -| **HTMX Integration** | ✅ Complete | -| **BPN API Integration** | ✅ Complete | -| **Dynamic Availability** | ✅ Complete | -| **Service Descriptions** | ✅ Complete | -| **License Plate Field** | ✅ Complete | -| **Age-Based Constraints** | ✅ Complete | -| **Rental Duration Filtering** | ✅ Complete | -| **Insurance Booking** | ✅ Complete | -| **Bulk Insurance Booking** | ✅ Complete | -| **API Availability Validation** | 📋 Planned | - -**Legend**: ✅ Complete | 📋 Planned - -## 🔧 Implementation Patterns - -### Field Handler Pattern -```php -class ParticipantTransportationOutboundFieldHandler extends AbstractParticipantFieldHandler -{ - public function getFieldName(): string { return 'transportationOutbound'; } - public function getDependencies(): array { return ['assignedRoomId']; } - public function shouldProcess(/* ... */): bool { /* conditional logic */ } - public function processField(/* ... */): void { /* field processing */ } -} -``` - -### Conditional Field States -```php -$this->fieldStateConditions['advancedServices'] = [ - 'hidden' => new AgeRangeCondition(null, 15), - 'required' => FieldValueCondition::equals('roomType', 'suite'), -]; -``` - -### Service Registration -```php -# config/services.yaml -App\Form\Service\ParticipantTransportationOutboundFieldHandler: - tags: [{ name: 'app.participant_field_handler', priority: 100 }] - -App\Service\ServiceAvailabilityCalculator: - # Automatically registered via autowiring -``` - -## 🧪 Testing Strategy - -### Test Coverage Areas -- **Unit Tests**: Service layer and business logic -- **Integration Tests**: Form processing and API communication -- **Field Handler Tests**: Conditional logic and dependencies -- **XML Processing Tests**: BPN API response parsing - -### Test Commands -```bash -# All tests -./vendor/bin/phpunit - -# Specific areas -./vendor/bin/phpunit tests/Service/ # Service layer -./vendor/bin/phpunit tests/BusProNet/ # API integration -./vendor/bin/phpunit tests/Form/ # Form processing - -# Test availability system -bin/console debug:container ServiceAvailabilityCalculator -``` - -## 📈 Performance Considerations - -### Optimization Strategies -- **Lazy Loading**: Field handlers loaded on demand -- **Caching**: API responses and computed choices -- **Dependency Tracking**: Efficient field state updates -- **HTMX Optimization**: Targeted DOM updates - -### Monitoring Points -- Form rendering performance -- HTMX response times -- BPN API communication latency -- Database query optimization -- Service availability calculation performance - -## 🔄 Development Workflow - -### Adding New Features - -1. **Plan**: Create implementation plan document -2. **Design**: Define interfaces and data structures -3. **Implement**: Follow established patterns -4. **Test**: Unit and integration testing -5. **Document**: Update relevant documentation -6. **Deploy**: Production deployment with monitoring - -### Code Standards -- PSR-12 compliance with `declare(strict_types=1)` -- PHP 8+ features (typed properties, constructor promotion) -- Immutable DateTime objects -- Explicit comparisons and type safety -- Comprehensive documentation - -## 📚 Related Resources - -### External Documentation -- [Symfony 6.4 Documentation](https://symfony.com/doc/6.4/index.html) -- [HTMX Documentation](https://htmx.org/docs/) -- [TailwindCSS Documentation](https://tailwindcss.com/docs) -- [Stimulus Handbook](https://stimulus.hotwired.dev/handbook/introduction) - -### Project-Specific Guides -- **[../CLAUDE.md](../CLAUDE.md)**: AI development assistance guidelines -- **Installation & Setup**: See main README.md -- **API Integration**: BusProNet XML API documentation (internal) -- **Deployment**: Production deployment procedures (internal) - -## 🎯 Future Roadmap - -### Planned Enhancements -- **Age-Based Field Constraints**: Complete implementation -- **Advanced Pricing Features**: Discounts, taxes, multi-currency -- **Enhanced BPN Integration**: Extended API coverage -- **Mobile Optimization**: Responsive design improvements -- **Analytics Integration**: User behavior tracking -- **Cross-Session Availability**: Extend availability tracking beyond single sessions -- **API Availability Validation**: Implement final validation stage against BusProNet API before booking confirmation - -### Technical Debt -- **Code Coverage**: Increase test coverage to 90%+ -- **Performance Optimization**: Form rendering improvements -- **Documentation**: API endpoint documentation -- **Monitoring**: Enhanced logging and metrics -- **Availability Testing**: Comprehensive test coverage for availability system -- **API Validation Integration**: Implement real-time availability validation via BusProNet API - ---- - -**Documentation Maintained By**: Development Team -**Last Updated**: 2025-01-XX -**Version**: 2.0 -**Status**: ✅ Current and Comprehensive - -For development assistance, see [CLAUDE.md](../CLAUDE.md) for AI-specific guidelines and project context. \ No newline at end of file diff --git a/docs/REFACTORING_BOOKING_DATA_PROCESSOR.md b/docs/REFACTORING_BOOKING_DATA_PROCESSOR.md deleted file mode 100644 index 786fcc7..0000000 --- a/docs/REFACTORING_BOOKING_DATA_PROCESSOR.md +++ /dev/null @@ -1,183 +0,0 @@ -# Booking Data Processor Refactoring Plan - -## Current Status (2025-10-05) - -We discovered a structural inconsistency between the UPDATE and CREATE booking flows while implementing the booking submission feature. - -## Problem Statement - -The UPDATE and CREATE flows use fundamentally different data structures, leading to code duplication and complexity: - -### UPDATE Flow (Current) -- `BookingEditDto` contains a `Booking` object -- `Booking` has centralized service arrays with participant mappings: - - `booking.additionalServices` - array of Service objects with `mapping` property (0-based indices) - - `booking.transportationServices` - array of Service objects with `mapping` property - - `booking.pickupsOutbound` - array of Pickup objects with `mapping` property -- `BookingDataProcessor.createUpdateRequestPayload()`: - - Resets all service mappings - - Iterates through participants - - Rebuilds service mappings by looking up services in booking data - - Removes unused services - - Converts 0-based indices to 1-based for API - -### CREATE Flow (Current) -- `BookingCreateDto` contains only `participants` array -- Each `ParticipantDto` has direct service references: - - `courses`, `skiPass`, `additionalServices`, `board`, `rentals` - - `transportationOutbound`, `transportationInbound` - - `pickupOutbound`, `pickupInbound` - - `insurance` -- `BookingDataProcessor.createBookingRequestPayload()`: - - Collects services directly from participants - - Groups by service ID - - Converts to 1-based participant IDs for API - -## Root Cause - -The UPDATE flow was designed to work with API-sourced `Booking` objects that already have centralized service mappings. The CREATE flow was designed from scratch with a simpler participant-centric approach. - -## Proposed Solution - -**Align both flows to use the participant-centric structure:** - -1. **Both DTOs work the same way:** - - Both have `participants` array - - Services are attached directly to participants - - No centralized service objects with mappings - -2. **Unified payload generation:** - - Use same `collect*Mappings()` methods for both flows - - Use same `addServicesFromMap()` helper - - Remove complex service manipulation in update flow - -3. **Benefits:** - - Single source of truth for service mappings - - Less code duplication - - Easier to understand and maintain - - Consistent patterns across all booking operations - -## Implementation Steps - -### Phase 1: Refactor BookingEditDto.fromBooking() -- [x] Already populates participant services correctly -- [x] Services are already attached to participants -- [ ] Verify all service types are covered - -### Phase 2: Refactor BookingDataProcessor.createUpdateRequestPayload() -- [ ] Remove `resetServiceMappings()` -- [ ] Remove `processParticipantServices()` (complex service lookup) -- [ ] Remove `processAdditionalServices()` -- [ ] Remove `processTransportationServices()` -- [ ] Remove `processPickupLocations()` -- [ ] Remove `removeUnusedServices()` -- [ ] Use `collect*Mappings()` methods instead (same as create flow) -- [ ] Update `buildServicesPayload()` to use collected maps -- [ ] Update `buildPickupPayload()` to use collected maps - -### Phase 3: Add convertServicesToMap() Helper -- [ ] Create helper to convert service objects with mapping to ID => participant IDs map -- [ ] This bridges the gap between old structure (if needed) and new structure - -### Phase 4: Testing -- [ ] Test update flow with all service types -- [ ] Test create flow (should remain unchanged) -- [ ] Verify API payloads are identical before/after refactoring -- [ ] Test edge cases (no services, all services, mixed scenarios) - -### Phase 5: Cleanup -- [ ] Remove unused methods from BookingDataProcessor -- [ ] Remove unused properties from Booking model (if any) -- [ ] Update documentation - -## Risk Assessment - -**MEDIUM RISK** - This refactoring touches critical booking update functionality that is already working in production. - -### Risks: -1. Breaking existing update flow -2. Subtle bugs in service mapping -3. Data loss if participant service references are incorrect -4. Payment/bank account handling might break - -### Mitigation: -1. Comprehensive testing before deployment -2. Keep git history clean with atomic commits -3. Test with real booking data from sandbox -4. Verify XML payloads match exactly (before/after) -5. Have rollback plan ready - -## Decision Point - -**Should we refactor NOW or LATER?** - -### Arguments for NOW: -- We're already in BookingDataProcessor -- Fresh understanding of both flows -- Prevents further divergence -- Makes current task (booking submission) cleaner - -### Arguments for LATER: -- Current task (booking submission) is incomplete -- Refactoring is significant and risky -- Could introduce bugs in working update flow -- Should be separate PR with focused testing -- Current booking submission is more urgent - -## Decision - -**REFACTOR LATER** - Complete the booking submission task first, then do this refactoring as a separate focused effort. - -**AGREED:** The CREATE flow's participant-centric structure is the new standard. The UPDATE flow should adopt this architecture in the future refactoring. - -### Reasoning: -1. Booking submission is nearly complete and is the immediate business need -2. Update flow is working and tested - don't break what works -3. Refactoring deserves dedicated focus and testing -4. Can create comprehensive tests for both flows first -5. Allows for proper code review and QA - -### Short-term Solution: -- Keep both flows separate for now -- Add the `convertServicesToMap()` helper to bridge structures -- Complete booking submission with current architecture -- Document this technical debt - -### Long-term Plan: -- Create separate refactoring task/issue -- Write comprehensive tests for update flow first -- Perform refactoring in dedicated branch -- Extensive testing with sandbox data -- Separate PR with focused review - -## Current Task: Booking Submission - -We are 60% complete with booking submission implementation: - -### Completed: -- [x] BookingResponse, PriceItem, PaymentTerms models -- [x] BookingResponseParser with pricing data -- [x] createBookingRequestPayload() in BookingDataProcessor -- [x] Payment type ID constants -- [x] Helper method addServicesFromMap() - -### Remaining: -- [ ] Add TYPE_BOOKING constant to ApiClient -- [ ] Add createBookingInquiry() and createBooking() to ApiClient -- [ ] Update ApiResponseParser to handle BUCHUNG response type -- [ ] Implement two-phase submission in CreateStep4Controller -- [ ] Add clearBookingCreateDto() to BookingService -- [ ] Create BookingSuccessController and template -- [ ] Test with sandbox API - -## Next Steps - -1. **IMMEDIATE:** Continue with booking submission task -2. **AFTER COMPLETION:** Create refactoring issue/task -3. **FUTURE:** Dedicated refactoring effort with proper testing - ---- - -**Document Created:** 2025-10-05 -**Status:** Deferred - Continue with booking submission -**Related:** Booking submission implementation (in progress) \ No newline at end of file diff --git a/docs/SERVICE_AVAILABILITY_SYSTEM.md b/docs/SERVICE_AVAILABILITY_SYSTEM.md deleted file mode 100644 index 9fe1510..0000000 --- a/docs/SERVICE_AVAILABILITY_SYSTEM.md +++ /dev/null @@ -1,370 +0,0 @@ -# Dynamic Service Availability System - -## Overview - -The Dynamic Service Availability System prevents overbooking within a single booking session by tracking service selections across all participants and dynamically adjusting availability in real-time. This ensures that services with limited capacity cannot be overbooked during the form creation process. - -## Problem Statement - -### Business Challenge -- Services have limited availability (e.g., "Advanced Ski Course: 5 available") -- Multiple participants in a booking can select the same services -- Without dynamic tracking, services could be overbooked within a single booking session -- Users need immediate feedback when services become unavailable - -### Technical Requirements -- Track service selections across all participants in current booking session -- Recalculate remaining availability during HTMX form refresh cycles -- Hide services that have reached capacity limits -- Maintain session-scoped availability (not permanent database changes) -- Support all service types with consistent behavior - -## Architecture Overview - -### Core Components - -#### 1. ServiceAvailabilityCalculator (`src/Service/ServiceAvailabilityCalculator.php`) -**Purpose**: Central service for calculating dynamic availability based on current selections - -**Key Methods**: -```php -public function calculateRemainingAvailability(BookingCreateDto $bookingDto, int $currentParticipantIndex): array -public function filterAvailableServices(array $services, BookingCreateDto $bookingDto, int $participantIndex): array -public function isServiceAvailable(int $serviceId, BookingCreateDto $bookingDto, int $participantIndex): bool -``` - -**Responsibilities**: -- Calculate service usage across all participants (excluding current participant) -- Determine remaining availability per service -- Filter service arrays to only include available services -- Handle all service types consistently - -#### 2. Enhanced ParticipantFieldOptionsProvider -**Integration Point**: Field option generation with availability filtering - -**Enhanced Methods**: -```php -private function filterServicesByAvailability(array $services, BookingDtoInterface $bookingDto, int $participantIndex): array -``` - -**Updated Field Providers**: -- Courses (`TOKEN_COURSES`) -- Additional Services (`TOKEN_ADDITIONAL`) -- Board/Meal Plans (`TOKEN_BOARD`) -- Rentals (`TOKEN_RENTALS`) -- Ski Passes (`TOKEN_SKI_PASS`) -- Transportation Services (Outbound/Inbound) - -## Implementation Details - -### Service Usage Calculation - -The system tracks how many participants have selected each service: - -```php -private function calculateServiceUsage(BookingCreateDto $bookingDto, int $currentParticipantIndex): array -{ - $serviceUsage = []; - - foreach ($bookingDto->participants as $index => $participant) { - // Skip current participant to avoid counting their potential selections - if ($index === $currentParticipantIndex) { - continue; - } - - // Count all service types for this participant - $this->countParticipantServiceUsage($participant, $serviceUsage); - } - - return $serviceUsage; -} -``` - -### Availability Calculation Logic - -For each service, remaining availability is calculated as: -``` -If service.available is null or <= 0: - Service is unlimited (always available) -Else: - Remaining = max(0, original_availability - usage_count) -``` - -### Service Type Handling - -The system handles all major service types: - -**Single Selection Services**: -- Board/Meal Plans -- Ski Passes -- Transportation (Outbound/Inbound) - -**Multiple Selection Services**: -- Courses -- Additional Services -- Rentals - -### Integration with Form System - -#### Field Option Filtering Chain -```php -'choices' => $this->filterServicesByAvailability( - $this->filterServicesByAgeConstraints( - $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES), - $bookingDto, - $participantIndex - ), - $bookingDto, - $participantIndex -), -``` - -#### HTMX Integration -- Availability recalculated during each form refresh cycle -- Services dynamically hidden when capacity reached -- Real-time feedback without full page refresh -- Maintains consistency across all participants - -## Business Rules - -### Availability Behavior -- **Available Services**: Shown normally with pricing -- **Unavailable Services**: Hidden completely from selection -- **High Availability**: Services with very high limits effectively always available -- **Per-Participant Limit**: Each participant can select a service maximum once - -### Service Capacity Management -- **Original Availability**: Parsed from XML data at booking initialization -- **Dynamic Availability**: Calculated in real-time based on current selections -- **Session Scope**: Availability tracking only within current booking session -- **No Persistence**: Changes not saved to database or XML files -- **Unlimited Services**: Services with null or ≤0 availability are treated as unlimited -- **Company Strategy**: High availability values used for services that should always be bookable - -### Edge Cases Handled -- Null or missing availability values (treated as unlimited availability) -- Zero or negative availability values (treated as unlimited availability) -- Services already selected by current participant (not counted against them) -- Invalid or missing participant data (gracefully ignored) -- Empty service arrays (handled without errors) -- Services without availability limits (always remain available) - -## Usage Examples - -### Scenario 1: Course Selection with Limited Capacity -``` -Initial State: -- Advanced Ski Course: 3 available - -Participant 1: Selects Advanced Ski Course → 2 remaining -Participant 2: Sees Advanced Ski Course available → Selects it → 1 remaining -Participant 3: Sees Advanced Ski Course available → Selects it → 0 remaining -Participant 4: Advanced Ski Course hidden (not available) - -Note: Most services will have unlimited availability (null or high values) and remain visible. -``` - -### Scenario 2: Multiple Service Types -``` -Services with Limits: -- Rental Helmet: 10 available -- Advanced Course: 2 available -- Premium Board: 5 available - -As participants select services: -- Each selection reduces availability for remaining participants -- Services become hidden when capacity reached -- Participants see only services they can still book -``` - -## Performance Considerations - -### Optimization Strategies -- **Lightweight Calculations**: Simple arithmetic operations only -- **No Database Queries**: All data from memory (DTO objects) -- **Cached Service Lists**: Service collections retrieved once per request -- **Efficient Filtering**: Array operations with minimal overhead - -### Scalability -- **Memory Usage**: Minimal additional memory footprint -- **Processing Time**: Linear time complexity O(n) where n = participant count -- **HTMX Performance**: No impact on response times -- **Large Bookings**: Efficient even with many participants -- **Unlimited Services**: Zero-cost filtering for services without availability limits - -## Configuration - -### Service Registration -The ServiceAvailabilityCalculator is automatically registered via Symfony's autowiring: - -```yaml -# config/services.yaml -services: - _defaults: - autowire: true - autoconfigure: true - - App\: - resource: '../src/' - exclude: - - '../src/Entity/' - - '../src/Kernel.php' -``` - -### Field Provider Integration -```php -public function __construct( - private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory, - private readonly ServiceAvailabilityCalculator $serviceAvailabilityCalculator, -) { - parent::__construct(); -} -``` - -## Testing Strategy - -### Unit Testing Scenarios -1. **Service Usage Calculation**: Test counting across multiple participants -2. **Availability Filtering**: Verify services hidden when capacity reached -3. **Edge Cases**: Handle null values, empty arrays, invalid data -4. **Service Types**: Test all service categories (courses, rentals, etc.) -5. **Current Participant Exclusion**: Ensure current participant selections not counted - -### Integration Testing -1. **Form Field Generation**: Verify filtered choices in field options -2. **HTMX Refresh Cycles**: Test availability updates during form interactions -3. **Multiple Participants**: Test complex scenarios with many participants -4. **Service Combinations**: Test mixed service types and availability levels - -### Manual Testing Scenarios -``` -Test Case 1: Basic Availability Reduction -- Create booking with 2 participants -- Select service with availability = 2 for participant 1 -- Verify participant 2 sees availability reduced -- Select same service for participant 2 -- Verify service hidden for additional participants - -Test Case 2: Mixed Service Types -- Test courses, rentals, and transportation together -- Verify each service type respects availability limits -- Confirm services with high limits remain available - -Test Case 3: HTMX Integration -- Make service selections via HTMX form refresh -- Verify real-time availability updates -- Test form submission and navigation between steps -``` - -## Error Handling - -### Graceful Degradation -- **Missing Availability Data**: Treats as unavailable (hidden) -- **Invalid Service Objects**: Safely ignored in calculations -- **Corrupted Participant Data**: Skips invalid participants -- **Service Lookup Failures**: Continues processing other services - -### Logging and Monitoring -- No explicit logging (availability is business logic, not error condition) -- Integrates with existing form processing error handling -- Symfony debug toolbar shows service container usage - -## Future Enhancements - -### Potential Improvements -1. **Availability Display**: Show remaining count in service labels (optional) -2. **Reservation System**: Temporary hold on services during selection -3. **Priority Booking**: VIP participants get access to limited services first -4. **Cross-Session Tracking**: Track availability across multiple booking sessions -5. **Analytics**: Collect data on service demand and capacity utilization - -### Performance Optimizations -1. **Caching Layer**: Cache availability calculations for identical participant sets -2. **Lazy Loading**: Only calculate availability for visible services -3. **Background Updates**: Pre-calculate availability for common scenarios -4. **Delta Updates**: Only recalculate changed services during HTMX updates - -## Troubleshooting - -### Common Issues - -**Services Always Hidden**: -- Check if services have availability limits set (should be null/0 for unlimited) -- Verify service ID matching between travel data and participant selections -- Confirm participant data structure is correct -- Most services should be unlimited and always visible - -**Availability Not Updating**: -- Verify HTMX integration is working -- Check that form refresh includes all participant data -- Ensure ServiceAvailabilityCalculator is being called - -**Performance Problems**: -- Review participant count and service selection complexity -- Check for inefficient service lookups or data processing -- Monitor memory usage with large booking sessions - -### Debugging Tools -```php -// Debug availability calculation -$calculator = $container->get(ServiceAvailabilityCalculator::class); -$availability = $calculator->calculateRemainingAvailability($bookingDto, $participantIndex); -dump($availability); - -// Debug service filtering -$filtered = $calculator->filterAvailableServices($services, $bookingDto, $participantIndex); -dump($filtered); -``` - -## Integration Points - -### Dependencies -- `App\BusProNet\Model\Service` - Service data objects -- `App\Form\Model\BookingCreateDto` - Booking and participant data -- `App\BusProNet\Constants` - Service type constants -- `App\BusProNet\Utility\DirectionMapper` - Transportation direction mapping - -### Related Systems -- **Field State System**: Availability filtering integrates with conditional field states -- **Pricing System**: Available services included in pricing calculations -- **HTMX Updates**: Availability changes trigger form refreshes -- **Form Handlers**: Service selections processed by specialized field handlers - -## Future Enhancements - -### Planned: API Validation Stage - -**Important Note**: The current availability system uses XML data that may become outdated during the booking creation process. A future enhancement will implement a **validation stage** that checks final service selections against real-time availability via the BusProNet API before final booking confirmation. - -#### Planned Implementation -- **Pre-Submission Validation**: Before final booking submission, validate all selected services against current API availability -- **Real-Time Check**: Call BusProNet API to get current availability status -- **Conflict Resolution**: Handle cases where selected services are no longer available -- **User Feedback**: Provide clear messaging when services become unavailable during booking process - -#### Technical Integration Points -```php -// Future validation service (planned) -class BookingValidationService -{ - public function validateServiceAvailability(BookingCreateDto $bookingDto): ValidationResult; - public function resolveAvailabilityConflicts(BookingCreateDto $bookingDto): ConflictResolution; -} -``` - -#### Business Logic -- **Session Availability**: Current system prevents overbooking within single booking session -- **API Validation**: Future system will prevent overbooking across all booking sessions system-wide -- **Two-Stage Protection**: Provides both immediate feedback and final validation - -This planned enhancement will complement the existing dynamic availability system by adding a final validation layer that ensures booking integrity against the authoritative BusProNet API data. - ---- - -**Implementation Status**: ✅ **Completed and Tested** -**Last Updated**: January 2025 -**Integration**: Seamless with existing form system -**Performance**: Optimized for real-time updates -**Testing**: Verified working in development environment -**Future Enhancement**: API validation stage planned for booking finalization -**Documentation**: Comprehensive with examples and troubleshooting \ No newline at end of file diff --git a/docs/SYSTEM_STATUS_2025.md b/docs/SYSTEM_STATUS_2025.md deleted file mode 100644 index fb22c15..0000000 --- a/docs/SYSTEM_STATUS_2025.md +++ /dev/null @@ -1,364 +0,0 @@ -# MyEP Next Booking System Status - 2025 - -## 🎯 Executive Summary - -**MyEP Next Booking** is a production-ready Symfony 6.4 travel booking application with comprehensive multi-step workflow, real-time pricing, and sophisticated form processing capabilities. The system successfully integrates with Bus Pro Net (BPN) XML API and features advanced conditional field logic, transportation services, and seamless user experience via HTMX. - -**Current Status**: ✅ **Production Ready** -**Architecture Maturity**: ✅ **Enterprise Grade** -**Feature Completeness**: 🎯 **Core Features Complete, Advanced Features Planned** - -## 🏗️ System Architecture Status - -### ✅ Core Components - COMPLETED - -#### Multi-Step Booking Workflow -- **Step 1**: Room selection with dynamic pricing ✅ -- **Step 2**: Participant details with conditional fields ✅ -- **Step 3**: Confirmation and BPN API submission ✅ -- **Navigation**: Seamless step progression with data persistence ✅ - -#### Advanced Form System -- **Field Handler Registry**: 15+ specialized field handlers ✅ -- **Conditional Field States**: Universal condition system ✅ -- **Real-time Updates**: HTMX integration with targeted triggers ✅ -- **XSS Protection**: Built-in security measures ✅ -- **Data Validation**: Comprehensive form validation ✅ - -#### Service Selection Framework -- **Transportation Services**: Outbound/inbound selection ✅ -- **Pickup Services**: Location-based conditional options ✅ -- **Parking Services**: Self-organized transport handling ✅ -- **Accommodation Services**: Board selection, room assignment ✅ -- **Activity Services**: Ski passes, courses, rentals ✅ -- **Rental Insurance**: Checkbox interface with conditional visibility ✅ -- **Body Dimensions**: Hidden unless rental services selected ✅ -- **Additional Services**: Flexible service extension system ✅ -- **Service Descriptions**: XML-based service descriptions with form integration ✅ -- **License Plate Field**: Optional vehicle identification field for parking participants ✅ - -#### Pricing & Display System -- **Inline Pricing**: Service costs in form options ✅ -- **Real-time Calculations**: Live pricing updates via HTMX ✅ -- **Smart Formatting**: Zero-price service handling ✅ -- **Unified Summary**: Integrated booking and pricing display ✅ -- **Service Integration**: Seamless pricing calculation flow ✅ - -#### BusProNet API Integration -- **XML Communication**: Request/response handling ✅ -- **Data Processing**: API response transformation ✅ -- **Error Handling**: Comprehensive error management ✅ -- **Caching Layer**: Performance optimization ✅ -- **Direction Mapping**: API/internal data translation ✅ - -### 🔄 Advanced Features - IN PROGRESS - -#### Age-Based Field Constraints -- **Planning**: Complete architecture documented ✅ -- **Foundation**: Conditional field system ready ✅ -- **Implementation**: Service filtering logic 🔄 -- **Testing**: Comprehensive test coverage ⏳ -- **Status**: Ready for implementation sprint - -#### Enhanced Pricing Features -- **Current**: Basic pricing with real-time updates ✅ -- **Planned**: Discounts, taxes, multi-currency support 🔄 -- **Foundation**: Extensible pricing architecture ✅ -- **Status**: Foundation ready for enhancement - -## 📊 Feature Completion Matrix - -| Component | Planning | Implementation | Testing | Documentation | Production | -|-----------|----------|---------------|---------|---------------|------------| -| **Core Booking Flow** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Room Selection** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Participant Management** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Transportation Services** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Service Selection** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Pricing Display** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Conditional Fields** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **HTMX Integration** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **BPN API Integration** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Service Descriptions** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **License Plate Field** | ✅ | ✅ | ✅ | ✅ | ✅ | -| **Age-Based Constraints** | ✅ | 🔄 | ⏳ | ✅ | ⏳ | -| **Advanced Pricing** | ✅ | ⏳ | ⏳ | 🔄 | ⏳ | -| **Mobile Optimization** | ✅ | ⏳ | ⏳ | ⏳ | ⏳ | - -**Legend**: ✅ Complete | 🔄 In Progress | ⏳ Planned - -## 🎛️ Technical Infrastructure Status - -### Backend Architecture ✅ **PRODUCTION READY** -- **Framework**: Symfony 6.4 LTS (Long-term support until 2027) -- **PHP**: 8.1+ with modern features (typed properties, enums, match expressions) -- **Database**: MariaDB 10.11+ with Doctrine ORM -- **API Integration**: Custom XML client with comprehensive error handling -- **Security**: OAuth2 authentication, XSS protection, input validation - -### Frontend Stack ✅ **MODERN & RESPONSIVE** -- **JavaScript**: Stimulus controllers for progressive enhancement -- **Dynamic Updates**: HTMX for seamless user interactions -- **Styling**: TailwindCSS with responsive design -- **Build Pipeline**: Webpack Encore with optimization -- **Templating**: Twig with component-based architecture - -### Development Workflow ✅ **ENTERPRISE GRADE** -- **Local Environment**: DDEV with PHP 8.2, MariaDB 10.11 -- **Code Quality**: PHP-CS-Fixer with PSR-12 compliance -- **Testing**: PHPUnit with Symfony bridge, comprehensive test coverage -- **Documentation**: Extensive architectural documentation -- **Version Control**: Git with feature branch workflow - -### Performance & Scalability ✅ **OPTIMIZED** -- **Caching**: Multi-layer caching (API responses, computed choices) -- **Database**: Optimized queries with eager loading -- **Frontend**: Lazy loading, targeted DOM updates via HTMX -- **File Operations**: Efficient SFTP integration with Flysystem -- **Monitoring**: Comprehensive logging with multiple channels - -## 🔧 Service Architecture Details - -### Field Handler System ✅ **COMPREHENSIVE** - -**Implemented Handlers** (15+ specialized processors): -``` -├── ParticipantTransportationOutboundFieldHandler # Bus/car transport selection -├── ParticipantTransportationInboundFieldHandler # Return transport -├── ParticipantPickupOutboundFieldHandler # Pickup location services -├── ParticipantPickupInboundFieldHandler # Return pickup services -├── ParticipantParkingFieldHandler # Self-organized parking -├── ParticipantBoardFieldHandler # Meal plan selection -├── ParticipantSkiPassFieldHandler # Ski pass options -├── ParticipantCoursesFieldHandler # Activity courses -├── ParticipantRentalsFieldHandler # Equipment rentals -├── ParticipantAdditionalServicesFieldHandler # Extra services -├── ParticipantAssignedRoomFieldHandler # Room assignments -├── ParticipantDateOfBirthFieldHandler # Age processing -├── ParticipantRemarksRoomFieldHandler # Special requests -├── ParticipantRentalInsuranceFieldHandler # Rental insurance checkbox -├── ParticipantLicensePlateFieldHandler # Vehicle license plate input -└── [Custom handlers easily extensible] -``` - -**Handler Capabilities**: -- Dependency resolution with circular dependency detection -- Conditional processing based on participant data -- Service registration via Symfony's service container -- Priority-based processing order -- Type-safe data handling with proper validation - -### Conditional Field State System ✅ **ADVANCED** - -**Available Conditions**: -- `AgeRangeCondition`: Age-based field behavior -- `FieldValueCondition`: Field interdependency logic -- `ServiceSubTypeCondition`: Service-specific conditions -- `CompositeCondition`: Complex AND/OR/NOT logic -- Custom conditions easily extensible - -**Field States**: -- `readonly`: Field visible but not editable -- `disabled`: Field interaction disabled -- `required`: Field becomes mandatory -- `hidden`: Field not displayed (CSS-based) - -**Real-world Examples**: -```php -// Age-based service restriction -$this->fieldStateConditions['advancedSkiCourse'] = [ - 'hidden' => new AgeRangeCondition(null, 15), // Hide for under 15 - 'required' => FieldValueCondition::equals('skillLevel', 'expert'), -]; - -// Transportation mutual exclusivity -$this->fieldStateConditions['pickupLocation'] = [ - 'hidden' => ServiceSubTypeCondition::equals('transportationOutbound', 'CAR'), -]; - -$this->fieldStateConditions['parkingRequired'] = [ - 'hidden' => ServiceSubTypeCondition::notEquals('transportationOutbound', 'CAR'), -]; -``` - -## 🚀 Performance Metrics - -### System Performance ✅ **OPTIMIZED** -- **Page Load Time**: <2s for form rendering with full data -- **HTMX Response Time**: <500ms for field updates -- **Database Queries**: Optimized with <10 queries per form load -- **API Response Time**: <1s for BPN API integration -- **Memory Usage**: <128MB for typical booking workflow - -### User Experience Metrics ✅ **EXCELLENT** -- **Form Interaction**: Real-time updates without page refresh -- **Field Dependencies**: Instant conditional field updates -- **Pricing Updates**: Live calculation display (<100ms) -- **Error Handling**: Graceful degradation with user-friendly messages -- **Mobile Responsiveness**: Fully responsive design - -### Code Quality Metrics ✅ **HIGH STANDARD** -- **PSR-12 Compliance**: 100% via automated php-cs-fixer -- **Type Coverage**: Strict types on all files -- **Test Coverage**: 80%+ on critical business logic -- **Documentation**: Comprehensive architectural documentation -- **Security**: XSS protection, input validation, secure API communication - -## 🗂️ Data Architecture - -### Entity Relationships ✅ **WELL-STRUCTURED** -``` -Travel (from BPN API) -├── Rooms[] (with pricing) -├── Services[] (board, activities, transport) -└── Availability (dates, capacity) - -Booking (internal) -├── BookingCreateDto (form data) -├── Participants[] (with services) -└── Pricing (calculated totals) - -API Integration -├── XML Request Building -├── Response Parsing -└── Data Transformation -``` - -### Data Flow Patterns ✅ **EFFICIENT** -1. **Form Submission** → DTO Validation → Field Handler Processing -2. **Service Selection** → Pricing Calculation → HTMX Update -3. **API Communication** → XML Building → Response Processing → Caching -4. **State Management** → Condition Evaluation → Field State Application - -## 🧪 Quality Assurance Status - -### Testing Coverage ✅ **COMPREHENSIVE** -- **Unit Tests**: Service layer, field handlers, conditions -- **Integration Tests**: API communication, form processing -- **XML Processing Tests**: BPN API response parsing -- **Field Handler Tests**: Conditional logic validation -- **HTMX Tests**: Dynamic update functionality - -### Code Quality Tools ✅ **AUTOMATED** -- **PHP-CS-Fixer**: PSR-12 compliance, @Symfony ruleset -- **PHPUnit**: Comprehensive test suite with coverage reporting -- **Symfony Console**: Built-in debugging and inspection tools -- **Static Analysis**: Type checking and dependency validation - -### Security Measures ✅ **ENTERPRISE GRADE** -- **XSS Protection**: Form transformer-based sanitization -- **CSRF Protection**: Symfony's built-in CSRF tokens -- **Input Validation**: Comprehensive form validation -- **API Security**: Secure XML communication with BPN -- **Authentication**: OAuth2-based user authentication - -## 📈 Current Capabilities - -### Booking Workflow ✅ **COMPLETE** -- **Multi-step Process**: Guided 3-step booking flow -- **Data Persistence**: Session-based data retention across steps -- **Validation**: Comprehensive validation at each step -- **Error Recovery**: Graceful error handling and user feedback -- **Confirmation**: Final booking confirmation with BPN API - -### Service Management ✅ **COMPREHENSIVE** -- **Transportation**: Bus/car selection with conditional pickup/parking -- **Accommodation**: Room selection with board plan options -- **Activities**: Ski passes, courses, equipment rentals -- **Pricing**: Real-time cost calculations with smart formatting -- **Dependencies**: Complex service interdependency handling - -### User Experience ✅ **MODERN** -- **Real-time Updates**: HTMX-powered seamless interactions -- **Progressive Enhancement**: Works without JavaScript (fallback) -- **Mobile Responsive**: Optimized for all device sizes -- **Accessibility**: Semantic HTML with ARIA labels -- **Performance**: Fast loading with targeted updates - -## 🔮 Future Roadmap - -### Planned Enhancements (Q1-Q2 2025) -1. **Age-Based Field Constraints** - Complete implementation ✅ Architecture Ready -2. **Advanced Pricing Features** - Discounts, taxes, currency support -3. **Mobile App Integration** - API endpoints for mobile client -4. **Enhanced Analytics** - User behavior tracking and insights -5. **Performance Optimization** - Further caching and optimization - -### Long-term Vision (2025-2026) -- **Multi-language Support** - Internationalization framework -- **Advanced Reporting** - Booking analytics and reporting -- **Third-party Integrations** - Payment gateways, CRM systems -- **Microservices Architecture** - Service-oriented architecture evolution -- **Real-time Collaboration** - Multi-user booking capabilities - -## 🎯 Success Metrics - -### Technical Achievements ✅ -- **Zero Critical Bugs** in production environment -- **99.9% Uptime** with robust error handling -- **Sub-2s Page Load** times across all booking steps -- **100% PSR-12 Compliance** with automated enforcement -- **Comprehensive Documentation** for all system components - -### Business Impact ✅ -- **Streamlined Booking Process** with 3-step workflow -- **Real-time Pricing** increases booking conversion -- **Transportation Integration** provides complete travel solution -- **Service Selection** enables upselling opportunities -- **BPN Integration** ensures data consistency and automation - -### User Experience ✅ -- **Intuitive Interface** with guided workflow -- **Real-time Feedback** via HTMX interactions -- **Mobile Optimization** for on-the-go bookings -- **Accessibility Compliance** for inclusive design -- **Error Prevention** through conditional field logic - -## 📋 Maintenance & Support - -### Regular Maintenance ✅ **AUTOMATED** -- **Dependency Updates**: Automated Symfony and package updates -- **Security Patches**: Immediate security update deployment -- **Performance Monitoring**: Continuous performance metrics -- **Log Analysis**: Automated log analysis and alerting -- **Database Optimization**: Regular query performance analysis - -### Support Infrastructure ✅ **COMPREHENSIVE** -- **Multi-channel Logging**: Structured logging across application layers -- **Error Tracking**: Comprehensive error capture and analysis -- **Performance Monitoring**: Real-time performance metrics -- **Documentation**: Up-to-date architectural and API documentation -- **Development Guidelines**: Clear development and contribution guidelines - -## 🏆 System Strengths - -### Architectural Excellence -- **Clean Architecture**: Separation of concerns with service layer -- **Extensibility**: Plugin-like field handler system -- **Maintainability**: Comprehensive documentation and testing -- **Scalability**: Efficient caching and database optimization -- **Security**: Multiple layers of security protection - -### Developer Experience -- **Modern PHP**: PHP 8.1+ features with strict typing -- **Framework Best Practices**: Symfony 6.4 LTS with recommended patterns -- **Code Quality**: Automated formatting and quality checks -- **Documentation**: Extensive architectural documentation -- **Testing**: Comprehensive test coverage with clear test patterns - -### Business Value -- **Feature Rich**: Comprehensive booking functionality -- **Integration Ready**: Seamless BPN API integration -- **User Focused**: Real-time interactions and feedback -- **Extensible**: Easy to add new services and features -- **Production Ready**: Robust error handling and performance - ---- - -**Document Status**: ✅ Current and Comprehensive -**Last Updated**: January 2025 -**Next Review**: March 2025 -**Maintained By**: Development Team -**Version**: 2.0 - -**System Ready For**: ✅ Production Deployment | ✅ Feature Extensions | ✅ Team Development \ No newline at end of file diff --git a/docs/archive/DOCUMENTATION_UPDATES_2025-09-02.md b/docs/archive/DOCUMENTATION_UPDATES_2025-09-02.md deleted file mode 100644 index 40c52ab..0000000 --- a/docs/archive/DOCUMENTATION_UPDATES_2025-09-02.md +++ /dev/null @@ -1,168 +0,0 @@ -# Documentation Updates - September 2, 2025 - -## Overview - -This document summarizes the comprehensive documentation updates made to reflect the booking flow debugging and enhancements completed during the HTMX Service Selection Bug Investigation. - -## Updated Documentation Files - -### 1. AGE_BASED_FIELDS_PLAN.md - -**Section Updated**: Service Selection Bug Fixes & HTMX Improvements (Lines 425-442) - -**Key Changes**: -- **HTMX Trigger Issue**: Added detailed explanation of HTMX attribute placement fix - - Root cause: HTMX attributes on container elements instead of individual inputs - - Solution: Moved HTMX triggers to individual checkbox/radio inputs for expanded choice fields - - Impact: Real-time dynamic updates now work reliably for all service fields - -- **Field Handler Data Issue**: Documented the Service object storage improvement - - Root cause: Field handlers storing service IDs instead of complete Service objects - - Solution: Updated all field handlers to retrieve and store complete Service entities - - Impact: Pricing calculator now has access to service price data - -- **Form Submission Reset Issue**: Clarified the choice_value configuration fix - - Root cause: Inconsistent choice_value configuration between service types - - Solution: Added 'choice_value' => 'id' to all service field providers - - Result: Consistent form submission behavior across all service types - -### 2. FORM_PROCESSING.md - -**Section Updated**: Field Handlers Architecture (Lines 235-251) - -**Key Additions**: -- **Data Storage Strategy**: Added explanation of why Service objects are stored instead of IDs - - Enables pricing calculations to access service price data - - Eliminates need for additional database lookups during price calculation - - Provides immediate access to all service metadata - -**Section Updated**: HTMX Dynamic Updates (Lines 349-372) - -**Key Additions**: -- **Service Field HTMX Integration**: Comprehensive explanation of expanded choice field HTMX handling - - Issue identification: Container-level HTMX attributes don't work for checkboxes/radios - - Solution implementation: Individual input-level HTMX triggers - - Code examples showing correct vs incorrect attribute placement - - Result: Real-time updates work reliably for all service selections - -### 3. PRICING_DISPLAY_IMPLEMENTATION.md - -**Status**: Completely rewritten to reflect completed implementation - -**Major Updates**: -- **Implementation Status**: Changed from "In Progress" to "✅ Implementation completed successfully" -- **Service Label Formatting**: Updated to reflect actual implementation with smart zero-price handling - - Zero-priced services display without price suffix - - Priced services show with formatted German pricing - - Consistent quantity display (1x, 2x) for all services - -- **Unified Summary Component**: Documented the single cohesive sticky sidebar approach - - Eliminated separate pricing summary template - - Integrated pricing into main booking summary - - Improved UX with unified layout - -- **Service Layer Integration**: Updated with actual implemented methods - - `calculateServiceTotal()` method with bug fix documentation - - `getServiceGroups()` method with corrected key usage (groupTotal vs totalPrice) - -- **Controller Integration**: Documented completed pricing integration - - Step 1 and Step 2 controller enhancements - - HTMX service field integration fixes - -- **Technical Considerations**: Updated HTMX integration section - - Fixed service field triggers for expanded choice types - - Real-time pricing updates working reliably - - Improved reliability documentation - -### 4. FIELD_STATE_SYSTEM.md - -**Section Added**: Service Field HTMX Integration Improvements (Lines 181-216) - -**New Content**: -- **Critical Fix Documentation**: Detailed explanation of expanded choice field HTMX handling -- **Issue Resolution**: Clear before/after comparison of HTMX attribute placement -- **Implementation Example**: Code snippets showing correct `attr` vs incorrect `row_attr` usage -- **Benefits Documentation**: List of improvements from the fix -- **Affected Field Types**: Comprehensive list of service fields that benefited from the fix - -### 5. AGE_CONSTRAINTS_MODEL_EXTENSION_PLAN.md - -**Section Added**: Related Completed Improvements (Lines 631-642) - -**New Content**: -- **Complementary Enhancements**: Documented how the completed form processing improvements support future age constraints implementation -- **Service Field HTMX Integration**: Reference to completed fixes -- **Field Handler Data Storage**: How Service object storage supports age constraint data access -- **Service Label Formatting**: Integration with age-restricted service pricing -- **Foundation Documentation**: How these improvements prepare for age constraint filtering - -## Summary of Issues Resolved - -### 🐞 HTMX Service Selection Issues -- **Problem**: HTMX requests not triggered for service fields -- **Root Cause**: Incorrect HTMX attribute placement on containers -- **Solution**: Individual input-level HTMX triggers for expanded choice fields -- **Documentation**: Updated in FORM_PROCESSING.md, FIELD_STATE_SYSTEM.md, AGE_BASED_FIELDS_PLAN.md - -### 🧠 Field Handler Logic -- **Problem**: Service IDs stored instead of Service objects -- **Impact**: Pricing calculator couldn't access service price data -- **Solution**: Updated all field handlers to store complete Service entities -- **Documentation**: Updated in FORM_PROCESSING.md, AGE_BASED_FIELDS_PLAN.md - -### 💰 Pricing Calculation Bug -- **Problem**: Selected services not included in total calculation -- **Root Cause**: Incorrect array key usage in `calculateServiceTotal()` -- **Solution**: Fixed to use 'groupTotal' instead of 'totalPrice' -- **Documentation**: Detailed in PRICING_DISPLAY_IMPLEMENTATION.md - -### 💡 UX & UI Enhancements -- **Improvement**: Unified booking summary with integrated pricing -- **Changes**: Eliminated separate pricing sidebar, improved layout -- **Service Labels**: Smart zero-price handling and consistent quantity display -- **Documentation**: Comprehensive updates in PRICING_DISPLAY_IMPLEMENTATION.md - -### 🧼 Code Quality Fixes -- **Issue**: Linter warnings on dynamic property access -- **Solution**: Proper type annotations and parameter typing -- **Documentation**: Noted in implementation completion status - -## Benefits Achieved - -### Technical Benefits -- ✅ Reliable HTMX dynamic updates for all service fields -- ✅ Consistent data flow from form submission to pricing calculation -- ✅ Clean separation of concerns with Service object storage -- ✅ Improved code quality with proper type annotations - -### UX Benefits -- ✅ Real-time pricing updates for all service selections -- ✅ Unified, sticky booking summary with clear pricing breakdown -- ✅ Smart service label formatting (no €0,00 for free services) -- ✅ Consistent quantity display across all services - -### Documentation Benefits -- ✅ Accurate reflection of current system behavior -- ✅ Clear troubleshooting information for similar issues -- ✅ Complete implementation status tracking -- ✅ Foundation documentation for future enhancements - -## Next Steps - -### 🔧 Future Enhancements (Optional) -- Add contextual tooltips to service options -- Implement Stimulus controllers for smooth field transitions -- Add placeholder cards for age-restricted fields -- Enhance pricing with discounts, tax breakdowns, or multi-currency support - -### 📚 Documentation Maintenance -- Ensure future updates to service field logic are reflected in updated docs -- Maintain accuracy of implementation status as system evolves -- Update troubleshooting sections based on any new issues discovered - ---- - -**Documentation Update Date**: September 2, 2025 -**Updated By**: Claude Code Assistant -**Status**: ✅ All critical documentation updates completed -**Impact**: Documentation now accurately reflects current system behavior and resolved issues \ No newline at end of file diff --git a/docs/archive/FAMILY_BOOKING_DETECTION_ISSUE.md b/docs/archive/FAMILY_BOOKING_DETECTION_ISSUE.md deleted file mode 100644 index e7823e0..0000000 --- a/docs/archive/FAMILY_BOOKING_DETECTION_ISSUE.md +++ /dev/null @@ -1,172 +0,0 @@ -# Family Booking Detection Issue - -## Problem Description - -The current family booking detection logic in `BookingCreateDto::isFamilyBooking()` has a critical flaw that causes incorrect classification of booking types, leading to wrong insurance options being displayed. - -### Current Logic (FLAWED) - -```php -// Current implementation in BookingCreateDto::isFamilyBooking() -$adults = 0; // Count of participants >= 18 years -$youngPeople = 0; // Count of participants <= 20 years - -foreach ($this->participants as $participant) { - $age = $participant->getAge($travelStartDate); - - if ($age >= 18) { - ++$adults; - } - - if ($age <= 20) { - ++$youngPeople; - } -} - -$isFamily = ($adults >= 1 && $adults <= 2) && ($youngPeople >= 1); -``` - -### The Problem - -**Overlapping Age Ranges**: The current logic creates overlapping age categories: -- **Adults**: ≥18 years -- **Young people**: ≤20 years - -This means participants aged 18-20 are counted as **BOTH** adults AND young people, causing incorrect family booking detection. - -### Example Scenario - -**Booking with 2 participants:** -- **Participant 1**: Born 1980 (age 44 at travel time) -- **Participant 2**: Born 2000 (age 24 at travel time) - -**Current logic result:** -- `adults = 2` (both participants ≥18) -- `youngPeople = 1` (the 24-year-old ≤20) -- `isFamily = (2 >= 1 && 2 <= 2) && (1 >= 1) = true` ❌ - -**Expected result:** This should be classified as an **individual/couple booking**, not a family booking. - -## Impact - -1. **Wrong insurance options**: Family insurances are shown for individual bookings -2. **User confusion**: Customers see inappropriate insurance options -3. **Business logic errors**: Pricing and eligibility calculations are incorrect - -## Suggested Solutions - -### Option 1: Non-Overlapping Age Ranges (Recommended) - -```php -// Suggested implementation -$adults = 0; // Count of participants >= 18 years -$children = 0; // Count of participants < 18 years - -foreach ($this->participants as $participant) { - $age = $participant->getAge($travelStartDate); - - if ($age >= 18) { - ++$adults; - } else { - ++$children; - } -} - -$isFamily = ($adults >= 1) && ($children >= 1); -``` - -**Benefits:** -- No overlapping age ranges -- Clear distinction between adults and children -- Matches insurance industry standards - -### Option 2: Insurance-Specific Age Ranges - -```php -// Alternative implementation based on insurance requirements -$adults = 0; // Count of participants >= 18 years -$minors = 0; // Count of participants < 18 years - -foreach ($this->participants as $participant) { - $age = $participant->getAge($travelStartDate); - - if ($age >= 18) { - ++$adults; - } elseif ($age < 18) { - ++$minors; - } - // Note: 18+ year olds are not counted as minors -} - -$isFamily = ($adults >= 1) && ($minors >= 1); -``` - -### Option 3: Configurable Age Thresholds - -```php -// More flexible approach with configurable thresholds -private const ADULT_AGE_THRESHOLD = 18; -private const CHILD_AGE_THRESHOLD = 18; // Same as adult threshold for non-overlap - -$adults = 0; -$children = 0; - -foreach ($this->participants as $participant) { - $age = $participant->getAge($travelStartDate); - - if ($age >= self::ADULT_AGE_THRESHOLD) { - ++$adults; - } elseif ($age < self::CHILD_AGE_THRESHOLD) { - ++$children; - } -} - -$isFamily = ($adults >= 1) && ($children >= 1); -``` - -## Business Rules to Clarify - -Before implementing a solution, the following business rules need to be clarified: - -1. **What defines a "family booking"?** - - Must have at least 1 adult (≥18) and at least 1 child (<18)? - - Or can it be 2 adults with children? - - Or any booking with children regardless of adult count? - -2. **Age thresholds:** - - Should 18-year-olds be considered adults or children? - - Are there different rules for different types of services? - -3. **Edge cases:** - - What about bookings with only adults (couples)? - - What about bookings with only children (group bookings)? - -## Implementation Notes - -- The fix should be implemented in `src/Form/Model/BookingCreateDto.php` -- Update the `isFamilyBooking()` method -- Add comprehensive unit tests for edge cases -- Consider adding configuration options for age thresholds -- Update documentation to reflect the new business rules - -## Testing Scenarios - -After implementation, test these scenarios: - -1. **Single adult** (should be individual booking) -2. **Two adults** (should be couple booking, not family) -3. **One adult + one child** (should be family booking) -4. **Two adults + one child** (should be family booking) -5. **Only children** (edge case - clarify business rule) -6. **18-year-old participant** (edge case - clarify classification) - -## Related Files - -- `src/Form/Model/BookingCreateDto.php` - Main implementation -- `src/Service/InsuranceMatchingService.php` - Uses family booking detection -- `tests/Form/Model/BookingCreateDtoTest.php` - Unit tests (to be updated) - - - - - diff --git a/docs/archive/PICKUP_PRICING_IMPLEMENTATION.md b/docs/archive/PICKUP_PRICING_IMPLEMENTATION.md deleted file mode 100644 index a1da587..0000000 --- a/docs/archive/PICKUP_PRICING_IMPLEMENTATION.md +++ /dev/null @@ -1,244 +0,0 @@ -# Pickup Pricing Implementation - -## Overview - -This document describes the implementation of pricing display for pickup choice labels in the MyEP Next Booking system. The implementation extends the existing service pricing pattern to include pickup locations, with special handling for negative prices as discounts. - -## Implementation Details - -### Enhanced Pricing Support - -The pickup pricing implementation follows the established pattern used for other bookable services while adding specific support for discount pricing: - -#### New Method: `formatPickupLabelWithPrice()` - -```php -private function formatPickupLabelWithPrice(?Pickup $pickup): string -{ - if (null === $pickup) { - return ''; - } - - $label = $this->formatPickupLabel($pickup); - - // Handle zero prices (no display) - if (null === $pickup->price || 0.0 === $pickup->price) { - return $label; - } - - // Handle negative prices (discounts) - if ($pickup->price < 0) { - return sprintf('%s (-%s€ Rabatt)', $label, number_format(abs($pickup->price), 2, ',', '.')); - } - - // Handle positive prices (costs) - return sprintf('%s (€%s)', $label, number_format($pickup->price, 2, ',', '.')); -} -``` - -### Updated Field Options - -Both outbound and inbound pickup fields now use the enhanced pricing formatter: - -```php -// Outbound Pickup -$this->fieldOptionProviders['pickupOutbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [ - 'label' => 'Zustieg Hinfahrt', - 'choices' => $bookingDto->travel->pickupsTo, - 'choice_label' => fn (?Pickup $pickup) => $this->formatPickupLabelWithPrice($pickup), - 'choice_value' => 'id', - 'expanded' => false, - 'multiple' => false, - 'required' => true, - 'placeholder' => 'Zustieg auswählen', -]; - -// Inbound Pickup -$this->fieldOptionProviders['pickupInbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [ - 'label' => 'Ausstieg Rückfahrt', - 'choices' => $bookingDto->travel->pickupsFro, - 'choice_label' => fn (?Pickup $pickup) => $this->formatPickupLabelWithPrice($pickup), - 'choice_value' => 'id', - 'expanded' => false, - 'multiple' => false, - 'required' => true, - 'placeholder' => 'Ausstieg auswählen', -]; -``` - -### Service Pricing Consistency - -The implementation also extends the existing `formatServiceLabelWithPrice()` method to handle negative service prices consistently: - -```php -private function formatServiceLabelWithPrice(?Service $service): string -{ - if (null === $service) { - return ''; - } - - // Handle zero prices (no display) - if (null === $service->price || 0.0 === $service->price) { - return $service->label; - } - - // Handle negative prices (discounts) - if ($service->price < 0) { - return sprintf('%s (-%s€ Rabatt)', $service->label, number_format(abs($service->price), 2, ',', '.')); - } - - // Handle positive prices (costs) - return sprintf('%s (€%s)', $service->label, number_format($service->price, 2, ',', '.')); -} -``` - -## Pricing Display Examples - -### Pickup Locations - -**Positive Pricing (Additional Cost):** -- `"München Hbf (€15,00)"` -- `"Nürnberg Zentral (€25,00)"` -- `"Augsburg Bahnhof (€12,50)"` - -**Zero Pricing (No Additional Cost):** -- `"Standardzustieg"` -- `"Hauptbahnhof"` -- `"Zentrum"` - -**Negative Pricing (Discount):** -- `"Nahverkehr (-5,00€ Rabatt)"` -- `"Sammelstelle (-10,00€ Rabatt)"` -- `"Gruppentarif (-15,00€ Rabatt)"` - -### Services (Updated Consistency) - -**Positive Pricing:** -- `"Skikurs Anfänger (€25,00)"` -- `"Versicherung (€15,00)"` -- `"5-Tage Skipass (€120,00)"` - -**Zero Pricing:** -- `"Vollpension"` -- `"Grundausstattung"` -- `"Standardleistung"` - -**Negative Pricing (Discounts):** -- `"Frühbucher-Bonus (-15,00€ Rabatt)"` -- `"Stammgast-Vorteil (-5,00€ Rabatt)"` -- `"Gruppen-Rabatt (-20,00€ Rabatt)"` - -## Technical Features - -### German Number Formatting - -All pricing uses German locale formatting: -- Decimal separator: Comma (`,`) -- Thousands separator: Period (`.`) -- Currency symbol: Euro (`€`) - -### Null Safety - -The implementation handles all edge cases: -- `null` pickup objects return empty string -- `null` prices treated as zero (no display) -- Proper type checking for price comparisons - -### Performance Considerations - -- Lightweight formatting methods with minimal overhead -- Reuses existing `formatPickupLabel()` logic -- No additional database queries or API calls -- Efficient string formatting with `sprintf()` - -## Integration Points - -### Form System Integration - -The pricing display integrates seamlessly with: -- **Conditional Field States**: Pickup fields show/hide based on transportation selection -- **HTMX Updates**: Real-time pricing updates when selections change -- **Field Handlers**: `ParticipantPickupOutboundFieldHandler` and `ParticipantPickupInboundFieldHandler` -- **Form Validation**: Maintains existing validation rules - -### Pricing Calculation System - -Pickup pricing integrates with the broader pricing system: -- **BookingService**: Pickup costs included in total calculations -- **Pricing Summary**: Pickup selections reflected in booking summary -- **Real-time Updates**: HTMX updates include pickup pricing changes - -## Business Logic - -### Discount Handling - -Negative pickup prices represent business discounts: -- **Volume Discounts**: Lower prices for group pickups -- **Location Incentives**: Discounts for convenient pickup locations -- **Promotional Offers**: Special pricing for certain routes -- **Loyalty Programs**: Reduced costs for repeat customers - -### Zero Price Logic - -Zero-priced pickups indicate: -- **Included Services**: No additional cost for standard pickups -- **Base Package**: Pickup included in base travel price -- **Promotional Free**: Temporarily free pickup locations - -## Files Modified - -1. **`src/Form/Service/ParticipantFieldOptionsProvider.php`** - - Added `formatPickupLabelWithPrice()` method - - Enhanced `formatServiceLabelWithPrice()` with discount handling - - Updated pickup field option providers - -2. **`docs/PRICING_DISPLAY_IMPLEMENTATION.md`** - - Updated service label formatting examples - - Added pickup services to affected service types - - Enhanced pricing logic documentation - -3. **`myep-next-booking/CLAUDE.md`** - - Added pricing display standards section - - Updated development guidelines for pricing - -## Testing Considerations - -### Manual Testing Scenarios - -1. **Positive Pickup Pricing**: Select pickup with additional cost -2. **Zero Pickup Pricing**: Select free pickup location -3. **Negative Pickup Pricing**: Select discounted pickup location -4. **Mixed Scenarios**: Combine different pickup price types -5. **HTMX Integration**: Verify real-time pricing updates - -### Test Data Requirements - -- Pickup objects with positive, zero, and negative prices -- Various German number formatting scenarios -- Edge cases with `null` values and empty strings - -## Future Enhancements - -### Potential Improvements - -- **Currency Selection**: Support for multiple currencies -- **Dynamic Pricing**: Time-based or demand-based pricing -- **Bulk Discounts**: Automatic discounts for group bookings -- **Regional Pricing**: Location-based price variations - -### Integration Opportunities - -- **Payment Gateway**: Direct integration with pricing calculations -- **Analytics**: Track pickup selection patterns and pricing impact -- **Reporting**: Detailed pickup pricing reports -- **API Extensions**: Expose pickup pricing via REST API - ---- - -**Implementation Status**: ✅ **Completed** -**Last Updated**: January 2025 -**Files Modified**: 3 -**Testing**: Manual verification required -**Documentation**: Updated and comprehensive - -This implementation successfully extends the pricing display system to include pickup locations while maintaining consistency with existing service pricing patterns and handling the unique business requirement for discount pricing display. \ No newline at end of file diff --git a/docs/archive/PRICING_DISPLAY_IMPLEMENTATION.md b/docs/archive/PRICING_DISPLAY_IMPLEMENTATION.md deleted file mode 100644 index 77ac223..0000000 --- a/docs/archive/PRICING_DISPLAY_IMPLEMENTATION.md +++ /dev/null @@ -1,282 +0,0 @@ -# Pricing Display Implementation Plan - -## Overview -Implement comprehensive pricing display functionality that shows costs both inline in form options and in a detailed sidebar summary with real-time updates via HTMX. - -## Goals -1. **Inline pricing** in form options (rooms, services) so users see costs while selecting -2. **Sidebar pricing summary** with itemized breakdown and grand total -3. **Real-time updates** via existing HTMX integration -4. **Professional UX** with clear pricing integrated into the booking flow - -## Implementation Strategy - -### 1. Core Pricing Service (`src/Service/BookingPriceCalculatorService.php`) - -**Purpose**: Central service for all pricing calculations - -**Key Methods**: -- `calculateRoomPricing(BookingCreateDto $bookingDto): array` -- `calculateServicePricing(BookingCreateDto $bookingDto): array` -- `calculateGrandTotal(BookingCreateDto $bookingDto): float` -- `getPricingBreakdown(BookingCreateDto $bookingDto): array` - -**Room Pricing Logic**: -```php -// For each selected room: quantity × room.price -foreach ($bookingDto->getSelectedRooms() as $roomSelection) { - $room = $bookingDto->travel->rooms[$roomSelection->roomId]; - $totalPrice = $roomSelection->quantity * $room->price; -} -``` - -**Service Pricing Logic**: -```php -// For each participant's selected services -foreach ($bookingDto->getParticipants() as $participant) { - // Handle different service types (single vs multiple selection) - $serviceTotal += $participant->skiPass?->price ?? 0; - $serviceTotal += array_sum(array_map(fn($s) => $s->price, $participant->courses)); - // etc. -} -``` - -### 2. Enhanced Form Field Options with Pricing ✅ COMPLETED - -#### A. Room Selection Forms ✅ COMPLETED -**File**: `src/Form/RoomSelectType.php` - -**Enhancement**: Choice labels include pricing information -```php -'choice_label' => function (Room $room) { - $priceText = $room->price ? sprintf(' (€%.2f pro Nacht)', $room->price) : ''; - return $room->label . $priceText; -} -``` - -#### B. Service Selection Forms ✅ COMPLETED -**File**: `src/Form/Service/ParticipantFieldOptionsProvider.php` - -**Enhancement**: Service labels include pricing via `formatServiceLabelWithPrice()` method -- Zero-priced services display without price suffix (e.g., "Vollpension" instead of "Vollpension (€0,00)") -- Priced services show with formatted price (e.g., "Halbpension (€45,00)") -- Negative prices display as discounts (e.g., "Frühbucher-Bonus (-15,00€ Rabatt)") -- All services consistently show quantity prefix (e.g., "1x", "2x") - -**Service Label Formatting Logic**: -```php -private function formatServiceLabelWithPrice(Service $service): string -{ - $label = $service->label; - - // Handle zero prices (no display) - if (null === $service->price || 0.0 === $service->price) { - return $label; - } - - // Handle negative prices (discounts) - if ($service->price < 0) { - return sprintf('%s (-%s€ Rabatt)', $label, number_format(abs($service->price), 2, ',', '.')); - } - - // Handle positive prices (costs) - return sprintf('%s (€%s)', $label, number_format($service->price, 2, ',', '.')); -} -``` - -**Affected Service Types**: -- Courses: `"Skikurs Anfänger (€25,00)"` or `"Frühbucher-Bonus (-15,00€ Rabatt)"` -- Additional Services: `"Versicherung (€15,00)"` or `"Stammgast-Vorteil (-5,00€ Rabatt)"` -- Ski Pass: `"5-Tage Skipass (€120,00)"` or `"Gruppen-Rabatt (-20,00€ Rabatt)"` -- Rentals: `"Ski-Set (€30,00)"` or `"Eigenes Equipment (-30,00€ Rabatt)"` -- Board: `"Halbpension (€45,00)"`, `"Vollpension"` (if €0,00), or `"Selbstverpflegung (-25,00€ Rabatt)"` -- Pickup Services: `"München Hbf (€15,00)"` or `"Nahverkehr (-5,00€ Rabatt)"` - -### 3. Enhanced Unified Booking Summary ✅ COMPLETED - -#### A. Unified Summary Component ✅ COMPLETED -**File**: `templates/booking/_summary.html.twig` - -The pricing summary has been integrated into the main booking summary, creating a single cohesive sticky sidebar that displays: - -**Travel Information Section**: -- Travel details (destination, dates, duration) -- Room selections with quantities -- Participant count summary - -**Pricing Summary Section** (integrated): -- Room pricing breakdown with quantities (e.g., "2x Doppelzimmer") -- Service selections by participant with pricing -- Grand total calculation - -**Key UX Improvements**: -- Single sticky summary box (eliminated separate pricing sidebar) -- Clean, unified layout with consistent typography -- Service labels show quantity and pricing appropriately -- Zero-priced services display without price suffix -- Real-time updates via HTMX for all pricing changes - -#### B. Deprecated Separate Pricing Template ✅ COMPLETED -**File**: `templates/booking/_pricing_summary.html.twig` - REMOVED - -The separate pricing summary template was removed in favor of the integrated approach within the main summary template for better UX. - -### 4. Service Layer Integration ✅ COMPLETED - -#### A. Enhanced BookingService ✅ COMPLETED -**File**: `src/Service/BookingService.php` - -**Implemented Pricing Methods**: -```php -public function calculateServiceTotal(BookingCreateDto $bookingDto): float -{ - // Groups services by type and calculates total pricing - $serviceGroups = $this->getServiceGroups($bookingDto); - return array_sum(array_column($serviceGroups, 'groupTotal')); -} - -private function getServiceGroups(BookingCreateDto $bookingDto): array -{ - // Groups selected services and calculates totals for pricing display - // Fixed bug: Uses 'groupTotal' instead of incorrect 'totalPrice' key -} -``` - -**Key Bug Fix**: The `calculateServiceTotal()` method was corrected to use `groupTotal` instead of `totalPrice` from the grouped array structure, ensuring selected services are properly included in the grand total calculation. - -### 5. Controller Integration ✅ COMPLETED - -#### A. Step 1 Controller Updates ✅ COMPLETED -**File**: `src/Controller/Booking/CreateStep1Controller.php` - -**Enhancement**: Integrated pricing calculations into existing summary methods -```php -// In index() method - pricing data included in summary -$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto); -// Pricing calculated and passed to template - -// In roomSummary() method - HTMX endpoint includes pricing updates -$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto); -return $this->render('booking/_summary.html.twig', [ - // existing data with integrated pricing -]); -``` - -#### B. Step 2 Controller Updates ✅ COMPLETED -**File**: `src/Controller/Booking/CreateStep2Controller.php` - -**Enhancement**: Service pricing calculations integrated into form refresh -```php -// In refresh() method - service selections update pricing in real-time -$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto); -// Service total calculation includes selected service pricing -``` - -#### C. HTMX Service Field Integration ✅ COMPLETED -**Critical Fix**: Service field HTMX triggers were moved from container elements to individual form inputs (checkboxes/radios) to ensure real-time updates work properly for expanded choice fields. - -### 6. Error Handling & Edge Cases ✅ COMPLETED - -**Price Data Handling** ✅ IMPLEMENTED: -- Zero prices handled gracefully (services show without price suffix) -- All prices rounded to 2 decimal places -- German number formatting implemented (comma as decimal separator) -- Null price handling via conditional display logic - -**Service Quantity Logic** ✅ IMPLEMENTED: -- Single services (skiPass): price × 1 per participant -- Multiple services (courses, rentals): sum of all selected service prices per participant -- Service quantity consistently displayed (1x, 2x) in labels -- Field handlers store complete Service objects (not just IDs) for pricing access - -**Field Handler Data Fix** ✅ COMPLETED: -- All service field handlers updated to store Service objects instead of service IDs -- Enables pricing calculator to access service price data -- Resolves issue where selected services were ignored in total calculations - -## Implementation Progress - -### ✅ Completed -- [x] Created implementation plan documentation -- [x] Enhanced BookingService with pricing calculation methods (`calculateServiceTotal`, `getServiceGroups`) -- [x] Updated ParticipantFieldOptionsProvider to add prices to service labels with smart formatting -- [x] Updated RoomSelectType to add prices to room labels -- [x] Integrated pricing into main summary template (unified approach) -- [x] Updated controllers to include pricing data in summary calculations -- [x] Fixed HTMX integration for service field real-time updates -- [x] Fixed field handler data storage (Service objects vs IDs) -- [x] Fixed pricing calculation bug (groupTotal vs totalPrice key) -- [x] Implemented smart service label formatting (zero-price handling) -- [x] Added consistent quantity display for all services -- [x] Completed comprehensive testing of pricing display and HTMX integration - -### ✅ Additional Improvements Completed -- [x] Unified booking summary layout (eliminated separate pricing sidebar) -- [x] Enhanced UX with sticky summary positioning -- [x] Fixed service field HTMX triggers for expanded choice types -- [x] Resolved linter warnings with proper type annotations -- [x] Streamlined service label formatting logic - -## Technical Considerations - -### Performance -- All pricing calculations done in-memory (no database queries) -- Calculations triggered only on form changes via HTMX -- Efficient array operations for service aggregation - -### HTMX Integration ✅ COMPLETED -- Uses existing `#booking-summary` target for seamless updates -- **Fixed service field triggers**: HTMX attributes moved to individual form inputs for expanded choice fields -- Real-time pricing updates work reliably for all service selections -- Maintains current real-time update behavior with improved reliability - -### Styling -- CSS classes for pricing components: - - `.pricing-summary` - Overall container - - `.pricing-section` - Room/service sections - - `.pricing-item` - Individual line items - - `.pricing-total` - Grand total display - -### Accessibility -- Proper semantic HTML structure -- ARIA labels for pricing information -- Screen reader friendly number formatting - -## Testing Strategy - -### Unit Tests -- Test pricing calculations with various room/service combinations -- Test edge cases (null prices, zero quantities) -- Test German number formatting - -### Integration Tests -- Test HTMX updates with pricing changes -- Test form submission with pricing data -- Test step navigation with pricing persistence - -### Manual Testing Scenarios -1. Select rooms and verify inline pricing appears -2. Change room quantities and verify total updates -3. Add/remove services and verify pricing updates -4. Navigate between steps and verify pricing persistence -5. Test with services that have null/zero prices - -## Future Enhancements - -### Potential Additions -- **Discounts/Promotions**: Add discount calculation logic -- **Currency Selection**: Support multiple currencies -- **Price History**: Track pricing changes during booking session -- **Export Pricing**: Add pricing breakdown to booking confirmations -- **Tax Calculations**: Add VAT/tax breakdown if needed - -### Performance Optimizations -- Cache frequently calculated pricing data -- Optimize service aggregation algorithms -- Consider lazy loading for complex pricing scenarios - ---- - -**Last Updated**: 2025-09-02 -**Status**: ✅ Implementation completed successfully -**Completed Features**: Inline pricing, unified summary, HTMX integration, service field fixes, and comprehensive UX improvements \ No newline at end of file diff --git a/docs/archive/README.md b/docs/archive/README.md deleted file mode 100644 index 4a6d86a..0000000 --- a/docs/archive/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# Archived Documentation - -This folder contains completed implementation plans and historical documentation that are no longer actively referenced but preserved for historical context. - -## Archived Implementation Plans - -### ✅ Completed Features - -#### implementation-plan-rental-skipass-duration-filtering.md -**Status**: Fully implemented -**Completion**: 2025-01-XX -**Summary**: Duration-based rental filtering based on skipass selection with exact date matching -**Implementation**: See `CLAUDE.md` section "Duration-Based Rental Filtering Implementation" - -#### insurance-booking-implementation-plan.md -**Status**: Fully implemented -**Completion**: 2025-01-XX -**Summary**: Comprehensive insurance booking system with eligibility filtering, auto-reassignment, and bulk booking -**Implementation**: See `CLAUDE.md` sections: -- "Insurance Field & Package Family Detection Implementation" -- "Insurance Auto-Reassignment & Handler Dependencies" -- "Bulk Insurance Booking Implementation" - -#### TRANSPORTATION_SERVICES_IMPLEMENTATION_PLAN.md -**Status**: Fully implemented -**Completion**: 2024-XX-XX -**Summary**: Transportation services with outbound/inbound transport, pickup locations, and parking -**Implementation**: See `CLAUDE.md` section "Transportation Services Implementation" - -#### PICKUP_PRICING_IMPLEMENTATION.md -**Status**: Fully implemented -**Completion**: 2024-XX-XX -**Summary**: Pickup pricing display with formatted labels -**Implementation**: Integrated into transportation services and pricing display standards - -#### PRICING_DISPLAY_IMPLEMENTATION.md -**Status**: Fully implemented -**Completion**: 2024-XX-XX -**Summary**: Real-time pricing system with inline costs and unified summary -**Implementation**: See `README.md` section "Core Architecture Documentation" - -## Archived Issue Documentation - -### FAMILY_BOOKING_DETECTION_ISSUE.md -**Status**: Resolved -**Resolution**: 2025-01-XX -**Summary**: Fixed overlapping age ranges in family booking detection logic -**Solution**: Implemented proper age-based filtering in `BookingCreateDto::isFamilyBooking()` using travel start date - -### DOCUMENTATION_UPDATES_2025-09-02.md -**Status**: Historical record -**Date**: 2025-09-02 -**Summary**: Record of HTMX service selection bug fixes and pricing implementation completion - ---- - -**Note**: All current and active documentation is maintained in the parent `docs/` directory. Refer to `../README.md` for current documentation index. \ No newline at end of file diff --git a/docs/archive/TRANSPORTATION_SERVICES_IMPLEMENTATION_PLAN.md b/docs/archive/TRANSPORTATION_SERVICES_IMPLEMENTATION_PLAN.md deleted file mode 100644 index 9d106e3..0000000 --- a/docs/archive/TRANSPORTATION_SERVICES_IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,948 +0,0 @@ -# Transportation Services Implementation Plan - -## Overview - -Implement comprehensive transportation services for the MyEP Next Booking system, supporting bus and self-organized (car/PKW) transportation with directional pickup selection, discount handling, and optional parking services. This implementation addresses BusProNet's inconsistent direction naming conventions while following established architectural patterns. - -## Current State Analysis - -### Existing Infrastructure ✅ - -**Transportation Data Model:** -- `Service` model has `direction`, `subType`, `price` properties -- `Travel` model has `transportationServices[]`, `pickupsTo[]`, `pickupsFro[]` -- `Booking` model supports transportation service mapping -- XML parsing handles transportation services and pickups -- Data processing includes transportation service management - -**Form System Integration:** -- `ParticipantDto` has transportation and pickup properties -- Field handler registry supports service processing -- Conditional field state system available -- HTMX integration for real-time updates -- Pricing integration system in place - -### Direction Naming Inconsistencies 🔍 - -**Problem Identified:** BusProNet uses inconsistent direction codes across different contexts: - -1. **Travel Data Context:** `'HIN'` and `'RUECK'` (full German words) -2. **Booking Data Context:** `'H'` and `'R'` (single letter abbreviations) -3. **Internal Properties:** `To`/`Fro` (archaic English) - -**Evidence:** -- Comment in `BookingEditDto.php`: `"Different keys for direction used in booking data (H <=> HIN, R <=> RUECK)!"` -- `Travel->getTransportationServicesByDirection('HIN'/'RUECK')` -- `Booking->getTransportationServiceForParticipantAndDirection($index, 'H'/'R')` - -### Missing Components 🚧 - -- Direction mapping utility for consistency -- Transportation field handlers and options providers -- Conditional pickup field logic (only show when bus selected) -- Parking service integration (subtype PAR) -- Modern English property naming (Outbound/Inbound) - -## Implementation Strategy - -### Phase 1: Foundation - Direction Mapping & Naming 🎯 - -#### 1.1 Create Direction Mapping Utility - -**File:** `src/BusProNet/Utility/DirectionMapper.php` - -```php - self::OUTBOUND_BOOKING, - self::INBOUND_TRAVEL => self::INBOUND_BOOKING, - default => throw new \InvalidArgumentException("Unknown travel direction: $travelDirection") - }; - } - - /** - * Maps booking direction code to travel direction code. - */ - public static function bookingToTravel(string $bookingDirection): string - { - return match($bookingDirection) { - self::OUTBOUND_BOOKING => self::OUTBOUND_TRAVEL, - self::INBOUND_BOOKING => self::INBOUND_TRAVEL, - default => throw new \InvalidArgumentException("Unknown booking direction: $bookingDirection") - }; - } - - /** - * Maps direction code to English name. - */ - public static function toEnglish(string $direction): string - { - return match($direction) { - self::OUTBOUND_TRAVEL, self::OUTBOUND_BOOKING => self::OUTBOUND, - self::INBOUND_TRAVEL, self::INBOUND_BOOKING => self::INBOUND, - default => throw new \InvalidArgumentException("Unknown direction: $direction") - }; - } - - /** - * Gets all outbound direction codes. - */ - public static function getOutboundCodes(): array - { - return [self::OUTBOUND_TRAVEL, self::OUTBOUND_BOOKING]; - } - - /** - * Gets all inbound direction codes. - */ - public static function getInboundCodes(): array - { - return [self::INBOUND_TRAVEL, self::INBOUND_BOOKING]; - } -} -``` - -#### 1.2 Update ParticipantDto Properties - -**Current Properties (archaic naming):** -```php -public ?Service $transportationServiceTo = null; -public ?Service $transportationServiceFro = null; -public ?Pickup $pickup = null; -``` - -**Updated Properties (modern English):** -```php -public ?Service $transportationOutbound = null; // Maps to 'HIN'/'H' -public ?Service $transportationInbound = null; // Maps to 'RUECK'/'R' -public ?Pickup $pickupOutbound = null; // Maps to pickupsTo -public ?Pickup $pickupInbound = null; // Maps to pickupsFro -public ?Service $parking = null; // New parking service -``` - -#### 1.3 Update BookingEditDto Direction Mapping - -**Current Implementation:** -```php -// Different keys for direction used in booking data (H <=> HIN, R <=> RUECK)! -$participantData->transportationServiceTo = $booking - ->getTransportationServiceForParticipantAndDirection($index, 'H'); -$participantData->transportationServiceFro = $booking - ->getTransportationServiceForParticipantAndDirection($index, 'R'); -``` - -**Updated Implementation:** -```php -use App\BusProNet\Utility\DirectionMapper; - -$participantData->transportationOutbound = $booking - ->getTransportationServiceForParticipantAndDirection($index, DirectionMapper::OUTBOUND_BOOKING); -$participantData->transportationInbound = $booking - ->getTransportationServiceForParticipantAndDirection($index, DirectionMapper::INBOUND_BOOKING); -``` - -### Phase 2: Transportation Field Implementation 🚀 - -#### 2.1 Transportation Service Field Handlers - -**A. Outbound Transportation Handler** -**File:** `src/Form/Service/ParticipantTransportationOutboundFieldHandler.php` - -```php -getParticipant($bookingDto, $participantIndex); - if (null === $participant) { - return; - } - - $selectedTransportation = $this->getFieldValue($submittedData, $this->getFieldName()); - - // Get available outbound transportation services - $availableServices = $bookingDto->travel->getTransportationServicesByDirection( - DirectionMapper::OUTBOUND_TRAVEL, - true // filter available - ); - - // Validate and convert selection to Service object - $validSelection = null; - if (null !== $selectedTransportation) { - if ($this->isServiceValidForParticipant($selectedTransportation, $availableServices, $bookingDto, $participantIndex)) { - $validSelection = $this->findServiceInAvailableServices($selectedTransportation, $availableServices); - } - } - - // Update participant with validated selection - $participant->transportationOutbound = $validSelection; - } - - // ... validation methods similar to existing handlers -} -``` - -**B. Inbound Transportation Handler** -**File:** `src/Form/Service/ParticipantTransportationInboundFieldHandler.php` -- Similar structure for inbound (RUECK) transportation -- Field name: `transportationInbound` -- Uses `DirectionMapper::INBOUND_TRAVEL` - -#### 2.2 Pickup Field Handlers - -**A. Outbound Pickup Handler** -**File:** `src/Form/Service/ParticipantPickupOutboundFieldHandler.php` - -```php -getParticipant($bookingDto, $participantIndex); - if (null === $participant) { - return; - } - - // Only process pickup if outbound transportation is bus - if (null === $participant->transportationOutbound || 'BUS' !== $participant->transportationOutbound->subType) { - $participant->pickupOutbound = null; // Clear pickup for non-bus transport - return; - } - - $selectedPickup = $this->getFieldValue($submittedData, $this->getFieldName()); - - // Validate pickup selection against available outbound pickups - $validSelection = null; - if (null !== $selectedPickup) { - $availablePickups = $bookingDto->travel->pickupsTo; - $validSelection = $this->findPickupInAvailable($selectedPickup, $availablePickups); - } - - $participant->pickupOutbound = $validSelection; - } - - // ... pickup validation methods -} -``` - -**B. Inbound Pickup Handler** -**File:** `src/Form/Service/ParticipantPickupInboundFieldHandler.php` -- Similar structure for inbound pickup -- Field name: `pickupInbound` -- Depends on `transportationInbound` -- Uses `travel->pickupsFro` - -#### 2.3 Parking Service Handler - -**File:** `src/Form/Service/ParticipantParkingFieldHandler.php` - -```php -getParticipant($bookingDto, $participantIndex); - if (null === $participant) { - return; - } - - // Check if parking is applicable (at least one PKW direction) - if (!$this->isParkingApplicable($participant)) { - $participant->parking = null; // Clear parking for bus-only transport - return; - } - - $parkingSelected = $this->getFieldValue($submittedData, $this->getFieldName()); - - // Store boolean value directly (true if checkbox checked, false otherwise) - $participant->parking = (bool) $parkingSelected; - } - - private function isParkingApplicable($participant): bool - { - return DirectionMapper::SUBTYPE_CAR_API === $participant->transportationOutbound?->subType; - } -} -``` - -#### 2.4 Field Options Provider Integration - -**Update:** `src/Form/Service/ParticipantFieldOptionsProvider.php` - -```php -protected function registerFieldOptionProviders(): void -{ - // ... existing providers - - // Outbound Transportation - $this->fieldOptionProviders['transportationOutbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ - 'label' => 'Hinfahrt', - 'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL), - 'choice_label' => fn(Service $service) => $this->formatTransportationServiceLabel($service), - 'choice_value' => 'id', - 'expanded' => true, - 'multiple' => false, - 'required' => true, - 'attr' => [ - 'hx-post' => $this->urlGenerator->generate('booking_create_step_2_refresh'), - 'hx-target' => '#booking-summary', - 'hx-trigger' => 'change', - ], - ]; - - // Inbound Transportation - $this->fieldOptionProviders['transportationInbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ - 'label' => 'Rückfahrt', - 'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::INBOUND_TRAVEL), - 'choice_label' => fn(Service $service) => $this->formatTransportationServiceLabel($service), - 'choice_value' => 'id', - 'expanded' => true, - 'multiple' => false, - 'required' => true, - 'attr' => [ - 'hx-post' => $this->urlGenerator->generate('booking_create_step_2_refresh'), - 'hx-target' => '#booking-summary', - 'hx-trigger' => 'change', - ], - ]; - - // Outbound Pickup (conditional) - $this->fieldOptionProviders['pickupOutbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ - 'label' => 'Zustieg Hinfahrt', - 'choices' => $bookingDto->travel->pickupsTo, - 'choice_label' => 'label', - 'choice_value' => 'id', - 'expanded' => false, // Dropdown for pickups - 'multiple' => false, - 'required' => true, - 'placeholder' => 'Zustieg auswählen', - ]; - - // Inbound Pickup (conditional) - $this->fieldOptionProviders['pickupInbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ - 'label' => 'Zustieg Rückfahrt', - 'choices' => $bookingDto->travel->pickupsFro, - 'choice_label' => 'label', - 'choice_value' => 'id', - 'expanded' => false, - 'multiple' => false, - 'required' => true, - 'placeholder' => 'Zustieg auswählen', - ]; - - // Parking (conditional - only shown when outbound transportation is PKW) - // Simple checkbox since there's only ever one parking type - $this->fieldOptionProviders['parking'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ - 'label' => $this->getParkingCheckboxLabel($bookingDto->travel->getAdditionalServicesBySubTypes('PAR', true)), - 'required' => false, - ]; -} - -/** - * Format transportation service labels with type and pricing. - */ -private function formatTransportationServiceLabel(Service $service): string -{ - $label = $service->label; - - // Add transportation type indicator - $typeIndicator = match($service->subType) { - // Transportation type icons removed for cleaner labels - default => '' - }; - - if ($typeIndicator) { - $label = $typeIndicator . ' ' . $label; - } - - // Add pricing with discount indication - if ($service->price > 0) { - $label .= sprintf(' (+€%.2f)', $service->price); - } elseif ($service->price < 0) { - $label .= sprintf(' (-€%.2f Discount)', abs($service->price)); - } - - // Add availability warning if limited - if (null !== $service->available && $service->available <= 5) { - $label .= sprintf(' (nur %d verfügbar)', $service->available); - } - - return $label; -} -``` - -### Phase 3: Conditional Field States & UX 🎨 - -#### 3.1 Service Sub-Type Condition - -**File:** `src/Form/Service/Condition/ServiceSubTypeCondition.php` - -```php -getParticipant($participantIndex); - if (null === $participant) { - return false; - } - - $transportationService = match($this->direction) { - 'outbound' => $participant->transportationOutbound, - 'inbound' => $participant->transportationInbound, - default => null, - }; - - if (null === $transportationService) { - return false; - } - - return $this->expectedType === $transportationService->subType; - } - - public function getDependentFields(): array - { - return match($this->direction) { - 'outbound' => ['transportationOutbound'], - 'inbound' => ['transportationInbound'], - default => [], - }; - } - - public function getDescription(): string - { - return sprintf('%s transportation is %s', ucfirst($this->direction), $this->expectedType); - } -} -``` - -#### 3.2 Update Field State Provider - -**Update:** `src/Form/Service/CreateFieldStateProvider.php` - -```php -use App\BusProNet\Utility\DirectionMapper; -use App\Form\Service\Condition\ServiceSubTypeCondition; - -protected function registerFieldStateConditions(): void -{ - // ... existing conditions - - // Transportation-related field conditions - - // Show outbound pickup only when transportation is BUS (hidden by default) - $this->fieldStateConditions['pickupOutbound'] = [ - 'hidden' => CompositeCondition::not( - ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_BUS_API) - ), - ]; - - // Show inbound pickup only when transportation is BUS (hidden by default) - $this->fieldStateConditions['pickupInbound'] = [ - 'hidden' => CompositeCondition::not( - ServiceSubTypeCondition::equals('transportationInbound', DirectionMapper::SUBTYPE_BUS_API) - ), - ]; - - // Show parking only when outbound transportation is PKW (hidden by default) - // Parking is offered at holiday destination for those arriving by car - $this->fieldStateConditions['parking'] = [ - 'hidden' => CompositeCondition::not( - ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_CAR_API) - ), - ]; -} -``` - -### Phase 4: Form Integration & Service Configuration ⚙️ - -#### 4.1 Update Form Type - -**Update:** `src/Form/BookingCreateParticipantType.php` - -```php -// Add transportation fields to dynamic fields list -$dynamicFields = [ - 'assignedRoomId' => ChoiceType::class, - 'remarksRoom' => TextareaType::class, - 'courses' => ChoiceType::class, - 'additionalServices' => ChoiceType::class, - 'board' => ChoiceType::class, - 'rentals' => ChoiceType::class, - 'skiPass' => ChoiceType::class, - 'transportationOutbound' => ChoiceType::class, // New - 'transportationInbound' => ChoiceType::class, // New - 'pickupOutbound' => ChoiceType::class, // New - 'pickupInbound' => ChoiceType::class, // New - 'parking' => CheckboxType::class, // New - Simple checkbox -]; -``` - -#### 4.2 Service Registration - -**Update:** `config/services.yaml` - -```yaml - # Transportation field handlers - App\Form\Service\ParticipantTransportationOutboundFieldHandler: - tags: - - { name: 'app.participant_field_handler', field: 'transportationOutbound' } - - App\Form\Service\ParticipantTransportationInboundFieldHandler: - tags: - - { name: 'app.participant_field_handler', field: 'transportationInbound' } - - App\Form\Service\ParticipantPickupOutboundFieldHandler: - tags: - - { name: 'app.participant_field_handler', field: 'pickupOutbound' } - - App\Form\Service\ParticipantPickupInboundFieldHandler: - tags: - - { name: 'app.participant_field_handler', field: 'pickupInbound' } - - App\Form\Service\ParticipantParkingFieldHandler: - tags: - - { name: 'app.participant_field_handler', field: 'parking' } -``` - -#### 4.3 Constants Update - -**Update:** `src/BusProNet/Utility/DirectionMapper.php` - -```php -// Transportation service sub-types (API format - German abbreviations) -public const SUBTYPE_BUS_API = 'BUS'; -public const SUBTYPE_CAR_API = 'PKW'; - -// Transportation service sub-types (internal format - English) -public const SUBTYPE_BUS = 'BUS'; -public const SUBTYPE_CAR = 'CAR'; - -/** - * Maps API transportation sub-type to internal sub-type. - */ -public static function apiToInternal(string $apiSubType): string -{ - return match ($apiSubType) { - self::SUBTYPE_BUS_API => self::SUBTYPE_BUS, - self::SUBTYPE_CAR_API => self::SUBTYPE_CAR, - default => throw new \InvalidArgumentException("Unknown API sub-type: $apiSubType"), - }; -} - -/** - * Maps internal transportation sub-type to API sub-type. - */ -public static function internalToApi(string $internalSubType): string -{ - return match ($internalSubType) { - self::SUBTYPE_BUS => self::SUBTYPE_BUS_API, - self::SUBTYPE_CAR => self::SUBTYPE_CAR_API, - default => throw new \InvalidArgumentException("Unknown internal sub-type: $internalSubType"), - }; -} -``` - -## UX Design & User Experience 🎯 - -### Section Organization - -**Transportation will be organized in logical sections:** - -```html - -
-

Anreise

-
- -
- {{ form_row(participant.transportationOutbound) }} - - - {% if participant.pickupOutbound is defined %} -
{{ form_row(participant.pickupOutbound) }}
- {% endif %} - {% if participant.parking is defined %} -
{{ form_row(participant.parking) }}
- {% endif %} -
- - -
- {{ form_row(participant.transportationInbound) }} - - {% if participant.pickupInbound is defined %} -
{{ form_row(participant.pickupInbound) }}
- {% endif %} -
-
-
-``` - -### Progressive Disclosure Features - -1. **Smart Field Visibility:** - - Pickup fields only appear when bus is selected - - Parking only appears when PKW is selected - - Smooth transitions using existing HTMX integration - -2. **Visual Indicators:** - - Transportation type icons (🚌 bus, 🚗 car) - - Pricing with discount indicators - - Availability warnings for limited services - - Required field indicators - -3. **Real-time Feedback:** - - Pricing updates immediately - - Pickup/parking fields show/hide smoothly - - Booking summary reflects transportation selections - - Validation feedback on selection changes - -## Pricing Integration 💰 - -### Transportation Service Pricing - -- **Bus Services:** Standard pricing per direction -- **PKW (Self-organized):** Often negative prices (discounts) -- **Parking:** Additional cost for PKW travelers -- **Combined Pricing:** Total transportation cost = outbound + inbound + parking - -### Service Label Examples - -- `🚌 Bus nach München (+€45,00)` -- `🚗 Eigenanreise (-€20,00 Discount)` -- `🅿️ Parkplatz Hotel (+€15,00)` -- `🚌 Bus Hinfahrt (nur 3 verfügbar)` - -## Data Flow & Validation 🔄 - -### Form Submission Flow - -1. **Transportation Selection:** User selects outbound/inbound transport -2. **Conditional Fields Update:** Pickup/parking fields show/hide via HTMX -3. **Field Handler Processing:** Services validated against age/availability -4. **Pricing Calculation:** Total transportation cost calculated -5. **Booking Summary Update:** Summary reflects all transportation selections - -### Validation Rules - -- **Transportation Required:** Both directions must have transportation -- **Pickup Required:** When bus is selected, pickup is mandatory -- **Parking Optional:** Available only with PKW transportation -- **Service Availability:** Validate against available quantities -- **Date Constraints:** Services must be valid for travel dates - -## Testing Strategy 🧪 - -### Unit Testing Focus - -1. **Direction Mapper:** Test all direction code conversions -2. **Field Handlers:** Test transportation/pickup processing logic -3. **Conditional States:** Test pickup/parking visibility logic -4. **Service Validation:** Test availability and age constraints - -### Integration Testing - -1. **Form Flow:** Complete transportation selection workflow -2. **HTMX Updates:** Real-time field visibility and pricing updates -3. **Data Processing:** Transportation data for BPN API submission -4. **Backward Compatibility:** Ensure existing booking edit still works - -### Manual Testing Scenarios - -1. **Bus Transportation:** Select bus both directions with pickups -2. **Mixed Transportation:** Bus one direction, PKW other direction -3. **PKW Transportation:** Self-organized both directions with parking -4. **Limited Availability:** Test behavior with limited service availability -5. **Discount Services:** Verify negative pricing for PKW options - -## Migration Strategy 🔄 - -### Backward Compatibility - -1. **Property Mapping:** Update existing code using old property names -2. **Direction Constants:** Maintain compatibility with existing direction codes -3. **Data Import:** Handle existing bookings with old property structure -4. **API Consistency:** Ensure BPN XML submission uses correct direction codes - -### Deployment Steps - -1. **Phase 1:** Deploy direction mapper and updated properties -2. **Phase 2:** Deploy field handlers and form integration -3. **Phase 3:** Deploy UX improvements and conditional states -4. **Phase 4:** Deploy pricing integration and final testing - -## Implementation Timeline 📅 - -### Sprint 1: Foundation (Week 1) - ✅ COMPLETED -- ✅ Create documentation -- ✅ Implement DirectionMapper utility (removed unused toEnglish method) -- ✅ Update ParticipantDto properties -- ✅ Update BookingEditDto mapping -- ✅ Create transportation field handlers (Outbound/Inbound) -- ✅ Create pickup field handlers with conditional logic -- ✅ Add parking service handler for self-organized transport -- ✅ Add TOKEN_PARKING constant -- ✅ Update ParticipantFieldOptionsProvider for transportation services -- ✅ Integrate transportation fields into BookingCreateParticipantType -- ✅ Create ServiceSubTypeCondition for field state management -- ✅ Update field state provider with transportation conditions -- ✅ Implement transportation type mapping for API/internal consistency - -### Sprint 2: UX & Data Model Optimization (Week 2) - ✅ COMPLETED -- ✅ Fixed parking field data model (Service object → boolean) -- ✅ Fixed pickup field form processing (Pickup object conversion) -- ✅ Optimized template layout (mutual exclusivity of pickup/parking) -- ✅ Updated field handlers for correct data types -- ✅ Enhanced conditional field state logic -- ✅ Improved form type configuration (CheckboxType for parking) -- ✅ Template optimization with shared field space - -### Sprint 3: Testing & Deployment (Week 3) - ✅ COMPLETED -- ✅ Form processing pipeline working correctly -- ✅ Conditional field visibility working -- ✅ Data synchronization between DTO and form fixed -- ✅ Template layout optimized and tested -- ✅ Comprehensive manual testing completed -- ✅ Pricing integration testing completed -- ✅ Production deployment ready - -## Key Implementation Highlights 🌟 - -### Transportation Type Mapping System - -**Problem Solved:** BusProNet uses German abbreviations ('PKW') while internal code should use English terminology ('CAR') for consistency. - -**Solution:** Enhanced `DirectionMapper` utility with bidirectional mapping: -- **API Format:** `SUBTYPE_CAR_API = 'PKW'`, `SUBTYPE_BUS_API = 'BUS'` -- **Internal Format:** `SUBTYPE_CAR = 'CAR'`, `SUBTYPE_BUS = 'BUS'` -- **Mapping Methods:** `apiToInternal()`, `internalToApi()`, validation helpers - -### Generic Service Sub-Type Condition - -**Achievement:** Created reusable `ServiceSubTypeCondition` instead of transportation-specific logic: -- Supports multiple operators: `equals`, `notEquals`, `in`, `notIn` -- Works with any service field, not just transportation -- Handles both API and internal sub-type values -- Provides static factory methods for common use cases - -### Data Model Optimizations - -**Parking Field Simplification:** -- **Problem:** Complex Service object storage for single checkbox -- **Solution:** Changed to simple `bool $parking = false` in ParticipantDto -- **Benefits:** Cleaner data model, simpler form processing, matches UX intent - -**Form Processing Fixes:** -- **Pickup Objects:** Fixed conversion from Pickup objects to IDs for form rendering -- **Data Synchronization:** Enhanced registry to handle object-to-scalar conversion -- **Type Safety:** Aligned form field types with DTO property types - -### Template Layout Optimization - -**Smart Space Utilization:** -- **Mutually Exclusive Fields:** Pickup (BUS) and parking (PKW) share layout space -- **Grid Layout:** Maintains clean 2-column transportation structure -- **Visual Balance:** Eliminates empty space and improves UX -- **Logical Grouping:** Related outbound fields stay together - -### Conditional UX Logic - -**Smart Field Visibility:** -- **Pickup Fields:** Hidden by default, only visible when respective transportation is selected AND is BUS -- **Parking Field:** Hidden by default, only visible when outbound transportation is selected AND is CAR (PKW) -- **Default State:** All conditional fields start hidden until relevant transportation is chosen -- **Template Optimization:** Outbound pickup and parking share the same layout space since they're mutually exclusive -- Uses API constants since Service objects contain API values -- Proper business logic: parking needed at destination for car arrivals - -### Backward Compatibility - -- Maintained all existing property names with deprecation notices -- API integration continues using BusProNet's expected format -- Internal code uses clean English naming -- Seamless migration path for existing functionality - -## Success Criteria ✅ - -### Technical Success -- ✅ Direction mapping handles all BPN inconsistencies correctly -- ✅ Transportation services integrate with existing form system -- ✅ Conditional pickup/parking fields work seamlessly -- ✅ HTMX integration prepared for real-time updates -- ✅ Field handlers follow established patterns -- ✅ Backward compatibility maintained -- ✅ Data model optimized for simplicity and type safety - -### UX Success -- ✅ Clear separation of outbound/inbound transportation -- ✅ Progressive disclosure prevents overwhelming users -- ✅ Optimized layout with shared field space -- ✅ Conditional field visibility working correctly -- ✅ Intuitive field organization with logical grouping -- ✅ Template layout optimized for mobile and desktop - -### Business Success -- ✅ Support for complex transportation scenarios -- ✅ Parking checkbox integration (boolean model) -- ✅ Conditional logic for transportation types -- ✅ Data structure ready for BPN API submission -- ✅ Scalable architecture for future enhancements -- ✅ Clean separation between pickup and parking business logic - ---- - -**Last Updated:** 2025-09-02 -**Status:** ✅ Core Implementation Completed -**Current State:** Ready for comprehensive testing and pricing integration -**Next Phase:** HTMX endpoints activation and final testing \ No newline at end of file diff --git a/docs/archive/implementation-plan-rental-skipass-duration-filtering.md b/docs/archive/implementation-plan-rental-skipass-duration-filtering.md deleted file mode 100644 index 823d8de..0000000 --- a/docs/archive/implementation-plan-rental-skipass-duration-filtering.md +++ /dev/null @@ -1,108 +0,0 @@ -# Implementation Plan: Duration-Based Rental Filtering Based on Skipass Selection - -## Overview -Implement cross-field dependency where rentals are only visible/selectable when a skipass is selected, and filter rentals to match the selected skipass's date range (dateFrom/dateTo). - -## Analysis -Based on code review: -- Both rentals and skipasses have `dateFrom` and `dateTo` properties for duration -- Current system already filters services by travel date range using `getAdditionalServicesBySubTypes($token, true, true)` -- Need to implement skipass-to-rental date matching logic -- Field state conditions system is already in place for hiding/showing fields -- Need to create a custom field options provider for duration-filtered rentals - -## Implementation Steps - -### 1. Create SkiPassSelectionCondition -**File**: `src/Form/Service/Condition/SkiPassSelectionCondition.php` -- Similar to `RentalSelectionCondition` but checks for skipass selection -- Evaluates both form data and participant DTO for skipass -- Returns true if participant has selected a skipass - -### 2. Update Field State Provider -**File**: `src/Form/Service/CreateFieldStateProvider.php` -- Add rentals field hidden condition based on skipass selection -- Similar to how rental insurance is hidden unless rentals are selected: -```php -$skiPassCondition = new SkiPassSelectionCondition(); -$this->fieldStateConditions['rentals'] = [ - 'hidden' => CompositeCondition::not($skiPassCondition), -]; -``` - -### 3. Create Duration-Based Rental Filtering -**File**: `src/Form/Service/ParticipantFieldOptionsProvider.php` -- Modify existing rentals field provider to filter by selected skipass duration -- Extract selected skipass from participant data -- Filter available rentals to only those with matching dateFrom/dateTo ranges -- Use exact date matching: `rental.dateFrom == skipass.dateFrom && rental.dateTo == skipass.dateTo` - -### 4. Update RentalsFieldHandler Dependencies -**File**: `src/Form/Service/ParticipantRentalsFieldHandler.php` -- Add `skiPass` to dependencies array: `['dateOfBirth', 'skiPass']` -- Add DTO cleanup: clear rentals when no skipass is selected -- Filter rentals by skipass duration in `processField()` method - -### 5. Update Field Handler Dependencies -**File**: `src/Form/Service/ParticipantRentalInsuranceFieldHandler.php` -- Update dependencies to include skipass: `['dateOfBirth', 'rentals', 'skiPass']` -- Modify visibility logic: rental insurance only shown when both skipass AND rentals selected - -## Key Technical Details - -### Duration Matching Logic -Rentals will be filtered to match skipass duration exactly: -```php -private function filterRentalsBySkiPassDuration(array $rentals, ?Service $selectedSkiPass): array -{ - if (null === $selectedSkiPass || null === $selectedSkiPass->dateFrom || null === $selectedSkiPass->dateTo) { - return []; // No skipass or invalid dates = no rentals - } - - return array_filter($rentals, function(Service $rental) use ($selectedSkiPass) { - return $rental->dateFrom?->format('Y-m-d') === $selectedSkiPass->dateFrom?->format('Y-m-d') - && $rental->dateTo?->format('Y-m-d') === $selectedSkiPass->dateTo?->format('Y-m-d'); - }); -} -``` - -### Field Visibility Chain -1. SkiPass: Always visible (after dateOfBirth) -2. Rentals: Only visible when skipass selected, filtered by skipass duration -3. Rental Insurance: Only visible when rentals selected (existing logic) -4. Body Dimensions: Only visible when rentals selected (existing logic) - -### HTMX Integration -- Existing HTMX system will handle dynamic updates when skipass selection changes -- Field state conditions will automatically trigger rental field visibility -- Rental options will be re-rendered with duration-filtered choices - -## Dependencies -- No new dependencies required -- Leverages existing field state condition system -- Uses existing Service model date properties -- Maintains backward compatibility with existing workflows - -## Progress Tracking - -### Status: Implementation Complete ✅ -- [x] System architecture analysis -- [x] Existing code review -- [x] Implementation strategy defined -- [x] Technical approach documented - -### Implementation Phase: COMPLETED ✅ -- [x] Step 1: Create SkiPassSelectionCondition -- [x] Step 2: Update Field State Provider -- [x] Step 3: Create Duration-Based Rental Filtering -- [x] Step 4: Update RentalsFieldHandler Dependencies -- [x] Step 5: Update Field Handler Dependencies -- [x] Testing: Verify cross-field dependencies work correctly -- [x] Testing: Verify HTMX updates work properly -- [x] Documentation: Update CLAUDE.md with new feature details - -## Notes -- Implementation follows existing architectural patterns -- Maintains consistency with current field state condition system -- Preserves backward compatibility -- Leverages existing HTMX infrastructure for dynamic updates \ No newline at end of file diff --git a/docs/archive/insurance-booking-implementation-plan.md b/docs/archive/insurance-booking-implementation-plan.md deleted file mode 100644 index 1afe00e..0000000 --- a/docs/archive/insurance-booking-implementation-plan.md +++ /dev/null @@ -1,409 +0,0 @@ -# Insurance Booking System Implementation Plan - -## Overview - -This document outlines the complete implementation plan for making travel insurances bookable per participant in the MyEP Next Booking system. The implementation includes sophisticated criteria matching, automatic re-selection when participant prices change, and comprehensive form integration. - -## Current System Status - -### ✅ Already Implemented -- **Insurance XML Parsing**: `InsuranceParser` and `InsuranceLoader` classes -- **Insurance Model**: Complete with price ranges, age constraints, dates, family flags -- **Family Status Logic**: `BookingCreateDto::isFamilyBooking()` method -- **Individual Pricing**: `BookingPriceCalculatorService::calculateIndividualParticipantPrice()` method -- **Field Handler Architecture**: Existing pattern for dynamic form fields -- **HTMX Integration**: Real-time form updates infrastructure - -### ✅ Recently Completed -- **Phase 1**: Insurance subtype parsing and type resolution system ✅ -- **Phase 2**: Enhanced age calculation with reference date support ✅ -- **Phase 3**: Insurance matching service with comprehensive criteria validation ✅ -- **Phase 4**: ParticipantDto enhancement with insurance property ✅ -- **Phase 5**: Form field handler for insurance selection processing ✅ - -### ❌ Remaining Implementation -- Form integration and field configuration -- Controller integration and data handling -- Frontend templates and UX -- Auto-reselection when participant price changes -- Advanced features and applicant control - -## Key Technical Discoveries - -### Insurance XML Structure Analysis -- **Individual Insurances**: Have `unterart` (subtype) attribute (RRV, PAK, OHN) -- **Insurance Packages**: No subtype but contain individual insurances -- **Family Detection**: Use `familienversicherung` boolean attribute (not label parsing) -- **Package Type Resolution**: Analyze contained insurances to determine dominant type - -### Age Calculation Requirement -- **Critical**: Use travel start date for age calculation, not current date -- Insurance eligibility based on participant's age at time of travel - -## Implementation Phases - -### Phase 1: Insurance Type System ✅ COMPLETED -**Goal**: Add type resolution capability to distinguish insurance types - -#### 1.1 Extend Insurance Model ✅ -- [x] Add `subType` property to `Insurance` model (from XML `unterart`) -- [x] Add computed `type` property (resolved via service) -- [x] Update serialization groups if needed - -#### 1.2 Update Insurance Parser ✅ -- [x] Modify `InsuranceParser::parseInsuranceNode()` to parse `unterart` attribute -- [x] Add subtype to individual insurance parsing -- [x] Ensure package parsing maintains existing functionality - -#### 1.3 Create Insurance Type Resolver ✅ -- [x] Create `InsuranceTypeResolver` service -- [x] Implement type resolution for individual insurances -- [x] Implement package type resolution via contained insurance analysis -- [x] Define type constants: `TRAVEL_CANCELLATION`, `TRAVEL_PROTECTION`, etc. -- [x] Handle family variants using `familyInsurance` boolean - -```php -// Type Constants -const TRAVEL_CANCELLATION = 'TRAVEL_CANCELLATION'; -const TRAVEL_CANCELLATION_FAMILY = 'TRAVEL_CANCELLATION_FAMILY'; -const TRAVEL_PROTECTION = 'TRAVEL_PROTECTION'; -const TRAVEL_PROTECTION_FAMILY = 'TRAVEL_PROTECTION_FAMILY'; -const DEDUCTIBLE = 'DEDUCTIBLE'; -``` - -#### 1.4 Testing ✅ -- [x] Create `InsuranceTypeResolverTest` -- [x] Test individual insurance type resolution -- [x] Test package type resolution -- [x] Test family variant detection -- [x] Verify existing insurance parsing still works - -### Phase 2: Enhanced Age Calculation ✅ COMPLETED -**Goal**: Support age calculation at specific dates (travel start date) - -#### 2.1 Update ParticipantDto ✅ -- [x] Enhanced existing `getAge()` method with optional reference date parameter -- [x] Keep existing `getAge(): ?int` for backward compatibility -- [x] Ensure proper null handling for missing birth dates - -#### 2.2 Testing ✅ -- [x] Add comprehensive tests for reference date age calculation -- [x] Test edge cases (leap years, same day, etc.) -- [x] Verify existing age calculation still works - -### Phase 3: Insurance Matching Service -**Goal**: Implement comprehensive insurance matching with all criteria - -#### 3.1 Create Insurance Matching Service -- [ ] Create `InsuranceMatchingService` class -- [ ] Implement `getMatchingInsurances()` method with all criteria: - - Age at travel date validation - - Price range validation - - Booking date validation - - Travel date validation - - Family insurance validation -- [ ] Implement `getMatchingInsurancesByType()` for auto-reselection -- [ ] Add "No Insurance" option handling - -#### 3.2 Auto-Reselection Logic -- [ ] Implement `handlePriceChange()` method -- [ ] Find matching insurance of same type when price changes -- [ ] Maintain coverage when possible, fallback to null -- [ ] Log auto-reselection events for debugging - -#### 3.3 Service Registration -- [ ] Register service in `services.yaml` -- [ ] Configure dependencies (InsuranceLoader, InsuranceTypeResolver) - -#### 3.4 Testing -- [ ] Create comprehensive `InsuranceMatchingServiceTest` -- [ ] Test all matching criteria combinations -- [ ] Test auto-reselection scenarios -- [ ] Test edge cases and boundary conditions - -### Phase 4: Participant DTO Enhancement -**Goal**: Add insurance property and integrate with pricing - -#### 4.1 Extend ParticipantDto -- [ ] Add `?Insurance $insurance = null` property -- [ ] Add validation groups if needed -- [ ] Ensure proper serialization/deserialization - -#### 4.2 Update Price Calculation -- [ ] Modify `BookingPriceCalculatorService::calculateParticipantServiceTotal()` -- [ ] Include insurance price in participant total -- [ ] Update `calculateIndividualParticipantPrice()` to include insurance -- [ ] Update `calculateAllParticipantIndividualPrices()` accordingly - -#### 4.3 Testing -- [ ] Update `BookingPriceCalculatorServiceTest` -- [ ] Test price calculation with insurance -- [ ] Test pricing without insurance -- [ ] Verify individual participant pricing includes insurance - -### Phase 5: Form Field Handler -**Goal**: Create form field handler for insurance selection - -#### 5.1 Create Insurance Field Handler -- [ ] Create `ParticipantInsuranceFieldHandler` extending `AbstractParticipantFieldHandler` -- [ ] Implement field processing logic -- [ ] Handle insurance selection validation -- [ ] Implement auto-reselection on price changes -- [ ] Clear insurance if criteria no longer match - -#### 5.2 Field Dependencies -- [ ] Define dependencies: `dateOfBirth` (for age calculation) -- [ ] Handle field visibility based on available insurances -- [ ] Implement proper error handling - -#### 5.3 Service Registration -- [ ] Register handler in `services.yaml` with proper tags -- [ ] Set appropriate priority in relation to other handlers - -#### 5.4 Testing -- [ ] Create `ParticipantInsuranceFieldHandlerTest` -- [ ] Test field processing -- [ ] Test auto-reselection scenarios -- [ ] Test validation logic - -### Phase 6: Form Integration -**Goal**: Add insurance field to participant form - -#### 6.1 Update Form Type -- [ ] Modify `BookingCreateParticipantType` -- [ ] Add insurance choice field -- [ ] Configure field type and options -- [ ] Add HTMX trigger attributes - -#### 6.2 Field Options Provider -- [ ] Add insurance field to `ParticipantFieldOptionsProvider` -- [ ] Generate "No Insurance" option -- [ ] Generate valid insurance options per participant -- [ ] Use `InsuranceMatchingService` for filtering -- [ ] Format options with price and type information - -#### 6.3 Field State Provider Integration -- [ ] Update `CreateFieldStateProvider` if needed -- [ ] Handle field visibility conditions -- [ ] Register field state conditions - -#### 6.4 Testing -- [ ] Test form field rendering -- [ ] Test field options generation -- [ ] Test HTMX integration - -### Phase 7: Controller Integration -**Goal**: Update controllers to handle insurance data - -#### 7.1 Update Step 2 Controller -- [ ] Ensure insurance data is included in template variables -- [ ] Handle insurance-related HTMX updates -- [ ] Update participant price calculations to include insurance - -#### 7.2 Error Handling -- [ ] Add proper error handling for insurance validation -- [ ] Handle insurance matching failures gracefully -- [ ] Provide user-friendly error messages - -#### 7.3 Testing -- [ ] Test controller responses include insurance data -- [ ] Test HTMX updates work correctly -- [ ] Test error scenarios - -### Phase 8: Frontend Templates -**Goal**: Add insurance field to templates and display pricing - -#### 8.1 Template Updates -- [ ] Add insurance field to `create_step_2.html.twig` -- [ ] Position field appropriately in participant sections -- [ ] Add proper labeling and help text -- [ ] Configure HTMX attributes for real-time updates - -#### 8.2 Pricing Display -- [ ] Update participant headers to show insurance pricing -- [ ] Include insurance in individual participant price display -- [ ] Update booking summary to include insurance totals - -#### 8.3 UX Enhancements -- [ ] Show criteria-based availability information -- [ ] Display auto-reselection notifications -- [ ] Add loading states for HTMX updates - -### Phase 9: Advanced Features -**Goal**: Implement applicant control and bulk operations - -#### 9.1 Applicant Insurance Control -- [ ] Add special UI for first participant (applicant) -- [ ] Implement bulk insurance selection -- [ ] Respect individual criteria while allowing bulk operations -- [ ] Add override capabilities with proper validation - -#### 9.2 Real-time Price Integration -- [ ] Monitor price changes via HTMX -- [ ] Trigger auto-reselection when participant price changes -- [ ] Provide visual feedback for automatic changes -- [ ] Update pricing summary dynamically - -#### 9.3 Enhanced UX -- [ ] Add insurance type grouping in selection -- [ ] Implement insurance comparison features -- [ ] Add detailed insurance information display -- [ ] Improve mobile responsiveness - -### Phase 10: Testing & Quality Assurance -**Goal**: Comprehensive testing and code quality - -#### 10.1 Integration Testing -- [ ] Create end-to-end insurance booking tests -- [ ] Test complete booking flow with insurances -- [ ] Test auto-reselection scenarios -- [ ] Test applicant control features - -#### 10.2 Performance Testing -- [ ] Test insurance matching performance with large datasets -- [ ] Optimize insurance filtering algorithms -- [ ] Test HTMX update performance - -#### 10.3 Code Quality -- [ ] Run PHP CS Fixer on all new files -- [ ] Ensure PSR-12 compliance -- [ ] Add proper PHPDoc documentation -- [ ] Review and optimize service dependencies - -#### 10.4 User Acceptance Testing -- [ ] Test with real insurance data -- [ ] Validate business logic with stakeholders -- [ ] Test edge cases and error scenarios -- [ ] Gather user feedback on UX - -## Technical Architecture - -### New Classes Overview - -``` -src/Service/ -├── InsuranceTypeResolver.php # Type resolution for insurances and packages -├── InsuranceMatchingService.php # Criteria-based insurance matching -└── Insurance/ - ├── Criteria/ - │ ├── AgeCriterion.php # Age-based matching - │ ├── PriceCriterion.php # Price range matching - │ ├── DateCriterion.php # Date validation - │ └── FamilyCriterion.php # Family insurance validation - └── Matcher/ - └── InsuranceMatcher.php # Core matching logic - -src/Form/Service/ -├── ParticipantInsuranceFieldHandler.php # Form field processing -└── Insurance/ - └── InsuranceFieldOptionsProvider.php # Field options generation - -tests/Service/ -├── InsuranceTypeResolverTest.php -├── InsuranceMatchingServiceTest.php -└── Insurance/ - └── Criteria/ - ├── AgeCriterionTest.php - ├── PriceCriterionTest.php - ├── DateCriterionTest.php - └── FamilyCriterionTest.php - -tests/Form/Service/ -└── ParticipantInsuranceFieldHandlerTest.php -``` - -### Database Changes -- **None required** - All insurance data loaded from XML - -### Configuration Changes -- Service registrations in `services.yaml` -- Field handler tags and priorities -- Form field configurations - -## Implementation Guidelines - -### Code Standards -- Follow PSR-12 coding standards -- Use `declare(strict_types=1)` on all files -- Apply Symfony coding standards via php-cs-fixer -- Use English for all variable and constant names -- Follow existing architectural patterns - -### Testing Standards -- Minimum 90% code coverage for new classes -- Unit tests for all service methods -- Integration tests for form processing -- End-to-end tests for complete booking flow -- Performance tests for matching algorithms - -### Documentation Standards -- PHPDoc for all public methods -- Business logic documentation in comments -- Update CLAUDE.md with new features -- Create user documentation for insurance features - -## Risk Mitigation - -### Technical Risks -1. **Performance**: Large insurance datasets could slow matching - - **Mitigation**: Implement caching and optimize algorithms -2. **Complexity**: Auto-reselection logic could introduce bugs - - **Mitigation**: Comprehensive testing and logging -3. **Data Integrity**: Price changes could cause inconsistent states - - **Mitigation**: Atomic operations and validation - -### Business Risks -1. **Incorrect Matching**: Wrong insurance eligibility could cause issues - - **Mitigation**: Thorough testing with real data and stakeholder validation -2. **User Confusion**: Complex insurance options could confuse users - - **Mitigation**: Clear UX design and comprehensive help text - -## Success Criteria - -### Functional Requirements -- ✅ Participants can select appropriate insurances based on all criteria -- ✅ Auto-reselection works when participant price changes -- ✅ Applicant can manage insurances for all participants -- ✅ Real-time pricing updates include insurance costs -- ✅ Form validation prevents invalid insurance selections - -### Performance Requirements -- ✅ Insurance matching completes within 100ms for typical datasets -- ✅ HTMX updates complete within 500ms -- ✅ Page load times remain under 2 seconds - -### Quality Requirements -- ✅ 90%+ code coverage for new functionality -- ✅ Zero critical bugs in production -- ✅ PSR-12 compliance for all new code -- ✅ Comprehensive documentation - -## Future Enhancements - -### Phase 11+ (Future) -- Insurance comparison tools -- Advanced filtering and search -- Insurance recommendation engine -- Historical insurance selection analytics -- Multi-language insurance descriptions -- PDF insurance documentation generation - -## Dependencies - -### External Dependencies -- Insurance XML data must be available and properly formatted -- BusProNet API integration for insurance booking -- Travel data must include proper date information - -### Internal Dependencies -- Existing individual pricing calculation system -- Field handler architecture -- HTMX integration infrastructure -- Family status determination logic - ---- - -**Document Version**: 1.0 -**Last Updated**: 2025-09-26 -**Author**: Claude Code Implementation Plan -**Status**: Ready for Implementation \ No newline at end of file diff --git a/docs/booking-dto-unification-plan.md b/docs/booking-dto-unification-plan.md deleted file mode 100644 index 0c3cc32..0000000 --- a/docs/booking-dto-unification-plan.md +++ /dev/null @@ -1,323 +0,0 @@ -# Booking DTO Unification Plan - -## Problem Statement - -The current implementation uses two separate DTOs (`BookingCreateDto` and `BookingEditDto`) with fundamentally different data structures: - -- **Create mode**: Services stored in participant DTOs (e.g., `$participant->insurance`, `$participant->courses`) -- **Edit mode**: Services stored in the booking object (e.g., `$booking->insurances`, `$booking->additionalServices`) - -This divergence causes multiple issues: -1. Pricing and summary calculations fail in edit mode -2. Field handlers need complex mode-specific logic -3. Data processor needs separate handling for create vs edit -4. Code duplication and increased complexity -5. Bugs due to assumptions about data structure - -## Solution: Unified BookingDto - -Create a single `BookingDto` class that stores all service selections in participant DTOs for BOTH create and edit modes. Different modes are handled through different instantiation methods. - -## Implementation Plan - -### Phase 1: Create Unified BookingDto Class - -**File**: `src/Form/Model/BookingDto.php` - -**Changes**: -- Merge `BookingCreateDto` and `BookingEditDto` into single `BookingDto` class -- Keep all participant-based service storage (insurance, courses, skiPass, rentals, transportation, etc.) -- Add `mode` property (MODE_CREATE or MODE_EDIT) -- Add `booking` property (null in create mode, Booking object in edit mode for metadata only) -- Implement `BookingDtoInterface` interface - -**Constructor signatures**: -```php -// Create mode -public function __construct(Travel $travel, int $agencyId) - -// Edit mode (static factory) -public static function fromBooking(Booking $booking, Travel $travel): static -``` - -**Key method**: -```php -public function getMode(): string -{ - return null !== $this->booking ? self::MODE_EDIT : self::MODE_CREATE; -} -``` - -### Phase 2: Update BookingDto::fromBooking() for Edit Mode - -**File**: `src/Form/Model/BookingDto.php` - -**Responsibilities**: -1. Extract services from booking object and assign to participant DTOs -2. Map insurances: `$participant->insurance = $booking->getInsuranceForParticipant($index)` -3. Map services: `$participant->courses = $booking->getAdditionalServicesForParticipantByGroup($index, 'COURSES')` -4. Map transportation: `$participant->transportationOutbound = $booking->getTransportationForParticipant($index, 'OUTBOUND')` -5. Map pickup locations -6. Map room assignments -7. Set body dimensions from applicant for participant 0 -8. Set parking from form data (need to check if stored in booking) - -**Data extraction methods needed**: -- `Booking::getInsuranceForParticipant(int $index): ?Insurance` -- Existing methods for other services can be reused - -### Phase 3: Update ParticipantDto - -**File**: `src/Form/Model/ParticipantDto.php` - -**Changes**: -- Already has all necessary service properties -- Ensure `fromPersonalData()` copies body dimensions correctly -- No structural changes needed - -### Phase 4: Update Form Types - -**Files to update**: -- `src/Form/BookingType.php` → Rename to `BookingType` (generic) -- `src/Form/BookingEditType.php` → Delete (use unified BookingType) -- `src/Form/BookingParticipantType.php` → Already works with ParticipantDto, should work unchanged - -**Changes**: -- Update `BookingType` to use `BookingDto::class` as data_class -- Remove mode-specific form type distinction -- Pass `edit_mode` option to child forms for field state provider selection - -### Phase 5: Update Controllers - -#### CreateStep2Controller -**File**: `src/Controller/Booking/CreateStep2Controller.php` - -**Changes**: -- Change `BookingCreateDto` → `BookingDto` -- Constructor instantiation remains same -- All logic should work unchanged (services already in participant DTOs) - -#### EditController -**File**: `src/Controller/Booking/EditController.php` - -**Changes**: -- Change `BookingEditDto` → `BookingDto` -- Change `BookingEditDto::fromBooking()` → `BookingDto::fromBooking()` -- Form type: change `BookingEditType` → `BookingType` with `['edit_mode' => true]` option -- All pricing and summary calculations should now work (same DTO structure as create) - -### Phase 6: Update BookingDataProcessor - -**File**: `src/BusProNet/DataProcessor/BookingDataProcessor.php` - -**Major simplification**: - -#### createBookingRequestPayload() (Create flow) -- Already works with participant DTOs -- Change signature: `BookingDto` instead of `BookingCreateDto` -- No other changes needed - -#### createUpdateRequestPayload() (Edit flow) -**Current problems**: -- Tries to read services from `$bookingDto->booking` object -- Complex mapping and resetting logic -- `processParticipantServices()` needs to add services to booking data from travel data - -**New approach**: -- Services already in participant DTOs (populated by `fromBooking()`) -- Can reuse create flow logic almost entirely -- Only difference: include `idbuchung` and participant `idadresseperson` in payload - -**Unified approach**: -```php -public function createPayload(BookingDto $bookingDto, string $type): array -{ - // Apply bulk insurance if enabled - $this->applyBulkInsuranceIfActive($bookingDto); - - // Collect service mappings (works same for both modes) - $serviceMap = $this->collectServiceMappings($bookingDto); - $transportationMap = $this->collectTransportationMappings($bookingDto); - $roomMap = $this->collectRoomMappings($bookingDto); - $pickupMap = $this->collectPickupMappings($bookingDto); - $insuranceMap = $this->collectInsuranceMappings($bookingDto); - - // Build payload based on mode - if (BookingDtoInterface::MODE_EDIT === $bookingDto->getMode()) { - return $this->buildUpdatePayload($bookingDto, ...maps); - } else { - return $this->buildCreatePayload($bookingDto, $type, ...maps); - } -} -``` - -**Simplifications**: -- Remove `processParticipantServices()` complexity -- Remove `resetServiceMappings()` -- Remove `removeUnusedServices()` -- Direct mapping from participant DTOs to payload - -### Phase 7: Update Field Handlers - -**Files**: All handlers in `src/Form/Service/` - -**Changes needed**: -- Handlers already work with participant DTOs -- No changes needed (they don't care about mode) -- Registry already handles both modes - -### Phase 8: Update Service Layer - -#### BookingService -**File**: `src/Service/BookingService.php` - -**Changes**: -- Update type hints: `BookingCreateDto|BookingEditDto` → `BookingDto` -- `getRoomSummaryAndParticipantCount()` should now work for both modes (same DTO structure) -- No logic changes needed - -#### PriceCalculator -**File**: `src/Service/PriceCalculator.php` - -**Changes**: -- Update type hints to use `BookingDto` -- All calculations work with participant DTOs, should work unchanged -- Mode detection: `$bookingDto->getMode()` instead of `instanceof` checks - -### Phase 9: Update Field State Providers - -**Files**: -- `src/Form/Service/CreateFieldStateProvider.php` -- `src/Form/Service/EditFieldStateProvider.php` - -**Changes**: -- Already use `BookingDtoInterface`, no changes needed -- Continue to be selected based on `edit_mode` form option - -### Phase 10: Update Templates - -**Files**: -- `templates/booking/create_step_2.html.twig` -- `templates/booking/edit.html.twig` - -**Changes**: -- Variable naming: `bookingCreateDto` → `bookingDto`, `bookingEditDto` → `bookingDto` -- All logic should work unchanged (both render participant forms) - -### Phase 11: Critical New Method in Booking Model - -**File**: `src/BusProNet/Model/Booking.php` - -**Add method**: -```php -public function getInsuranceForParticipant(int $index): ?Insurance -{ - foreach ($this->insurances as $insurance) { - if (in_array($index, $insurance->mapping)) { - return $insurance; - } - } - return null; -} -``` - -Similar methods may be needed for other services if not already present. - -## Migration Strategy - -### Step 1: Create new BookingDto (keep old DTOs) -- Create `src/Form/Model/BookingDto.php` -- Implement both constructor and `fromBooking()` -- Keep `BookingCreateDto` and `BookingEditDto` temporarily - -### Step 2: Update edit flow to use new DTO -- Update `EditController` to use `BookingDto` -- Update `BookingDataProcessor::createUpdateRequestPayload()` to accept `BookingDto` -- Test edit flow thoroughly - -### Step 3: Update create flow to use new DTO -- Update `CreateStep2Controller` to use `BookingDto` -- Test create flow thoroughly - -### Step 4: Cleanup -- Delete `BookingCreateDto.php` -- Delete `BookingEditDto.php` -- Delete `BookingEditType.php` -- Update all remaining type hints - -## Testing Checklist - -### Edit Flow -- [ ] Load existing booking with all service types -- [ ] Form displays all current selections correctly -- [ ] Summary sidebar shows all services and pricing -- [ ] Body dimensions show for applicant -- [ ] Change insurance (individual and bulk) -- [ ] Change services (courses, skiPass, rentals, board) -- [ ] Change transportation -- [ ] Submit changes successfully -- [ ] API receives correct payload with insurances -- [ ] After redirect, summary shows correctly - -### Create Flow -- [ ] Select rooms -- [ ] Add participants -- [ ] Select services for participants -- [ ] Select insurances (individual and bulk) -- [ ] Pricing calculates correctly -- [ ] Summary shows all selections -- [ ] Submit creates booking successfully - -### Field State System -- [ ] Conditional fields show/hide correctly in both modes -- [ ] Readonly fields work in edit mode -- [ ] Age-dependent fields work in both modes -- [ ] Bulk insurance checkbox works - -### Data Integrity -- [ ] No service data loss during mode transitions -- [ ] Insurance IDs match correctly -- [ ] Participant indices correct in both modes -- [ ] Room assignments preserved - -## Benefits of Unified DTO - -1. **Single source of truth**: All service selections in one place (participant DTOs) -2. **Simplified calculations**: Pricing, summary, and totals work identically for both modes -3. **Reduced complexity**: No mode-specific logic in services and calculators -4. **Easier testing**: One DTO structure to test -5. **Better maintainability**: Changes to service structure only need updating in one place -6. **Consistent field handlers**: Handlers work with same data structure regardless of mode -7. **Cleaner templates**: Same rendering logic for both modes - -## Risks and Mitigation - -### Risk: Breaking existing create flow -**Mitigation**: Migrate edit flow first, test thoroughly, then migrate create flow - -### Risk: Data loss during form processing -**Mitigation**: Extensive logging during migration, compare payloads before/after - -### Risk: Field handler incompatibility -**Mitigation**: Field handlers already work with ParticipantDto, minimal changes needed - -### Risk: Performance impact from data extraction -**Mitigation**: `fromBooking()` runs once per request, acceptable overhead - -## Timeline Estimate - -- **Phase 1-3** (DTO creation): 2 hours -- **Phase 4-5** (Forms & Controllers): 2 hours -- **Phase 6** (DataProcessor refactor): 3 hours -- **Phase 7-10** (Service layer & templates): 2 hours -- **Phase 11** (Booking model methods): 1 hour -- **Testing & Fixes**: 3 hours - -**Total**: ~13 hours - -## Notes - -- Current code has accumulated technical debt from multiple iterations -- Clean refactor will improve long-term maintainability -- Most existing logic can be reused (field handlers, conditions, validators) -- Main work is in `fromBooking()` extraction logic and DataProcessor simplification \ No newline at end of file