wip: modernized edit flow
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
# Booking DTO Unification Plan
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The current implementation uses two separate DTOs (`BookingCreateDto` and `BookingEditDto`) with fundamentally different data structures:
|
||||
|
||||
- **Create mode**: Services stored in participant DTOs (e.g., `$participant->insurance`, `$participant->courses`)
|
||||
- **Edit mode**: Services stored in the booking object (e.g., `$booking->insurances`, `$booking->additionalServices`)
|
||||
|
||||
This divergence causes multiple issues:
|
||||
1. Pricing and summary calculations fail in edit mode
|
||||
2. Field handlers need complex mode-specific logic
|
||||
3. Data processor needs separate handling for create vs edit
|
||||
4. Code duplication and increased complexity
|
||||
5. Bugs due to assumptions about data structure
|
||||
|
||||
## Solution: Unified BookingDto
|
||||
|
||||
Create a single `BookingDto` class that stores all service selections in participant DTOs for BOTH create and edit modes. Different modes are handled through different instantiation methods.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Create Unified BookingDto Class
|
||||
|
||||
**File**: `src/Form/Model/BookingDto.php`
|
||||
|
||||
**Changes**:
|
||||
- Merge `BookingCreateDto` and `BookingEditDto` into single `BookingDto` class
|
||||
- Keep all participant-based service storage (insurance, courses, skiPass, rentals, transportation, etc.)
|
||||
- Add `mode` property (MODE_CREATE or MODE_EDIT)
|
||||
- Add `booking` property (null in create mode, Booking object in edit mode for metadata only)
|
||||
- Implement `BookingDtoInterface` interface
|
||||
|
||||
**Constructor signatures**:
|
||||
```php
|
||||
// Create mode
|
||||
public function __construct(Travel $travel, int $agencyId)
|
||||
|
||||
// Edit mode (static factory)
|
||||
public static function fromBooking(Booking $booking, Travel $travel): static
|
||||
```
|
||||
|
||||
**Key method**:
|
||||
```php
|
||||
public function getMode(): string
|
||||
{
|
||||
return null !== $this->booking ? self::MODE_EDIT : self::MODE_CREATE;
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: Update BookingDto::fromBooking() for Edit Mode
|
||||
|
||||
**File**: `src/Form/Model/BookingDto.php`
|
||||
|
||||
**Responsibilities**:
|
||||
1. Extract services from booking object and assign to participant DTOs
|
||||
2. Map insurances: `$participant->insurance = $booking->getInsuranceForParticipant($index)`
|
||||
3. Map services: `$participant->courses = $booking->getAdditionalServicesForParticipantByGroup($index, 'COURSES')`
|
||||
4. Map transportation: `$participant->transportationOutbound = $booking->getTransportationForParticipant($index, 'OUTBOUND')`
|
||||
5. Map pickup locations
|
||||
6. Map room assignments
|
||||
7. Set body dimensions from applicant for participant 0
|
||||
8. Set parking from form data (need to check if stored in booking)
|
||||
|
||||
**Data extraction methods needed**:
|
||||
- `Booking::getInsuranceForParticipant(int $index): ?Insurance`
|
||||
- Existing methods for other services can be reused
|
||||
|
||||
### Phase 3: Update ParticipantDto
|
||||
|
||||
**File**: `src/Form/Model/ParticipantDto.php`
|
||||
|
||||
**Changes**:
|
||||
- Already has all necessary service properties
|
||||
- Ensure `fromPersonalData()` copies body dimensions correctly
|
||||
- No structural changes needed
|
||||
|
||||
### Phase 4: Update Form Types
|
||||
|
||||
**Files to update**:
|
||||
- `src/Form/BookingType.php` → Rename to `BookingType` (generic)
|
||||
- `src/Form/BookingEditType.php` → Delete (use unified BookingType)
|
||||
- `src/Form/BookingParticipantType.php` → Already works with ParticipantDto, should work unchanged
|
||||
|
||||
**Changes**:
|
||||
- Update `BookingType` to use `BookingDto::class` as data_class
|
||||
- Remove mode-specific form type distinction
|
||||
- Pass `edit_mode` option to child forms for field state provider selection
|
||||
|
||||
### Phase 5: Update Controllers
|
||||
|
||||
#### CreateStep2Controller
|
||||
**File**: `src/Controller/Booking/CreateStep2Controller.php`
|
||||
|
||||
**Changes**:
|
||||
- Change `BookingCreateDto` → `BookingDto`
|
||||
- Constructor instantiation remains same
|
||||
- All logic should work unchanged (services already in participant DTOs)
|
||||
|
||||
#### EditController
|
||||
**File**: `src/Controller/Booking/EditController.php`
|
||||
|
||||
**Changes**:
|
||||
- Change `BookingEditDto` → `BookingDto`
|
||||
- Change `BookingEditDto::fromBooking()` → `BookingDto::fromBooking()`
|
||||
- Form type: change `BookingEditType` → `BookingType` with `['edit_mode' => true]` option
|
||||
- All pricing and summary calculations should now work (same DTO structure as create)
|
||||
|
||||
### Phase 6: Update BookingDataProcessor
|
||||
|
||||
**File**: `src/BusProNet/DataProcessor/BookingDataProcessor.php`
|
||||
|
||||
**Major simplification**:
|
||||
|
||||
#### createBookingRequestPayload() (Create flow)
|
||||
- Already works with participant DTOs
|
||||
- Change signature: `BookingDto` instead of `BookingCreateDto`
|
||||
- No other changes needed
|
||||
|
||||
#### createUpdateRequestPayload() (Edit flow)
|
||||
**Current problems**:
|
||||
- Tries to read services from `$bookingDto->booking` object
|
||||
- Complex mapping and resetting logic
|
||||
- `processParticipantServices()` needs to add services to booking data from travel data
|
||||
|
||||
**New approach**:
|
||||
- Services already in participant DTOs (populated by `fromBooking()`)
|
||||
- Can reuse create flow logic almost entirely
|
||||
- Only difference: include `idbuchung` and participant `idadresseperson` in payload
|
||||
|
||||
**Unified approach**:
|
||||
```php
|
||||
public function createPayload(BookingDto $bookingDto, string $type): array
|
||||
{
|
||||
// Apply bulk insurance if enabled
|
||||
$this->applyBulkInsuranceIfActive($bookingDto);
|
||||
|
||||
// Collect service mappings (works same for both modes)
|
||||
$serviceMap = $this->collectServiceMappings($bookingDto);
|
||||
$transportationMap = $this->collectTransportationMappings($bookingDto);
|
||||
$roomMap = $this->collectRoomMappings($bookingDto);
|
||||
$pickupMap = $this->collectPickupMappings($bookingDto);
|
||||
$insuranceMap = $this->collectInsuranceMappings($bookingDto);
|
||||
|
||||
// Build payload based on mode
|
||||
if (BookingDtoInterface::MODE_EDIT === $bookingDto->getMode()) {
|
||||
return $this->buildUpdatePayload($bookingDto, ...maps);
|
||||
} else {
|
||||
return $this->buildCreatePayload($bookingDto, $type, ...maps);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Simplifications**:
|
||||
- Remove `processParticipantServices()` complexity
|
||||
- Remove `resetServiceMappings()`
|
||||
- Remove `removeUnusedServices()`
|
||||
- Direct mapping from participant DTOs to payload
|
||||
|
||||
### Phase 7: Update Field Handlers
|
||||
|
||||
**Files**: All handlers in `src/Form/Service/`
|
||||
|
||||
**Changes needed**:
|
||||
- Handlers already work with participant DTOs
|
||||
- No changes needed (they don't care about mode)
|
||||
- Registry already handles both modes
|
||||
|
||||
### Phase 8: Update Service Layer
|
||||
|
||||
#### BookingService
|
||||
**File**: `src/Service/BookingService.php`
|
||||
|
||||
**Changes**:
|
||||
- Update type hints: `BookingCreateDto|BookingEditDto` → `BookingDto`
|
||||
- `getRoomSummaryAndParticipantCount()` should now work for both modes (same DTO structure)
|
||||
- No logic changes needed
|
||||
|
||||
#### PriceCalculator
|
||||
**File**: `src/Service/PriceCalculator.php`
|
||||
|
||||
**Changes**:
|
||||
- Update type hints to use `BookingDto`
|
||||
- All calculations work with participant DTOs, should work unchanged
|
||||
- Mode detection: `$bookingDto->getMode()` instead of `instanceof` checks
|
||||
|
||||
### Phase 9: Update Field State Providers
|
||||
|
||||
**Files**:
|
||||
- `src/Form/Service/CreateFieldStateProvider.php`
|
||||
- `src/Form/Service/EditFieldStateProvider.php`
|
||||
|
||||
**Changes**:
|
||||
- Already use `BookingDtoInterface`, no changes needed
|
||||
- Continue to be selected based on `edit_mode` form option
|
||||
|
||||
### Phase 10: Update Templates
|
||||
|
||||
**Files**:
|
||||
- `templates/booking/create_step_2.html.twig`
|
||||
- `templates/booking/edit.html.twig`
|
||||
|
||||
**Changes**:
|
||||
- Variable naming: `bookingCreateDto` → `bookingDto`, `bookingEditDto` → `bookingDto`
|
||||
- All logic should work unchanged (both render participant forms)
|
||||
|
||||
### Phase 11: Critical New Method in Booking Model
|
||||
|
||||
**File**: `src/BusProNet/Model/Booking.php`
|
||||
|
||||
**Add method**:
|
||||
```php
|
||||
public function getInsuranceForParticipant(int $index): ?Insurance
|
||||
{
|
||||
foreach ($this->insurances as $insurance) {
|
||||
if (in_array($index, $insurance->mapping)) {
|
||||
return $insurance;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
Similar methods may be needed for other services if not already present.
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Step 1: Create new BookingDto (keep old DTOs)
|
||||
- Create `src/Form/Model/BookingDto.php`
|
||||
- Implement both constructor and `fromBooking()`
|
||||
- Keep `BookingCreateDto` and `BookingEditDto` temporarily
|
||||
|
||||
### Step 2: Update edit flow to use new DTO
|
||||
- Update `EditController` to use `BookingDto`
|
||||
- Update `BookingDataProcessor::createUpdateRequestPayload()` to accept `BookingDto`
|
||||
- Test edit flow thoroughly
|
||||
|
||||
### Step 3: Update create flow to use new DTO
|
||||
- Update `CreateStep2Controller` to use `BookingDto`
|
||||
- Test create flow thoroughly
|
||||
|
||||
### Step 4: Cleanup
|
||||
- Delete `BookingCreateDto.php`
|
||||
- Delete `BookingEditDto.php`
|
||||
- Delete `BookingEditType.php`
|
||||
- Update all remaining type hints
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Edit Flow
|
||||
- [ ] Load existing booking with all service types
|
||||
- [ ] Form displays all current selections correctly
|
||||
- [ ] Summary sidebar shows all services and pricing
|
||||
- [ ] Body dimensions show for applicant
|
||||
- [ ] Change insurance (individual and bulk)
|
||||
- [ ] Change services (courses, skiPass, rentals, board)
|
||||
- [ ] Change transportation
|
||||
- [ ] Submit changes successfully
|
||||
- [ ] API receives correct payload with insurances
|
||||
- [ ] After redirect, summary shows correctly
|
||||
|
||||
### Create Flow
|
||||
- [ ] Select rooms
|
||||
- [ ] Add participants
|
||||
- [ ] Select services for participants
|
||||
- [ ] Select insurances (individual and bulk)
|
||||
- [ ] Pricing calculates correctly
|
||||
- [ ] Summary shows all selections
|
||||
- [ ] Submit creates booking successfully
|
||||
|
||||
### Field State System
|
||||
- [ ] Conditional fields show/hide correctly in both modes
|
||||
- [ ] Readonly fields work in edit mode
|
||||
- [ ] Age-dependent fields work in both modes
|
||||
- [ ] Bulk insurance checkbox works
|
||||
|
||||
### Data Integrity
|
||||
- [ ] No service data loss during mode transitions
|
||||
- [ ] Insurance IDs match correctly
|
||||
- [ ] Participant indices correct in both modes
|
||||
- [ ] Room assignments preserved
|
||||
|
||||
## Benefits of Unified DTO
|
||||
|
||||
1. **Single source of truth**: All service selections in one place (participant DTOs)
|
||||
2. **Simplified calculations**: Pricing, summary, and totals work identically for both modes
|
||||
3. **Reduced complexity**: No mode-specific logic in services and calculators
|
||||
4. **Easier testing**: One DTO structure to test
|
||||
5. **Better maintainability**: Changes to service structure only need updating in one place
|
||||
6. **Consistent field handlers**: Handlers work with same data structure regardless of mode
|
||||
7. **Cleaner templates**: Same rendering logic for both modes
|
||||
|
||||
## Risks and Mitigation
|
||||
|
||||
### Risk: Breaking existing create flow
|
||||
**Mitigation**: Migrate edit flow first, test thoroughly, then migrate create flow
|
||||
|
||||
### Risk: Data loss during form processing
|
||||
**Mitigation**: Extensive logging during migration, compare payloads before/after
|
||||
|
||||
### Risk: Field handler incompatibility
|
||||
**Mitigation**: Field handlers already work with ParticipantDto, minimal changes needed
|
||||
|
||||
### Risk: Performance impact from data extraction
|
||||
**Mitigation**: `fromBooking()` runs once per request, acceptable overhead
|
||||
|
||||
## Timeline Estimate
|
||||
|
||||
- **Phase 1-3** (DTO creation): 2 hours
|
||||
- **Phase 4-5** (Forms & Controllers): 2 hours
|
||||
- **Phase 6** (DataProcessor refactor): 3 hours
|
||||
- **Phase 7-10** (Service layer & templates): 2 hours
|
||||
- **Phase 11** (Booking model methods): 1 hour
|
||||
- **Testing & Fixes**: 3 hours
|
||||
|
||||
**Total**: ~13 hours
|
||||
|
||||
## Notes
|
||||
|
||||
- Current code has accumulated technical debt from multiple iterations
|
||||
- Clean refactor will improve long-term maintainability
|
||||
- Most existing logic can be reused (field handlers, conditions, validators)
|
||||
- Main work is in `fromBooking()` extraction logic and DataProcessor simplification
|
||||
@@ -16,6 +16,7 @@ use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\Traits\ApiClientTrait;
|
||||
use App\BusProNet\XmlParser\ApiResponseParser;
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\BookingEditDto;
|
||||
use App\Form\Model\RegistrationDto;
|
||||
use League\Flysystem\FilesystemException;
|
||||
@@ -48,6 +49,7 @@ class ApiClient
|
||||
private readonly ApiResponseParser $responseParser,
|
||||
private readonly FilesystemOperator $xmlDump,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly BookingDataProcessor $bookingDataProcessor,
|
||||
array $options,
|
||||
) {
|
||||
$this->config = $this->resolveOptions($options);
|
||||
@@ -192,9 +194,9 @@ class ApiClient
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function updateBooking(BookingEditDto $formData, bool $debug = false): Notification|BookingUpdate
|
||||
public function updateBooking(BookingDto $formData, bool $debug = false): Notification|BookingUpdate
|
||||
{
|
||||
$payload = (new BookingDataProcessor())->createUpdateRequestPayload($formData);
|
||||
$payload = $this->bookingDataProcessor->createUpdateRequestPayload($formData);
|
||||
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
@@ -222,7 +224,7 @@ class ApiClient
|
||||
*/
|
||||
public function createBookingInquiry(BookingCreateDto $bookingDto, bool $debug = false): Notification|BookingResponse
|
||||
{
|
||||
$payload = (new BookingDataProcessor())->createBookingRequestPayload($bookingDto, 'Anfrage');
|
||||
$payload = $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, 'Anfrage');
|
||||
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
@@ -249,7 +251,7 @@ class ApiClient
|
||||
*/
|
||||
public function createBooking(BookingCreateDto $bookingDto, bool $debug = false): Notification|BookingResponse
|
||||
{
|
||||
$payload = (new BookingDataProcessor())->createBookingRequestPayload($bookingDto, 'Buchung');
|
||||
$payload = $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, 'Buchung');
|
||||
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
|
||||
@@ -5,9 +5,18 @@ declare(strict_types=1);
|
||||
namespace App\BusProNet\DataProcessor;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Address;
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\Communication;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\Utility\DirectionMapper;
|
||||
use App\Form\Model\BankAccountDto;
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\BookingEditDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Service\InsuranceMatchingService;
|
||||
|
||||
/**
|
||||
* Processes booking form data and converts it into BusProNet API payload format.
|
||||
@@ -20,6 +29,89 @@ use App\Form\Model\BookingEditDto;
|
||||
*/
|
||||
class BookingDataProcessor
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InsuranceMatchingService $insuranceMatchingService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BookingDto from an existing Booking entity (for edit mode).
|
||||
*
|
||||
* Extracts all service selections from the booking entity and assigns them
|
||||
* to participant DTOs, creating a unified data structure identical to create mode.
|
||||
*/
|
||||
public function createBookingDtoFromBooking(Booking $booking, Travel $travel): BookingDto
|
||||
{
|
||||
$dto = new BookingDto($travel, $booking->hotelId);
|
||||
|
||||
$dto->booking = $booking;
|
||||
$dto->agencyId = $booking->agencyId;
|
||||
|
||||
// Map payment ID from API to form payment method
|
||||
$dto->paymentMethod = match ($booking->paymentId) {
|
||||
(string) Constants::PAYMENT_TYPE_ID_DEBIT => Constants::PAYMENT_METHOD_DEBIT,
|
||||
(string) Constants::PAYMENT_TYPE_ID_TRANSFER => Constants::PAYMENT_METHOD_TRANSFER,
|
||||
default => Constants::PAYMENT_METHOD_TRANSFER,
|
||||
};
|
||||
|
||||
if (null !== $booking->bankAccount) {
|
||||
$dto->bankAccount = BankAccountDto::fromBankAccount($booking->bankAccount);
|
||||
}
|
||||
|
||||
foreach ($booking->participants as $index => $participant) {
|
||||
/** @var PersonalData $participant */
|
||||
$participantData = ParticipantDto::fromPersonalData($participant);
|
||||
$participantData->index = $index;
|
||||
|
||||
// Extract service selections from booking and assign to participant DTO
|
||||
$participantData->courses = $booking
|
||||
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_COURSES);
|
||||
|
||||
$participantData->skiPass = $booking->getSkiPassForParticipant($index);
|
||||
|
||||
$participantData->additionalServices = $booking
|
||||
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_ADDITIONAL);
|
||||
|
||||
$participantData->board = $booking
|
||||
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_BOARD);
|
||||
|
||||
$participantData->rentals = $booking
|
||||
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_RENTALS);
|
||||
|
||||
// Rental insurance (single service, not array)
|
||||
$participantData->rentalInsurance = $booking->getRentalInsuranceForParticipant($index);
|
||||
$participantData->rentalInsuranceSelected = null !== $participantData->rentalInsurance;
|
||||
|
||||
// Transportation services
|
||||
$participantData->transportationOutbound = $booking
|
||||
->getTransportationServiceForParticipantAndDirection($index, DirectionMapper::OUTBOUND_BOOKING);
|
||||
|
||||
$participantData->transportationInbound = $booking
|
||||
->getTransportationServiceForParticipantAndDirection($index, DirectionMapper::INBOUND_BOOKING);
|
||||
|
||||
// Pickup location
|
||||
$participantData->pickup = $booking->getPickupForParticipant($index);
|
||||
|
||||
// Parking (stored in form data, not in booking entity - needs special handling)
|
||||
// For now, leave as false - may need to extract from transportation services
|
||||
$participantData->parking = false;
|
||||
|
||||
// License plate (not stored in booking entity)
|
||||
$participantData->licensePlate = null;
|
||||
|
||||
// Insurance
|
||||
$participantData->insurance = $booking->getInsuranceForParticipant($index);
|
||||
|
||||
// Room assignment
|
||||
$room = $booking->getRoomForParticipant($index);
|
||||
$participantData->assignedRoomId = $room?->id;
|
||||
|
||||
$dto->participants[$index] = $participantData;
|
||||
}
|
||||
|
||||
return $dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an update request payload for the BusProNet API from booking form data.
|
||||
*
|
||||
@@ -34,18 +126,29 @@ class BookingDataProcessor
|
||||
* - Adding new services from travel data when participants select them
|
||||
* - Removing services with no participant mappings
|
||||
* - Updating participant personal data from form input
|
||||
* - Synchronizing applicant data with first participant details
|
||||
* - Building the final API payload structure
|
||||
*
|
||||
* @param BookingEditDto|null $formData The booking edit form data containing updated participant and service selections
|
||||
* IMPORTANT: The applicant's address must be preserved from the original booking data stored in the DTO,
|
||||
* as it can be lost during form binding when ParticipantDto stores address references.
|
||||
*
|
||||
* @param BookingDto|null $formData The booking edit form data containing updated participant and service selections
|
||||
*
|
||||
* @return array The structured payload array ready for BusProNet API submission
|
||||
*/
|
||||
public function createUpdateRequestPayload(?BookingEditDto $formData): array
|
||||
public function createUpdateRequestPayload(?BookingDto $formData): array
|
||||
{
|
||||
// Apply bulk insurance if enabled (modifies DTO in place)
|
||||
$this->applyBulkInsuranceIfActive($formData);
|
||||
|
||||
$bookingData = $formData->booking;
|
||||
$travelData = $formData->travel;
|
||||
|
||||
// CRITICAL: The applicant address may have been lost during form binding because ParticipantDto::fromPersonalData()
|
||||
// stores a reference to the address object, and Symfony's form binding can modify it in place.
|
||||
// Since we can't easily restore it here without the original booking, we rely on the EditController
|
||||
// to preserve the original booking's applicant data by fetching fresh data before calling updateBooking().
|
||||
// The applicant data should NEVER be modified in edit mode.
|
||||
|
||||
$this->resetServiceMappings($bookingData);
|
||||
|
||||
foreach ($formData->participants as $participant) {
|
||||
@@ -54,11 +157,10 @@ class BookingDataProcessor
|
||||
|
||||
$this->removeUnusedServices($bookingData);
|
||||
$this->updateParticipantPersonalData($formData->participants, $bookingData);
|
||||
$this->syncApplicantData($bookingData);
|
||||
|
||||
$payload = $this->buildBasePayload($bookingData);
|
||||
$this->addBankAccountToPayload($payload, $bookingData);
|
||||
$this->buildParticipantPayload($payload, $bookingData);
|
||||
$this->buildParticipantPayload($payload, $bookingData, $formData->participants);
|
||||
$this->buildServicesPayload($payload, $bookingData);
|
||||
$this->buildPickupPayload($payload, $bookingData);
|
||||
|
||||
@@ -71,9 +173,9 @@ class BookingDataProcessor
|
||||
* This ensures that service assignments are rebuilt from scratch based on current form selections.
|
||||
* Includes resetting room mappings to allow room reassignments during edit.
|
||||
*
|
||||
* @param object $bookingData The booking data object containing services to reset
|
||||
* @param Booking $bookingData The booking data object containing services to reset
|
||||
*/
|
||||
private function resetServiceMappings(object $bookingData): void
|
||||
private function resetServiceMappings(Booking $bookingData): void
|
||||
{
|
||||
$servicesToReset = [
|
||||
...$bookingData->additionalServices,
|
||||
@@ -81,6 +183,7 @@ class BookingDataProcessor
|
||||
...$bookingData->pickupsOutbound,
|
||||
...$bookingData->pickupsInbound,
|
||||
...$bookingData->rooms,
|
||||
...$bookingData->insurances,
|
||||
];
|
||||
|
||||
foreach ($servicesToReset as $service) {
|
||||
@@ -94,11 +197,11 @@ class BookingDataProcessor
|
||||
* This orchestrator method handles the complete service assignment workflow for one participant,
|
||||
* including additional services, transportation services, pickup locations, and room assignments.
|
||||
*
|
||||
* @param object $participant The participant data from the form
|
||||
* @param object $bookingData The booking data object to update
|
||||
* @param object $travelData The travel data containing available services
|
||||
* @param ParticipantDto $participant The participant data from the form
|
||||
* @param Booking $bookingData The booking data object to update
|
||||
* @param Travel $travelData The travel data containing available services
|
||||
*/
|
||||
private function processParticipantServices(object $participant, object $bookingData, object $travelData): void
|
||||
private function processParticipantServices(ParticipantDto $participant, Booking $bookingData, Travel $travelData): void
|
||||
{
|
||||
if (true === $participant->isCanceled()) {
|
||||
return;
|
||||
@@ -108,21 +211,22 @@ class BookingDataProcessor
|
||||
$this->processTransportationServices($participant, $bookingData, $travelData);
|
||||
$this->processPickupLocations($participant, $bookingData);
|
||||
$this->processRoomAssignment($participant, $bookingData);
|
||||
$this->processInsurance($participant, $bookingData, $travelData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes additional services for a participant.
|
||||
* Collects additional services from a participant for mapping.
|
||||
*
|
||||
* Maps additional services (courses, ski passes, board options, rentals) to the participant.
|
||||
* Adds new services to the booking if they don't already exist and sets individual pricing.
|
||||
* Extracts all additional services (courses, board, rentals, ski pass, rental insurance,
|
||||
* additional services) from a participant into a flat array of Service objects.
|
||||
*
|
||||
* @param object $participant The participant data from the form
|
||||
* @param object $bookingData The booking data object to update
|
||||
* @param object $travelData The travel data containing available services
|
||||
* @param ParticipantDto $participant The participant data
|
||||
*
|
||||
* @return array<Service> Array of services selected by this participant
|
||||
*/
|
||||
private function processAdditionalServices(object $participant, object $bookingData, object $travelData): void
|
||||
private function collectParticipantAdditionalServices(ParticipantDto $participant): array
|
||||
{
|
||||
$servicesToMap = [
|
||||
$services = [
|
||||
...$participant->courses,
|
||||
...$participant->additionalServices,
|
||||
...$participant->board,
|
||||
@@ -131,9 +235,31 @@ class BookingDataProcessor
|
||||
|
||||
// Add ski pass if selected (single service, not an array)
|
||||
if (null !== $participant->skiPass) {
|
||||
$servicesToMap[] = $participant->skiPass;
|
||||
$services[] = $participant->skiPass;
|
||||
}
|
||||
|
||||
// Add rental insurance if selected (single service, not an array)
|
||||
if (null !== $participant->rentalInsurance) {
|
||||
$services[] = $participant->rentalInsurance;
|
||||
}
|
||||
|
||||
return $services;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes additional services for a participant.
|
||||
*
|
||||
* Maps additional services (courses, ski passes, board options, rentals) to the participant.
|
||||
* Adds new services to the booking if they don't already exist and sets individual pricing.
|
||||
*
|
||||
* @param ParticipantDto $participant The participant data from the form
|
||||
* @param Booking $bookingData The booking data object to update
|
||||
* @param Travel $travelData The travel data containing available services
|
||||
*/
|
||||
private function processAdditionalServices(ParticipantDto $participant, Booking $bookingData, Travel $travelData): void
|
||||
{
|
||||
$servicesToMap = $this->collectParticipantAdditionalServices($participant);
|
||||
|
||||
foreach ($servicesToMap as $service) {
|
||||
if (false === isset($bookingData->additionalServices[$service->id])) {
|
||||
$serviceToAdd = $travelData->additionalServices[$service->id] ?? null;
|
||||
@@ -152,11 +278,11 @@ class BookingDataProcessor
|
||||
* Maps transportation services (both directions: to and from destination) to the participant.
|
||||
* Adds new transportation services to the booking if they don't already exist.
|
||||
*
|
||||
* @param object $participant The participant data from the form
|
||||
* @param object $bookingData The booking data object to update
|
||||
* @param object $travelData The travel data containing available services
|
||||
* @param ParticipantDto $participant The participant data from the form
|
||||
* @param Booking $bookingData The booking data object to update
|
||||
* @param Travel $travelData The travel data containing available services
|
||||
*/
|
||||
private function processTransportationServices(object $participant, object $bookingData, object $travelData): void
|
||||
private function processTransportationServices(ParticipantDto $participant, Booking $bookingData, Travel $travelData): void
|
||||
{
|
||||
foreach ([$participant->transportationOutbound, $participant->transportationInbound] as $service) {
|
||||
if (false === isset($bookingData->transportationServices[$service->id])) {
|
||||
@@ -176,10 +302,10 @@ class BookingDataProcessor
|
||||
* Only processes pickup locations for bus transportation services and maps the participant
|
||||
* to their selected pickup location.
|
||||
*
|
||||
* @param object $participant The participant data from the form
|
||||
* @param object $bookingData The booking data object to update
|
||||
* @param ParticipantDto $participant The participant data from the form
|
||||
* @param Booking $bookingData The booking data object to update
|
||||
*/
|
||||
private function processPickupLocations(object $participant, object $bookingData): void
|
||||
private function processPickupLocations(ParticipantDto $participant, Booking $bookingData): void
|
||||
{
|
||||
// Check if either transportation direction is BUS and pickup is selected
|
||||
$hasOutboundBus = null !== $participant->transportationOutbound && 'BUS' === $participant->transportationOutbound->subType;
|
||||
@@ -200,10 +326,10 @@ class BookingDataProcessor
|
||||
* booking edits while maintaining the constraint that participants can only be
|
||||
* assigned to room types that have already been booked.
|
||||
*
|
||||
* @param object $participant The participant data from the form
|
||||
* @param object $bookingData The booking data object to update
|
||||
* @param ParticipantDto $participant The participant data from the form
|
||||
* @param Booking $bookingData The booking data object to update
|
||||
*/
|
||||
private function processRoomAssignment(object $participant, object $bookingData): void
|
||||
private function processRoomAssignment(ParticipantDto $participant, Booking $bookingData): void
|
||||
{
|
||||
if (null === $participant->assignedRoomId) {
|
||||
return;
|
||||
@@ -218,15 +344,48 @@ class BookingDataProcessor
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes travel insurance for a participant.
|
||||
*
|
||||
* Maps insurance to the participant. Adds new insurances to the booking if they don't exist.
|
||||
* Insurance can be either individual or package-based, with automatic price-tier adjustment.
|
||||
*
|
||||
* @param ParticipantDto $participant The participant data from the form
|
||||
* @param Booking $bookingData The booking data object to update
|
||||
* @param Travel $travelData The travel data containing available insurances
|
||||
*/
|
||||
private function processInsurance(ParticipantDto $participant, Booking $bookingData, Travel $travelData): void
|
||||
{
|
||||
if (null === $participant->insurance) {
|
||||
return;
|
||||
}
|
||||
|
||||
$insurance = $participant->insurance;
|
||||
|
||||
if (false === isset($bookingData->insurances[$insurance->id])) {
|
||||
$insuranceToAdd = $travelData->insurances[$insurance->id] ?? null;
|
||||
if (null !== $insuranceToAdd) {
|
||||
$bookingData->insurances[$insurance->id] = $insuranceToAdd;
|
||||
$bookingData->insurances[$insurance->id]->individualPrice[$participant->index] = $insuranceToAdd->price;
|
||||
}
|
||||
}
|
||||
|
||||
// Only add mapping if insurance exists in booking data
|
||||
// This can legitimately be false if the insurance is no longer available in current travel data
|
||||
if (isset($bookingData->insurances[$insurance->id])) {
|
||||
$bookingData->insurances[$insurance->id]->mapping[] = $participant->index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes services and pickups with no participant mappings.
|
||||
*
|
||||
* Cleans up unused services to prevent empty services from being sent to the API.
|
||||
* This includes additional services, transportation services, and pickup locations.
|
||||
* This includes additional services, transportation services, pickup locations, and insurances.
|
||||
*
|
||||
* @param object $bookingData The booking data object to clean up
|
||||
* @param Booking $bookingData The booking data object to clean up
|
||||
*/
|
||||
private function removeUnusedServices(object $bookingData): void
|
||||
private function removeUnusedServices(Booking $bookingData): void
|
||||
{
|
||||
foreach ($bookingData->additionalServices as $service) {
|
||||
if (0 === count($service->mapping)) {
|
||||
@@ -251,6 +410,12 @@ class BookingDataProcessor
|
||||
unset($bookingData->pickupsInbound[$pickup->id]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($bookingData->insurances as $insurance) {
|
||||
if (0 === count($insurance->mapping)) {
|
||||
unset($bookingData->insurances[$insurance->id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,10 +424,13 @@ class BookingDataProcessor
|
||||
* Only processes participants with status 'F' (active/confirmed participants).
|
||||
* Updates all personal data fields and communication information.
|
||||
*
|
||||
* IMPORTANT: The applicant's address must never be modified. This method updates
|
||||
* participant addresses independently to ensure applicant data remains intact.
|
||||
*
|
||||
* @param array $participants The participants array from the form
|
||||
* @param object $bookingData The booking data object to update
|
||||
* @param Booking $bookingData The booking data object to update
|
||||
*/
|
||||
private function updateParticipantPersonalData(array $participants, object $bookingData): void
|
||||
private function updateParticipantPersonalData(array $participants, Booking $bookingData): void
|
||||
{
|
||||
foreach ($participants as $participant) {
|
||||
if ('F' !== $participant->status) {
|
||||
@@ -278,6 +446,19 @@ class BookingDataProcessor
|
||||
$bookingData->participants[$participant->index]->weight = $participant->weight;
|
||||
$bookingData->participants[$participant->index]->shoeSize = $participant->shoeSize;
|
||||
|
||||
// Update address if provided
|
||||
// Create new Address instance to avoid modifying any shared object references
|
||||
if (null !== $participant->address) {
|
||||
$newAddress = new Address();
|
||||
$newAddress->street = $participant->address->street;
|
||||
$newAddress->postCode = $participant->address->postCode;
|
||||
$newAddress->city = $participant->address->city;
|
||||
$newAddress->district = $participant->address->district;
|
||||
$newAddress->country = $participant->address->country;
|
||||
|
||||
$bookingData->participants[$participant->index]->address = $newAddress;
|
||||
}
|
||||
|
||||
if ($participant->email || $participant->mobile) {
|
||||
if (null === $bookingData->participants[$participant->index]->communication) {
|
||||
$bookingData->participants[$participant->index]->communication = new Communication();
|
||||
@@ -288,32 +469,16 @@ class BookingDataProcessor
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes applicant data with first participant's physical characteristics.
|
||||
*
|
||||
* The applicant (booking holder) inherits physical data from the first participant.
|
||||
*
|
||||
* @param object $bookingData The booking data object to update
|
||||
*/
|
||||
private function syncApplicantData(object $bookingData): void
|
||||
{
|
||||
if (false !== $firstParticipant = reset($bookingData->participants)) {
|
||||
$bookingData->applicant->height = $firstParticipant->height;
|
||||
$bookingData->applicant->weight = $firstParticipant->weight;
|
||||
$bookingData->applicant->shoeSize = $firstParticipant->shoeSize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the base API payload structure with booking information.
|
||||
*
|
||||
* Creates the main structure that will be populated with detailed data sections.
|
||||
*
|
||||
* @param object $bookingData The booking data object
|
||||
* @param Booking $bookingData The booking data object
|
||||
*
|
||||
* @return array The base payload structure
|
||||
*/
|
||||
private function buildBasePayload(object $bookingData): array
|
||||
private function buildBasePayload(Booking $bookingData): array
|
||||
{
|
||||
return [
|
||||
'idbuchung' => $bookingData->id,
|
||||
@@ -348,9 +513,9 @@ class BookingDataProcessor
|
||||
* Bank account information is required for direct debit payments.
|
||||
*
|
||||
* @param array $payload The payload array to modify
|
||||
* @param object $bookingData The booking data object
|
||||
* @param Booking $bookingData The booking data object
|
||||
*/
|
||||
private function addBankAccountToPayload(array &$payload, object $bookingData): void
|
||||
private function addBankAccountToPayload(array &$payload, Booking $bookingData): void
|
||||
{
|
||||
if (null !== $bookingData->bankAccount) {
|
||||
$payload['zahlung']['bankverbindung'] = [
|
||||
@@ -365,19 +530,42 @@ class BookingDataProcessor
|
||||
/**
|
||||
* Builds the participant list section of the payload.
|
||||
*
|
||||
* Includes status and personal data for each participant.
|
||||
* Includes status, personal data, and wishes (room remarks, license plate) for each participant.
|
||||
*
|
||||
* @param array $payload The payload array to modify
|
||||
* @param object $bookingData The booking data object
|
||||
* @param Booking $bookingData The booking data object
|
||||
* @param array $participantDtos The participant DTOs from the form (for wishes data)
|
||||
*/
|
||||
private function buildParticipantPayload(array &$payload, object $bookingData): void
|
||||
private function buildParticipantPayload(array &$payload, Booking $bookingData, array $participantDtos): void
|
||||
{
|
||||
foreach ($bookingData->participants as $index => $participant) {
|
||||
$payload['teilnehmerliste']['teilnehmer'][] = [
|
||||
$participantPayload = [
|
||||
'@id' => $index + 1,
|
||||
'status' => $bookingData->participantsStatus[$index],
|
||||
...$participant->toPayload(),
|
||||
];
|
||||
|
||||
// Add wishes (room remarks, license plate) from form DTO
|
||||
if (isset($participantDtos[$index])) {
|
||||
$dto = $participantDtos[$index];
|
||||
if (null !== $dto->remarksRoom || null !== $dto->licensePlate) {
|
||||
$wishes = [];
|
||||
|
||||
if (null !== $dto->remarksRoom && '' !== trim($dto->remarksRoom)) {
|
||||
$wishes['unterbringungswunsch'] = $dto->remarksRoom;
|
||||
}
|
||||
|
||||
if (null !== $dto->licensePlate && '' !== trim($dto->licensePlate)) {
|
||||
$wishes['beförderungswunsch'] = $dto->licensePlate;
|
||||
}
|
||||
|
||||
if (false === empty($wishes)) {
|
||||
$participantPayload['wünsche'] = $wishes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$payload['teilnehmerliste']['teilnehmer'][] = $participantPayload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,9 +576,9 @@ class BookingDataProcessor
|
||||
* with participant mappings and quantities.
|
||||
*
|
||||
* @param array $payload The payload array to modify
|
||||
* @param object $bookingData The booking data object
|
||||
* @param Booking $bookingData The booking data object
|
||||
*/
|
||||
private function buildServicesPayload(array &$payload, object $bookingData): void
|
||||
private function buildServicesPayload(array &$payload, Booking $bookingData): void
|
||||
{
|
||||
foreach ($bookingData->additionalServices as $service) {
|
||||
$payload['zusatzleistungen']['zusatzleistung'][] = [
|
||||
@@ -419,6 +607,20 @@ class BookingDataProcessor
|
||||
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $room->mapping)),
|
||||
];
|
||||
}
|
||||
|
||||
// Add insurances with participant mappings
|
||||
if (false === empty($bookingData->insurances)) {
|
||||
$payload['versicherungen']['versicherung'] = [];
|
||||
foreach ($bookingData->insurances as $insurance) {
|
||||
if (count($insurance->mapping) > 0) {
|
||||
$payload['versicherungen']['versicherung'][] = [
|
||||
'@idversicherung' => $insurance->id,
|
||||
'@anzahl' => count($insurance->mapping),
|
||||
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $insurance->mapping)),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -427,9 +629,9 @@ class BookingDataProcessor
|
||||
* Only included in payload if there are actual pickup assignments for bus transportation.
|
||||
*
|
||||
* @param array $payload The payload array to modify
|
||||
* @param object $bookingData The booking data object
|
||||
* @param Booking $bookingData The booking data object
|
||||
*/
|
||||
private function buildPickupPayload(array &$payload, object $bookingData): void
|
||||
private function buildPickupPayload(array &$payload, Booking $bookingData): void
|
||||
{
|
||||
if (0 < count($bookingData->pickupsOutbound)) {
|
||||
$payload['zustiege']['zustieg'] = [];
|
||||
@@ -451,13 +653,16 @@ class BookingDataProcessor
|
||||
* payment information. The booking type determines whether this is an inquiry validation
|
||||
* ('Anfrage') or a final booking commit ('Buchung').
|
||||
*
|
||||
* @param BookingCreateDto $bookingDto The booking creation form data
|
||||
* @param BookingDto $bookingDto The booking creation form data
|
||||
* @param string $bookingType Either 'Anfrage' (inquiry) or 'Buchung' (final booking)
|
||||
*
|
||||
* @return array The structured payload array for BusProNet API submission
|
||||
*/
|
||||
public function createBookingRequestPayload(BookingCreateDto $bookingDto, string $bookingType): array
|
||||
public function createBookingRequestPayload(BookingDto $bookingDto, string $bookingType): array
|
||||
{
|
||||
// Apply bulk insurance if enabled (modifies DTO in place)
|
||||
$this->applyBulkInsuranceIfActive($bookingDto);
|
||||
|
||||
$firstParticipant = $bookingDto->participants[0];
|
||||
|
||||
$payload = [
|
||||
@@ -802,4 +1007,59 @@ class BookingDataProcessor
|
||||
|
||||
return $insuranceMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies bulk insurance assignment if the applicant has enabled it.
|
||||
*
|
||||
* When bulk insurance is active (applicant's bulkInsuranceBooking = true), this method
|
||||
* assigns the applicant's insurance TYPE to all dependent participants with automatic
|
||||
* price tier adjustment based on each participant's total cost.
|
||||
*
|
||||
* IMPORTANT: In edit mode, bulk insurance only applies to participants who:
|
||||
* 1. Currently have NO insurance assigned (insurance === null)
|
||||
* 2. OR whose insurance was already assigned via previous bulk operation
|
||||
*
|
||||
* This prevents overriding individually selected insurances that are locked.
|
||||
*
|
||||
* @param BookingDto $bookingDto The booking DTO (create or edit flow)
|
||||
*/
|
||||
private function applyBulkInsuranceIfActive(BookingDto $bookingDto): void
|
||||
{
|
||||
$applicant = $bookingDto->getParticipant(0);
|
||||
|
||||
// Check if bulk insurance is enabled and applicant has selected an insurance
|
||||
if (null === $applicant || false === $applicant->bulkInsuranceBooking || null === $applicant->insurance) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all available insurances from travel data
|
||||
$availableInsurances = array_values($bookingDto->travel->insurances);
|
||||
|
||||
// Use InsuranceMatchingService for proper type-based assignment with price tier matching
|
||||
$assignments = $this->insuranceMatchingService->batchAssignInsuranceToParticipants(
|
||||
$availableInsurances,
|
||||
$applicant->insurance,
|
||||
$bookingDto
|
||||
);
|
||||
|
||||
// Apply assignments to dependent participants (skip applicant at index 0)
|
||||
foreach ($assignments as $index => $insurance) {
|
||||
if (0 === $index) {
|
||||
continue; // Skip applicant
|
||||
}
|
||||
|
||||
$participant = $bookingDto->getParticipant($index);
|
||||
if (null === $participant) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// In edit mode: only apply bulk insurance if participant has no insurance
|
||||
// This respects the rule that once assigned, insurance cannot be changed
|
||||
if ($bookingDto instanceof BookingEditDto && null !== $participant->insurance) {
|
||||
continue; // Skip participants with existing insurance assignment
|
||||
}
|
||||
|
||||
$participant->insurance = $insurance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
|
||||
/**
|
||||
* Represents a booking with participants, services, and pricing information.
|
||||
*
|
||||
@@ -192,6 +194,22 @@ class Booking
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves rental insurance for a specific participant.
|
||||
*/
|
||||
public function getRentalInsuranceForParticipant(int $participantIndex): ?Service
|
||||
{
|
||||
$rentalInsurances = $this->getAdditionalServicesByGroup(Constants::TOKEN_RENTAL_INSURANCE);
|
||||
|
||||
foreach ($rentalInsurances as $service) {
|
||||
if (in_array($participantIndex, $service->mapping)) {
|
||||
return $service;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves room assignment for a specific participant.
|
||||
*
|
||||
|
||||
@@ -39,6 +39,10 @@ class PersonalData
|
||||
public ?\DateTimeImmutable $dateOfBirth = null;
|
||||
public ?string $remarks = null;
|
||||
|
||||
// Wishes (unterbringungswunsch, beförderungswunsch)
|
||||
public ?string $remarksRoom = null;
|
||||
public ?string $licensePlate = null;
|
||||
|
||||
#[Assert\Valid(groups: ['personal_data'])]
|
||||
public Address $address;
|
||||
|
||||
|
||||
@@ -174,6 +174,13 @@ class BookingParser extends AbstractParser
|
||||
$personalData->communication->email = '[email protected]';
|
||||
}
|
||||
|
||||
// Parse wishes (room remarks and license plate)
|
||||
$wishesNode = $node->filterXPath('//wünsche');
|
||||
if (0 < $wishesNode->count()) {
|
||||
$personalData->remarksRoom = $this->getStringOrNullValue($wishesNode->filterXPath('//unterbringungswunsch'));
|
||||
$personalData->licensePlate = $this->getStringOrNullValue($wishesNode->filterXPath('//beförderungswunsch'));
|
||||
}
|
||||
|
||||
return $personalData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\BusProNet\Model\Address;
|
||||
use App\BusProNet\Model\Communication;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use voku\helper\AntiXSS;
|
||||
|
||||
class PersonalDataParser extends AbstractParser
|
||||
{
|
||||
@@ -58,8 +57,7 @@ class PersonalDataParser extends AbstractParser
|
||||
$remarksNode = $addressDataNode->filterXPath('//bemerkung');
|
||||
|
||||
if (0 < $remarksNode->count()) {
|
||||
$antiXss = new AntiXSS();
|
||||
$personalData->remarks = $antiXss->xss_clean($remarksNode->text());
|
||||
$personalData->remarks = $remarksNode->text();
|
||||
}
|
||||
|
||||
return $personalData;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Controller\Booking;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\DataProcessor\BookingDataProcessor;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\BusProNet\XmlLoader\PickupLoader;
|
||||
@@ -10,7 +11,7 @@ use App\Controller\Traits\BookingDataTrait;
|
||||
use App\Controller\Traits\HtmxControllerTrait;
|
||||
use App\Entity\User;
|
||||
use App\Form\BookingEditType;
|
||||
use App\Form\Model\BookingEditDto;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Security\Crypt;
|
||||
use App\Service\BookingPriceCalculatorService;
|
||||
use App\Service\BookingService;
|
||||
@@ -32,6 +33,7 @@ class EditController extends AbstractController
|
||||
|
||||
public function __construct(
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly BookingDataProcessor $bookingDataProcessor,
|
||||
private readonly TravelDataService $travelDataService,
|
||||
private readonly BookingService $bookingService,
|
||||
private readonly BookingPriceCalculatorService $priceCalculator,
|
||||
@@ -87,7 +89,7 @@ class EditController extends AbstractController
|
||||
$this->travelDataService->patchMutability($travelData, $mutableData);
|
||||
|
||||
// Create DTO for form
|
||||
$formData = BookingEditDto::fromBooking($bookingData, $travelData);
|
||||
$formData = $this->bookingDataProcessor->createBookingDtoFromBooking($bookingData, $travelData);
|
||||
|
||||
// Calculate pricing data for template
|
||||
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($formData);
|
||||
@@ -151,9 +153,7 @@ class EditController extends AbstractController
|
||||
return $this->render('booking/edit.html.twig', [
|
||||
'bookingData' => $bookingData,
|
||||
'bookingEditDto' => $formData,
|
||||
'travelData' => $travelData,
|
||||
'mutableData' => $mutableData,
|
||||
'availabilities' => $availabilities,
|
||||
'form' => $form->createView(),
|
||||
'pricingData' => $summary['pricing'],
|
||||
'participantCount' => $summary['participantCount'],
|
||||
@@ -203,8 +203,8 @@ class EditController extends AbstractController
|
||||
$this->travelDataService->patchAvailabilities($travelData, $availabilities);
|
||||
$this->travelDataService->patchMutability($travelData, $mutableData);
|
||||
|
||||
// Create DTO for form
|
||||
$formData = BookingEditDto::fromBooking($bookingData, $travelData);
|
||||
// Create fresh DTO from booking data
|
||||
$formData = $this->bookingDataProcessor->createBookingDtoFromBooking($bookingData, $travelData);
|
||||
|
||||
// Process form data without validation to capture current state
|
||||
$form = $this->createForm(BookingEditType::class, $formData, [
|
||||
@@ -242,6 +242,7 @@ class EditController extends AbstractController
|
||||
'assignmentCounts' => $roomAssignmentCounts,
|
||||
'participantPrices' => $participantPrices,
|
||||
'groupedSelectedRooms' => $groupedSelectedRooms,
|
||||
'mutableData' => $mutableData,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -260,11 +261,11 @@ class EditController extends AbstractController
|
||||
*
|
||||
* @return array<array{type: string, message: string}> Array of notification messages
|
||||
*/
|
||||
private function collectParticipantNotifications(BookingEditDto $bookingEditDto): array
|
||||
private function collectParticipantNotifications(BookingDto $bookingDto): array
|
||||
{
|
||||
$notifications = [];
|
||||
|
||||
foreach ($bookingEditDto->participants as $participant) {
|
||||
foreach ($bookingDto->participants as $participant) {
|
||||
if ([] !== $participant->notifications) {
|
||||
foreach ($participant->notifications as $notification) {
|
||||
$notifications[] = $notification;
|
||||
|
||||
@@ -11,7 +11,6 @@ trait BookingDataTrait
|
||||
{
|
||||
public function fetchBookingData(string $email, string $password, int $id): Booking|Notification|null
|
||||
{
|
||||
// Fetch booking data via API and cache result for a short ttl to check permissions
|
||||
$cacheKey = sprintf('bpn_booking_%d', $id);
|
||||
try {
|
||||
$bookingData = $this->cache->get($cacheKey, function (ItemInterface $item) use ($email, $password, $id) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Form\Model\BookingEditDto;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\ParticipantFieldHandlerRegistry;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
@@ -27,7 +27,7 @@ class BookingEditType extends AbstractType
|
||||
|
||||
public function onPreSetData(FormEvent $event): void
|
||||
{
|
||||
/** @var BookingEditDto $data */
|
||||
/** @var BookingDto $data */
|
||||
$data = $event->getData();
|
||||
$form = $event->getForm();
|
||||
|
||||
@@ -46,7 +46,7 @@ class BookingEditType extends AbstractType
|
||||
$form = $event->getForm();
|
||||
$submittedData = $event->getData();
|
||||
|
||||
/** @var BookingEditDto $bookingDto */
|
||||
/** @var BookingDto $bookingDto */
|
||||
$bookingDto = $form->getData();
|
||||
|
||||
// Process field handlers and synchronize submitted data with cleaned DTO state
|
||||
@@ -71,7 +71,7 @@ class BookingEditType extends AbstractType
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => BookingEditDto::class,
|
||||
'data_class' => BookingDto::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Form;
|
||||
|
||||
use App\BusProNet\Form\CountryType;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Service\Contract\FieldOptionsProviderInterface;
|
||||
use App\Form\Service\Contract\FieldStateProviderInterface;
|
||||
@@ -14,6 +14,7 @@ use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@@ -108,7 +109,7 @@ class BookingParticipantType extends AbstractType
|
||||
/**
|
||||
* Adds base fields to the form with field states applied.
|
||||
*/
|
||||
private function addBaseFields(FormInterface $form, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
private function addBaseFields(FormInterface $form, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
// Get field states for base fields
|
||||
$allFieldStates = $this->fieldStateProvider->getAllFieldStates($bookingDto, $participantIndex);
|
||||
@@ -151,12 +152,12 @@ class BookingParticipantType extends AbstractType
|
||||
], $getFieldState('email')))
|
||||
->add('mobile', TextType::class, $this->mergeFieldState([
|
||||
'label' => 'Telefon (mobil)',
|
||||
'required' => 0 === $participantIndex,
|
||||
'required' => false,
|
||||
'sanitize_html' => true,
|
||||
], $getFieldState('mobile')))
|
||||
->add('address', AddressType::class, $this->mergeFieldState([
|
||||
'label' => 'Adresse',
|
||||
'required' => 0 === $participantIndex,
|
||||
'required' => false,
|
||||
], $getFieldState('address')));
|
||||
|
||||
// Add body dimensions with state handling - use shouldIncludeField method
|
||||
@@ -172,11 +173,11 @@ class BookingParticipantType extends AbstractType
|
||||
* field states change based on submitted data.
|
||||
*
|
||||
* @param FormInterface $form The form to modify
|
||||
* @param BookingDtoInterface $bookingDto The booking data for context
|
||||
* @param BookingDto $bookingDto The booking data for context
|
||||
* @param int $participantIndex The participant index
|
||||
* @param array<string, mixed> $formData Submitted form data for state calculation
|
||||
*/
|
||||
private function removeExcludedFields(FormInterface $form, BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): void
|
||||
private function removeExcludedFields(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array $formData = []): void
|
||||
{
|
||||
foreach (ParticipantDto::DYNAMIC_FIELDS as $fieldName) {
|
||||
if ($form->has($fieldName) && !$this->fieldStateProvider->shouldIncludeField($fieldName, $bookingDto, $participantIndex, $formData)) {
|
||||
@@ -188,7 +189,7 @@ class BookingParticipantType extends AbstractType
|
||||
/**
|
||||
* Rebuilds all fields with updated states based on submitted data.
|
||||
*/
|
||||
private function rebuildFieldsWithStates(FormInterface $form, BookingDtoInterface $bookingDto, int $participantIndex, array $submittedData): void
|
||||
private function rebuildFieldsWithStates(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array $submittedData): void
|
||||
{
|
||||
// First, remove fields that should be excluded entirely
|
||||
$this->removeExcludedFields($form, $bookingDto, $participantIndex, $submittedData);
|
||||
@@ -220,7 +221,7 @@ class BookingParticipantType extends AbstractType
|
||||
/**
|
||||
* Adds all configured dynamic fields to the form with state conditions applied.
|
||||
*/
|
||||
private function addDynamicFields(FormInterface $form, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
private function addDynamicFields(FormInterface $form, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
$dynamicFields = [
|
||||
'assignedRoomId' => ChoiceType::class,
|
||||
@@ -236,10 +237,14 @@ class BookingParticipantType extends AbstractType
|
||||
'pickup' => ChoiceType::class,
|
||||
'parking' => CheckboxType::class,
|
||||
'licensePlate' => TextType::class,
|
||||
'bulkInsuranceBooking' => CheckboxType::class,
|
||||
'insurance' => InsuranceChoiceType::class,
|
||||
];
|
||||
|
||||
// Insurance fields only available in create mode (API limitation)
|
||||
if (BookingDto::MODE_CREATE === $bookingDto->getMode()) {
|
||||
$dynamicFields['bulkInsuranceBooking'] = CheckboxType::class;
|
||||
$dynamicFields['insurance'] = InsuranceChoiceType::class;
|
||||
}
|
||||
|
||||
foreach ($dynamicFields as $fieldName => $fieldType) {
|
||||
if ($this->fieldOptionsProvider->hasFieldOptions($fieldName)) {
|
||||
// Check if field should be included in the form at all
|
||||
|
||||
@@ -33,6 +33,17 @@ class BankAccountDto
|
||||
#[Assert\IsTrue(message: 'Bitte akzeptieren Sie das SEPA-Mandat.')]
|
||||
public bool $sepaMandateAccepted = false;
|
||||
|
||||
public static function fromBankAccount(\App\BusProNet\Model\BankAccount $bankAccount): static
|
||||
{
|
||||
$instance = new static();
|
||||
$instance->iban = $bankAccount->iban;
|
||||
$instance->accountHolder = $bankAccount->holder;
|
||||
$instance->bankName = $bankAccount->bankName;
|
||||
$instance->sepaMandateAccepted = true;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns IBAN formatted with spaces for display (e.g., DE12 3456 7890 1234 5678 90).
|
||||
*/
|
||||
|
||||
@@ -37,6 +37,11 @@ class BookingCreateDto implements BookingDtoInterface
|
||||
{
|
||||
}
|
||||
|
||||
public function getMode(): string
|
||||
{
|
||||
return BookingDtoInterface::MODE_CREATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, RoomSelectionDto>
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Model;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
|
||||
/**
|
||||
* Unified DTO for both booking creation and editing workflows.
|
||||
*
|
||||
* This DTO consolidates the previously separate BookingCreateDto and BookingEditDto
|
||||
* into a single class that handles both modes. All service selections are stored
|
||||
* in participant DTOs regardless of mode, ensuring consistent data structure and
|
||||
* simplifying pricing calculations, field handlers, and template rendering.
|
||||
*/
|
||||
class BookingDto
|
||||
{
|
||||
public const MODE_CREATE = 'create';
|
||||
public const MODE_EDIT = 'edit';
|
||||
|
||||
public int $currentStep = 1;
|
||||
|
||||
/**
|
||||
* @var array<int, RoomSelectionDto>
|
||||
*/
|
||||
#[Assert\Valid]
|
||||
public array $roomSelections = [];
|
||||
|
||||
/**
|
||||
* @var array<int, ParticipantDto>
|
||||
*/
|
||||
#[Assert\Valid]
|
||||
public array $participants = [];
|
||||
|
||||
#[Assert\Choice(
|
||||
choices: [Constants::PAYMENT_METHOD_TRANSFER, Constants::PAYMENT_METHOD_DEBIT],
|
||||
message: 'Bitte wählen Sie eine gültige Zahlungsart.'
|
||||
)]
|
||||
public ?string $paymentMethod = Constants::PAYMENT_METHOD_TRANSFER;
|
||||
|
||||
public ?BankAccountDto $bankAccount = null;
|
||||
|
||||
public ?int $agencyId = null;
|
||||
|
||||
/**
|
||||
* Reference to booking entity (only populated in edit mode).
|
||||
*/
|
||||
public ?Booking $booking = null;
|
||||
|
||||
public function __construct(public Travel $travel, public int $hotelId)
|
||||
{
|
||||
}
|
||||
|
||||
public function getMode(): string
|
||||
{
|
||||
return null !== $this->booking ? self::MODE_EDIT : self::MODE_CREATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, RoomSelectionDto>
|
||||
*/
|
||||
public function getSelectedRooms(): array
|
||||
{
|
||||
// In edit mode, rooms are fixed - return empty array
|
||||
if (self::MODE_EDIT === $this->getMode()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter($this->roomSelections, function (RoomSelectionDto $roomSelection) {
|
||||
return 0 < $roomSelection->quantity;
|
||||
});
|
||||
}
|
||||
|
||||
public function getParticipants(): array
|
||||
{
|
||||
return $this->participants;
|
||||
}
|
||||
|
||||
public function hasParticipant(int $index): bool
|
||||
{
|
||||
return isset($this->participants[$index]);
|
||||
}
|
||||
|
||||
public function getParticipant(int $index): ?ParticipantDto
|
||||
{
|
||||
return $this->participants[$index] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this is a family booking based on participant age distribution.
|
||||
*
|
||||
* A family booking is defined as:
|
||||
* - 1 or 2 participants aged 18 or older (adults)
|
||||
* - At least 1 participant younger than 18 (children)
|
||||
*/
|
||||
public function isFamilyBooking(): bool
|
||||
{
|
||||
$adults = 0;
|
||||
$children = 0;
|
||||
|
||||
$travelStartDate = $this->travel->dateFrom;
|
||||
|
||||
foreach ($this->participants as $participant) {
|
||||
$age = $participant->getAge($travelStartDate);
|
||||
|
||||
if (null === $age) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($age >= 18) {
|
||||
++$adults;
|
||||
} else {
|
||||
++$children;
|
||||
}
|
||||
}
|
||||
|
||||
return ($adults >= 1 && $adults <= 2) && ($children >= 1);
|
||||
}
|
||||
|
||||
public function isCanceled(): bool
|
||||
{
|
||||
return null !== $this->booking && 'S' === $this->booking->status;
|
||||
}
|
||||
|
||||
public function isOption(): bool
|
||||
{
|
||||
return null !== $this->booking && 'O' === $this->booking->status;
|
||||
}
|
||||
|
||||
#[Assert\Callback(callback: 'validateRoomSelection', groups: ['booking_create_step_1'])]
|
||||
public function validateRoomSelection(ExecutionContextInterface $context): void
|
||||
{
|
||||
$selectedRooms = $this->getSelectedRooms();
|
||||
|
||||
if (0 === count($selectedRooms)) {
|
||||
$context->buildViolation('Bitte mindestens ein Zimmer/Bett auswählen')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
#[Assert\Callback]
|
||||
public function validateBankAccount(ExecutionContextInterface $context): void
|
||||
{
|
||||
if (Constants::PAYMENT_METHOD_DEBIT !== $this->paymentMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $this->bankAccount) {
|
||||
$context->buildViolation('Bitte geben Sie Ihre Bankverbindung an.')
|
||||
->atPath('bankAccount')
|
||||
->addViolation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $this->bankAccount->iban || '' === trim($this->bankAccount->iban)) {
|
||||
$context->buildViolation('Bitte geben Sie Ihre IBAN ein.')
|
||||
->atPath('bankAccount.iban')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null === $this->bankAccount->accountHolder || '' === trim($this->bankAccount->accountHolder)) {
|
||||
$context->buildViolation('Bitte geben Sie den Kontoinhaber ein.')
|
||||
->atPath('bankAccount.accountHolder')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (false === $this->bankAccount->sepaMandateAccepted) {
|
||||
$context->buildViolation('Bitte akzeptieren Sie das SEPA-Mandat.')
|
||||
->atPath('bankAccount.sepaMandateAccepted')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,16 @@ namespace App\Form\Model;
|
||||
*/
|
||||
interface BookingDtoInterface
|
||||
{
|
||||
public const MODE_CREATE = 'create';
|
||||
public const MODE_EDIT = 'edit';
|
||||
|
||||
/**
|
||||
* Gets the booking mode (create or edit).
|
||||
*
|
||||
* @return string One of MODE_CREATE or MODE_EDIT constants
|
||||
*/
|
||||
public function getMode(): string;
|
||||
|
||||
/**
|
||||
* Gets all participants in the booking.
|
||||
*
|
||||
|
||||
@@ -21,6 +21,11 @@ class BookingEditDto implements BookingDtoInterface
|
||||
{
|
||||
}
|
||||
|
||||
public function getMode(): string
|
||||
{
|
||||
return BookingDtoInterface::MODE_EDIT;
|
||||
}
|
||||
|
||||
public static function fromBooking(Booking $booking, Travel $travel): static
|
||||
{
|
||||
$instance = new static($booking, $travel);
|
||||
@@ -30,7 +35,9 @@ class BookingEditDto implements BookingDtoInterface
|
||||
|
||||
foreach ($booking->participants as $index => $participant) {
|
||||
/** @var PersonalData $participant */
|
||||
$participantData = ParticipantDto::fromPersonalData($participant);
|
||||
// For the first participant (applicant), use applicant data instead of participant data
|
||||
$personalData = 0 === $index && null !== $booking->applicant ? $booking->applicant : $participant;
|
||||
$participantData = ParticipantDto::fromPersonalData($personalData);
|
||||
$participantData->index = $index;
|
||||
|
||||
$participantData->courses = $booking
|
||||
@@ -102,7 +109,10 @@ class BookingEditDto implements BookingDtoInterface
|
||||
/**
|
||||
* Gets all selected rooms for the booking (edit context).
|
||||
*
|
||||
* @return array<int, RoomSelectionDto> always returns an empty array for edit DTOs unless implemented
|
||||
* In edit mode, rooms are fixed and not selectable - returns empty array.
|
||||
* Participant count should be derived from actual participants, not room selections.
|
||||
*
|
||||
* @return array<int, RoomSelectionDto>
|
||||
*/
|
||||
public function getSelectedRooms(): array
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Validator\Constraints as AppAssert;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[AppAssert\Participant(groups: ['booking_edit', 'booking_create_step_2'])]
|
||||
#[AppAssert\ApplicantAddress(groups: ['booking_create_step_2'])]
|
||||
class ParticipantDto
|
||||
{
|
||||
/**
|
||||
@@ -132,6 +133,12 @@ class ParticipantDto
|
||||
$instance->weight = $personalData->weight;
|
||||
$instance->shoeSize = $personalData->shoeSize;
|
||||
|
||||
// Clone address to prevent shared object references that could cause mutations
|
||||
$instance->address = null !== $personalData->address ? clone $personalData->address : null;
|
||||
|
||||
$instance->remarksRoom = $personalData->remarksRoom;
|
||||
$instance->licensePlate = $personalData->licensePlate;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Abstract;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldOptionsProviderInterface;
|
||||
|
||||
/**
|
||||
@@ -37,13 +37,13 @@ abstract class AbstractFieldOptionsProvider implements FieldOptionsProviderInter
|
||||
* static options defined in the form type.
|
||||
*
|
||||
* @param string $fieldName The name of the field to configure
|
||||
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data for context (create or edit)
|
||||
* @param int $participantIndex The index of the participant being configured
|
||||
* @param array $options Additional options to customize field behavior
|
||||
*
|
||||
* @return array<string, mixed> Symfony form field options, or empty array if field not supported
|
||||
*/
|
||||
public function getFieldOptions(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex, array $options = []): array
|
||||
public function getFieldOptions(string $fieldName, BookingDto $bookingDto, int $participantIndex, array $options = []): array
|
||||
{
|
||||
// Check if we have a provider for this field
|
||||
if (false === isset($this->fieldOptionProviders[$fieldName])) {
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Abstract;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
use App\Form\Service\Contract\FieldStateProviderInterface;
|
||||
use App\Form\Service\Trait\FormTraversalTrait;
|
||||
@@ -36,13 +36,13 @@ abstract class AbstractFieldStateProvider implements FieldStateProviderInterface
|
||||
* condition independently to allow early field exclusion.
|
||||
*
|
||||
* @param string $fieldName The name of the field to evaluate
|
||||
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data for context (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data for condition evaluation
|
||||
*
|
||||
* @return bool True if the field should be included in the form, false if it should be excluded
|
||||
*/
|
||||
public function shouldIncludeField(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): bool
|
||||
public function shouldIncludeField(string $fieldName, BookingDto $bookingDto, int $participantIndex, array $formData = []): bool
|
||||
{
|
||||
if (false === isset($this->fieldStateConditions[$fieldName]['hidden'])) {
|
||||
return true; // No hidden condition means field should be included
|
||||
@@ -64,13 +64,13 @@ abstract class AbstractFieldStateProvider implements FieldStateProviderInterface
|
||||
* excluded from the form entirely rather than hidden with CSS.
|
||||
*
|
||||
* @param string $fieldName The name of the field to evaluate
|
||||
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data for context (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data for condition evaluation
|
||||
*
|
||||
* @return array<string, mixed> Symfony form field options for state modifications
|
||||
*/
|
||||
public function getFieldState(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): array
|
||||
public function getFieldState(string $fieldName, BookingDto $bookingDto, int $participantIndex, array $formData = []): array
|
||||
{
|
||||
if (false === isset($this->fieldStateConditions[$fieldName])) {
|
||||
return [];
|
||||
@@ -139,13 +139,13 @@ abstract class AbstractFieldStateProvider implements FieldStateProviderInterface
|
||||
/**
|
||||
* Calculates field states for all configured fields at once.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data for context (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data for condition evaluation
|
||||
*
|
||||
* @return array<string, array<string, mixed>> Field states indexed by field name
|
||||
*/
|
||||
public function getAllFieldStates(BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): array
|
||||
public function getAllFieldStates(BookingDto $bookingDto, int $participantIndex, array $formData = []): array
|
||||
{
|
||||
$allStates = [];
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Abstract;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\ParticipantFieldHandlerInterface;
|
||||
|
||||
/**
|
||||
@@ -55,12 +55,12 @@ abstract class AbstractParticipantFieldHandler implements ParticipantFieldHandle
|
||||
* if the participant exists at the given index. Returns null if the
|
||||
* participant doesn't exist, preventing array access errors.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO containing participants (create or edit)
|
||||
* @param BookingDto $bookingDto The booking DTO containing participants (create or edit)
|
||||
* @param int $participantIndex The index of the participant to retrieve
|
||||
*
|
||||
* @return object|null The participant object, or null if not found
|
||||
*/
|
||||
protected function getParticipant(BookingDtoInterface $bookingDto, int $participantIndex): ?object
|
||||
protected function getParticipant(BookingDto $bookingDto, int $participantIndex): ?object
|
||||
{
|
||||
$participants = $bookingDto->getParticipants();
|
||||
|
||||
@@ -129,12 +129,12 @@ abstract class AbstractParticipantFieldHandler implements ParticipantFieldHandle
|
||||
* processing results.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO (potentially modified by processing)
|
||||
* @param BookingDto $bookingDto The booking DTO (potentially modified by processing)
|
||||
* @param int $participantIndex The participant index being processed
|
||||
*
|
||||
* @return array<string, array<string, mixed>> Empty array (no state modifications by default)
|
||||
*/
|
||||
public function getFieldStateModifications(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): array
|
||||
public function getFieldStateModifications(array $submittedData, BookingDto $bookingDto, int $participantIndex): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
@@ -12,7 +12,7 @@ use App\Form\Service\Contract\FieldConditionInterface;
|
||||
*/
|
||||
class AdditionalServicesMutabilityCondition implements FieldConditionInterface
|
||||
{
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
return false === $bookingDto->travel->additionalServicesMutable;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
@@ -54,13 +54,13 @@ class AgeRangeCondition implements FieldConditionInterface
|
||||
* checks if it falls within the configured age range. Returns false if
|
||||
* the participant has no date of birth set.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data (unused for age conditions)
|
||||
*
|
||||
* @return bool True if the participant's age meets the criteria, false otherwise
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
$participant = $bookingDto->getParticipant($participantIndex);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
@@ -21,13 +21,13 @@ class ApplicantCondition implements FieldConditionInterface
|
||||
/**
|
||||
* Evaluates if the participant is the applicant.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data (unused)
|
||||
*
|
||||
* @return bool True if the participant is the applicant, false otherwise
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
// Default: applicant is the first participant (index 0)
|
||||
return 0 === $participantIndex;
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
use App\Service\ParticipantEligibilityService;
|
||||
|
||||
@@ -36,13 +36,13 @@ class BookingEligibilityCondition implements FieldConditionInterface
|
||||
* Returns true when the participant is INELIGIBLE (should hide service fields).
|
||||
* Returns false when the participant is eligible (show normal form fields).
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data
|
||||
* @param BookingDto $bookingDto The current booking data
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data for condition evaluation
|
||||
*
|
||||
* @return bool True if participant is ineligible (no skipasses available for their age)
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
// Invert the eligibility check since conditions typically evaluate to TRUE for "hide"
|
||||
// isParticipantEligible() returns TRUE when eligible, we need TRUE when INELIGIBLE
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
@@ -29,13 +29,13 @@ class BulkInsuranceBookingCondition implements FieldConditionInterface
|
||||
* For dependent participants, returns true if the applicant has bulk insurance booking enabled,
|
||||
* regardless of whether an insurance is selected (applies to "no insurance" as well).
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data
|
||||
* @param BookingDto $bookingDto The current booking data
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data for condition evaluation
|
||||
*
|
||||
* @return bool True if bulk insurance booking is active for this dependent participant
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
// Applicant is never affected by bulk insurance booking (they control it)
|
||||
if (0 === $participantIndex) {
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
@@ -64,13 +64,13 @@ class CompositeCondition implements FieldConditionInterface
|
||||
* evaluation for optimal performance. The evaluation stops as soon as the
|
||||
* final result can be determined.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data for condition evaluation
|
||||
*
|
||||
* @return bool True if the composite condition is met, false otherwise
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
return match ($this->operator) {
|
||||
self::OPERATOR_AND => $this->evaluateAnd($bookingDto, $participantIndex, $formData),
|
||||
@@ -168,7 +168,7 @@ class CompositeCondition implements FieldConditionInterface
|
||||
* Returns false as soon as any condition evaluates to false,
|
||||
* avoiding unnecessary evaluation of remaining conditions.
|
||||
*/
|
||||
private function evaluateAnd(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
private function evaluateAnd(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
foreach ($this->conditions as $condition) {
|
||||
if (!$condition->evaluate($bookingDto, $participantIndex, $formData)) {
|
||||
@@ -185,7 +185,7 @@ class CompositeCondition implements FieldConditionInterface
|
||||
* Returns true as soon as any condition evaluates to true,
|
||||
* avoiding unnecessary evaluation of remaining conditions.
|
||||
*/
|
||||
private function evaluateOr(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
private function evaluateOr(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
foreach ($this->conditions as $condition) {
|
||||
if ($condition->evaluate($bookingDto, $participantIndex, $formData)) {
|
||||
@@ -199,7 +199,7 @@ class CompositeCondition implements FieldConditionInterface
|
||||
/**
|
||||
* Evaluates NOT logic by inverting the result of the single condition.
|
||||
*/
|
||||
private function evaluateNot(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
private function evaluateNot(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
return !$this->conditions[0]->evaluate($bookingDto, $participantIndex, $formData);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
@@ -27,17 +27,24 @@ class DateOfBirthProvidedCondition implements FieldConditionInterface
|
||||
* Checks if the participant exists and has a non-null dateOfBirth property.
|
||||
* This is a prerequisite for showing age-dependent form fields and services.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data
|
||||
* @param BookingDto $bookingDto The current booking data
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data (unused for this condition)
|
||||
*
|
||||
* @return bool True if the participant has provided their date of birth, false otherwise
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
$participant = $bookingDto->getParticipant($participantIndex);
|
||||
$result = null !== $participant && null !== $participant->dateOfBirth;
|
||||
|
||||
return null !== $participant && null !== $participant->dateOfBirth;
|
||||
error_log(sprintf('[DOB Condition] Participant %d: dateOfBirth=%s, result=%s',
|
||||
$participantIndex,
|
||||
$participant?->dateOfBirth?->format('Y-m-d') ?? 'NULL',
|
||||
$result ? 'TRUE' : 'FALSE'
|
||||
));
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
@@ -60,13 +60,13 @@ class FieldValueCondition implements FieldConditionInterface
|
||||
* against the expected value using the configured operator. Supports
|
||||
* both participant-level fields and booking-level fields.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data for condition evaluation
|
||||
*
|
||||
* @return bool True if the field value meets the condition criteria, false otherwise
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
$fieldValue = $this->getFieldValue($formData, $participantIndex, $bookingDto);
|
||||
|
||||
@@ -175,7 +175,7 @@ class FieldValueCondition implements FieldConditionInterface
|
||||
/**
|
||||
* Retrieves field value from form data or participant data.
|
||||
*/
|
||||
private function getFieldValue(array $formData, int $participantIndex, BookingDtoInterface $bookingDto): mixed
|
||||
private function getFieldValue(array $formData, int $participantIndex, BookingDto $bookingDto): mixed
|
||||
{
|
||||
// First check participant-specific form data
|
||||
if (isset($formData['participants'][$participantIndex][$this->fieldName])) {
|
||||
|
||||
@@ -4,72 +4,36 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingEditDto;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
* Condition that determines if insurance fields are editable based on booking and travel dates.
|
||||
* Condition that determines if insurance fields are editable.
|
||||
*
|
||||
* 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
|
||||
* Insurance cannot be modified via the BusProNet API after booking creation,
|
||||
* so insurance fields are always readonly in edit mode and editable in create mode.
|
||||
*/
|
||||
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
|
||||
* - Create mode: Always editable (return false)
|
||||
* - Edit mode: Always readonly (return true) - API limitation
|
||||
*
|
||||
* @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)
|
||||
* @param BookingDto $bookingDto The current booking data
|
||||
* @param int $participantIndex The participant index (unused)
|
||||
* @param array<string, mixed> $formData Current form data (unused)
|
||||
*
|
||||
* @return bool True if field should be readonly, false if editable
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
public function evaluate(BookingDto $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;
|
||||
// Insurance cannot be updated via API - always readonly in edit mode
|
||||
return BookingDto::MODE_EDIT === $bookingDto->getMode();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,10 +56,6 @@ class InsuranceMutabilityCondition implements FieldConditionInterface
|
||||
*/
|
||||
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
|
||||
);
|
||||
return 'Insurance is not editable in edit mode (API limitation)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
@@ -13,7 +13,7 @@ use App\Form\Service\Contract\FieldConditionInterface;
|
||||
* This is used to set fields as readonly or disabled if the booking or participant
|
||||
* is not allowed to be modified (e.g., after a certain workflow step or status).
|
||||
*
|
||||
* The logic assumes the BookingDtoInterface or its participant DTOs expose a
|
||||
* The logic assumes the BookingDto or its participant DTOs expose a
|
||||
* 'personalDataMutable' property or method. Adjust as needed for your domain.
|
||||
*/
|
||||
class MutabilityCondition implements FieldConditionInterface
|
||||
@@ -21,13 +21,13 @@ class MutabilityCondition implements FieldConditionInterface
|
||||
/**
|
||||
* Evaluates if the participant's personal data is mutable.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data (unused)
|
||||
*
|
||||
* @return bool True if the participant's data is mutable, false otherwise
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
$participant = $bookingDto->getParticipant($participantIndex);
|
||||
if (null === $participant) {
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
@@ -12,7 +12,7 @@ use App\Form\Service\Contract\FieldConditionInterface;
|
||||
*/
|
||||
class PickupsMutabilityCondition implements FieldConditionInterface
|
||||
{
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
return false === $bookingDto->travel->pickupsMutable;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
@@ -28,13 +28,13 @@ class RentalSelectionCondition implements FieldConditionInterface
|
||||
* which would indicate rental services have been selected and body
|
||||
* dimensions should be required for proper equipment sizing.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data for condition evaluation
|
||||
*
|
||||
* @return bool True if rental services are selected, false otherwise
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
// First check submitted form data for rental selections
|
||||
if (isset($formData['participants'][$participantIndex]['rentals'])) {
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\BusProNet\Model\Room;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
@@ -35,13 +35,13 @@ class RoomSelectionCondition implements FieldConditionInterface
|
||||
* Checks both submitted form data and participant DTO data to determine
|
||||
* if the selected room matches any of the configured room codes.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data for condition evaluation
|
||||
*
|
||||
* @return bool True if room with matching code is selected, false otherwise
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
// First check submitted form data for room selection
|
||||
if (isset($formData['participants'][$participantIndex]['assignedRoomId'])) {
|
||||
@@ -95,11 +95,11 @@ class RoomSelectionCondition implements FieldConditionInterface
|
||||
* Finds a room by ID in the available travel rooms.
|
||||
*
|
||||
* @param int $roomId The room ID to find
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO containing travel data
|
||||
* @param BookingDto $bookingDto The booking DTO containing travel data
|
||||
*
|
||||
* @return Room|null The found room or null if not found
|
||||
*/
|
||||
private function findRoomById(int $roomId, BookingDtoInterface $bookingDto): ?Room
|
||||
private function findRoomById(int $roomId, BookingDto $bookingDto): ?Room
|
||||
{
|
||||
foreach ($bookingDto->travel->rooms as $room) {
|
||||
if ($room->id === $roomId) {
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
@@ -52,13 +52,13 @@ class ServiceSubTypeCondition implements FieldConditionInterface
|
||||
* property against the expected value(s). Handles both form data and
|
||||
* participant DTO data sources.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data for condition evaluation
|
||||
*
|
||||
* @return bool True if the service sub-type meets the condition criteria, false otherwise
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
$service = $this->getService($formData, $participantIndex, $bookingDto);
|
||||
|
||||
@@ -161,7 +161,7 @@ class ServiceSubTypeCondition implements FieldConditionInterface
|
||||
/**
|
||||
* Retrieves service from form data or participant data.
|
||||
*/
|
||||
private function getService(array $formData, int $participantIndex, BookingDtoInterface $bookingDto): ?Service
|
||||
private function getService(array $formData, int $participantIndex, BookingDto $bookingDto): ?Service
|
||||
{
|
||||
// First check participant-specific form data
|
||||
if (isset($formData['participants'][$participantIndex][$this->serviceFieldName])) {
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
@@ -28,13 +28,13 @@ class SkiPassSelectionCondition implements FieldConditionInterface
|
||||
* which would indicate a skipass has been selected and rental services
|
||||
* should be made available with duration filtering applied.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data for condition evaluation
|
||||
*
|
||||
* @return bool True if a skipass is selected, false otherwise
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
// First check submitted form data for skipass selection
|
||||
if (isset($formData['participants'][$participantIndex]['skiPass'])) {
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Condition;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Contract\FieldConditionInterface;
|
||||
|
||||
/**
|
||||
@@ -12,7 +12,7 @@ use App\Form\Service\Contract\FieldConditionInterface;
|
||||
*/
|
||||
class TransportationServicesMutabilityCondition implements FieldConditionInterface
|
||||
{
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
return false === $bookingDto->travel->transportationServicesMutable;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Contract;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
|
||||
/**
|
||||
* Interface for evaluating field state conditions.
|
||||
@@ -29,13 +29,13 @@ interface FieldConditionInterface
|
||||
* state of the booking, participant data, and submitted form values.
|
||||
* The result is used to determine field state (enabled/disabled/readonly).
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data (may include partial submissions)
|
||||
*
|
||||
* @return bool True if the condition is met, false otherwise
|
||||
*/
|
||||
public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool;
|
||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool;
|
||||
|
||||
/**
|
||||
* Returns field names that trigger re-evaluation of this condition.
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Contract;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
|
||||
/**
|
||||
* Interface for providing dynamic field options based on context.
|
||||
@@ -42,13 +42,13 @@ interface FieldOptionsProviderInterface
|
||||
* static options defined in the form type.
|
||||
*
|
||||
* @param string $fieldName The name of the field to configure
|
||||
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data for context (create or edit)
|
||||
* @param int $participantIndex The index of the participant being configured
|
||||
* @param array $options Additional options to customize field behavior
|
||||
*
|
||||
* @return array<string, mixed> Symfony form field options, or empty array if field not supported
|
||||
*/
|
||||
public function getFieldOptions(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex, array $options = []): array;
|
||||
public function getFieldOptions(string $fieldName, BookingDto $bookingDto, int $participantIndex, array $options = []): array;
|
||||
|
||||
/**
|
||||
* Checks whether a field has option provider support.
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Contract;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
|
||||
/**
|
||||
* Interface for providing dynamic field state based on conditions.
|
||||
@@ -38,13 +38,13 @@ interface FieldStateProviderInterface
|
||||
* condition independently to allow early field exclusion during form building.
|
||||
*
|
||||
* @param string $fieldName The name of the field to evaluate
|
||||
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data for context (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data for condition evaluation
|
||||
*
|
||||
* @return bool True if the field should be included in the form, false if it should be excluded
|
||||
*/
|
||||
public function shouldIncludeField(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): bool;
|
||||
public function shouldIncludeField(string $fieldName, BookingDto $bookingDto, int $participantIndex, array $formData = []): bool;
|
||||
|
||||
/**
|
||||
* Calculates the dynamic state for a specified field.
|
||||
@@ -65,13 +65,13 @@ interface FieldStateProviderInterface
|
||||
* - 'attr' => ['class' => 'conditional-field'] - Add CSS classes
|
||||
*
|
||||
* @param string $fieldName The name of the field to evaluate
|
||||
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data for context (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data (may include partial submissions)
|
||||
*
|
||||
* @return array<string, mixed> Symfony form field options for state modifications, empty if no changes needed
|
||||
*/
|
||||
public function getFieldState(string $fieldName, BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): array;
|
||||
public function getFieldState(string $fieldName, BookingDto $bookingDto, int $participantIndex, array $formData = []): array;
|
||||
|
||||
/**
|
||||
* Checks whether a field has state conditions configured.
|
||||
@@ -110,11 +110,11 @@ interface FieldStateProviderInterface
|
||||
* when multiple field states need to be determined simultaneously. It's
|
||||
* particularly useful during form building and bulk state updates.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data for context (create or edit)
|
||||
* @param BookingDto $bookingDto The current booking data for context (create or edit)
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
* @param array<string, mixed> $formData Current form data for condition evaluation
|
||||
*
|
||||
* @return array<string, array<string, mixed>> Field states indexed by field name
|
||||
*/
|
||||
public function getAllFieldStates(BookingDtoInterface $bookingDto, int $participantIndex, array $formData = []): array;
|
||||
public function getAllFieldStates(BookingDto $bookingDto, int $participantIndex, array $formData = []): array;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Contract;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
|
||||
/**
|
||||
* Interface for handling dynamic participant form field processing and state modification.
|
||||
@@ -32,10 +32,10 @@ interface ParticipantFieldHandlerInterface
|
||||
* Processes the participant field data from submitted form data and updates the DTO.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit)
|
||||
* @param BookingDto $bookingDto The booking DTO to update (create or edit)
|
||||
* @param int $participantIndex The participant index being processed
|
||||
*/
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void;
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void;
|
||||
|
||||
/**
|
||||
* Determines if this handler should process the field based on submitted participant data.
|
||||
@@ -50,12 +50,12 @@ interface ParticipantFieldHandlerInterface
|
||||
* to enable/disable/hide fields based on the handler's processing results.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO (potentially modified by processing)
|
||||
* @param BookingDto $bookingDto The booking DTO (potentially modified by processing)
|
||||
* @param int $participantIndex The participant index being processed
|
||||
*
|
||||
* @return array<string, array<string, mixed>> Field state modifications indexed by field name
|
||||
*/
|
||||
public function getFieldStateModifications(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): array;
|
||||
public function getFieldStateModifications(array $submittedData, BookingDto $bookingDto, int $participantIndex): array;
|
||||
|
||||
/**
|
||||
* Returns field names whose state is affected by this handler's processing.
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Utility\DirectionMapper;
|
||||
use App\Form\Service\Abstract\AbstractFieldStateProvider;
|
||||
use App\Form\Service\Condition\ApplicantCondition;
|
||||
use App\Form\Service\Condition\BookingEligibilityCondition;
|
||||
use App\Form\Service\Condition\BulkInsuranceBookingCondition;
|
||||
use App\Form\Service\Condition\CompositeCondition;
|
||||
@@ -34,10 +35,11 @@ use App\Service\ParticipantEligibilityService;
|
||||
class CreateFieldStateProvider extends AbstractFieldStateProvider
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ParticipantEligibilityService $participantEligibilityService
|
||||
private readonly ParticipantEligibilityService $participantEligibilityService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers field state conditions for the create workflow.
|
||||
*
|
||||
@@ -124,22 +126,7 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
|
||||
$this->fieldStateConditions['bulkInsuranceBooking'] = [
|
||||
'hidden' => CompositeCondition::or(
|
||||
CompositeCondition::not($dateOfBirthProvidedCondition), // Hide until date of birth provided
|
||||
new class() implements \App\Form\Service\Contract\FieldConditionInterface {
|
||||
public function evaluate(\App\Form\Model\BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
|
||||
{
|
||||
return $participantIndex > 0; // Hide for all participants except applicant
|
||||
}
|
||||
|
||||
public function getDependentFields(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Participant is not the applicant';
|
||||
}
|
||||
}
|
||||
CompositeCondition::not(new ApplicantCondition()) // Hide for non-applicants
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Form\Service\Condition\ApplicantCondition;
|
||||
use App\Form\Service\Condition\CompositeCondition;
|
||||
use App\Form\Service\Condition\DateOfBirthProvidedCondition;
|
||||
use App\Form\Service\Condition\FieldValueCondition;
|
||||
use App\Form\Service\Condition\InsuranceMutabilityCondition;
|
||||
use App\Form\Service\Condition\MutabilityCondition;
|
||||
use App\Form\Service\Condition\PickupsMutabilityCondition;
|
||||
use App\Form\Service\Condition\RentalSelectionCondition;
|
||||
@@ -43,7 +44,8 @@ class EditFieldStateProvider extends AbstractFieldStateProvider
|
||||
$transportationServicesMutabilityCondition = new TransportationServicesMutabilityCondition();
|
||||
$pickupsMutabilityCondition = new PickupsMutabilityCondition();
|
||||
|
||||
// Make all personal data fields readonly if not mutable OR if applicant
|
||||
// Make all personal data fields readonly if participant not mutable
|
||||
// Note: First participant is now treated as independent from applicant and can be edited
|
||||
$personalDataFields = [
|
||||
'firstName',
|
||||
'lastName',
|
||||
@@ -55,13 +57,15 @@ class EditFieldStateProvider extends AbstractFieldStateProvider
|
||||
];
|
||||
foreach ($personalDataFields as $field) {
|
||||
$this->fieldStateConditions[$field] = [
|
||||
'readonly' => CompositeCondition::or(
|
||||
new ApplicantCondition(),
|
||||
new MutabilityCondition()
|
||||
),
|
||||
'readonly' => CompositeCondition::not(new MutabilityCondition()),
|
||||
];
|
||||
}
|
||||
|
||||
// Address fields - readonly if participant not mutable
|
||||
$this->fieldStateConditions['address'] = [
|
||||
'readonly' => CompositeCondition::not(new MutabilityCondition()),
|
||||
];
|
||||
|
||||
// Conditional visibility for service fields (same as create flow)
|
||||
$rentalCondition = new RentalSelectionCondition();
|
||||
$skiPassCondition = new SkiPassSelectionCondition();
|
||||
@@ -72,33 +76,40 @@ class EditFieldStateProvider extends AbstractFieldStateProvider
|
||||
'hidden' => CompositeCondition::not($rentalCondition),
|
||||
];
|
||||
|
||||
// Age-dependent service fields - hidden until birth date provided
|
||||
// Age-dependent service fields - hidden until birth date provided (except for applicant who always has DOB)
|
||||
// Also readonly if services not mutable
|
||||
// For applicant in edit mode: DOB is always available (patched from booking), so never hide
|
||||
$hideUntilDobCondition = CompositeCondition::and(
|
||||
CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||
CompositeCondition::not(new ApplicantCondition()) // Don't hide for applicant
|
||||
);
|
||||
|
||||
$this->fieldStateConditions['courses'] = [
|
||||
'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||
'hidden' => $hideUntilDobCondition,
|
||||
'readonly' => $additionalServicesMutabilityCondition,
|
||||
];
|
||||
|
||||
$this->fieldStateConditions['additionalServices'] = [
|
||||
'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||
'hidden' => $hideUntilDobCondition,
|
||||
'readonly' => $additionalServicesMutabilityCondition,
|
||||
];
|
||||
|
||||
$this->fieldStateConditions['board'] = [
|
||||
'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||
'hidden' => $hideUntilDobCondition,
|
||||
'readonly' => $additionalServicesMutabilityCondition,
|
||||
];
|
||||
|
||||
// Skipass - hidden until birth date, readonly if services not mutable
|
||||
// Skipass - hidden until birth date (except applicant), readonly if services not mutable
|
||||
$this->fieldStateConditions['skiPass'] = [
|
||||
'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||
'hidden' => $hideUntilDobCondition,
|
||||
'readonly' => $additionalServicesMutabilityCondition,
|
||||
];
|
||||
|
||||
// Rentals - shown only when skipass selected, readonly if services not mutable
|
||||
// For applicant: only check skipass, not DOB
|
||||
$this->fieldStateConditions['rentals'] = [
|
||||
'hidden' => CompositeCondition::or(
|
||||
CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||
$hideUntilDobCondition,
|
||||
CompositeCondition::not($skiPassCondition)
|
||||
),
|
||||
'readonly' => $additionalServicesMutabilityCondition,
|
||||
@@ -110,14 +121,14 @@ class EditFieldStateProvider extends AbstractFieldStateProvider
|
||||
'readonly' => $additionalServicesMutabilityCondition,
|
||||
];
|
||||
|
||||
// Transportation fields - hidden until birth date, readonly if transportation not mutable
|
||||
// Transportation fields - hidden until birth date (except applicant), readonly if transportation not mutable
|
||||
$this->fieldStateConditions['transportationOutbound'] = [
|
||||
'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||
'hidden' => $hideUntilDobCondition,
|
||||
'readonly' => $transportationServicesMutabilityCondition,
|
||||
];
|
||||
|
||||
$this->fieldStateConditions['transportationInbound'] = [
|
||||
'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||
'hidden' => $hideUntilDobCondition,
|
||||
'readonly' => $transportationServicesMutabilityCondition,
|
||||
];
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
|
||||
/**
|
||||
@@ -84,10 +84,10 @@ class ParticipantAdditionalServicesFieldHandler extends AbstractParticipantField
|
||||
* 5. Updates participant with filtered valid selections
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit)
|
||||
* @param BookingDto $bookingDto The booking DTO to update (create or edit)
|
||||
* @param int $participantIndex The index of the participant being processed
|
||||
*/
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
// Safely get the participant object, returning early if not found
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
@@ -122,7 +122,7 @@ class ParticipantAdditionalServicesFieldHandler extends AbstractParticipantField
|
||||
*
|
||||
* @param array $selectedServices List of currently selected services
|
||||
* @param array $availableServices List of all available additional services
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO for context
|
||||
* @param BookingDto $bookingDto The booking DTO for context
|
||||
* @param int $participantIndex The participant index for age evaluation
|
||||
*
|
||||
* @return array Filtered array of valid service selections
|
||||
@@ -130,7 +130,7 @@ class ParticipantAdditionalServicesFieldHandler extends AbstractParticipantField
|
||||
private function filterValidServiceSelections(
|
||||
array $selectedServices,
|
||||
array $availableServices,
|
||||
BookingDtoInterface $bookingDto,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
): array {
|
||||
$validSelections = [];
|
||||
@@ -156,7 +156,7 @@ class ParticipantAdditionalServicesFieldHandler extends AbstractParticipantField
|
||||
*
|
||||
* @param mixed $selectedService The selected service to validate
|
||||
* @param array $availableServices Array of available services
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO for context
|
||||
* @param BookingDto $bookingDto The booking DTO for context
|
||||
* @param int $participantIndex The participant index for age evaluation
|
||||
*
|
||||
* @return bool True if the service is valid for the participant, false otherwise
|
||||
@@ -164,7 +164,7 @@ class ParticipantAdditionalServicesFieldHandler extends AbstractParticipantField
|
||||
private function isServiceValidForParticipant(
|
||||
mixed $selectedService,
|
||||
array $availableServices,
|
||||
BookingDtoInterface $bookingDto,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
): bool {
|
||||
// Find the service in available services
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
|
||||
/**
|
||||
@@ -53,10 +53,10 @@ class ParticipantAssignedRoomFieldHandler extends AbstractParticipantFieldHandle
|
||||
* 4. Updates the participant's assignedRoomId property
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit)
|
||||
* @param BookingDto $bookingDto The booking DTO to update (create or edit)
|
||||
* @param int $participantIndex The index of the participant being processed
|
||||
*/
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
// Safely get the participant object, returning early if not found
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
|
||||
/**
|
||||
@@ -49,7 +49,7 @@ class ParticipantBoardFieldHandler extends AbstractParticipantFieldHandler
|
||||
return true; // Always process to handle deselection cases
|
||||
}
|
||||
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
if (null === $participant) {
|
||||
@@ -72,7 +72,7 @@ class ParticipantBoardFieldHandler extends AbstractParticipantFieldHandler
|
||||
private function filterValidServiceSelections(
|
||||
array $selectedServices,
|
||||
array $availableServices,
|
||||
BookingDtoInterface $bookingDto,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
): array {
|
||||
$validSelections = [];
|
||||
@@ -93,7 +93,7 @@ class ParticipantBoardFieldHandler extends AbstractParticipantFieldHandler
|
||||
private function isServiceValidForParticipant(
|
||||
mixed $selectedService,
|
||||
array $availableServices,
|
||||
BookingDtoInterface $bookingDto,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
): bool {
|
||||
$service = $this->findServiceInAvailableServices($selectedService, $availableServices);
|
||||
|
||||
@@ -4,30 +4,23 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
use App\Service\InsuranceMatchingService;
|
||||
|
||||
/**
|
||||
* Handles bulk insurance booking for the applicant (first participant).
|
||||
* Handles bulk insurance booking checkbox for the applicant (first participant).
|
||||
*
|
||||
* When the applicant enables bulk insurance booking, their selected insurance type
|
||||
* (subType + familyInsurance) is automatically assigned to all participants with
|
||||
* automatic price tier adjustment based on each participant's individual travel price.
|
||||
* This handler only manages the checkbox state - it does NOT assign insurance to
|
||||
* dependent participants. The actual bulk insurance assignment happens in the
|
||||
* BookingDataProcessor during API submission, keeping the form layer clean and
|
||||
* avoiding cross-participant modifications in field handlers.
|
||||
*
|
||||
* This handler processes the bulkInsuranceBooking checkbox state and triggers
|
||||
* insurance assignment to dependent participants when enabled.
|
||||
*
|
||||
* Dependencies: insurance (applicant must have insurance selected before enabling bulk)
|
||||
* The checkbox state is used by:
|
||||
* - Templates: To display "wie Anmelder" for dependent participants
|
||||
* - Processors: To apply applicant's insurance to all participants before API submission
|
||||
*/
|
||||
class ParticipantBulkInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InsuranceMatchingService $insuranceMatchingService,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getFieldName(): string
|
||||
{
|
||||
return 'bulkInsuranceBooking';
|
||||
@@ -35,8 +28,8 @@ class ParticipantBulkInsuranceFieldHandler extends AbstractParticipantFieldHandl
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
// Depends on insurance field to ensure insurance is selected before bulk assignment
|
||||
return ['insurance'];
|
||||
// No dependencies - just manages checkbox state
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,120 +51,22 @@ class ParticipantBulkInsuranceFieldHandler extends AbstractParticipantFieldHandl
|
||||
/**
|
||||
* Processes the bulk insurance booking checkbox for the applicant.
|
||||
*
|
||||
* When bulk insurance is enabled and applicant has insurance selected,
|
||||
* assigns the same insurance type to all participants based on their
|
||||
* individual pricing and eligibility criteria.
|
||||
* This handler ONLY stores the checkbox state. The actual insurance assignment
|
||||
* to dependent participants is handled by BookingDataProcessor during API submission.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO to update
|
||||
* @param BookingDto $bookingDto The booking DTO to update
|
||||
* @param int $participantIndex The index of the participant (must be 0)
|
||||
*/
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
if (null === $participant || !$bookingDto instanceof BookingCreateDto) {
|
||||
if (null === $participant) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get checkbox value from submitted data
|
||||
// Get checkbox value from submitted data and store it
|
||||
$bulkInsuranceBooking = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||
$isBulkEnabled = (bool) $bulkInsuranceBooking;
|
||||
|
||||
// Store checkbox state
|
||||
$participant->bulkInsuranceBooking = $isBulkEnabled;
|
||||
|
||||
// If bulk insurance is enabled and applicant has insurance, assign to all participants
|
||||
if (true === $isBulkEnabled && null !== $participant->insurance) {
|
||||
$this->applyBulkInsuranceToAllParticipants($bookingDto, $participant->insurance);
|
||||
}
|
||||
|
||||
// If bulk insurance was CHANGED from enabled to disabled, clear dependent participants' insurances
|
||||
// Don't clear if it was never enabled (to allow independent insurance selection)
|
||||
if (false === $isBulkEnabled && $this->wasBulkInsurancePreviouslyEnabled($bookingDto)) {
|
||||
$this->clearDependentParticipantsInsurance($bookingDto);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the applicant's insurance type to all participants with automatic price tier adjustment.
|
||||
*
|
||||
* Uses InsuranceMatchingService to find the appropriate price tier for each participant
|
||||
* based on their individual travel price and eligibility criteria.
|
||||
*
|
||||
* @param BookingCreateDto $bookingDto The booking DTO with all participants
|
||||
* @param object $applicantInsurance The insurance selected by the applicant
|
||||
*/
|
||||
private function applyBulkInsuranceToAllParticipants(BookingCreateDto $bookingDto, object $applicantInsurance): void
|
||||
{
|
||||
$availableInsurances = $bookingDto->travel->insurances ?? [];
|
||||
|
||||
// Get insurance assignments for all participants
|
||||
$assignments = $this->insuranceMatchingService->batchAssignInsuranceToParticipants(
|
||||
$availableInsurances,
|
||||
$applicantInsurance,
|
||||
$bookingDto
|
||||
);
|
||||
|
||||
// Apply assignments to participants (skip applicant index 0, they keep their selection)
|
||||
foreach ($assignments as $index => $insurance) {
|
||||
if (0 === $index) {
|
||||
continue; // Skip applicant
|
||||
}
|
||||
|
||||
$participant = $bookingDto->getParticipant($index);
|
||||
if (null !== $participant) {
|
||||
$participant->insurance = $insurance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears insurance selections for dependent participants when bulk booking is disabled.
|
||||
*
|
||||
* @param BookingCreateDto $bookingDto The booking DTO with all participants
|
||||
*/
|
||||
private function clearDependentParticipantsInsurance(BookingCreateDto $bookingDto): void
|
||||
{
|
||||
foreach ($bookingDto->getParticipants() as $index => $participant) {
|
||||
if (0 === $index) {
|
||||
continue; // Skip applicant
|
||||
}
|
||||
|
||||
$participant->insurance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if bulk insurance was previously enabled by checking if dependent participants
|
||||
* have the same insurance type as the applicant.
|
||||
*
|
||||
* This prevents clearing independent insurance selections when the checkbox is simply unchecked
|
||||
* without ever having been enabled.
|
||||
*
|
||||
* @param BookingCreateDto $bookingDto The booking DTO with all participants
|
||||
*
|
||||
* @return bool True if bulk was previously active (dependent participants have matching insurance)
|
||||
*/
|
||||
private function wasBulkInsurancePreviouslyEnabled(BookingCreateDto $bookingDto): bool
|
||||
{
|
||||
$applicant = $bookingDto->participants[0] ?? null;
|
||||
if (null === $applicant || null === $applicant->insurance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if any dependent participant has insurance that matches the applicant
|
||||
// If so, bulk was likely previously enabled
|
||||
foreach ($bookingDto->participants as $index => $participant) {
|
||||
if (0 === $index) {
|
||||
continue; // Skip applicant
|
||||
}
|
||||
|
||||
if (null !== $participant->insurance) {
|
||||
// If any dependent has insurance, assume bulk was previously enabled
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
$participant->bulkInsuranceBooking = (bool) $bulkInsuranceBooking;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
|
||||
/**
|
||||
@@ -77,10 +77,10 @@ class ParticipantCoursesFieldHandler extends AbstractParticipantFieldHandler
|
||||
* no longer appropriate for the participant's age are automatically removed.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit)
|
||||
* @param BookingDto $bookingDto The booking DTO to update (create or edit)
|
||||
* @param int $participantIndex The index of the participant being processed
|
||||
*/
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
// Safely get the participant object, returning early if not found
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
@@ -111,7 +111,7 @@ class ParticipantCoursesFieldHandler extends AbstractParticipantFieldHandler
|
||||
*
|
||||
* @param array $selectedServices List of currently selected courses
|
||||
* @param array $availableServices List of all available courses
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO for context
|
||||
* @param BookingDto $bookingDto The booking DTO for context
|
||||
* @param int $participantIndex The participant index for age evaluation
|
||||
*
|
||||
* @return array Filtered array of valid course selections
|
||||
@@ -119,7 +119,7 @@ class ParticipantCoursesFieldHandler extends AbstractParticipantFieldHandler
|
||||
private function filterValidServiceSelections(
|
||||
array $selectedServices,
|
||||
array $availableServices,
|
||||
BookingDtoInterface $bookingDto,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
): array {
|
||||
$validSelections = [];
|
||||
@@ -142,7 +142,7 @@ class ParticipantCoursesFieldHandler extends AbstractParticipantFieldHandler
|
||||
*
|
||||
* @param mixed $selectedService The selected course to validate
|
||||
* @param array $availableServices Array of available courses
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO for context
|
||||
* @param BookingDto $bookingDto The booking DTO for context
|
||||
* @param int $participantIndex The participant index for age evaluation
|
||||
*
|
||||
* @return bool True if the course is valid for the participant, false otherwise
|
||||
@@ -150,7 +150,7 @@ class ParticipantCoursesFieldHandler extends AbstractParticipantFieldHandler
|
||||
private function isServiceValidForParticipant(
|
||||
mixed $selectedService,
|
||||
array $availableServices,
|
||||
BookingDtoInterface $bookingDto,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
): bool {
|
||||
// Find the service in available services
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
|
||||
/**
|
||||
@@ -53,10 +53,10 @@ class ParticipantDateOfBirthFieldHandler extends AbstractParticipantFieldHandler
|
||||
* 4. Updates the participant's dateOfBirth property
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit)
|
||||
* @param BookingDto $bookingDto The booking DTO to update (create or edit)
|
||||
* @param int $participantIndex The index of the participant being processed
|
||||
*/
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
// Safely get the participant object, returning early if not found
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace App\Form\Service;
|
||||
use App\BusProNet\Model\Insurance;
|
||||
use App\BusProNet\Model\Pickup;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Service\Contract\ParticipantFieldHandlerInterface;
|
||||
|
||||
@@ -75,12 +75,14 @@ class ParticipantFieldHandlerRegistry
|
||||
* automatically cleared.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted form data containing participants array
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO to update with processed field values
|
||||
* @param BookingDto $bookingDto The booking DTO to update with processed field values
|
||||
*
|
||||
* @return array<string, mixed> The synchronized submitted data reflecting DTO changes
|
||||
*/
|
||||
public function processFieldsAndSync(array $submittedData, BookingDtoInterface $bookingDto): array
|
||||
public function processFieldsAndSync(array $submittedData, BookingDto $bookingDto): array
|
||||
{
|
||||
error_log(sprintf('[ProcessFieldsAndSync] Mode: %s', $bookingDto->getMode()));
|
||||
|
||||
// Process all field handlers to clean the DTO
|
||||
$this->processFields($submittedData, $bookingDto);
|
||||
|
||||
@@ -105,9 +107,9 @@ class ParticipantFieldHandlerRegistry
|
||||
* family detection which needs all participants' ages to be processed first).
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted form data containing participants array
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO to update with processed field values (create or edit)
|
||||
* @param BookingDto $bookingDto The booking DTO to update with processed field values (create or edit)
|
||||
*/
|
||||
public function processFields(array $submittedData, BookingDtoInterface $bookingDto): void
|
||||
public function processFields(array $submittedData, BookingDto $bookingDto): void
|
||||
{
|
||||
// Early return if no participant data exists in submission
|
||||
if (false === isset($submittedData['participants']) || false === is_array($submittedData['participants'])) {
|
||||
@@ -245,15 +247,18 @@ class ParticipantFieldHandlerRegistry
|
||||
* that the form continues processing with cleaned data rather than the original
|
||||
* submitted data that may contain invalid selections.
|
||||
*
|
||||
* In edit mode, this also adds the applicant's personal data to the submitted data
|
||||
* for participant 0, ensuring those fields are bound to the DTO by handleRequest.
|
||||
*
|
||||
* The synchronization is generic and works with any field handlers by examining
|
||||
* the current DTO state and updating the corresponding submitted data fields.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The original submitted form data
|
||||
* @param BookingDtoInterface $bookingDto The DTO with cleaned data from field handlers
|
||||
* @param BookingDto $bookingDto The DTO with cleaned data from field handlers
|
||||
*
|
||||
* @return array<string, mixed> Updated submitted data reflecting DTO state
|
||||
*/
|
||||
private function syncSubmittedDataWithDto(array $submittedData, BookingDtoInterface $bookingDto): array
|
||||
private function syncSubmittedDataWithDto(array $submittedData, BookingDto $bookingDto): array
|
||||
{
|
||||
// Ensure participants array exists in submitted data
|
||||
if (false === isset($submittedData['participants']) || false === is_array($submittedData['participants'])) {
|
||||
@@ -272,7 +277,7 @@ class ParticipantFieldHandlerRegistry
|
||||
}
|
||||
|
||||
// Update participant data to match cleaned DTO state
|
||||
$submittedData['participants'][$index] = $this->syncParticipantData($participantData, $participant);
|
||||
$submittedData['participants'][$index] = $this->syncParticipantData($participantData, $participant, $index, $bookingDto);
|
||||
}
|
||||
|
||||
return $submittedData;
|
||||
@@ -285,13 +290,20 @@ class ParticipantFieldHandlerRegistry
|
||||
* any changes made by field handlers. It automatically detects which fields have
|
||||
* been processed by checking against registered handlers.
|
||||
*
|
||||
* In edit mode for participant 0 (applicant), this also adds the personal data fields
|
||||
* that were patched from the applicant, ensuring they're bound to the DTO.
|
||||
*
|
||||
* @param array<string, mixed> $participantData The submitted participant data
|
||||
* @param ParticipantDto $participant The cleaned participant DTO
|
||||
* @param int $index The participant index
|
||||
* @param BookingDto $bookingDto The booking DTO for mode detection
|
||||
*
|
||||
* @return array<string, mixed> Updated participant data with synchronized field values
|
||||
*/
|
||||
private function syncParticipantData(array $participantData, ParticipantDto $participant): array
|
||||
private function syncParticipantData(array $participantData, ParticipantDto $participant, int $index, BookingDto $bookingDto): array
|
||||
{
|
||||
error_log(sprintf('[Sync] Participant %d fields in submission: %s', $participant->index ?? -1, implode(', ', array_keys($participantData))));
|
||||
|
||||
// Sync fields for all registered handlers
|
||||
foreach ($this->handlers as $fieldName => $handler) {
|
||||
// Only sync fields that were in the original submission
|
||||
@@ -299,6 +311,7 @@ class ParticipantFieldHandlerRegistry
|
||||
if (property_exists($participant, $fieldName) && array_key_exists($fieldName, $participantData)) {
|
||||
$dtoValue = $participant->{$fieldName};
|
||||
$participantData[$fieldName] = $this->convertDtoValueToSubmittedFormat($dtoValue);
|
||||
error_log(sprintf('[Sync] Participant %d: synced %s', $participant->index ?? -1, $fieldName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,6 +375,17 @@ class ParticipantFieldHandlerRegistry
|
||||
return $value->id;
|
||||
}
|
||||
|
||||
// Handle Address objects -> convert to array of properties
|
||||
if ($value instanceof \App\BusProNet\Model\Address) {
|
||||
return [
|
||||
'street' => $value->street,
|
||||
'postCode' => $value->postCode,
|
||||
'city' => $value->city,
|
||||
'district' => $value->district,
|
||||
'country' => $value->country,
|
||||
];
|
||||
}
|
||||
|
||||
// Handle DateTimeInterface -> convert to string format
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
return $value->format('Y-m-d');
|
||||
|
||||
@@ -9,7 +9,7 @@ use App\BusProNet\Model\Pickup;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Utility\DirectionMapper;
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\BookingEditDto;
|
||||
use App\Form\Service\Abstract\AbstractFieldOptionsProvider;
|
||||
use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory;
|
||||
@@ -82,11 +82,11 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
protected function registerFieldOptionProviders(): void
|
||||
{
|
||||
// Room assignment field provider (available for both create and edit workflows)
|
||||
$this->fieldOptionProviders['assignedRoomId'] = function (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) {
|
||||
$this->fieldOptionProviders['assignedRoomId'] = function (BookingDto $bookingDto, int $participantIndex, array $options = []) {
|
||||
$choiceLoader = null;
|
||||
$disabled = false;
|
||||
|
||||
if ($bookingDto instanceof BookingCreateDto) {
|
||||
if (BookingDto::MODE_CREATE === $bookingDto->getMode()) {
|
||||
// Create context: use selected rooms from step 1
|
||||
$choiceLoader = $this->roomChoiceLoaderFactory->createForCreate(
|
||||
$bookingDto->participants,
|
||||
@@ -95,7 +95,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
);
|
||||
// Disable when only one room type selected (auto-assigned)
|
||||
$disabled = 1 === count($bookingDto->getSelectedRooms());
|
||||
} elseif ($bookingDto instanceof BookingEditDto) {
|
||||
} elseif (BookingDto::MODE_EDIT === $bookingDto->getMode()) {
|
||||
// Edit context: use already-booked rooms from booking
|
||||
$choiceLoader = $this->roomChoiceLoaderFactory->createForEdit(
|
||||
$bookingDto->participants,
|
||||
@@ -121,7 +121,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
};
|
||||
|
||||
// Courses field provider - provides age-appropriate courses from travel data
|
||||
$this->fieldOptionProviders['courses'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
$this->fieldOptionProviders['courses'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Kurse',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
@@ -156,7 +156,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
];
|
||||
|
||||
// Additional services field provider - provides age-appropriate additional services with mandatory pre-selection
|
||||
$this->fieldOptionProviders['additionalServices'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
$this->fieldOptionProviders['additionalServices'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Zusatzleistungen',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
@@ -199,7 +199,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
];
|
||||
|
||||
// Board field provider - provides age-appropriate board options from travel data
|
||||
$this->fieldOptionProviders['board'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
$this->fieldOptionProviders['board'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Verpflegung',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
@@ -229,7 +229,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
];
|
||||
|
||||
// Rentals field provider - provides age-appropriate rental options filtered by selected skipass duration
|
||||
$this->fieldOptionProviders['rentals'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
$this->fieldOptionProviders['rentals'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Leihmaterial',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
@@ -268,7 +268,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
];
|
||||
|
||||
// Rental insurance field provider - provides rental insurance options when rental services are selected
|
||||
$this->fieldOptionProviders['rentalInsurance'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
$this->fieldOptionProviders['rentalInsurance'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => $this->getRentalInsuranceCheckboxLabel($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTAL_INSURANCE, true, true)),
|
||||
'required' => false,
|
||||
'property_path' => 'rentalInsuranceSelected',
|
||||
@@ -276,7 +276,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
];
|
||||
|
||||
// License plate field provider - provides text input for vehicle license plate when parking is selected
|
||||
$this->fieldOptionProviders['licensePlate'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
$this->fieldOptionProviders['licensePlate'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Kennzeichen',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
@@ -288,7 +288,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
];
|
||||
|
||||
// Skipass field provider - provides age-appropriate skipass options from travel data filtered by date range
|
||||
$this->fieldOptionProviders['skiPass'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
$this->fieldOptionProviders['skiPass'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Skipass',
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
@@ -323,7 +323,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
];
|
||||
|
||||
// Room remarks field provider - provides textarea for room-specific remarks (only for 'mbz' rooms)
|
||||
$this->fieldOptionProviders['remarksRoom'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
$this->fieldOptionProviders['remarksRoom'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Wünsche oder Anmerkungen zum Zimmer',
|
||||
'required' => false,
|
||||
'sanitize_html' => true,
|
||||
@@ -335,7 +335,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
// Transportation field providers - handles outbound/inbound transportation and pickup selection
|
||||
|
||||
// Outbound Transportation
|
||||
$this->fieldOptionProviders['transportationOutbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
$this->fieldOptionProviders['transportationOutbound'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Hinfahrt',
|
||||
'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL),
|
||||
'choice_label' => fn (Service $service) => $this->formatTransportationServiceLabel($service),
|
||||
@@ -366,7 +366,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
];
|
||||
|
||||
// Inbound Transportation
|
||||
$this->fieldOptionProviders['transportationInbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
$this->fieldOptionProviders['transportationInbound'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Rückfahrt',
|
||||
'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::INBOUND_TRAVEL),
|
||||
'choice_label' => fn (Service $service) => $this->formatTransportationServiceLabel($service),
|
||||
@@ -398,7 +398,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
|
||||
// Pickup (conditional - only shown when either transportation direction is bus)
|
||||
// Uses outbound pickups list, applies to both directions
|
||||
$this->fieldOptionProviders['pickup'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
$this->fieldOptionProviders['pickup'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Zu- und Ausstieg',
|
||||
'choices' => $bookingDto->travel->pickupsOutbound,
|
||||
'choice_label' => fn (?Pickup $pickup) => $this->formatPickupLabelWithPrice($pickup),
|
||||
@@ -411,13 +411,14 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
|
||||
// Parking (conditional - only shown when outbound transportation is PKW)
|
||||
// Simple checkbox since there's only ever one parking type
|
||||
$this->fieldOptionProviders['parking'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
$this->fieldOptionProviders['parking'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => $this->getParkingCheckboxLabel($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_PARKING, true)),
|
||||
'required' => false,
|
||||
];
|
||||
|
||||
// Bulk insurance booking checkbox (applicant only - controls insurance assignment for all participants)
|
||||
$this->fieldOptionProviders['bulkInsuranceBooking'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
// Only registered in create mode - insurance cannot be modified in edit mode due to API limitation
|
||||
$this->fieldOptionProviders['bulkInsuranceBooking'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Für alle Teilnehmer buchen',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
@@ -428,7 +429,8 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
];
|
||||
|
||||
// Insurance field provider - provides age and eligibility filtered insurances for participants
|
||||
$this->fieldOptionProviders['insurance'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
|
||||
// Only registered in create mode - insurance cannot be modified in edit mode due to API limitation
|
||||
$this->fieldOptionProviders['insurance'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Reiseversicherung',
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
@@ -461,12 +463,12 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
* is only available for the exact duration of the skipass.
|
||||
*
|
||||
* @param array $rentals Array of rental Service objects to filter
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO containing participant data
|
||||
* @param BookingDto $bookingDto The booking DTO containing participant data
|
||||
* @param int $participantIndex Index of the participant to evaluate
|
||||
*
|
||||
* @return array Filtered array of rentals matching skipass duration
|
||||
*/
|
||||
private function filterRentalsBySkiPassDuration(array $rentals, BookingDtoInterface $bookingDto, int $participantIndex): array
|
||||
private function filterRentalsBySkiPassDuration(array $rentals, BookingDto $bookingDto, int $participantIndex): array
|
||||
{
|
||||
$participant = $bookingDto->getParticipant($participantIndex);
|
||||
if (null === $participant || null === $participant->skiPass) {
|
||||
@@ -586,12 +588,12 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
* Checks if a service should be rendered as read-only due to unavailability.
|
||||
*
|
||||
* @param Service $service The service to check
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO containing participant data
|
||||
* @param BookingDto $bookingDto The booking DTO containing participant data
|
||||
* @param int $participantIndex Index of the participant currently selecting services
|
||||
*
|
||||
* @return bool True if the service should be read-only due to unavailability
|
||||
*/
|
||||
private function isServiceUnavailableForParticipant(Service $service, BookingDtoInterface $bookingDto, int $participantIndex): bool
|
||||
private function isServiceUnavailableForParticipant(Service $service, BookingDto $bookingDto, int $participantIndex): bool
|
||||
{
|
||||
if (!$bookingDto instanceof BookingCreateDto) {
|
||||
// For non-create workflows, don't apply availability restrictions
|
||||
@@ -609,12 +611,12 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
* returns empty array to be handled by field visibility conditions.
|
||||
*
|
||||
* @param array $services Array of Service objects to filter
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO containing participant data
|
||||
* @param BookingDto $bookingDto The booking DTO containing participant data
|
||||
* @param int $participantIndex Index of the participant to evaluate
|
||||
*
|
||||
* @return array Filtered array of available services
|
||||
*/
|
||||
private function filterServicesByAgeConstraints(array $services, BookingDtoInterface $bookingDto, int $participantIndex): array
|
||||
private function filterServicesByAgeConstraints(array $services, BookingDto $bookingDto, int $participantIndex): array
|
||||
{
|
||||
$ageEvaluator = new ServiceAgeEvaluator();
|
||||
|
||||
@@ -664,12 +666,12 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
/**
|
||||
* Gets eligible insurances for a participant based on eligibility criteria.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO containing travel and participant data
|
||||
* @param BookingDto $bookingDto The booking DTO containing travel and participant data
|
||||
* @param int $participantIndex The index of the participant to get eligible insurances for
|
||||
*
|
||||
* @return array Array of eligible insurance objects filtered by age, family status, and other constraints
|
||||
*/
|
||||
private function getEligibleInsurances(BookingDtoInterface $bookingDto, int $participantIndex): array
|
||||
private function getEligibleInsurances(BookingDto $bookingDto, int $participantIndex): array
|
||||
{
|
||||
$participant = $bookingDto->getParticipant($participantIndex);
|
||||
if (null === $participant) {
|
||||
@@ -682,11 +684,6 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
// They are only available as part of packages
|
||||
$availableInsurances = array_filter($availableInsurances, fn ($insurance) => !$insurance->complementary);
|
||||
|
||||
// Only apply insurance filtering for BookingCreateDto (creation workflow)
|
||||
if (!$bookingDto instanceof BookingCreateDto) {
|
||||
return $availableInsurances;
|
||||
}
|
||||
|
||||
// Use insurance matching service to filter based on eligibility criteria
|
||||
return $this->insuranceMatchingService->getEligibleInsurances(
|
||||
$availableInsurances,
|
||||
|
||||
@@ -5,8 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Model\Insurance;
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
use App\Service\InsuranceMatchingService;
|
||||
|
||||
@@ -112,13 +111,13 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
||||
* participant data and automatically reassigns to the correct price tier if needed.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit)
|
||||
* @param BookingDto $bookingDto The booking DTO to update (create or edit)
|
||||
* @param int $participantIndex The index of the participant being processed
|
||||
*/
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
$participant = $bookingDto->getParticipant($participantIndex);
|
||||
if (null === $participant || !$bookingDto instanceof BookingCreateDto) {
|
||||
if (null === $participant) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -206,12 +205,12 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
||||
* Currently no field state modifications are needed for insurance selection.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO (potentially modified by processing)
|
||||
* @param BookingDto $bookingDto The booking DTO (potentially modified by processing)
|
||||
* @param int $participantIndex The participant index being processed
|
||||
*
|
||||
* @return array<string, array<string, mixed>> Empty array - no field state modifications
|
||||
*/
|
||||
public function getFieldStateModifications(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): array
|
||||
public function getFieldStateModifications(array $submittedData, BookingDto $bookingDto, int $participantIndex): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
|
||||
/**
|
||||
@@ -69,10 +69,10 @@ class ParticipantLicensePlateFieldHandler extends AbstractParticipantFieldHandle
|
||||
* the license plate is automatically cleared to maintain data consistency.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit)
|
||||
* @param BookingDto $bookingDto The booking DTO to update (create or edit)
|
||||
* @param int $participantIndex The index of the participant being processed
|
||||
*/
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
// Safely get the participant object, returning early if not found
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace App\Form\Service;
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Utility\DirectionMapper;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
|
||||
/**
|
||||
@@ -46,7 +46,7 @@ class ParticipantParkingFieldHandler extends AbstractParticipantFieldHandler
|
||||
return true; // Always process for state changes
|
||||
}
|
||||
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
if (null === $participant) {
|
||||
@@ -95,11 +95,11 @@ class ParticipantParkingFieldHandler extends AbstractParticipantFieldHandler
|
||||
* Gets the first (and typically only) parking service for pricing calculation.
|
||||
* Returns null if no parking services are available.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO containing travel data
|
||||
* @param BookingDto $bookingDto The booking DTO containing travel data
|
||||
*
|
||||
* @return Service|null The parking service object, or null if not found
|
||||
*/
|
||||
private function findParkingService(BookingDtoInterface $bookingDto): ?Service
|
||||
private function findParkingService(BookingDto $bookingDto): ?Service
|
||||
{
|
||||
$parkingServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_PARKING, true);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Model\Pickup;
|
||||
use App\BusProNet\Utility\DirectionMapper;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
|
||||
/**
|
||||
@@ -35,7 +35,7 @@ class ParticipantPickupFieldHandler extends AbstractParticipantFieldHandler
|
||||
return true; // Always process to handle clearing pickup
|
||||
}
|
||||
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
if (null === $participant) {
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
|
||||
/**
|
||||
@@ -41,10 +41,10 @@ class ParticipantRemarksRoomFieldHandler extends AbstractParticipantFieldHandler
|
||||
* the normalization of empty strings to null values.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit)
|
||||
* @param BookingDto $bookingDto The booking DTO to update (create or edit)
|
||||
* @param int $participantIndex The index of the participant being processed
|
||||
*/
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
// Safely get the participant object, returning early if not found
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
|
||||
/**
|
||||
@@ -74,10 +74,10 @@ class ParticipantRentalInsuranceFieldHandler extends AbstractParticipantFieldHan
|
||||
* is no longer appropriate for the participant's age, it is automatically cleared.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit)
|
||||
* @param BookingDto $bookingDto The booking DTO to update (create or edit)
|
||||
* @param int $participantIndex The index of the participant being processed
|
||||
*/
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
// Safely get the participant object, returning early if not found
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
@@ -118,11 +118,11 @@ class ParticipantRentalInsuranceFieldHandler extends AbstractParticipantFieldHan
|
||||
* Gets the first (and typically only) rental insurance service.
|
||||
* Returns null if no rental insurance services are available.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO containing travel data
|
||||
* @param BookingDto $bookingDto The booking DTO containing travel data
|
||||
*
|
||||
* @return Service|null The rental insurance service object, or null if not found
|
||||
*/
|
||||
private function findRentalInsuranceService(BookingDtoInterface $bookingDto): ?Service
|
||||
private function findRentalInsuranceService(BookingDto $bookingDto): ?Service
|
||||
{
|
||||
$rentalInsuranceServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTAL_INSURANCE, true, true);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
|
||||
/**
|
||||
@@ -57,7 +57,7 @@ class ParticipantRentalsFieldHandler extends AbstractParticipantFieldHandler
|
||||
return true; // Always process to handle deselection cases
|
||||
}
|
||||
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
if (null === $participant) {
|
||||
@@ -108,7 +108,7 @@ class ParticipantRentalsFieldHandler extends AbstractParticipantFieldHandler
|
||||
private function filterValidServiceSelections(
|
||||
array $selectedServices,
|
||||
array $availableServices,
|
||||
BookingDtoInterface $bookingDto,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
): array {
|
||||
$validSelections = [];
|
||||
@@ -129,7 +129,7 @@ class ParticipantRentalsFieldHandler extends AbstractParticipantFieldHandler
|
||||
private function isServiceValidForParticipant(
|
||||
mixed $selectedService,
|
||||
array $availableServices,
|
||||
BookingDtoInterface $bookingDto,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
): bool {
|
||||
$service = $this->findServiceInAvailableServices($selectedService, $availableServices);
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
|
||||
/**
|
||||
@@ -79,10 +79,10 @@ class ParticipantSkiPassFieldHandler extends AbstractParticipantFieldHandler
|
||||
* age or exceeds the travel date range, it is automatically cleared.
|
||||
*
|
||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit)
|
||||
* @param BookingDto $bookingDto The booking DTO to update (create or edit)
|
||||
* @param int $participantIndex The index of the participant being processed
|
||||
*/
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
// Safely get the participant object, returning early if not found
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
@@ -117,7 +117,7 @@ class ParticipantSkiPassFieldHandler extends AbstractParticipantFieldHandler
|
||||
*
|
||||
* @param mixed $selectedService The selected skipass to validate
|
||||
* @param array $availableServices Array of available skipasses
|
||||
* @param BookingDtoInterface $bookingDto The booking DTO for context
|
||||
* @param BookingDto $bookingDto The booking DTO for context
|
||||
* @param int $participantIndex The participant index for age evaluation
|
||||
*
|
||||
* @return bool True if the skipass is valid for the participant, false otherwise
|
||||
@@ -125,7 +125,7 @@ class ParticipantSkiPassFieldHandler extends AbstractParticipantFieldHandler
|
||||
private function isServiceValidForParticipant(
|
||||
mixed $selectedService,
|
||||
array $availableServices,
|
||||
BookingDtoInterface $bookingDto,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
): bool {
|
||||
// Find the service in available services
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Utility\DirectionMapper;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
|
||||
/**
|
||||
@@ -41,7 +41,7 @@ class ParticipantTransportationInboundFieldHandler extends AbstractParticipantFi
|
||||
return true; // Always process to handle deselection cases
|
||||
}
|
||||
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
if (null === $participant) {
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Utility\DirectionMapper;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
|
||||
/**
|
||||
@@ -41,7 +41,7 @@ class ParticipantTransportationOutboundFieldHandler extends AbstractParticipantF
|
||||
return true; // Always process to handle deselection cases
|
||||
}
|
||||
|
||||
public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
if (null === $participant) {
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
|
||||
/**
|
||||
* Evaluates service availability based on participant age constraints.
|
||||
@@ -40,12 +40,12 @@ class ServiceAgeEvaluator
|
||||
* age at the time of travel, not their current age.
|
||||
*
|
||||
* @param Service $service The service to evaluate
|
||||
* @param BookingDtoInterface $bookingDto The booking containing participant data
|
||||
* @param BookingDto $bookingDto The booking containing participant data
|
||||
* @param int $participantIndex The index of the participant to evaluate
|
||||
*
|
||||
* @return bool True if the service is available for the participant
|
||||
*/
|
||||
public function isServiceAvailableForParticipant(Service $service, BookingDtoInterface $bookingDto, int $participantIndex): bool
|
||||
public function isServiceAvailableForParticipant(Service $service, BookingDto $bookingDto, int $participantIndex): bool
|
||||
{
|
||||
$participant = $bookingDto->getParticipant($participantIndex);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service\Trait;
|
||||
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
|
||||
/**
|
||||
@@ -17,17 +17,17 @@ use Symfony\Component\Form\FormInterface;
|
||||
trait FormTraversalTrait
|
||||
{
|
||||
/**
|
||||
* Gets the BookingDtoInterface from the root of the form tree.
|
||||
* Gets the BookingDto from the root of the form tree.
|
||||
*
|
||||
* This helper method traverses up the form tree to find the root form
|
||||
* and extracts the BookingDtoInterface data. This is shared logic used
|
||||
* and extracts the BookingDto data. This is shared logic used
|
||||
* by both create and edit participant forms.
|
||||
*
|
||||
* @param FormInterface $form The form to start traversing from
|
||||
*
|
||||
* @return BookingDtoInterface|null The booking DTO or null if not found
|
||||
* @return BookingDto|null The booking DTO or null if not found
|
||||
*/
|
||||
public function getBookingDtoFromForm(FormInterface $form): ?BookingDtoInterface
|
||||
public function getBookingDtoFromForm(FormInterface $form): ?BookingDto
|
||||
{
|
||||
// Traverse up the form tree to get the root form's data
|
||||
$rootForm = $form;
|
||||
@@ -37,6 +37,6 @@ trait FormTraversalTrait
|
||||
|
||||
$data = $rootForm->getData();
|
||||
|
||||
return $data instanceof BookingDtoInterface ? $data : null;
|
||||
return $data instanceof BookingDto ? $data : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
|
||||
/**
|
||||
@@ -22,7 +22,7 @@ readonly class InsuranceEligibilityCriteria
|
||||
public \DateTimeImmutable $bookingDate,
|
||||
public float $travelPrice,
|
||||
public int $travelDurationDays,
|
||||
public BookingCreateDto $booking,
|
||||
public BookingDto $booking,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use App\BusProNet\Model\Insurance;
|
||||
use App\BusProNet\Model\Room;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
|
||||
/**
|
||||
@@ -22,17 +22,18 @@ use App\Form\Model\ParticipantDto;
|
||||
class BookingPriceCalculatorService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ParticipantEligibilityService $participantEligibilityService
|
||||
private readonly ParticipantEligibilityService $participantEligibilityService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates comprehensive pricing breakdown for a booking.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The booking data to calculate pricing for
|
||||
* @param BookingDto $bookingDto The booking data to calculate pricing for
|
||||
*
|
||||
* @return array{rooms: array, services: array, grandTotal: float} Complete pricing breakdown
|
||||
*/
|
||||
public function getPricingBreakdown(BookingDtoInterface $bookingDto): array
|
||||
public function getPricingBreakdown(BookingDto $bookingDto): array
|
||||
{
|
||||
$roomPricing = $this->calculateRoomPricing($bookingDto);
|
||||
$servicePricing = $this->calculateServicePricing($bookingDto);
|
||||
@@ -48,11 +49,11 @@ class BookingPriceCalculatorService
|
||||
/**
|
||||
* Calculates pricing for all selected rooms.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The booking data containing room selections
|
||||
* @param BookingDto $bookingDto The booking data containing room selections
|
||||
*
|
||||
* @return array Array of room pricing data with labels, quantities, and totals
|
||||
*/
|
||||
public function calculateRoomPricing(BookingDtoInterface $bookingDto): array
|
||||
public function calculateRoomPricing(BookingDto $bookingDto): array
|
||||
{
|
||||
$roomPricing = [];
|
||||
|
||||
@@ -95,11 +96,11 @@ class BookingPriceCalculatorService
|
||||
*
|
||||
* Only includes services from eligible participants (those with available skipasses for their age).
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The booking data containing participants and their service selections
|
||||
* @param BookingDto $bookingDto The booking data containing participants and their service selections
|
||||
*
|
||||
* @return array Array of service groups with each group containing services of the same subtype
|
||||
*/
|
||||
public function calculateServicePricing(BookingDtoInterface $bookingDto): array
|
||||
public function calculateServicePricing(BookingDto $bookingDto): array
|
||||
{
|
||||
$participants = $bookingDto->getParticipants();
|
||||
|
||||
@@ -132,11 +133,11 @@ class BookingPriceCalculatorService
|
||||
/**
|
||||
* Calculates the grand total for the entire booking.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The booking data to calculate total for
|
||||
* @param BookingDto $bookingDto The booking data to calculate total for
|
||||
*
|
||||
* @return float The grand total price
|
||||
*/
|
||||
public function calculateGrandTotal(BookingDtoInterface $bookingDto): float
|
||||
public function calculateGrandTotal(BookingDto $bookingDto): float
|
||||
{
|
||||
$roomTotal = $this->calculateRoomTotal($bookingDto);
|
||||
$serviceTotal = $this->calculateServiceTotal($bookingDto);
|
||||
@@ -147,7 +148,7 @@ class BookingPriceCalculatorService
|
||||
/**
|
||||
* Calculates total price for all rooms.
|
||||
*/
|
||||
public function calculateRoomTotal(BookingDtoInterface $bookingDto): float
|
||||
public function calculateRoomTotal(BookingDto $bookingDto): float
|
||||
{
|
||||
$roomPricing = $this->calculateRoomPricing($bookingDto);
|
||||
|
||||
@@ -157,7 +158,7 @@ class BookingPriceCalculatorService
|
||||
/**
|
||||
* Calculates total price for all services.
|
||||
*/
|
||||
public function calculateServiceTotal(BookingDtoInterface $bookingDto): float
|
||||
public function calculateServiceTotal(BookingDto $bookingDto): float
|
||||
{
|
||||
$servicePricing = $this->calculateServicePricing($bookingDto);
|
||||
|
||||
@@ -194,12 +195,12 @@ class BookingPriceCalculatorService
|
||||
* This method calculates the complete price breakdown for a single participant,
|
||||
* including their room allocation (full room price) and all selected services.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The booking data containing all participants
|
||||
* @param BookingDto $bookingDto The booking data containing all participants
|
||||
* @param int $participantIndex The index of the participant to calculate for
|
||||
*
|
||||
* @return float The total price for the specified participant
|
||||
*/
|
||||
public function calculateIndividualParticipantPrice(BookingDtoInterface $bookingDto, int $participantIndex): float
|
||||
public function calculateIndividualParticipantPrice(BookingDto $bookingDto, int $participantIndex): float
|
||||
{
|
||||
if (false === $bookingDto instanceof BookingCreateDto) {
|
||||
return 0.0;
|
||||
@@ -221,8 +222,8 @@ class BookingPriceCalculatorService
|
||||
}
|
||||
}
|
||||
|
||||
// Add service prices for this participant
|
||||
$totalPrice += $this->calculateParticipantServiceTotal($participant);
|
||||
// Add service prices for this participant (with booking context for bulk insurance)
|
||||
$totalPrice += $this->calculateParticipantServiceTotal($participant, true, $bookingDto);
|
||||
|
||||
return $totalPrice;
|
||||
}
|
||||
@@ -230,11 +231,11 @@ class BookingPriceCalculatorService
|
||||
/**
|
||||
* Calculates individual prices for all participants in a booking.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The booking data containing all participants
|
||||
* @param BookingDto $bookingDto The booking data containing all participants
|
||||
*
|
||||
* @return array Array indexed by participant index containing individual prices
|
||||
*/
|
||||
public function calculateAllParticipantIndividualPrices(BookingDtoInterface $bookingDto): array
|
||||
public function calculateAllParticipantIndividualPrices(BookingDto $bookingDto): array
|
||||
{
|
||||
if (false === $bookingDto instanceof BookingCreateDto) {
|
||||
return [];
|
||||
@@ -256,12 +257,12 @@ class BookingPriceCalculatorService
|
||||
* This method is used for insurance eligibility filtering to avoid circular dependency
|
||||
* where insurance selection affects travel price which affects insurance eligibility.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The booking data containing all participants
|
||||
* @param BookingDto $bookingDto The booking data containing all participants
|
||||
* @param int $participantIndex The index of the participant to calculate for
|
||||
*
|
||||
* @return float The total price for the specified participant excluding insurance
|
||||
*/
|
||||
public function calculateIndividualParticipantPriceExcludingInsurance(BookingDtoInterface $bookingDto, int $participantIndex): float
|
||||
public function calculateIndividualParticipantPriceExcludingInsurance(BookingDto $bookingDto, int $participantIndex): float
|
||||
{
|
||||
if (false === $bookingDto instanceof BookingCreateDto) {
|
||||
return 0.0;
|
||||
@@ -300,11 +301,11 @@ class BookingPriceCalculatorService
|
||||
*
|
||||
* Note: Transportation discounts will be handled generically by groupServicesBySubtype as "Beförderung - Rabatt"
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The booking data containing participants
|
||||
* @param BookingDto $bookingDto The booking data containing participants
|
||||
*
|
||||
* @return array Array of transportation line items (Zustieg, Parkplatz)
|
||||
*/
|
||||
private function aggregateTransportationServices(BookingDtoInterface $bookingDto): array
|
||||
private function aggregateTransportationServices(BookingDto $bookingDto): array
|
||||
{
|
||||
$participants = $bookingDto->getParticipants();
|
||||
|
||||
@@ -529,10 +530,11 @@ class BookingPriceCalculatorService
|
||||
*
|
||||
* @param ParticipantDto $participant The participant to calculate services for
|
||||
* @param bool $includeInsurance Whether to include insurance pricing (default: true)
|
||||
* @param BookingDto|null $bookingDto Optional booking DTO for bulk insurance resolution
|
||||
*
|
||||
* @return float The total service cost for this participant
|
||||
*/
|
||||
private function calculateParticipantServiceTotal(ParticipantDto $participant, bool $includeInsurance = true): float
|
||||
private function calculateParticipantServiceTotal(ParticipantDto $participant, bool $includeInsurance = true, ?BookingDto $bookingDto = null): float
|
||||
{
|
||||
$serviceTotal = 0.0;
|
||||
|
||||
@@ -545,8 +547,10 @@ class BookingPriceCalculatorService
|
||||
$serviceTotal += $participant->rentalInsurance->price;
|
||||
}
|
||||
|
||||
if ($includeInsurance && null !== $participant->insurance && null !== $participant->insurance->price) {
|
||||
$serviceTotal += $participant->insurance->price;
|
||||
// Get effective insurance (considering bulk insurance for dependent participants)
|
||||
$effectiveInsurance = $this->getEffectiveInsurance($participant, $bookingDto);
|
||||
if ($includeInsurance && null !== $effectiveInsurance && null !== $effectiveInsurance->price) {
|
||||
$serviceTotal += $effectiveInsurance->price;
|
||||
}
|
||||
|
||||
// Transportation services
|
||||
@@ -630,4 +634,40 @@ class BookingPriceCalculatorService
|
||||
$serviceAggregation[$serviceKey]['participantCount'] += $quantity;
|
||||
$serviceAggregation[$serviceKey]['totalPrice'] += $insurance->price * $quantity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the effective insurance for a participant, considering bulk insurance assignment.
|
||||
*
|
||||
* When bulk insurance is active and the participant is a dependent (index > 0),
|
||||
* returns the applicant's insurance. Otherwise returns the participant's own insurance.
|
||||
*
|
||||
* This method is used for pricing calculations to show correct prices when bulk
|
||||
* insurance is enabled, even though the actual assignment happens in the processor.
|
||||
*
|
||||
* @param ParticipantDto $participant The participant to get insurance for
|
||||
* @param BookingDto|null $bookingDto Optional booking DTO for bulk insurance check
|
||||
*
|
||||
* @return Insurance|null The effective insurance for pricing purposes
|
||||
*/
|
||||
private function getEffectiveInsurance(ParticipantDto $participant, ?BookingDto $bookingDto): ?Insurance
|
||||
{
|
||||
// If no booking context, use participant's own insurance
|
||||
if (null === $bookingDto) {
|
||||
return $participant->insurance;
|
||||
}
|
||||
|
||||
// Applicant always uses their own insurance
|
||||
if (0 === $participant->index) {
|
||||
return $participant->insurance;
|
||||
}
|
||||
|
||||
// Check if bulk insurance is active
|
||||
$applicant = $bookingDto->getParticipant(0);
|
||||
if (null === $applicant || false === $applicant->bulkInsuranceBooking) {
|
||||
return $participant->insurance;
|
||||
}
|
||||
|
||||
// Bulk insurance is active - use applicant's insurance for dependent participants
|
||||
return $applicant->insurance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\BusProNet\Model\Travel;
|
||||
use App\Exception\BookingSessionNotFoundException;
|
||||
use App\Exception\NoRoomsAvailableException;
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\RoomSelectionDto;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
@@ -15,6 +16,7 @@ class BookingService
|
||||
{
|
||||
public const BOOKING_CREATE_KEY = 'booking_create';
|
||||
public const BOOKING_CREATE_BASELINE_KEY = 'booking_create_baseline_snapshot';
|
||||
public const BOOKING_EDIT_KEY = 'booking_edit';
|
||||
|
||||
public function __construct(
|
||||
private readonly TravelDataService $travelDataService,
|
||||
@@ -81,6 +83,46 @@ class BookingService
|
||||
throw new BookingSessionNotFoundException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the booking DTO to the session.
|
||||
*
|
||||
* @param Request $request The HTTP request with session
|
||||
* @param object $bookingDto The booking DTO to persist
|
||||
* @param string $mode The booking mode (create/edit)
|
||||
*/
|
||||
public function saveBookingDto(Request $request, object $bookingDto, string $mode): void
|
||||
{
|
||||
$key = BookingDto::MODE_CREATE === $mode ? self::BOOKING_CREATE_KEY : self::BOOKING_EDIT_KEY;
|
||||
$request->getSession()->set($key, $bookingDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the booking DTO from the session.
|
||||
*
|
||||
* @param Request $request The HTTP request containing session data
|
||||
* @param string $mode The booking mode (create/edit)
|
||||
*
|
||||
* @return object|null The booking DTO from session or null if not found
|
||||
*/
|
||||
public function getBookingDto(Request $request, string $mode): ?object
|
||||
{
|
||||
$key = BookingDto::MODE_CREATE === $mode ? self::BOOKING_CREATE_KEY : self::BOOKING_EDIT_KEY;
|
||||
|
||||
return $request->getSession()->get($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the booking DTO from the session.
|
||||
*
|
||||
* @param Request $request The HTTP request with session
|
||||
* @param string $mode The booking mode (create/edit)
|
||||
*/
|
||||
public function clearBookingDto(Request $request, string $mode): void
|
||||
{
|
||||
$key = BookingDto::MODE_CREATE === $mode ? self::BOOKING_CREATE_KEY : self::BOOKING_EDIT_KEY;
|
||||
$request->getSession()->remove($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the booking creation DTO to the session.
|
||||
*
|
||||
@@ -92,7 +134,7 @@ class BookingService
|
||||
*/
|
||||
public function saveBookingCreateDto(Request $request, BookingCreateDto $bookingCreateDto): void
|
||||
{
|
||||
$request->getSession()->set(self::BOOKING_CREATE_KEY, $bookingCreateDto);
|
||||
$this->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,10 +251,10 @@ class BookingService
|
||||
*
|
||||
* @return array<int, int> an array where the key is the room ID and the value is the count of assigned participants
|
||||
*/
|
||||
public function getRoomAssignmentCounts(BookingCreateDto $bookingCreateDto): array
|
||||
public function getRoomAssignmentCounts(BookingDto $bookingDto): array
|
||||
{
|
||||
$counts = [];
|
||||
foreach ($bookingCreateDto->participants as $participant) {
|
||||
foreach ($bookingDto->getParticipants() as $participant) {
|
||||
if (null !== $participant->assignedRoomId) {
|
||||
$counts[$participant->assignedRoomId] = ($counts[$participant->assignedRoomId] ?? 0) + 1;
|
||||
}
|
||||
@@ -226,11 +268,18 @@ class BookingService
|
||||
*
|
||||
* @return array{selectedRooms: array, participantCount: int, pricing: array}
|
||||
*/
|
||||
public function getRoomSummaryAndParticipantCount(BookingCreateDto $bookingCreateDto): array
|
||||
public function getRoomSummaryAndParticipantCount(BookingDto $bookingDto): array
|
||||
{
|
||||
$selectedRooms = $bookingCreateDto->getSelectedRooms();
|
||||
$participantCount = $this->getParticipantsCount($selectedRooms, $bookingCreateDto->travel);
|
||||
$pricing = $this->priceCalculator->getPricingBreakdown($bookingCreateDto);
|
||||
$selectedRooms = $bookingDto->getSelectedRooms();
|
||||
|
||||
// In edit mode, count actual participants; in create mode, calculate from room selections
|
||||
if (BookingDto::MODE_EDIT === $bookingDto->getMode()) {
|
||||
$participantCount = count($bookingDto->getParticipants());
|
||||
} else {
|
||||
$participantCount = $this->getParticipantsCount($selectedRooms, $bookingDto->travel);
|
||||
}
|
||||
|
||||
$pricing = $this->priceCalculator->getPricingBreakdown($bookingDto);
|
||||
|
||||
return [
|
||||
'selectedRooms' => $selectedRooms,
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Service;
|
||||
use App\BusProNet\Model\Insurance;
|
||||
use App\BusProNet\Traits\SortByPriceTrait;
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Model\InsuranceEligibilityCriteria;
|
||||
use Carbon\Carbon;
|
||||
@@ -21,6 +22,7 @@ use Carbon\Carbon;
|
||||
class InsuranceMatchingService
|
||||
{
|
||||
use SortByPriceTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly BookingPriceCalculatorService $priceCalculatorService,
|
||||
) {
|
||||
@@ -31,11 +33,11 @@ class InsuranceMatchingService
|
||||
*
|
||||
* @param array<Insurance> $insurances Available insurances to filter
|
||||
* @param ParticipantDto $participant The participant to match insurances for
|
||||
* @param BookingCreateDto $booking The booking context for additional criteria
|
||||
* @param BookingDto $booking The booking context for additional criteria
|
||||
*
|
||||
* @return array<Insurance> Filtered array of eligible insurances
|
||||
*/
|
||||
public function getEligibleInsurances(array $insurances, ParticipantDto $participant, BookingCreateDto $booking): array
|
||||
public function getEligibleInsurances(array $insurances, ParticipantDto $participant, BookingDto $booking): array
|
||||
{
|
||||
$criteria = $this->createEligibilityCriteria($participant, $booking);
|
||||
|
||||
@@ -61,11 +63,11 @@ class InsuranceMatchingService
|
||||
* @param array<Insurance> $availableInsurances All available insurances
|
||||
* @param Insurance $currentInsurance The currently selected insurance
|
||||
* @param ParticipantDto $participant The participant to reassign for
|
||||
* @param BookingCreateDto $booking The booking context
|
||||
* @param BookingDto $booking The booking context
|
||||
*
|
||||
* @return Insurance|null The reassigned insurance or null if no suitable match found
|
||||
*/
|
||||
public function reassignInsuranceForPriceChange(array $availableInsurances, Insurance $currentInsurance, ParticipantDto $participant, BookingCreateDto $booking): ?Insurance
|
||||
public function reassignInsuranceForPriceChange(array $availableInsurances, Insurance $currentInsurance, ParticipantDto $participant, BookingDto $booking): ?Insurance
|
||||
{
|
||||
// Group insurances of the same type
|
||||
$sameTypeInsurances = $this->filterInsurancesByType($availableInsurances, $currentInsurance);
|
||||
@@ -90,13 +92,13 @@ class InsuranceMatchingService
|
||||
*
|
||||
* @param array<Insurance> $availableInsurances All available insurances
|
||||
* @param Insurance $selectedInsurance The insurance selected by the applicant
|
||||
* @param BookingCreateDto $booking The booking with all participants
|
||||
* @param BookingDto $booking The booking with all participants
|
||||
*
|
||||
* @return array<int, Insurance|null> Array indexed by participant index with assigned insurances
|
||||
*
|
||||
* @internal Reserved for future feature implementation
|
||||
*/
|
||||
public function batchAssignInsuranceToParticipants(array $availableInsurances, Insurance $selectedInsurance, BookingCreateDto $booking): array
|
||||
public function batchAssignInsuranceToParticipants(array $availableInsurances, Insurance $selectedInsurance, BookingDto $booking): array
|
||||
{
|
||||
$assignments = [];
|
||||
|
||||
@@ -118,7 +120,7 @@ class InsuranceMatchingService
|
||||
* This method performs early validation before creating the criteria object
|
||||
* to avoid unnecessary object instantiation when criteria cannot be satisfied.
|
||||
*/
|
||||
private function createEligibilityCriteria(ParticipantDto $participant, BookingCreateDto $booking): ?InsuranceEligibilityCriteria
|
||||
private function createEligibilityCriteria(ParticipantDto $participant, BookingDto $booking): ?InsuranceEligibilityCriteria
|
||||
{
|
||||
// Early return if travel dates are missing - cannot evaluate any criteria
|
||||
$travelStartDate = $booking->travel->dateFrom;
|
||||
@@ -183,8 +185,13 @@ class InsuranceMatchingService
|
||||
* Family insurances should only be available for family bookings,
|
||||
* and individual insurances should only be available for non-family bookings.
|
||||
*/
|
||||
private function checkFamilyInsuranceConstraints(Insurance $insurance, BookingCreateDto $booking): bool
|
||||
private function checkFamilyInsuranceConstraints(Insurance $insurance, BookingDto $booking): bool
|
||||
{
|
||||
// Family booking detection only available in BookingCreateDto
|
||||
if (!$booking instanceof BookingCreateDto) {
|
||||
return true; // Skip family constraints for edit mode
|
||||
}
|
||||
|
||||
$isFamilyBooking = $booking->isFamilyBooking();
|
||||
|
||||
// If it's a family insurance, it should only be available for family bookings
|
||||
@@ -308,12 +315,12 @@ class InsuranceMatchingService
|
||||
* It excludes insurance prices to prevent circular dependency where insurance selection
|
||||
* affects travel price which then affects insurance eligibility.
|
||||
*
|
||||
* @param BookingCreateDto $booking The booking to calculate price for
|
||||
* @param BookingDto $booking The booking to calculate price for
|
||||
* @param int $participantIndex The participant index to calculate for
|
||||
*
|
||||
* @return float The total travel price for the participant excluding insurance
|
||||
*/
|
||||
private function calculateTravelPrice(BookingCreateDto $booking, int $participantIndex): float
|
||||
private function calculateTravelPrice(BookingDto $booking, int $participantIndex): float
|
||||
{
|
||||
// Use the price calculator to get the participant's individual price excluding insurance
|
||||
return $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($booking, $participantIndex);
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace App\Service;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Spatie\Blink\Blink;
|
||||
|
||||
@@ -31,12 +31,12 @@ class ParticipantEligibilityService
|
||||
*
|
||||
* Results are cached per request to avoid redundant calculations.
|
||||
*
|
||||
* @param BookingDtoInterface $bookingDto The current booking data
|
||||
* @param BookingDto $bookingDto The current booking data
|
||||
* @param int $participantIndex The index of the participant being evaluated
|
||||
*
|
||||
* @return bool True if participant is eligible (has available skipasses)
|
||||
*/
|
||||
public function isParticipantEligible(BookingDtoInterface $bookingDto, int $participantIndex): bool
|
||||
public function isParticipantEligible(BookingDto $bookingDto, int $participantIndex): bool
|
||||
{
|
||||
$participant = $bookingDto->getParticipant($participantIndex);
|
||||
|
||||
@@ -66,7 +66,7 @@ class ParticipantEligibilityService
|
||||
/**
|
||||
* Checks if a skipass service is available for the given participant based on age constraints.
|
||||
*/
|
||||
private function isSkiPassAvailableForParticipant(Service $service, BookingDtoInterface $bookingDto, int $participantIndex): bool
|
||||
private function isSkiPassAvailableForParticipant(Service $service, BookingDto $bookingDto, int $participantIndex): bool
|
||||
{
|
||||
$participant = $bookingDto->getParticipant($participantIndex);
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ class TravelDataService
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
throw $e;
|
||||
} catch (HotelNotFoundException | HotelNotInTravelException $e) {
|
||||
} catch (HotelNotFoundException|HotelNotInTravelException $e) {
|
||||
$this->logger->debug('Hotel not found in XML', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
@@ -433,7 +433,44 @@ class TravelDataService
|
||||
*
|
||||
* @return BaseData|null The mutability data or null if not available or error occurred
|
||||
*/
|
||||
public function getMutabilityData(int $dateId): ?BaseData
|
||||
/**
|
||||
* Gets mutability data for a travel date.
|
||||
*
|
||||
* @param int $dateId The travel date ID for API call
|
||||
* @param bool $cached Whether to use cached data (default: true, TTL: 12 hours)
|
||||
*
|
||||
* @return BaseData|null The mutability data or null if not available or error occurred
|
||||
*/
|
||||
public function getMutabilityData(int $dateId, bool $cached = true): ?BaseData
|
||||
{
|
||||
if ($cached) {
|
||||
$cacheKey = sprintf('mutability_%d', $dateId);
|
||||
|
||||
try {
|
||||
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($dateId) {
|
||||
// 12 hours TTL - mutability dates have date-only granularity
|
||||
$item->expiresAfter(43200);
|
||||
|
||||
return $this->fetchMutabilityData($dateId);
|
||||
});
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$this->logger->error('Cache error in getMutabilityData', [
|
||||
'dateId' => $dateId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
// Fallback to direct API call
|
||||
return $this->fetchMutabilityData($dateId);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->fetchMutabilityData($dateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches mutability data directly from the API without caching.
|
||||
*/
|
||||
private function fetchMutabilityData(int $dateId): ?BaseData
|
||||
{
|
||||
try {
|
||||
$mutableData = $this->apiClient->getMutableData($dateId);
|
||||
@@ -482,16 +519,44 @@ class TravelDataService
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch availability data from API.
|
||||
*
|
||||
* Retrieves availability information from the API for a specific travel date.
|
||||
* Handles API errors and notification responses gracefully.
|
||||
* Gets availability data for a travel date.
|
||||
*
|
||||
* @param int $dateId The travel date ID for API call
|
||||
* @param bool $cached Whether to use cached data (default: false, TTL when cached: 60 seconds)
|
||||
* @param int $ttl Cache TTL in seconds when $cached is true (default: 60 seconds)
|
||||
*
|
||||
* @return BaseData|null The availability data or null if not available or error occurred
|
||||
*/
|
||||
public function getAvailabilityData(int $dateId): ?BaseData
|
||||
public function getAvailabilityData(int $dateId, bool $cached = false, int $ttl = 60): ?BaseData
|
||||
{
|
||||
if ($cached) {
|
||||
$cacheKey = sprintf('availability_%d', $dateId);
|
||||
|
||||
try {
|
||||
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($dateId, $ttl) {
|
||||
// Short TTL - availability is volatile and changes with bookings
|
||||
$item->expiresAfter($ttl);
|
||||
|
||||
return $this->fetchAvailabilityData($dateId);
|
||||
});
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$this->logger->error('Cache error in getAvailabilityData', [
|
||||
'dateId' => $dateId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
// Fallback to direct API call
|
||||
return $this->fetchAvailabilityData($dateId);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->fetchAvailabilityData($dateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches availability data directly from the API without caching.
|
||||
*/
|
||||
private function fetchAvailabilityData(int $dateId): ?BaseData
|
||||
{
|
||||
try {
|
||||
$availabilities = $this->apiClient->getAvailabilities($dateId);
|
||||
@@ -524,9 +589,7 @@ class TravelDataService
|
||||
/**
|
||||
* Fetch availability data with short-term caching.
|
||||
*
|
||||
* Retrieves availability information from the API with caching to reduce
|
||||
* API calls during booking form interactions. Uses a short TTL to ensure
|
||||
* reasonably fresh data while avoiding excessive API requests.
|
||||
* @deprecated Use getAvailabilityData($dateId, cached: true) instead
|
||||
*
|
||||
* @param int $dateId The travel date ID for API call
|
||||
* @param int $ttl Cache TTL in seconds (default: 60 seconds)
|
||||
@@ -535,23 +598,7 @@ class TravelDataService
|
||||
*/
|
||||
public function getAvailabilityDataCached(int $dateId, int $ttl = 60): ?BaseData
|
||||
{
|
||||
$cacheKey = sprintf('availability_%d', $dateId);
|
||||
|
||||
try {
|
||||
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($dateId, $ttl) {
|
||||
$item->expiresAfter($ttl);
|
||||
|
||||
return $this->getAvailabilityData($dateId);
|
||||
});
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$this->logger->error('Cache error in getAvailabilityDataCached', [
|
||||
'dateId' => $dateId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
// Fallback to direct API call
|
||||
return $this->getAvailabilityData($dateId);
|
||||
}
|
||||
return $this->getAvailabilityData($dateId, cached: true, ttl: $ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -606,7 +653,8 @@ class TravelDataService
|
||||
{
|
||||
try {
|
||||
$insurances = $this->insuranceLoader->loadAll();
|
||||
$travel->insurances = array_values($insurances);
|
||||
// Keep insurances indexed by ID for efficient lookups
|
||||
$travel->insurances = $insurances;
|
||||
|
||||
// Hydrate package relationships after loading
|
||||
// Packages lose their containedInsurances during serialization, so rebuild them
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Twig;
|
||||
|
||||
use App\BusProNet\DataProvider\CountryDataProvider;
|
||||
use App\Form\Model\BookingDtoInterface;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Service\ParticipantEligibilityService;
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\RuntimeExtensionInterface;
|
||||
@@ -97,7 +97,7 @@ class AppRuntime implements RuntimeExtensionInterface
|
||||
return $this->countryDataProvider->get($nationality)?->nationality;
|
||||
}
|
||||
|
||||
public function isParticipantEligible(BookingDtoInterface $bookingDto, int $participantIndex): bool
|
||||
public function isParticipantEligible(BookingDto $bookingDto, int $participantIndex): bool
|
||||
{
|
||||
return $this->participantEligibilityService->isParticipantEligible($bookingDto, $participantIndex);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
#[\Attribute]
|
||||
class ApplicantAddress extends Constraint
|
||||
{
|
||||
public function getTargets(): string
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
class ApplicantAddressValidator extends ConstraintValidator
|
||||
{
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
/** @var ParticipantDto $participant */
|
||||
$participant = $value;
|
||||
|
||||
// Only validate for the first participant (applicant in create mode)
|
||||
if (false === $participant->isApplicant()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure address object exists
|
||||
if (null === $participant->address) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate required address fields
|
||||
if (null === $participant->address->street || '' === trim($participant->address->street)) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('address.street')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
|
||||
if (null === $participant->address->postCode || '' === trim($participant->address->postCode)) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('address.postCode')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
|
||||
if (null === $participant->address->city || '' === trim($participant->address->city)) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('address.city')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
|
||||
if (null === $participant->address->country || '' === trim($participant->address->country)) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('address.country')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
|
||||
// Mobile/phone is mandatory for applicant in create mode
|
||||
if (null === $participant->mobile || '' === trim($participant->mobile)) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('mobile')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ class ParticipantValidator extends ConstraintValidator
|
||||
$this->assertBodyMeasurementsValid($participant);
|
||||
$this->assertTransportationSelected($participant);
|
||||
$this->assertPickupSelected($participant);
|
||||
$this->assertApplicantAddressValid($participant);
|
||||
}
|
||||
|
||||
public function assertBodyMeasurementsValid(ParticipantDto $participant): void
|
||||
@@ -68,49 +67,4 @@ class ParticipantValidator extends ConstraintValidator
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
public function assertApplicantAddressValid(ParticipantDto $participant): void
|
||||
{
|
||||
// Address is only mandatory for the applicant (first participant)
|
||||
if (false === $participant->isApplicant()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check each required address field for applicant
|
||||
if (null === $participant->address->street || '' === trim($participant->address->street)) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('address.street')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
|
||||
if (null === $participant->address->postCode || '' === trim($participant->address->postCode)) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('address.postCode')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
|
||||
if (null === $participant->address->city || '' === trim($participant->address->city)) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('address.city')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
|
||||
if (null === $participant->address->country || '' === trim($participant->address->country)) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('address.country')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
|
||||
// Mobile/phone is mandatory for applicant
|
||||
if (null === $participant->mobile || '' === trim($participant->mobile)) {
|
||||
$this->context->buildViolation('Bitte angeben')
|
||||
->atPath('mobile')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,48 @@
|
||||
<div class="booking-summary p-8 bg-gray-50 rounded-lg border sticky top-4">
|
||||
<h3 class="font-bold text-xl mb-6 text-gray-800">Buchungsübersicht</h3>
|
||||
|
||||
{# Mutability information (edit mode only) #}
|
||||
{% if mutableData is defined %}
|
||||
<div class="mb-6 pb-4 border-b border-gray-200">
|
||||
<h4 class="font-semibold text-lg mb-3 text-gray-800">Aktualisierung möglich bis</h4>
|
||||
<ul class="space-y-1 text-sm text-gray-600">
|
||||
<li class="flex items-start">
|
||||
<span class="mr-2">•</span>
|
||||
<span>
|
||||
<span class="font-medium">Zusatzleistungen:</span>
|
||||
{% if mutableData.items['additional_services'].mutable %}
|
||||
{{ mutableData.items['additional_services'].mutableBefore|date('d.m.Y') }}
|
||||
{% else %}
|
||||
<span class="text-red-600">nicht mehr möglich</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="mr-2">•</span>
|
||||
<span>
|
||||
<span class="font-medium">Beförderung:</span>
|
||||
{% if mutableData.items['transportation'].mutable %}
|
||||
{{ mutableData.items['transportation'].mutableBefore|date('d.m.Y') }}
|
||||
{% else %}
|
||||
<span class="text-red-600">nicht mehr möglich</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
<span class="mr-2">•</span>
|
||||
<span>
|
||||
<span class="font-medium">Zustiege:</span>
|
||||
{% if mutableData.items['pickup'].mutable %}
|
||||
{{ mutableData.items['pickup'].mutableBefore|date('d.m.Y') }}
|
||||
{% else %}
|
||||
<span class="text-red-600">nicht mehr möglich</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Travel Information #}
|
||||
<div class="mb-6 pb-4 border-b border-gray-200">
|
||||
<h4 class="font-semibold text-lg mb-3 text-gray-800">Reiseinformationen</h4>
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<fieldset>
|
||||
<legend class="w-full flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-bold text-xl">Teilnehmer:in {{ loop.index }}</span>
|
||||
<span class="font-bold text-xl">{{ loop.index == 1 ? 'Anmelder:in' : 'Teilnehmer:in ' ~ loop.index }}</span>
|
||||
{% if isEligible and participantPrices is defined and participantPrices[loop.index0] is defined and participantPrices[loop.index0] > 0.0 %}
|
||||
<span class="text-sm font-medium text-gray-600 bg-gray-100 px-2 py-1 rounded">
|
||||
€{{ participantPrices[loop.index0]|number_format(2, ',', '.') }}
|
||||
@@ -213,10 +213,12 @@
|
||||
<fieldset class="mb-1">
|
||||
<legend class="font-semibold mb-1">Reiseversicherung</legend>
|
||||
<div class="text-sm text-gray-600">
|
||||
{% if participantData.insurance %}
|
||||
{{ participantData.insurance.label }}
|
||||
{% if participantData.insurance.price and participantData.insurance.price > 0 %}
|
||||
<span class="text-gray-500">(€{{ participantData.insurance.price|number_format(2, ',', '.') }})</span>
|
||||
{% set applicantInsurance = form.vars.data.participants[0].insurance %}
|
||||
{% if applicantInsurance %}
|
||||
{{ applicantInsurance.label }}
|
||||
{% set participantPrice = participantPrices[loop.index0] %}
|
||||
{% if participantPrice.insurance and participantPrice.insurance > 0 %}
|
||||
<span class="text-gray-500">(€{{ participantPrice.insurance|number_format(2, ',', '.') }})</span>
|
||||
{% endif %}
|
||||
<span class="italic text-gray-500 ml-2">– wie Anmelder</span>
|
||||
{% else %}
|
||||
|
||||
@@ -53,104 +53,7 @@
|
||||
{% block content %}
|
||||
{% include '_partials/_flashes.html.twig' %}
|
||||
<div {{ stimulus_controller('toast') }}></div>
|
||||
<h1 class="text-3xl font-semibold pb-2">Buchung bearbeiten</h1>
|
||||
|
||||
{# Booking metadata section #}
|
||||
<div class="flex flex-col space-y-8 pb-8">
|
||||
<div class="p-8 border border-gray-300 rounded-md"
|
||||
{{ stimulus_controller('toggle', { 'open': true }, { 'closed': 'hidden' }) }}>
|
||||
<div role="button" tabindex="0" class="flex items-center justify-between focus:outline-2 focus:outline-offset-2 focus:outline-primary-light" {{ stimulus_action('toggle', 'toggle', 'click') | stimulus_action('toggle', 'toggle', 'keydown.space') }}>
|
||||
<span class="block text-2xl font-semibold">Reisedaten</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6" {{ stimulus_target('toggle', 'icon') }}>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-x-8 gap-y-4 pt-4" {{ stimulus_target('toggle', 'toggle') }}>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">
|
||||
Reise
|
||||
</h2>
|
||||
{{ bookingData.travelName }}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">
|
||||
Reisezeitraum
|
||||
</h3>
|
||||
{{ travelData.dateFrom|date('d.m.Y') }} - {{ travelData.dateTo|date('d.m.Y') }}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">
|
||||
Buchungsdatum
|
||||
</h3>
|
||||
{{ bookingData.bookingDate|date('d.m.Y') }}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">
|
||||
Vorgangsnummer
|
||||
</h3>
|
||||
{{ bookingData.bookingNumber }}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">
|
||||
Rechnungsnummer
|
||||
</h3>
|
||||
{{ bookingData.invoiceNumber }}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">
|
||||
Anmelder:in
|
||||
</h3>
|
||||
{{ bookingData.applicant.firstName }} {{ bookingData.applicant.name }}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">
|
||||
Anzahl Teilnehmer:innen
|
||||
</h3>
|
||||
{{ bookingData.participants|length }}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">
|
||||
Gesamtpreis
|
||||
</h3>
|
||||
{{ bookingData.calculatedTotalPrice|format_currency('EUR') }}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">
|
||||
Inklusivleistungen
|
||||
</h3>
|
||||
{% if travelData.includedServices | length == 0 %}
|
||||
-
|
||||
{% else %}
|
||||
<ul class="list-disc pl-4">
|
||||
{% for selectionGroup in travelData.includedServices %}
|
||||
{% for service in selectionGroup.selections %}
|
||||
<li>
|
||||
{{ service.label }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">
|
||||
Aktualisierung möglich bis
|
||||
</h3>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>
|
||||
Zusatzleistungen: {% if mutableData.items['additional_services'].mutable %}{{ mutableData.items['additional_services'].mutableBefore|date('d.m.Y') }}{% else %}nicht mehr möglich{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
Beförderung: {% if mutableData.items['transportation'].mutable %}{{ mutableData.items['transportation'].mutableBefore|date('d.m.Y') }}{% else %}nicht mehr möglich{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
Zustiege: {% if mutableData.items['pickup'].mutable %}{{ mutableData.items['pickup'].mutableBefore|date('d.m.Y') }}{% else %}nicht mehr möglich{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-3xl font-semibold pb-8">Buchung bearbeiten</h1>
|
||||
|
||||
{# Grid layout with 2/3 form + 1/3 summary #}
|
||||
<div class="grid grid-cols-3 gap-8">
|
||||
@@ -172,7 +75,7 @@
|
||||
<fieldset>
|
||||
<legend class="w-full flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="font-bold text-xl">Teilnehmer:in {{ loop.index }}</span>
|
||||
<span class="font-bold text-xl">{{ loop.index == 1 ? 'Anmelder:in' : 'Teilnehmer:in ' ~ loop.index }}</span>
|
||||
{% if isCanceled %}
|
||||
<span class="inline-block px-2 py-1 text-xs bg-red-700 text-white rounded">storniert</span>
|
||||
{% endif %}
|
||||
@@ -207,7 +110,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{# Personal data section #}
|
||||
{# Personal data section - readonly for applicant (edited via profile settings) #}
|
||||
<div class="grid grid-cols-2 gap-4 pb-4">
|
||||
{{ form_row(participant.firstName) }}
|
||||
{{ form_row(participant.lastName) }}
|
||||
@@ -223,8 +126,29 @@
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{ form_row(participant.email) }}
|
||||
{% if loop.index0 == 0 and bookingData.applicant.communication and bookingData.applicant.communication.mobile %}
|
||||
{# First participant: use applicant's mobile as placeholder #}
|
||||
{{ form_row(participant.mobile, {'attr': {'placeholder': bookingData.applicant.communication.mobile}}) }}
|
||||
{% else %}
|
||||
{{ form_row(participant.mobile) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if participant.address is defined %}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{% if loop.index0 == 0 and bookingData.applicant.address %}
|
||||
{# First participant: use applicant's address as placeholder #}
|
||||
{{ form_row(participant.address.street, {'attr': {'placeholder': bookingData.applicant.address.street}}) }}
|
||||
{{ form_row(participant.address.postCode, {'attr': {'placeholder': bookingData.applicant.address.postCode}}) }}
|
||||
{{ form_row(participant.address.city, {'attr': {'placeholder': bookingData.applicant.address.city}}) }}
|
||||
{{ form_row(participant.address.country, {'attr': {'placeholder': bookingData.applicant.address.country}}) }}
|
||||
{% else %}
|
||||
{{ form_row(participant.address.street) }}
|
||||
{{ form_row(participant.address.postCode) }}
|
||||
{{ form_row(participant.address.city) }}
|
||||
{{ form_row(participant.address.country) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if participant.bodyDimensions is defined %}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{ form_row(participant.bodyDimensions.height) }}
|
||||
@@ -317,6 +241,54 @@
|
||||
}
|
||||
}) }}
|
||||
|
||||
<div class="col-span-2">
|
||||
{# Insurance field OR assigned insurance display for dependent participants #}
|
||||
{% set participantData = participant.vars.data %}
|
||||
{% set showBulkInsurance = loop.index > 1 and form.vars.data.participants[0].bulkInsuranceBooking %}
|
||||
|
||||
{% if showBulkInsurance %}
|
||||
<fieldset class="mb-1">
|
||||
<legend class="font-semibold mb-1">Reiseversicherung</legend>
|
||||
<div class="text-sm text-gray-600">
|
||||
{% set applicantInsurance = form.vars.data.participants[0].insurance %}
|
||||
{% if applicantInsurance %}
|
||||
<span class="italic text-gray-500">wie Anmelder: {{ applicantInsurance.label }}</span>
|
||||
{% else %}
|
||||
<span class="italic text-gray-500">wie Anmelder</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</fieldset>
|
||||
{% else %}
|
||||
<fieldset class="mb-1">
|
||||
<legend class="font-semibold mb-1">Reiseversicherung</legend>
|
||||
|
||||
{# Bulk insurance booking checkbox (applicant only) #}
|
||||
{% if participant.bulkInsuranceBooking is defined %}
|
||||
{{ form_row(participant.bulkInsuranceBooking, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path('app_booking_edit_refresh', {'id': bookingData.id}),
|
||||
'hx-swap': 'none'
|
||||
}
|
||||
}) }}
|
||||
{% endif %}
|
||||
|
||||
{% if participant.insurance is defined %}
|
||||
{{ form_row(participant.insurance, {
|
||||
'attr': {
|
||||
'hx-trigger': 'change',
|
||||
'hx-post': path('app_booking_edit_refresh', {'id': bookingData.id}),
|
||||
'hx-swap': 'none'
|
||||
},
|
||||
'label': false
|
||||
}) }}
|
||||
{% else %}
|
||||
<div class="text-sm text-gray-500">Nicht wählbar</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<h3 class="text-xl font-semibold mb-4">Hin-/Rückreise</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
@@ -401,7 +373,8 @@
|
||||
'bookingCreateDto': bookingEditDto,
|
||||
'participantCount': participantCount,
|
||||
'groupedSelectedRooms': groupedSelectedRooms,
|
||||
'assignmentCounts': assignmentCounts
|
||||
'assignmentCounts': assignmentCounts,
|
||||
'mutableData': mutableData|default(null)
|
||||
} %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user