# 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