Files
myep/docs/INSURANCE_EDIT_IMPLEMENTATION.md
T

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:

  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

// 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:

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::$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:

// 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)

  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)

  • Add $insurances property to Booking model (line 45)
  • Add $mapping and $individualPrice properties to Insurance model (lines 101-106)
  • Create BookingInsurancesParser for parsing booking insurance XML
  • Integrate parser into BookingParser constructor 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 $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)

  • 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::$insurances exists 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->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)

  • 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 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:

// 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

  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