24 KiB
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:
- ✅ Phase 0 Added: Prerequisites phase completed - insurance parsing infrastructure was missing and has been implemented
- ⚠️ Condition Signature Corrected:
FieldConditionInterfaceusesevaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData)NOT the old signature shown in original plan - ⚠️ Phase 5 May Not Be Needed: No hardcoded HTMX routes found in current codebase - needs verification
- ⚠️ 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
- Standard Case: Insurance editable up to 30 days before travel date
- Late Booking Case: When booking date is < 30 days before travel, insurance editable up to 3 days after booking date
Time-Based Logic
// 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
trueif field should be readonly (locked) - Returns
falseif field should be editable
Key Logic:
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:
// 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:
/**
* 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::$insurancesproperty exists and is populated by parser - If missing, add to
BookingParsersimilar 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:
// 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:
'attr' => [
'data-participant-form-target' => 'insuranceInput',
'hx-post' => '/booking/create/refresh-participant', // HARDCODED!
'hx-trigger' => 'change',
],
Solution:
// 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:
{# 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:
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:
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:
/**
* 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:
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:
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:
// 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:
## 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)
-
ParticipantInsuranceFieldHandler (200+ lines)
- Already works with
BookingDtoInterface - Auto-reassignment logic for price tier changes
- Bulk insurance support for dependent participants
- User notification system
- Already works with
-
InsuranceMatchingService (300+ lines)
- Eligibility filtering by age, price, family status
- Price tier reassignment logic
- Age constraint evaluation at travel date
-
ParticipantBulkInsuranceFieldHandler (100+ lines)
- Batch insurance assignment to all participants
- Price tier adjustment per participant
-
InsuranceChoiceType (80+ lines)
- Tooltip support with product info URLs
- Label formatting with pricing
-
InsuranceParser (150+ lines)
- 3-pass parsing for package family detection
- Complementary insurance handling
New/Modified Components (~135 lines total)
- InsuranceMutabilityCondition (~50 lines) - NEW
- Booking::getInsuranceForParticipant() (~15 lines) - NEW
- BookingEditDto insurance population (~10 lines) - MODIFIED
- EditFieldStateProvider (~5 lines) - MODIFIED
- ParticipantFieldOptionsProvider HTMX routes (~10 lines) - MODIFIED
- edit.html.twig (~3 lines) - MODIFIED
- 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
BookingParserbefore 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)
- Add
$insurancesproperty toBookingmodel (line 45) - Add
$mappingand$individualPriceproperties toInsurancemodel (lines 101-106) - Create
BookingInsurancesParserfor parsing booking insurance XML - Integrate parser into
BookingParserconstructor and parse method - Write comprehensive tests (3 tests, 20 assertions - all passing)
- Apply php-cs-fixer to all modified files
- Verify insurance data flow from XML → Parser → Booking model
Files Modified:
src/BusProNet/Model/Booking.php- Added$insurancespropertysrc/BusProNet/Model/Insurance.php- Added$mappingand$individualPricepropertiessrc/BusProNet/XmlParser/BookingInsurancesParser.php- NEW parsersrc/BusProNet/XmlParser/BookingParser.php- Integrated insurance parsingtests/BusProNet/XmlParser/BookingInsurancesParserTest.php- NEW test file
Phase 1: Mutability Condition ✅ COMPLETED (2025-10-07)
- Create
InsuranceMutabilityCondition.php - IMPORTANT: Use correct signature:
evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool - Implement standard case logic (30 days before travel)
- Implement late booking case logic (3 days after booking)
- Add comprehensive PHPDoc
- Apply php-cs-fixer
- Write unit tests (10 test cases, 13 assertions - all passing)
- 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)
- Verify
Booking::$insurancesexists and is populated (DONE in Phase 0) - Add insurance extraction to
BookingEditDto::fromBooking() - 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->insuranceproperty - Positioned after pickup handling, before room assignment
- Follows same pattern as other participant data extraction
Phase 3: Helper Method ✅ COMPLETED (2025-10-07)
- Create
Booking::getInsuranceForParticipant() - Add PHPDoc with examples
- Apply php-cs-fixer
- Write unit tests (3 test cases, 14 assertions - all passing)
Files Created/Modified:
src/BusProNet/Model/Booking.php(lines 154-170) - NEW helper methodtests/BusProNet/Model/BookingTest.php(lines 74-137) - NEW tests
Implementation Details:
- Searches through
$insurancesarray for participant mapping - Returns
Insurance|nullbased on participant index - Handles empty mapping arrays with
?? []operator - Follows same pattern as
getPickupForParticipant()andgetSkiPassForParticipant() - 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:
// 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():
// 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
-
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
-
Data Persistence:
- Edit insurance selection
- Submit form
- Verify API payload includes insurance data
- Verify insurance mapping correct in payload
-
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
DateTimeImmutablefor 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
BookingEditDto::fromBooking()populates insurance from API dataEditFieldStateProviderapplies mutability condition- Template renders field with readonly state if locked
- On submit,
BookingDataProcessorrebuilds insurance mappings - 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