wip: insurance booking in edit flow
This commit is contained in:
@@ -0,0 +1,693 @@
|
|||||||
|
# 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
|
||||||
@@ -42,6 +42,7 @@ class Booking
|
|||||||
public array $pickupsOutbound = [];
|
public array $pickupsOutbound = [];
|
||||||
public array $pickupsInbound = [];
|
public array $pickupsInbound = [];
|
||||||
public array $surcharges = [];
|
public array $surcharges = [];
|
||||||
|
public array $insurances = [];
|
||||||
public ?int $invoiceNumber = null;
|
public ?int $invoiceNumber = null;
|
||||||
public ?float $totalPrice = null;
|
public ?float $totalPrice = null;
|
||||||
public ?string $travelInfoUrl = null;
|
public ?string $travelInfoUrl = null;
|
||||||
@@ -150,6 +151,24 @@ class Booking
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
{
|
||||||
|
foreach ($this->insurances as $insurance) {
|
||||||
|
if (in_array($participantIndex, $insurance->mapping ?? [], true)) {
|
||||||
|
return $insurance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves ski pass for a specific participant.
|
* Retrieves ski pass for a specific participant.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -95,6 +95,16 @@ class Insurance
|
|||||||
#[Groups(['api:single', 'api:list'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public array $containedInsuranceIds = [];
|
public array $containedInsuranceIds = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<int> Participant indices assigned to this insurance (booking data only)
|
||||||
|
*/
|
||||||
|
public array $mapping = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<int, float> Individual prices per participant index (booking data only)
|
||||||
|
*/
|
||||||
|
public array $individualPrice = [];
|
||||||
|
|
||||||
public function __toString(): string
|
public function __toString(): string
|
||||||
{
|
{
|
||||||
return (string) $this->id;
|
return (string) $this->id;
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\BusProNet\XmlParser;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Insurance;
|
||||||
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses insurance data from booking XML responses.
|
||||||
|
*
|
||||||
|
* This parser handles the simplified insurance structure found in booking data,
|
||||||
|
* which includes participant mappings and individual pricing.
|
||||||
|
*/
|
||||||
|
class BookingInsurancesParser extends AbstractParser
|
||||||
|
{
|
||||||
|
public function parse(Crawler $result): array
|
||||||
|
{
|
||||||
|
$insurances = [];
|
||||||
|
|
||||||
|
$result->each(function (Crawler $node) use (&$insurances) {
|
||||||
|
$insurance = new Insurance();
|
||||||
|
$insurance->id = (int) $node->attr('idversicherung');
|
||||||
|
$insurance->label = $node->attr('bezeichnung');
|
||||||
|
$insurance->price = $this->stringToFloat($node->attr('gesamtpreis'));
|
||||||
|
|
||||||
|
// Parse participant mapping (zuordnung attribute)
|
||||||
|
$mapping = $this->stringToArray($node->attr('zuordnung'));
|
||||||
|
$insurance->mapping = array_map(function ($index) {
|
||||||
|
return (int) $index - 1;
|
||||||
|
}, $mapping);
|
||||||
|
|
||||||
|
// Parse individual prices per participant
|
||||||
|
$individualPrices = array_map(
|
||||||
|
function ($price) {
|
||||||
|
return $this->stringToFloat($price);
|
||||||
|
},
|
||||||
|
$this->stringToArray($node->attr('einzelpreis', ''), '/')
|
||||||
|
);
|
||||||
|
$insurance->individualPrice = array_combine($insurance->mapping, $individualPrices);
|
||||||
|
|
||||||
|
$insurances[$insurance->id] = $insurance;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $insurances;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ class BookingParser extends AbstractParser
|
|||||||
private readonly RoomsParser $roomsParser;
|
private readonly RoomsParser $roomsParser;
|
||||||
private readonly PickupsParser $pickupsParser;
|
private readonly PickupsParser $pickupsParser;
|
||||||
private readonly SurchargesParser $surchargesParser;
|
private readonly SurchargesParser $surchargesParser;
|
||||||
|
private readonly BookingInsurancesParser $insurancesParser;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -23,6 +24,7 @@ class BookingParser extends AbstractParser
|
|||||||
$this->roomsParser = new RoomsParser();
|
$this->roomsParser = new RoomsParser();
|
||||||
$this->pickupsParser = new PickupsParser();
|
$this->pickupsParser = new PickupsParser();
|
||||||
$this->surchargesParser = new SurchargesParser();
|
$this->surchargesParser = new SurchargesParser();
|
||||||
|
$this->insurancesParser = new BookingInsurancesParser();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function parse(Crawler $node): Booking
|
public function parse(Crawler $node): Booking
|
||||||
@@ -101,6 +103,11 @@ class BookingParser extends AbstractParser
|
|||||||
$booking->surcharges = $this->surchargesParser->parse($surchargesData);
|
$booking->surcharges = $this->surchargesParser->parse($surchargesData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$insurancesData = $node->filterXPath('//versicherungen/versicherung');
|
||||||
|
if (0 < $insurancesData->count()) {
|
||||||
|
$booking->insurances = $this->insurancesParser->parse($insurancesData);
|
||||||
|
}
|
||||||
|
|
||||||
return $booking;
|
return $booking;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ class BookingEditDto implements BookingDtoInterface
|
|||||||
$pickup = $booking->getPickupForParticipant($index);
|
$pickup = $booking->getPickupForParticipant($index);
|
||||||
$participantData->pickup = $pickup;
|
$participantData->pickup = $pickup;
|
||||||
|
|
||||||
|
// Insurance - get insurance for participant
|
||||||
|
$insurance = $booking->getInsuranceForParticipant($index);
|
||||||
|
$participantData->insurance = $insurance;
|
||||||
|
|
||||||
// Room assignment - extract from booking room mappings
|
// Room assignment - extract from booking room mappings
|
||||||
$room = $booking->getRoomForParticipant($index);
|
$room = $booking->getRoomForParticipant($index);
|
||||||
$participantData->assignedRoomId = $room?->id;
|
$participantData->assignedRoomId = $room?->id;
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form\Service\Condition;
|
||||||
|
|
||||||
|
use App\Form\Model\BookingDtoInterface;
|
||||||
|
use App\Form\Model\BookingEditDto;
|
||||||
|
use App\Form\Service\Contract\FieldConditionInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Condition that determines if insurance fields are editable based on booking and travel dates.
|
||||||
|
*
|
||||||
|
* Implements time-based mutability constraints for insurance booking in the edit flow:
|
||||||
|
* - Standard case: Insurance editable up to 30 days before travel date
|
||||||
|
* - Late booking case: If booking made < 30 days before travel, insurance editable up to 3 days after booking date
|
||||||
|
*/
|
||||||
|
class InsuranceMutabilityCondition implements FieldConditionInterface
|
||||||
|
{
|
||||||
|
private const DAYS_BEFORE_TRAVEL_THRESHOLD = 30;
|
||||||
|
private const DAYS_AFTER_BOOKING_THRESHOLD = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates whether the insurance field should be readonly.
|
||||||
|
*
|
||||||
|
* Returns true if the field should be readonly (locked), false if editable.
|
||||||
|
*
|
||||||
|
* Logic:
|
||||||
|
* 1. Always editable in create flow
|
||||||
|
* 2. In edit flow, check standard case: editable if >= 30 days before travel
|
||||||
|
* 3. In edit flow, check late booking case: editable if within 3 days of booking date
|
||||||
|
* 4. Otherwise: readonly
|
||||||
|
*
|
||||||
|
* @param BookingDtoInterface $bookingDto The current booking data
|
||||||
|
* @param int $participantIndex The participant index (unused for insurance mutability)
|
||||||
|
* @param array<string, mixed> $formData Current form data (unused for insurance mutability)
|
||||||
|
*
|
||||||
|
* @return bool True if field should be readonly, false if editable
|
||||||
|
*/
|
||||||
|
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||||
|
{
|
||||||
|
// Only apply mutability constraints to edit flow
|
||||||
|
if (!$bookingDto instanceof BookingEditDto) {
|
||||||
|
return false; // Always editable in create flow
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = \Carbon\Carbon::now()->toDateTimeImmutable();
|
||||||
|
$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 >= self::DAYS_BEFORE_TRAVEL_THRESHOLD) {
|
||||||
|
return false; // Editable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Late booking case: Check if booking was made < 30 days before travel
|
||||||
|
$daysFromBookingToTravel = $bookingDate->diff($travelDate)->days;
|
||||||
|
$wasLateBooking = $daysFromBookingToTravel < self::DAYS_BEFORE_TRAVEL_THRESHOLD;
|
||||||
|
|
||||||
|
if ($wasLateBooking) {
|
||||||
|
// Editable if within 3 days of booking date
|
||||||
|
$daysSinceBooking = $bookingDate->diff($now)->days;
|
||||||
|
|
||||||
|
return $daysSinceBooking > self::DAYS_AFTER_BOOKING_THRESHOLD; // True = readonly (past threshold)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: Not editable (readonly)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns field names that trigger re-evaluation of this condition.
|
||||||
|
*
|
||||||
|
* Insurance mutability is based on dates, not other form fields,
|
||||||
|
* so no field dependencies are needed.
|
||||||
|
*
|
||||||
|
* @return string[] Empty array - no field dependencies
|
||||||
|
*/
|
||||||
|
public function getDependentFields(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a human-readable description of this condition.
|
||||||
|
*
|
||||||
|
* @return string Description of the insurance mutability logic
|
||||||
|
*/
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
'Insurance is not editable (>= %d days before travel or > %d days after late booking)',
|
||||||
|
self::DAYS_BEFORE_TRAVEL_THRESHOLD,
|
||||||
|
self::DAYS_AFTER_BOOKING_THRESHOLD
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Tests\BusProNet\Model;
|
namespace App\Tests\BusProNet\Model;
|
||||||
|
|
||||||
use App\BusProNet\Model\Booking;
|
use App\BusProNet\Model\Booking;
|
||||||
|
use App\BusProNet\Model\Insurance;
|
||||||
use App\BusProNet\Model\Service;
|
use App\BusProNet\Model\Service;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
@@ -69,4 +70,69 @@ class BookingTest extends TestCase
|
|||||||
$result = $booking->getSkiPassForParticipant(0);
|
$result = $booking->getSkiPassForParticipant(0);
|
||||||
$this->assertNull($result);
|
$this->assertNull($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testGetInsuranceForParticipantReturnsCorrectInsurance(): void
|
||||||
|
{
|
||||||
|
$booking = new Booking();
|
||||||
|
|
||||||
|
// Create insurance for participants 0 and 2
|
||||||
|
$insurance1 = new Insurance();
|
||||||
|
$insurance1->id = 100;
|
||||||
|
$insurance1->label = 'Reiseschutz Platin';
|
||||||
|
$insurance1->mapping = [0, 2];
|
||||||
|
|
||||||
|
// Create insurance for participant 1
|
||||||
|
$insurance2 = new Insurance();
|
||||||
|
$insurance2->id = 200;
|
||||||
|
$insurance2->label = 'Reiseschutz Gold';
|
||||||
|
$insurance2->mapping = [1];
|
||||||
|
|
||||||
|
$booking->insurances = [
|
||||||
|
100 => $insurance1,
|
||||||
|
200 => $insurance2,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Test: participant 0 should get insurance1
|
||||||
|
$result = $booking->getInsuranceForParticipant(0);
|
||||||
|
$this->assertSame($insurance1, $result);
|
||||||
|
$this->assertEquals(100, $result->id);
|
||||||
|
|
||||||
|
// Test: participant 1 should get insurance2
|
||||||
|
$result = $booking->getInsuranceForParticipant(1);
|
||||||
|
$this->assertSame($insurance2, $result);
|
||||||
|
$this->assertEquals(200, $result->id);
|
||||||
|
|
||||||
|
// Test: participant 2 should get insurance1
|
||||||
|
$result = $booking->getInsuranceForParticipant(2);
|
||||||
|
$this->assertSame($insurance1, $result);
|
||||||
|
|
||||||
|
// Test: participant 3 should get null (no insurance assigned)
|
||||||
|
$result = $booking->getInsuranceForParticipant(3);
|
||||||
|
$this->assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetInsuranceForParticipantReturnsNullWhenNoInsurances(): void
|
||||||
|
{
|
||||||
|
$booking = new Booking();
|
||||||
|
$booking->insurances = [];
|
||||||
|
|
||||||
|
$result = $booking->getInsuranceForParticipant(0);
|
||||||
|
$this->assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetInsuranceForParticipantHandlesEmptyMapping(): void
|
||||||
|
{
|
||||||
|
$booking = new Booking();
|
||||||
|
|
||||||
|
// Create insurance with empty mapping
|
||||||
|
$insurance = new Insurance();
|
||||||
|
$insurance->id = 100;
|
||||||
|
$insurance->label = 'Reiseschutz';
|
||||||
|
$insurance->mapping = [];
|
||||||
|
|
||||||
|
$booking->insurances = [100 => $insurance];
|
||||||
|
|
||||||
|
$result = $booking->getInsuranceForParticipant(0);
|
||||||
|
$this->assertNull($result, 'Should return null when mapping is empty');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\BusProNet\XmlParser;
|
||||||
|
|
||||||
|
use App\BusProNet\XmlParser\BookingInsurancesParser;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
|
|
||||||
|
class BookingInsurancesParserTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testParseInsurancesFromBookingXml(): void
|
||||||
|
{
|
||||||
|
$xml = <<<'XML'
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ergebnis>
|
||||||
|
<versicherungen>
|
||||||
|
<versicherung idversicherung="177240" bezeichnung="Reiseschutz Platin Auto/Bahn/Bus (Europa)" anzahl="1"
|
||||||
|
zuordnung="1" gesamtpreis="45,00" einzelpreis="45,00"></versicherung>
|
||||||
|
</versicherungen>
|
||||||
|
</ergebnis>
|
||||||
|
XML;
|
||||||
|
|
||||||
|
$crawler = new Crawler($xml);
|
||||||
|
$insurancesData = $crawler->filterXPath('//versicherungen/versicherung');
|
||||||
|
|
||||||
|
$parser = new BookingInsurancesParser();
|
||||||
|
$insurances = $parser->parse($insurancesData);
|
||||||
|
|
||||||
|
$this->assertCount(1, $insurances);
|
||||||
|
$this->assertArrayHasKey(177240, $insurances);
|
||||||
|
|
||||||
|
$insurance = $insurances[177240];
|
||||||
|
$this->assertEquals(177240, $insurance->id);
|
||||||
|
$this->assertEquals('Reiseschutz Platin Auto/Bahn/Bus (Europa)', $insurance->label);
|
||||||
|
$this->assertEquals(45.0, $insurance->price);
|
||||||
|
$this->assertEquals([0], $insurance->mapping); // zuordnung="1" maps to participant index 0
|
||||||
|
$this->assertEquals([0 => 45.0], $insurance->individualPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParseMultipleInsurances(): void
|
||||||
|
{
|
||||||
|
$xml = <<<'XML'
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ergebnis>
|
||||||
|
<versicherungen>
|
||||||
|
<versicherung idversicherung="100" bezeichnung="Insurance A" anzahl="1"
|
||||||
|
zuordnung="1" gesamtpreis="45,00" einzelpreis="45,00"></versicherung>
|
||||||
|
<versicherung idversicherung="200" bezeichnung="Insurance B" anzahl="1"
|
||||||
|
zuordnung="2" gesamtpreis="30,00" einzelpreis="30,00"></versicherung>
|
||||||
|
</versicherungen>
|
||||||
|
</ergebnis>
|
||||||
|
XML;
|
||||||
|
|
||||||
|
$crawler = new Crawler($xml);
|
||||||
|
$insurancesData = $crawler->filterXPath('//versicherungen/versicherung');
|
||||||
|
|
||||||
|
$parser = new BookingInsurancesParser();
|
||||||
|
$insurances = $parser->parse($insurancesData);
|
||||||
|
|
||||||
|
$this->assertCount(2, $insurances);
|
||||||
|
|
||||||
|
// First insurance
|
||||||
|
$insurance1 = $insurances[100];
|
||||||
|
$this->assertEquals(100, $insurance1->id);
|
||||||
|
$this->assertEquals('Insurance A', $insurance1->label);
|
||||||
|
$this->assertEquals(45.0, $insurance1->price);
|
||||||
|
$this->assertEquals([0], $insurance1->mapping); // zuordnung="1" maps to index 0
|
||||||
|
$this->assertEquals([0 => 45.0], $insurance1->individualPrice);
|
||||||
|
|
||||||
|
// Second insurance
|
||||||
|
$insurance2 = $insurances[200];
|
||||||
|
$this->assertEquals(200, $insurance2->id);
|
||||||
|
$this->assertEquals('Insurance B', $insurance2->label);
|
||||||
|
$this->assertEquals(30.0, $insurance2->price);
|
||||||
|
$this->assertEquals([1], $insurance2->mapping); // zuordnung="2" maps to index 1
|
||||||
|
$this->assertEquals([1 => 30.0], $insurance2->individualPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParseEmptyInsurancesReturnsEmptyArray(): void
|
||||||
|
{
|
||||||
|
$xml = <<<'XML'
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ergebnis>
|
||||||
|
<versicherungen>
|
||||||
|
</versicherungen>
|
||||||
|
</ergebnis>
|
||||||
|
XML;
|
||||||
|
|
||||||
|
$crawler = new Crawler($xml);
|
||||||
|
$insurancesData = $crawler->filterXPath('//versicherungen/versicherung');
|
||||||
|
|
||||||
|
$parser = new BookingInsurancesParser();
|
||||||
|
$insurances = $parser->parse($insurancesData);
|
||||||
|
|
||||||
|
$this->assertCount(0, $insurances);
|
||||||
|
$this->assertIsArray($insurances);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Form\Service\Condition;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Booking;
|
||||||
|
use App\BusProNet\Model\Travel;
|
||||||
|
use App\Form\Model\BookingCreateDto;
|
||||||
|
use App\Form\Model\BookingEditDto;
|
||||||
|
use App\Form\Service\Condition\InsuranceMutabilityCondition;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class InsuranceMutabilityConditionTest extends TestCase
|
||||||
|
{
|
||||||
|
private InsuranceMutabilityCondition $condition;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->condition = new InsuranceMutabilityCondition();
|
||||||
|
|
||||||
|
// Set a fixed "now" for time-dependent tests
|
||||||
|
Carbon::setTestNow('2025-01-15 12:00:00');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
// Reset Carbon's test time after each test
|
||||||
|
Carbon::setTestNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAlwaysEditableInCreateFlow(): void
|
||||||
|
{
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->dateFrom = new \DateTimeImmutable('+10 days');
|
||||||
|
|
||||||
|
$bookingDto = new BookingCreateDto($travel, 123);
|
||||||
|
|
||||||
|
$result = $this->condition->evaluate($bookingDto, 0, []);
|
||||||
|
|
||||||
|
$this->assertFalse($result, 'Insurance should always be editable in create flow');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEditableWhen40DaysBeforeTravel(): void
|
||||||
|
{
|
||||||
|
// Now is 2025-01-15, travel is 40 days later: 2025-02-24
|
||||||
|
$travelDate = new \DateTimeImmutable('2025-02-24 12:00:00');
|
||||||
|
$bookingDate = new \DateTimeImmutable('2025-01-05 12:00:00');
|
||||||
|
|
||||||
|
$bookingDto = $this->createEditDto($travelDate, $bookingDate);
|
||||||
|
|
||||||
|
$result = $this->condition->evaluate($bookingDto, 0, []);
|
||||||
|
|
||||||
|
$this->assertFalse($result, 'Insurance should be editable when 40 days before travel (standard case)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEditableWhenExactly30DaysBeforeTravel(): void
|
||||||
|
{
|
||||||
|
// Now is fixed at 2025-01-15 12:00:00
|
||||||
|
// Travel date exactly 30 days later: 2025-02-14 12:00:00
|
||||||
|
$travelDate = new \DateTimeImmutable('2025-02-14 12:00:00');
|
||||||
|
$bookingDate = new \DateTimeImmutable('2024-12-20 12:00:00');
|
||||||
|
|
||||||
|
$bookingDto = $this->createEditDto($travelDate, $bookingDate);
|
||||||
|
|
||||||
|
$result = $this->condition->evaluate($bookingDto, 0, []);
|
||||||
|
|
||||||
|
$this->assertFalse($result, 'Insurance should be editable when exactly 30 days before travel (edge case)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReadonlyWhen20DaysBeforeTravel(): void
|
||||||
|
{
|
||||||
|
// Now is 2025-01-15, travel is 20 days later: 2025-02-04
|
||||||
|
$travelDate = new \DateTimeImmutable('2025-02-04 12:00:00');
|
||||||
|
$bookingDate = new \DateTimeImmutable('2025-01-05 12:00:00');
|
||||||
|
|
||||||
|
$bookingDto = $this->createEditDto($travelDate, $bookingDate);
|
||||||
|
|
||||||
|
$result = $this->condition->evaluate($bookingDto, 0, []);
|
||||||
|
|
||||||
|
$this->assertTrue($result, 'Insurance should be readonly when 20 days before travel (standard case)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEditableWhen2DaysAfterLateBooking(): void
|
||||||
|
{
|
||||||
|
// Now is 2025-01-15, booking was 2 days ago: 2025-01-13
|
||||||
|
// Travel is 15 days after booking: 2025-01-28 (late booking)
|
||||||
|
$bookingDate = new \DateTimeImmutable('2025-01-13 12:00:00');
|
||||||
|
$travelDate = new \DateTimeImmutable('2025-01-28 12:00:00');
|
||||||
|
|
||||||
|
$bookingDto = $this->createEditDto($travelDate, $bookingDate);
|
||||||
|
|
||||||
|
$result = $this->condition->evaluate($bookingDto, 0, []);
|
||||||
|
|
||||||
|
$this->assertFalse($result, 'Insurance should be editable 2 days after late booking');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEditableWhenExactly3DaysAfterLateBooking(): void
|
||||||
|
{
|
||||||
|
// Now is 2025-01-15, booking was exactly 3 days ago: 2025-01-12
|
||||||
|
// Travel is 15 days after booking: 2025-01-27 (late booking)
|
||||||
|
$bookingDate = new \DateTimeImmutable('2025-01-12 12:00:00');
|
||||||
|
$travelDate = new \DateTimeImmutable('2025-01-27 12:00:00');
|
||||||
|
|
||||||
|
$bookingDto = $this->createEditDto($travelDate, $bookingDate);
|
||||||
|
|
||||||
|
$result = $this->condition->evaluate($bookingDto, 0, []);
|
||||||
|
|
||||||
|
$this->assertFalse($result, 'Insurance should be editable exactly 3 days after late booking (edge case)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReadonlyWhen5DaysAfterLateBooking(): void
|
||||||
|
{
|
||||||
|
// Now is 2025-01-15, booking was 5 days ago: 2025-01-10
|
||||||
|
// Travel is 15 days after booking: 2025-01-25 (late booking)
|
||||||
|
$bookingDate = new \DateTimeImmutable('2025-01-10 12:00:00');
|
||||||
|
$travelDate = new \DateTimeImmutable('2025-01-25 12:00:00');
|
||||||
|
|
||||||
|
$bookingDto = $this->createEditDto($travelDate, $bookingDate);
|
||||||
|
|
||||||
|
$result = $this->condition->evaluate($bookingDto, 0, []);
|
||||||
|
|
||||||
|
$this->assertTrue($result, 'Insurance should be readonly 5 days after late booking');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReadonlyWhenTravelDatePassed(): void
|
||||||
|
{
|
||||||
|
// Now is 2025-01-15, travel was 5 days ago: 2025-01-10
|
||||||
|
$travelDate = new \DateTimeImmutable('2025-01-10 12:00:00');
|
||||||
|
$bookingDate = new \DateTimeImmutable('2024-12-01 12:00:00');
|
||||||
|
|
||||||
|
$bookingDto = $this->createEditDto($travelDate, $bookingDate);
|
||||||
|
|
||||||
|
$result = $this->condition->evaluate($bookingDto, 0, []);
|
||||||
|
|
||||||
|
$this->assertTrue($result, 'Insurance should be readonly when travel date has passed');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetDependentFieldsReturnsEmptyArray(): void
|
||||||
|
{
|
||||||
|
$result = $this->condition->getDependentFields();
|
||||||
|
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertEmpty($result, 'Insurance mutability has no field dependencies');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetDescriptionReturnsString(): void
|
||||||
|
{
|
||||||
|
$result = $this->condition->getDescription();
|
||||||
|
|
||||||
|
$this->assertIsString($result);
|
||||||
|
$this->assertStringContainsString('30', $result);
|
||||||
|
$this->assertStringContainsString('3', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createEditDto(\DateTimeImmutable $travelDate, \DateTimeImmutable $bookingDate): BookingEditDto
|
||||||
|
{
|
||||||
|
$booking = new Booking();
|
||||||
|
$booking->bookingDate = $bookingDate;
|
||||||
|
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->dateFrom = $travelDate;
|
||||||
|
|
||||||
|
return new BookingEditDto($booking, $travel);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user