When editing a booking, services that were previously booked but are no
longer available in the travel catalog were being dropped. This caused
API error 650 ("Anzahl Leistung stimmt nicht mit Teilnehmerzuordnung
überein") because the service participant counts no longer matched.
The fix ensures that in edit mode, booked services are merged with
travel data services in both:
- Form rendering (ParticipantFieldOptionsProvider): so checkboxes appear
- Handler validation (AbstractParticipantFieldHandler): so selections
are accepted
This allows users to keep their existing service selections or
deliberately replace them with other available options. Create mode
remains unchanged.
160 lines
6.4 KiB
PHP
160 lines
6.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Form\Service;
|
|
|
|
use App\BusProNet\Constants;
|
|
use App\BusProNet\Model\Service;
|
|
use App\Form\Model\BookingDto;
|
|
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
|
|
|
/**
|
|
* Handles processing of the skiPass field for booking participants.
|
|
*
|
|
* This handler manages skipass selection for participants in the booking
|
|
* creation process. It processes the skiPass field from form submissions,
|
|
* validates age-appropriate and date-valid skipasses, and updates the
|
|
* participant DTO with the valid selection.
|
|
*
|
|
* Key responsibilities:
|
|
* - Validates single skipass selection against age constraints
|
|
* - Validates skipass date ranges against travel dates
|
|
* - Clears skipass if no longer available due to age or date changes
|
|
* - Maintains data consistency during HTMX form updates
|
|
* - Prevents form validation errors from stale skipass selections
|
|
*
|
|
* Dependencies: dateOfBirth (must be processed first for age evaluation)
|
|
*/
|
|
class ParticipantSkiPassFieldHandler extends AbstractParticipantFieldHandler
|
|
{
|
|
/**
|
|
* Returns the form field name this handler processes.
|
|
*
|
|
* @return string The field name 'skiPass'
|
|
*/
|
|
public function getFieldName(): string
|
|
{
|
|
return 'skiPass';
|
|
}
|
|
|
|
/**
|
|
* Returns the field dependencies for proper processing order.
|
|
*
|
|
* This handler depends on dateOfBirth being processed first because
|
|
* age evaluation requires the participant's birth date to be available.
|
|
*
|
|
* @return string[] Array containing 'dateOfBirth' dependency
|
|
*/
|
|
public function getDependencies(): array
|
|
{
|
|
return ['dateOfBirth'];
|
|
}
|
|
|
|
/**
|
|
* Determines if this handler should process the field based on submitted data.
|
|
*
|
|
* For service selection fields like skipasses, we always need to process
|
|
* to handle cases where the selection is cleared (field not present in data).
|
|
* This ensures the participant DTO is updated with null when no
|
|
* skipass is selected.
|
|
*
|
|
* @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
|
|
*
|
|
* @return bool Always returns true for service selection fields
|
|
*/
|
|
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
|
{
|
|
return true; // Always process to handle deselection cases
|
|
}
|
|
|
|
/**
|
|
* Processes the skiPass field for a specific participant.
|
|
*
|
|
* This method extracts the skipass selection from submitted form data,
|
|
* validates the selection against the participant's age constraints and
|
|
* travel date constraints, and updates the participant DTO with the valid
|
|
* selection. If the skipass is no longer appropriate for the participant's
|
|
* age or exceeds the travel date range, it is automatically cleared.
|
|
*
|
|
* @param array<string, mixed> $submittedData The submitted participant form data
|
|
* @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, BookingDto $bookingDto, int $participantIndex): void
|
|
{
|
|
// Safely get the participant object, returning early if not found
|
|
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
|
if (null === $participant) {
|
|
return;
|
|
}
|
|
|
|
// Extract current skipass selection from submitted data
|
|
$selectedSkiPass = $this->getFieldValue($submittedData, $this->getFieldName());
|
|
|
|
// Get all skipasses from travel data (includes booked skipasses in edit mode)
|
|
$availableSkipasses = $this->getAvailableServicesWithBooked(
|
|
$bookingDto,
|
|
$participantIndex,
|
|
Constants::TOKEN_SKI_PASS,
|
|
true
|
|
);
|
|
|
|
// For single selection, validate the selected skipass and convert ID to Service object
|
|
$validSelection = null;
|
|
if (null !== $selectedSkiPass) {
|
|
if ($this->isServiceValidForParticipant($selectedSkiPass, $availableSkipasses, $bookingDto, $participantIndex)) {
|
|
// Convert the submitted ID back to the Service object
|
|
$validSelection = $this->findServiceInAvailableServices($selectedSkiPass, $availableSkipasses);
|
|
}
|
|
}
|
|
|
|
// Update participant with validated selection
|
|
$participant->skiPass = $validSelection;
|
|
}
|
|
|
|
/**
|
|
* Validates if a selected skipass is still valid for the participant.
|
|
*
|
|
* This method checks both age constraints (via birth year ranges) and
|
|
* date constraints (skipass dates must be within travel dates).
|
|
*
|
|
* @param mixed $selectedService The selected skipass to validate
|
|
* @param array $availableServices Array of available skipasses
|
|
* @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
|
|
*/
|
|
private function isServiceValidForParticipant(
|
|
mixed $selectedService,
|
|
array $availableServices,
|
|
BookingDto $bookingDto,
|
|
int $participantIndex,
|
|
): bool {
|
|
// Find the service in available services
|
|
$service = $this->findServiceInAvailableServices($selectedService, $availableServices);
|
|
|
|
if (null === $service) {
|
|
return false; // Service not found in available services
|
|
}
|
|
|
|
// Check if service has age constraints
|
|
$ageEvaluator = new ServiceAgeEvaluator();
|
|
if ($ageEvaluator->canEvaluate($service)) {
|
|
// Validate service against participant's age
|
|
if (false === $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex)) {
|
|
return false; // Age constraints not met
|
|
}
|
|
}
|
|
|
|
// Date constraints are already handled by the travel->getAdditionalServicesBySubTypes
|
|
// method with date filtering enabled (true, true parameters), so if the service
|
|
// is in the available services list, it already passed date validation.
|
|
|
|
return true; // Service passed both age and date validation
|
|
}
|
|
}
|