wip: refactor to cards with individual participant forms
This commit is contained in:
+289
-179
@@ -4,7 +4,81 @@
|
|||||||
|
|
||||||
Transform create (step 2) and edit flows from accordion-style "all forms at once" to a card overview with lazy-loaded individual participant forms. This addresses UX and performance issues with large groups (50+ participants).
|
Transform create (step 2) and edit flows from accordion-style "all forms at once" to a card overview with lazy-loaded individual participant forms. This addresses UX and performance issues with large groups (50+ participants).
|
||||||
|
|
||||||
**Status**: Planning Complete - Ready for Implementation
|
**Status**: Planning Complete - Ready for Implementation (SIMPLIFIED ARCHITECTURE - 2025-01-14)
|
||||||
|
|
||||||
|
## Architecture Revision (2025-01-14)
|
||||||
|
|
||||||
|
**Major Simplification**: Removed wrapper forms and DTOs!
|
||||||
|
|
||||||
|
**What Changed:**
|
||||||
|
- ❌ Removed: `ParticipantFormDto` - not needed
|
||||||
|
- ❌ Removed: `ParticipantFormType` wrapper - not needed
|
||||||
|
- ✅ Simplified: Pass `BookingDto` via form options (`booking_context`)
|
||||||
|
- ✅ Simplified: Reuse `BookingParticipantType` directly
|
||||||
|
- ✅ Simplified: Field handlers called manually in controller
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
The initial plan had unnecessary abstraction layers. We realized we can simply pass the `BookingDto` as a form option instead of wrapping everything. This is cleaner, more explicit, and easier to understand.
|
||||||
|
|
||||||
|
## Implementation Discovery: Field Handler Mode Awareness (2025-01-14)
|
||||||
|
|
||||||
|
**Problem Encountered:**
|
||||||
|
When promoting participant forms to standalone (autonomous) forms in the card flow, field handlers needed to be called from within `BookingParticipantType`'s PRE_SUBMIT event. This created a critical issue in edit mode:
|
||||||
|
|
||||||
|
- The BPN API **does not return insurance data** for privacy/security reasons
|
||||||
|
- In edit mode, the insurance field handler would see missing insurance data in submitted forms
|
||||||
|
- The handler would incorrectly interpret this as "user wants to clear insurance"
|
||||||
|
- Result: Insurance data would be lost during edit operations
|
||||||
|
|
||||||
|
**Root Cause:**
|
||||||
|
Field handlers had no way to distinguish between:
|
||||||
|
1. **Create mode**: Missing data = user didn't select anything (clear it)
|
||||||
|
2. **Edit mode**: Missing data = API didn't provide it (preserve existing value)
|
||||||
|
|
||||||
|
**Solution Implemented:**
|
||||||
|
Added mode awareness to the field handler system:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Interface change
|
||||||
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool;
|
||||||
|
|
||||||
|
// Insurance handler implementation
|
||||||
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||||
|
{
|
||||||
|
// Skip processing in edit mode - API doesn't return insurance data
|
||||||
|
if ($mode === BookingDto::MODE_EDIT) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process normally in create mode
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Architecture Changes:**
|
||||||
|
- ✅ Added `$mode` parameter to `ParticipantFieldHandlerInterface::shouldProcess()`
|
||||||
|
- ✅ Updated `AbstractParticipantFieldHandler` with mode parameter
|
||||||
|
- ✅ Added helper methods: `isEditMode()`, `isCreateMode()` for future use
|
||||||
|
- ✅ Updated `ParticipantFieldHandlerRegistry` to pass `$bookingDto->mode` to handlers
|
||||||
|
- ✅ Updated all 13+ field handlers to accept mode parameter
|
||||||
|
- ✅ Insurance handler skips processing entirely in edit mode
|
||||||
|
- ✅ All other handlers continue processing normally in both modes
|
||||||
|
|
||||||
|
**Why Only Insurance Needs Special Treatment:**
|
||||||
|
- **Insurance**: API doesn't return data → must skip processing in edit mode
|
||||||
|
- **Services** (ski pass, rentals, courses): API returns data → process normally
|
||||||
|
- **Transportation**: API returns data → process normally
|
||||||
|
- **Room assignments**: API returns data → process normally
|
||||||
|
- **All other fields**: API returns data → process normally
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ Insurance data preserved correctly in edit mode
|
||||||
|
- ✅ Explicit mode-aware behavior (no implicit assumptions)
|
||||||
|
- ✅ Clean separation: only handlers that need it check mode
|
||||||
|
- ✅ Future-proof: easy to add mode-specific logic to other handlers if needed
|
||||||
|
- ✅ Tests updated to verify edit mode behavior
|
||||||
|
|
||||||
|
**Implementation Status:** ✅ **COMPLETED** (2025-01-14)
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
@@ -26,6 +100,27 @@ Transform create (step 2) and edit flows from accordion-style "all forms at once
|
|||||||
| **Room Assignment** | Keep in participant form | Existing behavior maintained |
|
| **Room Assignment** | Keep in participant form | Existing behavior maintained |
|
||||||
| **Migration Strategy** | Separate controllers (-v2 routes) | Safe parallel development, easy comparison |
|
| **Migration Strategy** | Separate controllers (-v2 routes) | Safe parallel development, easy comparison |
|
||||||
| **Lazy Loading** | Forms loaded via HTMX on demand | Critical for performance with 50+ participants |
|
| **Lazy Loading** | Forms loaded via HTMX on demand | Critical for performance with 50+ participants |
|
||||||
|
| **BookingDto Context** | Pass via form options | Simple, explicit, no wrappers needed |
|
||||||
|
|
||||||
|
## Key Simplification
|
||||||
|
|
||||||
|
**No wrapper forms or DTOs needed!** We pass `BookingDto` directly via form options:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Controller creates form with explicit context
|
||||||
|
$form = $this->createForm(BookingParticipantType::class, $participant, [
|
||||||
|
'booking_context' => $bookingDto, // Passed explicitly
|
||||||
|
'edit_mode' => false,
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ No `ParticipantFormDto` wrapper
|
||||||
|
- ✅ No `ParticipantFormType` wrapper
|
||||||
|
- ✅ Reuse existing `BookingParticipantType` directly
|
||||||
|
- ✅ Explicit data flow (no "magic" form tree traversal)
|
||||||
|
- ✅ Field handlers called manually in controller (like current Step 2)
|
||||||
|
- ✅ Clean, maintainable, easy to understand
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -114,198 +209,193 @@ public function processFieldsForParticipant(
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1.3 Create `ParticipantFormDto`
|
### 1.3 Update `BookingParticipantType` to Accept `booking_context` Option
|
||||||
|
|
||||||
**File**: `src/Form/Model/ParticipantFormDto.php`
|
**File**: `src/Form/BookingParticipantType.php`
|
||||||
|
|
||||||
**Purpose**: Dedicated DTO for editing a single participant with full booking context
|
**Purpose**: Make form type autonomous - it processes its own field handlers when used standalone
|
||||||
|
|
||||||
**Why This Is Needed**:
|
**Why This Simplification**:
|
||||||
The existing `BookingParticipantType` uses `getBookingDtoFromForm()` which traverses up the form tree to find a parent form with `BookingDto` as its data. This DTO provides that context while maintaining clean separation between the booking flow state and individual participant editing.
|
In the card flow, there's no parent form to orchestrate field handler execution. The form type becomes **self-contained and autonomous** - it receives `BookingDto` via options and processes its own field handlers in PRE_SUBMIT.
|
||||||
|
|
||||||
**Architecture Benefits**:
|
**Architecture Benefits**:
|
||||||
- ✅ Clean separation: `BookingDto` = full booking flow state, `ParticipantFormDto` = single participant editing context
|
- ✅ No wrapper forms needed
|
||||||
- ✅ No pollution of BookingDto with editing-specific properties
|
- ✅ No extra DTOs needed
|
||||||
- ✅ Type-safe and explicit relationships
|
- ✅ No parent form needed
|
||||||
- ✅ Participant has full booking context for conditional logic
|
- ✅ Explicit data flow via options
|
||||||
- ✅ Field state providers work unchanged
|
- ✅ Field handlers called automatically in PRE_SUBMIT
|
||||||
|
- ✅ Controller stays thin - just creates form and handles result
|
||||||
|
- ✅ Form type is responsible for its own data processing
|
||||||
|
|
||||||
**Implementation**:
|
**Key Insight**: We're **promoting the child form to be in charge**. It's no longer a dumb child controlled by a parent - it's a smart, autonomous form that handles everything itself.
|
||||||
|
|
||||||
|
**Modification to `__construct()` - Add Field Handler Registry**:
|
||||||
|
|
||||||
```php
|
```php
|
||||||
|
public function __construct(
|
||||||
|
private readonly FieldOptionsProviderInterface $fieldOptionsProvider,
|
||||||
|
private readonly CreateFieldStateProvider $createFieldStateProvider,
|
||||||
|
private readonly EditFieldStateProvider $editFieldStateProvider,
|
||||||
|
private readonly ParticipantFieldHandlerRegistry $fieldHandlerRegistry, // NEW: for autonomous processing
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Modification to `configureOptions()`**:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => ParticipantDto::class,
|
||||||
|
'selected_rooms' => [],
|
||||||
|
'edit_mode' => false,
|
||||||
|
'booking_context' => null, // NEW: Optional BookingDto for card flows
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resolver->setAllowedTypes('selected_rooms', 'array');
|
||||||
|
$resolver->setAllowedTypes('edit_mode', 'bool');
|
||||||
|
$resolver->setAllowedTypes('booking_context', ['null', BookingDto::class]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Modification to `buildForm()`**:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
// Select field state provider based on edit_mode option
|
||||||
|
$this->fieldStateProvider = $options['edit_mode']
|
||||||
|
? $this->editFieldStateProvider
|
||||||
|
: $this->createFieldStateProvider;
|
||||||
|
|
||||||
|
// Capture booking context for use in event listeners
|
||||||
|
$bookingContext = $options['booking_context'];
|
||||||
|
|
||||||
|
$builder
|
||||||
|
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($bookingContext) {
|
||||||
|
$this->onPreSetData($event, $bookingContext);
|
||||||
|
})
|
||||||
|
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($bookingContext) {
|
||||||
|
// Process field handlers FIRST (before form binding and validation)
|
||||||
|
// This ensures data is cleaned before Symfony processes it
|
||||||
|
if (null !== $bookingContext) {
|
||||||
|
$this->processFieldHandlers($event, $bookingContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then rebuild fields with updated states
|
||||||
|
$this->onPreSubmit($event, $bookingContext);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DTO for editing a single participant within a booking context.
|
* Process field handlers for this participant.
|
||||||
*
|
*
|
||||||
* This DTO encapsulates the participant being edited along with the full
|
* Field handlers are executed in PRE_SUBMIT to clean and transform data
|
||||||
* booking context needed for field state evaluation, pricing calculations,
|
* before Symfony binds it to the form. This matches the pattern used in
|
||||||
* and conditional logic.
|
* the old BookingCreateStep2Type parent form.
|
||||||
*/
|
*/
|
||||||
class ParticipantFormDto
|
private function processFieldHandlers(FormEvent $event, BookingDto $bookingContext): void
|
||||||
{
|
{
|
||||||
public function __construct(
|
$form = $event->getForm();
|
||||||
public BookingDto $bookingContext,
|
$submittedData = $event->getData();
|
||||||
public ParticipantDto $participant,
|
|
||||||
) {}
|
if (false === is_array($submittedData)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var ParticipantDto $participant */
|
||||||
|
$participant = $form->getData();
|
||||||
|
|
||||||
|
if (null === $participant || false === property_exists($participant, 'index')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process all field handlers for this participant in dependency order
|
||||||
|
$this->fieldHandlerRegistry->processFieldsForParticipant(
|
||||||
|
$submittedData,
|
||||||
|
$bookingContext,
|
||||||
|
$participant->index
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function onPreSetData(FormEvent $event, ?BookingDto $bookingContext): void
|
||||||
|
{
|
||||||
|
/** @var ParticipantDto|null $participantData */
|
||||||
|
$participantData = $event->getData();
|
||||||
|
$form = $event->getForm();
|
||||||
|
|
||||||
|
if (null === $participantData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card flow: BookingDto passed via options
|
||||||
|
// Accordion flow (if we had one): traverse form tree
|
||||||
|
$bookingDto = $bookingContext ?? $this->fieldStateProvider->getBookingDtoFromForm($form);
|
||||||
|
|
||||||
|
if (null === $bookingDto) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add base fields with states applied
|
||||||
|
$this->addBaseFields($form, $bookingDto, $participantData->index);
|
||||||
|
|
||||||
|
// Add dynamic fields
|
||||||
|
$this->addDynamicFields($form, $bookingDto, $participantData->index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function onPreSubmit(FormEvent $event, ?BookingDto $bookingContext): void
|
||||||
|
{
|
||||||
|
$submittedData = $event->getData();
|
||||||
|
$form = $event->getForm();
|
||||||
|
|
||||||
|
if (false === is_array($submittedData)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card flow: BookingDto passed via options
|
||||||
|
// Accordion flow (if we had one): traverse form tree
|
||||||
|
$bookingDto = $bookingContext ?? $this->fieldStateProvider->getBookingDtoFromForm($form);
|
||||||
|
|
||||||
|
if (null === $bookingDto) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get participant index from form data
|
||||||
|
$participantData = $form->getData();
|
||||||
|
if (null === $participantData || false === property_exists($participantData, 'index')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild all fields with updated states based on submitted data
|
||||||
|
$this->rebuildFieldsWithStates($form, $bookingDto, $participantData->index, $submittedData);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key Points**:
|
**Usage in Controllers**:
|
||||||
- `bookingContext` provides full booking state for field conditions
|
|
||||||
- `participant` is the participant being edited
|
|
||||||
- Participant index available via `$participant->index` (no separate property needed)
|
|
||||||
|
|
||||||
**Todos**:
|
|
||||||
- [ ] Create DTO class in `src/Form/Model/`
|
|
||||||
- [ ] Add constructor with type hints
|
|
||||||
- [ ] Write unit tests for DTO instantiation
|
|
||||||
- [ ] Test with field state providers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 1.4 Create `ParticipantFormType`
|
|
||||||
|
|
||||||
**File**: `src/Form/ParticipantFormType.php`
|
|
||||||
|
|
||||||
**Purpose**: Form type for editing a single participant with automatic field handler processing
|
|
||||||
|
|
||||||
**Implementation**:
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
class ParticipantFormType extends AbstractType
|
// Card flow: Pass booking context explicitly
|
||||||
{
|
$form = $this->createForm(BookingParticipantType::class, $participant, [
|
||||||
public function __construct(
|
'booking_context' => $bookingDto,
|
||||||
private readonly ParticipantFieldHandlerRegistry $participantFieldHandlerRegistry,
|
'edit_mode' => false,
|
||||||
) {}
|
]);
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
|
||||||
{
|
|
||||||
$builder
|
|
||||||
->add('participant', BookingParticipantType::class, [
|
|
||||||
'edit_mode' => $options['edit_mode'],
|
|
||||||
])
|
|
||||||
->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'onPreSubmit']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process field handlers before form binding and validation.
|
|
||||||
*
|
|
||||||
* This follows the same pattern as BookingCreateStep2Type - field handlers
|
|
||||||
* are called during PRE_SUBMIT to clean and validate data before binding.
|
|
||||||
*/
|
|
||||||
public function onPreSubmit(FormEvent $event): void
|
|
||||||
{
|
|
||||||
$form = $event->getForm();
|
|
||||||
$submittedData = $event->getData();
|
|
||||||
|
|
||||||
/** @var ParticipantFormDto $participantFormDto */
|
|
||||||
$participantFormDto = $form->getData();
|
|
||||||
$bookingDto = $participantFormDto->bookingContext;
|
|
||||||
$participantIndex = $participantFormDto->participant->index;
|
|
||||||
|
|
||||||
// Extract participant data from submitted form
|
|
||||||
$participantSubmittedData = $submittedData['participant'] ?? [];
|
|
||||||
|
|
||||||
// Process field handlers for this single participant
|
|
||||||
$this->participantFieldHandlerRegistry->processFieldsForParticipant(
|
|
||||||
$participantSubmittedData,
|
|
||||||
$bookingDto,
|
|
||||||
$participantIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
// Note: No need to sync data back - the DTO is updated by reference
|
|
||||||
// and handleRequest() will bind the updated values
|
|
||||||
}
|
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver): void
|
|
||||||
{
|
|
||||||
$resolver->setDefaults([
|
|
||||||
'data_class' => ParticipantFormDto::class,
|
|
||||||
'edit_mode' => false,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$resolver->setAllowedTypes('edit_mode', 'bool');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key Points**:
|
|
||||||
- Field handlers called automatically in PRE_SUBMIT event (matches existing pattern in `BookingCreateStep2Type`)
|
|
||||||
- Controller doesn't need to call field handlers manually
|
|
||||||
- Uses `processFieldsForParticipant()` to process only the current participant
|
|
||||||
- Root form data is `ParticipantFormDto` which provides `BookingDto` context via `bookingContext` property
|
|
||||||
|
|
||||||
**Todos**:
|
**Todos**:
|
||||||
- [ ] Create form type with DI for field handler registry
|
- [ ] Add `ParticipantFieldHandlerRegistry` to constructor (DI)
|
||||||
- [ ] Implement buildForm with participant field
|
- [ ] Add `booking_context` option to `configureOptions()`
|
||||||
- [ ] Implement onPreSubmit event listener
|
- [ ] Update `buildForm()` to capture `booking_context` and add PRE_SUBMIT listener
|
||||||
- [ ] Add proper PHPDoc
|
- [ ] Add `processFieldHandlers()` private method to call registry
|
||||||
- [ ] Test field handler execution
|
- [ ] Update `onPreSetData()` to accept and use `$bookingContext` parameter
|
||||||
- [ ] Test validation behavior
|
- [ ] Update `onPreSubmit()` to accept and use `$bookingContext` parameter
|
||||||
|
- [ ] Test with card flow (booking_context provided)
|
||||||
---
|
- [ ] Verify field handlers execute automatically
|
||||||
|
|
||||||
### 1.5 Update `FormTraversalTrait`
|
|
||||||
|
|
||||||
**File**: `src/Form/Service/Trait/FormTraversalTrait.php`
|
|
||||||
|
|
||||||
**Purpose**: Support both `BookingDto` and `ParticipantFormDto` as root form data
|
|
||||||
|
|
||||||
**Modification**:
|
|
||||||
|
|
||||||
```php
|
|
||||||
trait FormTraversalTrait
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Gets the BookingDto from the root of the form tree.
|
|
||||||
*
|
|
||||||
* Supports both direct BookingDto (used in current flows) and
|
|
||||||
* ParticipantFormDto wrapper (used in card-based flows).
|
|
||||||
*
|
|
||||||
* @param FormInterface $form The form to start traversing from
|
|
||||||
*
|
|
||||||
* @return BookingDto|null The booking DTO or null if not found
|
|
||||||
*/
|
|
||||||
public function getBookingDtoFromForm(FormInterface $form): ?BookingDto
|
|
||||||
{
|
|
||||||
// Traverse up the form tree to get the root form's data
|
|
||||||
$rootForm = $form;
|
|
||||||
while ($rootForm->getParent()) {
|
|
||||||
$rootForm = $rootForm->getParent();
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $rootForm->getData();
|
|
||||||
|
|
||||||
// Direct BookingDto (used in current flows)
|
|
||||||
if ($data instanceof BookingDto) {
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParticipantFormDto wrapper (used in card-based flows)
|
|
||||||
if ($data instanceof ParticipantFormDto) {
|
|
||||||
return $data->bookingContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why This Works**:
|
|
||||||
- Existing flows use `BookingDto` directly → no changes needed
|
|
||||||
- New card flows use `ParticipantFormDto` → extracts `bookingContext`
|
|
||||||
- Field state providers work with both approaches seamlessly
|
|
||||||
- `BookingParticipantType` remains completely unchanged
|
|
||||||
|
|
||||||
**Todos**:
|
|
||||||
- [ ] Add `ParticipantFormDto` support to trait
|
|
||||||
- [ ] Add `use` statement for `ParticipantFormDto`
|
|
||||||
- [ ] Test with existing flows (should work unchanged)
|
|
||||||
- [ ] Test with new card flows
|
|
||||||
- [ ] Verify field state providers work correctly
|
- [ ] Verify field state providers work correctly
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 1.6 Create `ParticipantCardFlowTrait`
|
### 1.4 Create `ParticipantCardFlowTrait`
|
||||||
|
|
||||||
**File**: `src/Controller/Booking/ParticipantCardFlowTrait.php`
|
**File**: `src/Controller/Booking/ParticipantCardFlowTrait.php`
|
||||||
|
|
||||||
@@ -415,23 +505,35 @@ Implementation steps:
|
|||||||
Implementation steps:
|
Implementation steps:
|
||||||
1. Load BookingDto from session
|
1. Load BookingDto from session
|
||||||
2. Validate participant index
|
2. Validate participant index
|
||||||
3. Create form for `$bookingDto->participants[$index]` using `BookingParticipantType`
|
3. Create form for `$bookingDto->participants[$index]` using `BookingParticipantType` with `booking_context` option:
|
||||||
4. Set HTMX attributes for refresh endpoint
|
```php
|
||||||
5. `$form->handleRequest($request)`
|
$form = $this->createForm(BookingParticipantType::class, $participant, [
|
||||||
6. If submitted and valid:
|
'booking_context' => $bookingDto,
|
||||||
- Extract participant data from submitted form
|
'edit_mode' => false,
|
||||||
- Process field handlers via `processFieldsForParticipant()`
|
'validation_groups' => ['booking_create_step_2'],
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
4. `$form->handleRequest($request)`
|
||||||
|
- **Note**: Field handlers are called automatically by the form in PRE_SUBMIT
|
||||||
|
5. If submitted and valid:
|
||||||
- Save BookingDto to session
|
- Save BookingDto to session
|
||||||
- HTMX redirect to cards view
|
- HTMX redirect to cards view
|
||||||
7. Calculate summary data for sidebar
|
6. Calculate summary data for sidebar
|
||||||
8. Render `_participant_form_standalone.html.twig` with form and summary
|
7. Render `_participant_form_standalone.html.twig` with form and summary
|
||||||
|
|
||||||
**Action: `refreshParticipantForm(int $index, Request $request): Response`**
|
**Action: `refreshParticipantForm(int $index, Request $request): Response`**
|
||||||
|
|
||||||
Implementation steps:
|
Implementation steps:
|
||||||
1. Load BookingDto from session
|
1. Load BookingDto from session
|
||||||
2. Enrich with fresh availability data
|
2. Enrich with fresh availability data
|
||||||
3. Create form with `validation_groups: false`
|
3. Create form with `booking_context` option and `validation_groups: false`:
|
||||||
|
```php
|
||||||
|
$form = $this->createForm(BookingParticipantType::class, $participant, [
|
||||||
|
'booking_context' => $bookingDto,
|
||||||
|
'edit_mode' => false,
|
||||||
|
'validation_groups' => false,
|
||||||
|
]);
|
||||||
|
```
|
||||||
4. `$form->handleRequest($request)`
|
4. `$form->handleRequest($request)`
|
||||||
5. Extract participant data from submitted form
|
5. Extract participant data from submitted form
|
||||||
6. Process field handlers via `processFieldsForParticipant()`
|
6. Process field handlers via `processFieldsForParticipant()`
|
||||||
@@ -922,8 +1024,16 @@ Standard Symfony pattern - single action for both GET and POST:
|
|||||||
public function editParticipant(int $index, Request $request): Response
|
public function editParticipant(int $index, Request $request): Response
|
||||||
{
|
{
|
||||||
$bookingDto = $this->loadBookingDtoOrFail($request, BookingDto::MODE_CREATE);
|
$bookingDto = $this->loadBookingDtoOrFail($request, BookingDto::MODE_CREATE);
|
||||||
|
$participant = $bookingDto->participants[$index] ?? null;
|
||||||
|
|
||||||
$form = $this->createParticipantForm($bookingDto, $index, [
|
if (null === $participant) {
|
||||||
|
throw new \InvalidArgumentException('Invalid participant index');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create form with booking context passed explicitly
|
||||||
|
$form = $this->createForm(BookingParticipantType::class, $participant, [
|
||||||
|
'booking_context' => $bookingDto,
|
||||||
|
'edit_mode' => false,
|
||||||
'validation_groups' => ['booking_create_step_2'],
|
'validation_groups' => ['booking_create_step_2'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -36,14 +36,15 @@ abstract class AbstractParticipantFieldHandler implements ParticipantFieldHandle
|
|||||||
*
|
*
|
||||||
* Default implementation checks if the field exists in the submitted data.
|
* Default implementation checks if the field exists in the submitted data.
|
||||||
* Override this method for more complex processing conditions (e.g., only
|
* Override this method for more complex processing conditions (e.g., only
|
||||||
* process if certain other conditions are met).
|
* process if certain other conditions are met, or skip processing in edit mode).
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param int $participantIndex The index of the participant being processed
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*
|
*
|
||||||
* @return bool True if the handler should process this field, false otherwise
|
* @return bool True if the handler should process this field, false otherwise
|
||||||
*/
|
*/
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||||
{
|
{
|
||||||
return isset($submittedData[$this->getFieldName()]);
|
return isset($submittedData[$this->getFieldName()]);
|
||||||
}
|
}
|
||||||
@@ -67,6 +68,35 @@ abstract class AbstractParticipantFieldHandler implements ParticipantFieldHandle
|
|||||||
return $participants[$participantIndex] ?? null;
|
return $participants[$participantIndex] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the booking flow is in edit mode.
|
||||||
|
*
|
||||||
|
* Handlers may need to behave differently in edit mode, particularly when
|
||||||
|
* dealing with data that isn't returned by the BPN API (like insurance selections).
|
||||||
|
* In edit mode, missing data doesn't mean "clear this field" - it means the API
|
||||||
|
* doesn't provide it and it should be preserved as-is.
|
||||||
|
*
|
||||||
|
* @param BookingDto $bookingDto The booking DTO to check
|
||||||
|
*
|
||||||
|
* @return bool True if in edit mode, false otherwise
|
||||||
|
*/
|
||||||
|
protected function isEditMode(BookingDto $bookingDto): bool
|
||||||
|
{
|
||||||
|
return $bookingDto->mode === BookingDto::MODE_EDIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the booking flow is in create mode.
|
||||||
|
*
|
||||||
|
* @param BookingDto $bookingDto The booking DTO to check
|
||||||
|
*
|
||||||
|
* @return bool True if in create mode, false otherwise
|
||||||
|
*/
|
||||||
|
protected function isCreateMode(BookingDto $bookingDto): bool
|
||||||
|
{
|
||||||
|
return $bookingDto->mode === BookingDto::MODE_CREATE;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely extracts a field value from submitted participant data.
|
* Safely extracts a field value from submitted participant data.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -39,8 +39,12 @@ interface ParticipantFieldHandlerInterface
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if this handler should process the field based on submitted participant data.
|
* Determines if this handler should process the field based on submitted participant data.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
|
* @param int $participantIndex The participant index being processed
|
||||||
*/
|
*/
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool;
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns field state modifications that should be applied after processing.
|
* Returns field state modifications that should be applied after processing.
|
||||||
|
|||||||
@@ -59,11 +59,12 @@ class ParticipantAdditionalServicesFieldHandler extends AbstractParticipantField
|
|||||||
* services are selected.
|
* services are selected.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param int $participantIndex The index of the participant being processed
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*
|
*
|
||||||
* @return bool Always returns true for service selection fields
|
* @return bool Always returns true for service selection fields
|
||||||
*/
|
*/
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||||
{
|
{
|
||||||
return true; // Always process to handle deselection cases
|
return true; // Always process to handle deselection cases
|
||||||
}
|
}
|
||||||
@@ -84,6 +85,7 @@ class ParticipantAdditionalServicesFieldHandler extends AbstractParticipantField
|
|||||||
* 5. Updates participant with filtered valid selections
|
* 5. Updates participant with filtered valid selections
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param BookingDto $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
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -40,11 +40,12 @@ class ParticipantBoardFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
* services are selected.
|
* services are selected.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param int $participantIndex The index of the participant being processed
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*
|
*
|
||||||
* @return bool Always returns true for service selection fields
|
* @return bool Always returns true for service selection fields
|
||||||
*/
|
*/
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||||
{
|
{
|
||||||
return true; // Always process to handle deselection cases
|
return true; // Always process to handle deselection cases
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,11 +39,12 @@ class ParticipantBulkInsuranceFieldHandler extends AbstractParticipantFieldHandl
|
|||||||
* the bulkInsuranceBooking checkbox - their insurance is controlled by the handler.
|
* the bulkInsuranceBooking checkbox - their insurance is controlled by the handler.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param int $participantIndex The index of the participant being processed
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*
|
*
|
||||||
* @return bool True if this is the applicant, false otherwise
|
* @return bool True if this is the applicant, false otherwise
|
||||||
*/
|
*/
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||||
{
|
{
|
||||||
return 0 === $participantIndex; // Only process for applicant
|
return 0 === $participantIndex; // Only process for applicant
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,11 +59,12 @@ class ParticipantCoursesFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
* services are selected.
|
* services are selected.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param int $participantIndex The index of the participant being processed
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*
|
*
|
||||||
* @return bool Always returns true for service selection fields
|
* @return bool Always returns true for service selection fields
|
||||||
*/
|
*/
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||||
{
|
{
|
||||||
return true; // Always process to handle deselection cases
|
return true; // Always process to handle deselection cases
|
||||||
}
|
}
|
||||||
@@ -77,6 +78,7 @@ class ParticipantCoursesFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
* no longer appropriate for the participant's age are automatically removed.
|
* no longer appropriate for the participant's age are automatically removed.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param BookingDto $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
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Form\Service;
|
namespace App\Form\Service;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Address;
|
||||||
use App\BusProNet\Model\Insurance;
|
use App\BusProNet\Model\Insurance;
|
||||||
use App\BusProNet\Model\Pickup;
|
use App\BusProNet\Model\Pickup;
|
||||||
use App\BusProNet\Model\Service;
|
use App\BusProNet\Model\Service;
|
||||||
@@ -132,13 +133,42 @@ class ParticipantFieldHandlerRegistry
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Let each handler decide if it should process this participant's data
|
// Let each handler decide if it should process this participant's data
|
||||||
if ($handler->shouldProcess($participantData, (int) $participantIndex)) {
|
if ($handler->shouldProcess($participantData, $bookingDto->mode, (int) $participantIndex)) {
|
||||||
$handler->processField($participantData, $bookingDto, (int) $participantIndex);
|
$handler->processField($participantData, $bookingDto, (int) $participantIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes field handlers for a single participant.
|
||||||
|
*
|
||||||
|
* This method is used by the card-based booking flows where participants are
|
||||||
|
* edited individually. Unlike processFields() which processes all participants,
|
||||||
|
* this method applies all handlers to only the specified participant.
|
||||||
|
*
|
||||||
|
* Handlers are executed in dependency order to ensure proper data consistency.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $participantData Submitted data for one participant
|
||||||
|
* @param BookingDto $bookingDto The booking DTO to update
|
||||||
|
* @param int $participantIndex Index of participant to process
|
||||||
|
*/
|
||||||
|
public function processFieldsForParticipant(array $participantData, BookingDto $bookingDto, int $participantIndex): void
|
||||||
|
{
|
||||||
|
// Get handlers sorted by dependency order (uses cache if available)
|
||||||
|
$sortedHandlerNames = $this->getSortedHandlers();
|
||||||
|
|
||||||
|
// Apply each handler to the specified participant
|
||||||
|
foreach ($sortedHandlerNames as $handlerName) {
|
||||||
|
$handler = $this->handlers[$handlerName];
|
||||||
|
|
||||||
|
// Let each handler decide if it should process this participant's data
|
||||||
|
if ($handler->shouldProcess($participantData, $bookingDto->mode, $participantIndex)) {
|
||||||
|
$handler->processField($participantData, $bookingDto, $participantIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns handler names sorted by dependency order using cached results when possible.
|
* Returns handler names sorted by dependency order using cached results when possible.
|
||||||
*
|
*
|
||||||
@@ -376,7 +406,7 @@ class ParticipantFieldHandlerRegistry
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle Address objects -> convert to array of properties
|
// Handle Address objects -> convert to array of properties
|
||||||
if ($value instanceof \App\BusProNet\Model\Address) {
|
if ($value instanceof Address) {
|
||||||
return [
|
return [
|
||||||
'street' => $value->street,
|
'street' => $value->street,
|
||||||
'postCode' => $value->postCode,
|
'postCode' => $value->postCode,
|
||||||
|
|||||||
@@ -76,27 +76,34 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
/**
|
/**
|
||||||
* Determines if this handler should process the field based on submitted data.
|
* Determines if this handler should process the field based on submitted data.
|
||||||
*
|
*
|
||||||
* For insurance selection fields, we always need to process to handle cases
|
* Insurance handler should NOT process in edit mode because the BPN API does not
|
||||||
* where the selection is cleared (field not present in data). This ensures
|
* return insurance data. In edit mode, insurance data must be preserved as-is
|
||||||
* the participant DTO is updated with null when no insurance is selected.
|
* and passed through to the update endpoint unchanged.
|
||||||
*
|
*
|
||||||
* However, when bulk insurance booking is active for a dependent participant,
|
* In create mode, we always need to process to handle cases where the selection
|
||||||
* we skip processing to prevent overwriting the insurance assigned by the
|
* is cleared (field not present in data). However, when bulk insurance booking
|
||||||
* bulk insurance handler.
|
* is active for a dependent participant, we skip processing to prevent overwriting
|
||||||
|
* the insurance assigned by the bulk insurance handler.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param int $participantIndex The index of the participant being processed
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*
|
*
|
||||||
* @return bool True if processing should occur, false if bulk insurance handles it
|
* @return bool True if processing should occur, false otherwise
|
||||||
*/
|
*/
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||||
{
|
{
|
||||||
|
// In edit mode, insurance data is not available from API - skip processing entirely
|
||||||
|
if ($mode === BookingDto::MODE_EDIT) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip processing for dependent participants when bulk insurance booking is active
|
// Skip processing for dependent participants when bulk insurance booking is active
|
||||||
if ($participantIndex > 0 && $this->isBulkInsuranceBookingActive($submittedData)) {
|
if ($participantIndex > 0 && $this->isBulkInsuranceBookingActive($submittedData)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true; // Always process to handle deselection cases
|
return true; // Always process in create mode to handle deselection cases
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -52,11 +52,12 @@ class ParticipantLicensePlateFieldHandler extends AbstractParticipantFieldHandle
|
|||||||
* the participant DTO is updated correctly when parking availability changes.
|
* the participant DTO is updated correctly when parking availability changes.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param int $participantIndex The index of the participant being processed
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*
|
*
|
||||||
* @return bool Always returns true for license plate fields
|
* @return bool Always returns true for license plate fields
|
||||||
*/
|
*/
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||||
{
|
{
|
||||||
return true; // Always process to handle deselection cases
|
return true; // Always process to handle deselection cases
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,11 +37,12 @@ class ParticipantParkingFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
* participant DTO is updated correctly when transportation switches to bus-only.
|
* participant DTO is updated correctly when transportation switches to bus-only.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param int $participantIndex The index of the participant being processed
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*
|
*
|
||||||
* @return bool Always returns true for parking selection fields
|
* @return bool Always returns true for parking selection fields
|
||||||
*/
|
*/
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||||
{
|
{
|
||||||
return true; // Always process for state changes
|
return true; // Always process for state changes
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class ParticipantPickupFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
return ['transportationOutbound', 'transportationInbound'];
|
return ['transportationOutbound', 'transportationInbound'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||||
{
|
{
|
||||||
return true; // Always process to handle clearing pickup
|
return true; // Always process to handle clearing pickup
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,11 +56,12 @@ class ParticipantRentalInsuranceFieldHandler extends AbstractParticipantFieldHan
|
|||||||
* rental insurance is selected.
|
* rental insurance is selected.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param int $participantIndex The index of the participant being processed
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*
|
*
|
||||||
* @return bool Always returns true for service selection fields
|
* @return bool Always returns true for service selection fields
|
||||||
*/
|
*/
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||||
{
|
{
|
||||||
return true; // Always process to handle deselection cases
|
return true; // Always process to handle deselection cases
|
||||||
}
|
}
|
||||||
@@ -74,6 +75,7 @@ class ParticipantRentalInsuranceFieldHandler extends AbstractParticipantFieldHan
|
|||||||
* is no longer appropriate for the participant's age, it is automatically cleared.
|
* is no longer appropriate for the participant's age, it is automatically cleared.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param BookingDto $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
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -48,11 +48,12 @@ class ParticipantRentalsFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
* services are selected.
|
* services are selected.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param int $participantIndex The index of the participant being processed
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*
|
*
|
||||||
* @return bool Always returns true for service selection fields
|
* @return bool Always returns true for service selection fields
|
||||||
*/
|
*/
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||||
{
|
{
|
||||||
return true; // Always process to handle deselection cases
|
return true; // Always process to handle deselection cases
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,11 +60,12 @@ class ParticipantSkiPassFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
* skipass is selected.
|
* skipass is selected.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param int $participantIndex The index of the participant being processed
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*
|
*
|
||||||
* @return bool Always returns true for service selection fields
|
* @return bool Always returns true for service selection fields
|
||||||
*/
|
*/
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||||
{
|
{
|
||||||
return true; // Always process to handle deselection cases
|
return true; // Always process to handle deselection cases
|
||||||
}
|
}
|
||||||
@@ -79,6 +80,7 @@ class ParticipantSkiPassFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
* age or exceeds the travel date range, it is automatically cleared.
|
* age or exceeds the travel date range, it is automatically cleared.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param BookingDto $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
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -32,11 +32,12 @@ class ParticipantTransportationInboundFieldHandler extends AbstractParticipantFi
|
|||||||
* participant DTO is updated with null when no transportation is selected.
|
* participant DTO is updated with null when no transportation is selected.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param int $participantIndex The index of the participant being processed
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*
|
*
|
||||||
* @return bool Always returns true for transportation selection fields
|
* @return bool Always returns true for transportation selection fields
|
||||||
*/
|
*/
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||||
{
|
{
|
||||||
return true; // Always process to handle deselection cases
|
return true; // Always process to handle deselection cases
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,11 +32,12 @@ class ParticipantTransportationOutboundFieldHandler extends AbstractParticipantF
|
|||||||
* participant DTO is updated with null when no transportation is selected.
|
* participant DTO is updated with null when no transportation is selected.
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $submittedData The submitted participant form data
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
||||||
|
* @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT)
|
||||||
* @param int $participantIndex The index of the participant being processed
|
* @param int $participantIndex The index of the participant being processed
|
||||||
*
|
*
|
||||||
* @return bool Always returns true for transportation selection fields
|
* @return bool Always returns true for transportation selection fields
|
||||||
*/
|
*/
|
||||||
public function shouldProcess(array $submittedData, int $participantIndex): bool
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||||
{
|
{
|
||||||
return true; // Always process to handle deselection cases
|
return true; // Always process to handle deselection cases
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ use Symfony\Component\Form\FormInterface;
|
|||||||
* This trait contains common form navigation logic used across different
|
* This trait contains common form navigation logic used across different
|
||||||
* form services and types. It centralizes the logic for finding root forms
|
* form services and types. It centralizes the logic for finding root forms
|
||||||
* and extracting booking DTOs from form hierarchies.
|
* and extracting booking DTOs from form hierarchies.
|
||||||
|
*
|
||||||
|
* Note: Card-based flows pass BookingDto via form options instead of
|
||||||
|
* relying on form tree traversal.
|
||||||
*/
|
*/
|
||||||
trait FormTraversalTrait
|
trait FormTraversalTrait
|
||||||
{
|
{
|
||||||
@@ -20,8 +23,8 @@ trait FormTraversalTrait
|
|||||||
* Gets the BookingDto 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
|
* This helper method traverses up the form tree to find the root form
|
||||||
* and extracts the BookingDto data. This is shared logic used
|
* and extracts the BookingDto data. This is used as a fallback when
|
||||||
* by both create and edit participant forms.
|
* BookingDto is not passed explicitly via form options.
|
||||||
*
|
*
|
||||||
* @param FormInterface $form The form to start traversing from
|
* @param FormInterface $form The form to start traversing from
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Form\Model\BookingDto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts card display data for participants in card-based booking flows.
|
||||||
|
*
|
||||||
|
* Provides participant name, room assignment, and individual pricing for
|
||||||
|
* display in the card overview UI.
|
||||||
|
*/
|
||||||
|
class ParticipantCardDataService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly BookingPriceCalculatorService $priceCalculator,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get card data for a single participant.
|
||||||
|
*
|
||||||
|
* @return array{name: string, roomName: string, price: string}
|
||||||
|
*/
|
||||||
|
public function getCardData(BookingDto $bookingDto, int $index): array
|
||||||
|
{
|
||||||
|
$participant = $bookingDto->participants[$index] ?? null;
|
||||||
|
|
||||||
|
if (null === $participant) {
|
||||||
|
throw new \InvalidArgumentException(sprintf('Participant at index %d does not exist', $index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract participant name with fallback
|
||||||
|
$name = $this->getParticipantName($participant, $index);
|
||||||
|
|
||||||
|
// Extract room name
|
||||||
|
$roomName = $this->getRoomName($bookingDto, $participant);
|
||||||
|
|
||||||
|
// Calculate and format individual price
|
||||||
|
$price = $this->getFormattedPrice($bookingDto, $index);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $name,
|
||||||
|
'roomName' => $roomName,
|
||||||
|
'price' => $price,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get card data for all participants.
|
||||||
|
*
|
||||||
|
* @return array<int, array{name: string, roomName: string, price: string}>
|
||||||
|
*/
|
||||||
|
public function getAllCardsData(BookingDto $bookingDto): array
|
||||||
|
{
|
||||||
|
$cardsData = [];
|
||||||
|
|
||||||
|
foreach ($bookingDto->participants as $index => $participant) {
|
||||||
|
$cardsData[$index] = $this->getCardData($bookingDto, $index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cardsData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get participant name with fallback to generic label.
|
||||||
|
*/
|
||||||
|
private function getParticipantName(object $participant, int $index): string
|
||||||
|
{
|
||||||
|
$firstName = $participant->firstName ?? '';
|
||||||
|
$lastName = $participant->lastName ?? '';
|
||||||
|
|
||||||
|
$name = trim($firstName . ' ' . $lastName);
|
||||||
|
|
||||||
|
if ('' === $name) {
|
||||||
|
return sprintf('Teilnehmer %d', $index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get room name from travel model.
|
||||||
|
*/
|
||||||
|
private function getRoomName(BookingDto $bookingDto, object $participant): string
|
||||||
|
{
|
||||||
|
$roomId = $participant->assignedRoomId ?? null;
|
||||||
|
|
||||||
|
if (null === $roomId) {
|
||||||
|
return 'Kein Zimmer zugewiesen';
|
||||||
|
}
|
||||||
|
|
||||||
|
$room = $bookingDto->travel->getRoomById($roomId);
|
||||||
|
|
||||||
|
if (null === $room) {
|
||||||
|
return 'Unbekanntes Zimmer';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $room->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate and format individual participant price.
|
||||||
|
*/
|
||||||
|
private function getFormattedPrice(BookingDto $bookingDto, int $index): string
|
||||||
|
{
|
||||||
|
$prices = $this->priceCalculator->calculateAllParticipantIndividualPrices($bookingDto);
|
||||||
|
|
||||||
|
$price = $prices[$index] ?? 0.0;
|
||||||
|
|
||||||
|
return number_format($price, 2, ',', '.') . ' €';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,16 +32,37 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase
|
|||||||
{
|
{
|
||||||
$dependencies = $this->handler->getDependencies();
|
$dependencies = $this->handler->getDependencies();
|
||||||
|
|
||||||
$this->assertEquals(['dateOfBirth'], $dependencies);
|
// Insurance handler depends on all price-affecting fields to ensure accurate reassignment
|
||||||
|
$expectedDependencies = [
|
||||||
|
'dateOfBirth',
|
||||||
|
'skiPass',
|
||||||
|
'rentals',
|
||||||
|
'courses',
|
||||||
|
'additionalServices',
|
||||||
|
'board',
|
||||||
|
'transportationOutbound',
|
||||||
|
'transportationInbound',
|
||||||
|
'pickup',
|
||||||
|
'parking',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertEquals($expectedDependencies, $dependencies);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testShouldProcessAlwaysReturnsTrue(): void
|
public function testShouldProcessReturnsTrueInCreateMode(): void
|
||||||
{
|
{
|
||||||
$result = $this->handler->shouldProcess([], 0);
|
$result = $this->handler->shouldProcess([], BookingDto::MODE_CREATE, 0);
|
||||||
|
|
||||||
$this->assertTrue($result);
|
$this->assertTrue($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testShouldProcessReturnsFalseInEditMode(): void
|
||||||
|
{
|
||||||
|
$result = $this->handler->shouldProcess([], BookingDto::MODE_EDIT, 0);
|
||||||
|
|
||||||
|
$this->assertFalse($result, 'Insurance handler should not process in edit mode as API does not return insurance data');
|
||||||
|
}
|
||||||
|
|
||||||
public function testProcessFieldSetsInsuranceToNullWhenNoParticipant(): void
|
public function testProcessFieldSetsInsuranceToNullWhenNoParticipant(): void
|
||||||
{
|
{
|
||||||
$bookingDto = $this->createMockBookingDto();
|
$bookingDto = $this->createMockBookingDto();
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ class ParticipantLicensePlateFieldHandlerTest extends TestCase
|
|||||||
|
|
||||||
public function testShouldProcessAlwaysReturnsTrue(): void
|
public function testShouldProcessAlwaysReturnsTrue(): void
|
||||||
{
|
{
|
||||||
$this->assertTrue($this->handler->shouldProcess([], 0));
|
$this->assertTrue($this->handler->shouldProcess([], BookingDto::MODE_CREATE, 0));
|
||||||
$this->assertTrue($this->handler->shouldProcess(['some' => 'data'], 5));
|
$this->assertTrue($this->handler->shouldProcess(['some' => 'data'], BookingDto::MODE_EDIT, 5));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testProcessFieldWithoutParticipant(): void
|
public function testProcessFieldWithoutParticipant(): void
|
||||||
|
|||||||
@@ -0,0 +1,319 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Room;
|
||||||
|
use App\BusProNet\Model\Travel;
|
||||||
|
use App\Form\Model\BookingDto;
|
||||||
|
use App\Form\Model\ParticipantDto;
|
||||||
|
use App\Service\BookingPriceCalculatorService;
|
||||||
|
use App\Service\ParticipantCardDataService;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ParticipantCardDataServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
private ParticipantCardDataService $service;
|
||||||
|
private BookingPriceCalculatorService $priceCalculator;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->priceCalculator = $this->createMock(BookingPriceCalculatorService::class);
|
||||||
|
$this->service = new ParticipantCardDataService($this->priceCalculator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCardDataWithFullParticipantData(): void
|
||||||
|
{
|
||||||
|
// Create test room
|
||||||
|
$room = new Room();
|
||||||
|
$room->id = 1;
|
||||||
|
$room->name = 'Doppelzimmer';
|
||||||
|
$room->price = 100.0;
|
||||||
|
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->rooms = [$room];
|
||||||
|
|
||||||
|
// Create participant with full data
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->firstName = 'Max';
|
||||||
|
$participant->lastName = 'Mustermann';
|
||||||
|
$participant->assignedRoomId = 1;
|
||||||
|
|
||||||
|
$bookingDto = new BookingDto($travel, 1);
|
||||||
|
$bookingDto->participants = [$participant];
|
||||||
|
|
||||||
|
// Mock price calculation
|
||||||
|
$this->priceCalculator
|
||||||
|
->expects($this->once())
|
||||||
|
->method('calculateAllParticipantIndividualPrices')
|
||||||
|
->with($bookingDto)
|
||||||
|
->willReturn([450.50]);
|
||||||
|
|
||||||
|
$result = $this->service->getCardData($bookingDto, 0);
|
||||||
|
|
||||||
|
$this->assertEquals('Max Mustermann', $result['name']);
|
||||||
|
$this->assertEquals('Doppelzimmer', $result['roomName']);
|
||||||
|
$this->assertEquals('450,50 €', $result['price']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCardDataWithPartialName(): void
|
||||||
|
{
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->rooms = [];
|
||||||
|
|
||||||
|
// Participant with only first name
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->firstName = 'Max';
|
||||||
|
$participant->lastName = null;
|
||||||
|
|
||||||
|
$bookingDto = new BookingDto($travel, 1);
|
||||||
|
$bookingDto->participants = [$participant];
|
||||||
|
|
||||||
|
$this->priceCalculator
|
||||||
|
->method('calculateAllParticipantIndividualPrices')
|
||||||
|
->willReturn([0.0]);
|
||||||
|
|
||||||
|
$result = $this->service->getCardData($bookingDto, 0);
|
||||||
|
|
||||||
|
$this->assertEquals('Max', $result['name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCardDataWithNoName(): void
|
||||||
|
{
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->rooms = [];
|
||||||
|
|
||||||
|
// Participant without name
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->firstName = null;
|
||||||
|
$participant->lastName = null;
|
||||||
|
|
||||||
|
$bookingDto = new BookingDto($travel, 1);
|
||||||
|
$bookingDto->participants = [$participant];
|
||||||
|
|
||||||
|
$this->priceCalculator
|
||||||
|
->method('calculateAllParticipantIndividualPrices')
|
||||||
|
->willReturn([0.0]);
|
||||||
|
|
||||||
|
$result = $this->service->getCardData($bookingDto, 0);
|
||||||
|
|
||||||
|
$this->assertEquals('Teilnehmer 1', $result['name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCardDataWithEmptyName(): void
|
||||||
|
{
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->rooms = [];
|
||||||
|
|
||||||
|
// Participant with empty strings for names
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->firstName = ' ';
|
||||||
|
$participant->lastName = ' ';
|
||||||
|
|
||||||
|
$bookingDto = new BookingDto($travel, 1);
|
||||||
|
$bookingDto->participants = [$participant];
|
||||||
|
|
||||||
|
$this->priceCalculator
|
||||||
|
->method('calculateAllParticipantIndividualPrices')
|
||||||
|
->willReturn([0.0]);
|
||||||
|
|
||||||
|
$result = $this->service->getCardData($bookingDto, 0);
|
||||||
|
|
||||||
|
$this->assertEquals('Teilnehmer 1', $result['name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCardDataWithNoRoomAssignment(): void
|
||||||
|
{
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->rooms = [];
|
||||||
|
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->firstName = 'Max';
|
||||||
|
$participant->lastName = 'Mustermann';
|
||||||
|
$participant->assignedRoomId = null;
|
||||||
|
|
||||||
|
$bookingDto = new BookingDto($travel, 1);
|
||||||
|
$bookingDto->participants = [$participant];
|
||||||
|
|
||||||
|
$this->priceCalculator
|
||||||
|
->method('calculateAllParticipantIndividualPrices')
|
||||||
|
->willReturn([0.0]);
|
||||||
|
|
||||||
|
$result = $this->service->getCardData($bookingDto, 0);
|
||||||
|
|
||||||
|
$this->assertEquals('Kein Zimmer zugewiesen', $result['roomName']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCardDataWithUnknownRoom(): void
|
||||||
|
{
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->rooms = [];
|
||||||
|
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->firstName = 'Max';
|
||||||
|
$participant->lastName = 'Mustermann';
|
||||||
|
$participant->assignedRoomId = 999; // Non-existent room
|
||||||
|
|
||||||
|
$bookingDto = new BookingDto($travel, 1);
|
||||||
|
$bookingDto->participants = [$participant];
|
||||||
|
|
||||||
|
$this->priceCalculator
|
||||||
|
->method('calculateAllParticipantIndividualPrices')
|
||||||
|
->willReturn([0.0]);
|
||||||
|
|
||||||
|
$result = $this->service->getCardData($bookingDto, 0);
|
||||||
|
|
||||||
|
$this->assertEquals('Unbekanntes Zimmer', $result['roomName']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCardDataWithZeroPrice(): void
|
||||||
|
{
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->rooms = [];
|
||||||
|
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->firstName = 'Max';
|
||||||
|
$participant->lastName = 'Mustermann';
|
||||||
|
|
||||||
|
$bookingDto = new BookingDto($travel, 1);
|
||||||
|
$bookingDto->participants = [$participant];
|
||||||
|
|
||||||
|
$this->priceCalculator
|
||||||
|
->method('calculateAllParticipantIndividualPrices')
|
||||||
|
->willReturn([0.0]);
|
||||||
|
|
||||||
|
$result = $this->service->getCardData($bookingDto, 0);
|
||||||
|
|
||||||
|
$this->assertEquals('0,00 €', $result['price']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCardDataWithInvalidIndex(): void
|
||||||
|
{
|
||||||
|
$travel = new Travel();
|
||||||
|
$bookingDto = new BookingDto($travel, 1);
|
||||||
|
$bookingDto->participants = [];
|
||||||
|
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('Participant at index 0 does not exist');
|
||||||
|
|
||||||
|
$this->service->getCardData($bookingDto, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAllCardsDataWithMultipleParticipants(): void
|
||||||
|
{
|
||||||
|
// Create test rooms
|
||||||
|
$room1 = new Room();
|
||||||
|
$room1->id = 1;
|
||||||
|
$room1->name = 'Einzelzimmer';
|
||||||
|
|
||||||
|
$room2 = new Room();
|
||||||
|
$room2->id = 2;
|
||||||
|
$room2->name = 'Doppelzimmer';
|
||||||
|
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->rooms = [$room1, $room2];
|
||||||
|
|
||||||
|
// Create participants
|
||||||
|
$participant1 = new ParticipantDto();
|
||||||
|
$participant1->firstName = 'Max';
|
||||||
|
$participant1->lastName = 'Mustermann';
|
||||||
|
$participant1->assignedRoomId = 1;
|
||||||
|
|
||||||
|
$participant2 = new ParticipantDto();
|
||||||
|
$participant2->firstName = 'Anna';
|
||||||
|
$participant2->lastName = 'Schmidt';
|
||||||
|
$participant2->assignedRoomId = 2;
|
||||||
|
|
||||||
|
$participant3 = new ParticipantDto();
|
||||||
|
$participant3->firstName = null;
|
||||||
|
$participant3->lastName = null;
|
||||||
|
$participant3->assignedRoomId = 2;
|
||||||
|
|
||||||
|
$bookingDto = new BookingDto($travel, 1);
|
||||||
|
$bookingDto->participants = [$participant1, $participant2, $participant3];
|
||||||
|
|
||||||
|
// Mock price calculation
|
||||||
|
$this->priceCalculator
|
||||||
|
->expects($this->exactly(3))
|
||||||
|
->method('calculateAllParticipantIndividualPrices')
|
||||||
|
->with($bookingDto)
|
||||||
|
->willReturn([450.0, 500.0, 480.0]);
|
||||||
|
|
||||||
|
$result = $this->service->getAllCardsData($bookingDto);
|
||||||
|
|
||||||
|
$this->assertCount(3, $result);
|
||||||
|
|
||||||
|
// First participant
|
||||||
|
$this->assertEquals('Max Mustermann', $result[0]['name']);
|
||||||
|
$this->assertEquals('Einzelzimmer', $result[0]['roomName']);
|
||||||
|
$this->assertEquals('450,00 €', $result[0]['price']);
|
||||||
|
|
||||||
|
// Second participant
|
||||||
|
$this->assertEquals('Anna Schmidt', $result[1]['name']);
|
||||||
|
$this->assertEquals('Doppelzimmer', $result[1]['roomName']);
|
||||||
|
$this->assertEquals('500,00 €', $result[1]['price']);
|
||||||
|
|
||||||
|
// Third participant (no name)
|
||||||
|
$this->assertEquals('Teilnehmer 3', $result[2]['name']);
|
||||||
|
$this->assertEquals('Doppelzimmer', $result[2]['roomName']);
|
||||||
|
$this->assertEquals('480,00 €', $result[2]['price']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAllCardsDataWithEmptyParticipants(): void
|
||||||
|
{
|
||||||
|
$travel = new Travel();
|
||||||
|
$bookingDto = new BookingDto($travel, 1);
|
||||||
|
$bookingDto->participants = [];
|
||||||
|
|
||||||
|
$result = $this->service->getAllCardsData($bookingDto);
|
||||||
|
|
||||||
|
$this->assertEmpty($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPriceFormattingWithLargeAmount(): void
|
||||||
|
{
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->rooms = [];
|
||||||
|
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->firstName = 'Max';
|
||||||
|
$participant->lastName = 'Mustermann';
|
||||||
|
|
||||||
|
$bookingDto = new BookingDto($travel, 1);
|
||||||
|
$bookingDto->participants = [$participant];
|
||||||
|
|
||||||
|
$this->priceCalculator
|
||||||
|
->method('calculateAllParticipantIndividualPrices')
|
||||||
|
->willReturn([1234.56]);
|
||||||
|
|
||||||
|
$result = $this->service->getCardData($bookingDto, 0);
|
||||||
|
|
||||||
|
$this->assertEquals('1.234,56 €', $result['price']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFallbackNameIndexingIsOneBasedNotZeroBased(): void
|
||||||
|
{
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->rooms = [];
|
||||||
|
|
||||||
|
$participant1 = new ParticipantDto();
|
||||||
|
$participant2 = new ParticipantDto();
|
||||||
|
$participant3 = new ParticipantDto();
|
||||||
|
|
||||||
|
$bookingDto = new BookingDto($travel, 1);
|
||||||
|
$bookingDto->participants = [$participant1, $participant2, $participant3];
|
||||||
|
|
||||||
|
$this->priceCalculator
|
||||||
|
->method('calculateAllParticipantIndividualPrices')
|
||||||
|
->willReturn([0.0, 0.0, 0.0]);
|
||||||
|
|
||||||
|
$result1 = $this->service->getCardData($bookingDto, 0);
|
||||||
|
$result2 = $this->service->getCardData($bookingDto, 1);
|
||||||
|
$result3 = $this->service->getCardData($bookingDto, 2);
|
||||||
|
|
||||||
|
$this->assertEquals('Teilnehmer 1', $result1['name']);
|
||||||
|
$this->assertEquals('Teilnehmer 2', $result2['name']);
|
||||||
|
$this->assertEquals('Teilnehmer 3', $result3['name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user