Files
myep/src/Form/Service/ParticipantRentalsFieldHandler.php
T
Björn Fromme 957e20d763 fix: preserve selected services from booking even when unavailable
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.
2026-03-16 12:02:28 +01:00

192 lines
7.0 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 rentals field for booking participants.
*
* This handler manages rental equipment selections for participants in the booking
* creation process. It processes the rentals field from form submissions,
* filters out age-inappropriate options and duration-inappropriate options, and
* updates the participant DTO with only valid selections.
*
* Dependencies: dateOfBirth (for age evaluation) and skiPass (for duration filtering)
*/
class ParticipantRentalsFieldHandler extends AbstractParticipantFieldHandler
{
public function getFieldName(): string
{
return 'rentals';
}
/**
* Returns the field dependencies for proper processing order.
*
* This handler depends on both dateOfBirth (for age evaluation) and skiPass
* (for duration filtering and field visibility logic).
*
* @return string[] Array containing 'dateOfBirth' and 'skiPass' dependencies
*/
public function getDependencies(): array
{
return ['dateOfBirth', 'skiPass'];
}
/**
* Determines if this handler should process the field based on submitted data.
*
* For service selection fields like rentals, we always need to process
* to handle cases where all selections are cleared (field not present in data).
* This ensures the participant DTO is updated with an empty array when no
* services are 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
}
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
{
$participant = $this->getParticipant($bookingDto, $participantIndex);
if (null === $participant) {
return;
}
// Store previous rental state BEFORE checking skipass
$previousRentals = $participant->rentals;
// Check if rentals should be available based on skipass selection
if (null === $participant->skiPass) {
// No skipass selected = clear all rental selections
if ([] !== $previousRentals) {
$participant->addNotification(
'warning',
'Ausrüstung wurde entfernt (kein Skipass ausgewählt)'
);
}
$participant->rentals = [];
return;
}
$selectedRentals = $this->getFieldValue($submittedData, $this->getFieldName()) ?? [];
// Get available rentals from travel data (includes booked rentals in edit mode)
$availableRentals = $this->getAvailableServicesWithBooked(
$bookingDto,
$participantIndex,
Constants::TOKEN_RENTALS,
true
);
// Filter rentals by skipass duration to ensure only matching rentals are available
$durationFilteredRentals = $this->filterRentalsBySkiPassDuration($availableRentals, $participant);
$validSelections = $this->filterValidServiceSelections(
$selectedRentals,
$durationFilteredRentals,
$bookingDto,
$participantIndex
);
// Notify if rentals were cleared due to skipass duration change
// Only show notification if user had submitted rentals that got filtered out,
// not if the user intentionally cleared the selection (empty submission)
if ([] !== $previousRentals && [] === $validSelections && [] !== $selectedRentals) {
$participant->addNotification(
'warning',
'Ausrüstung wurde entfernt (andere Skipass-Dauer ausgewählt)'
);
}
$participant->rentals = $validSelections;
}
private function filterValidServiceSelections(
array $selectedServices,
array $availableServices,
BookingDto $bookingDto,
int $participantIndex,
): array {
$validSelections = [];
foreach ($selectedServices as $selectedService) {
if ($this->isServiceValidForParticipant($selectedService, $availableServices, $bookingDto, $participantIndex)) {
// Convert the selected service ID/data back to the actual Service object
$serviceObject = $this->findServiceInAvailableServices($selectedService, $availableServices);
if (null !== $serviceObject) {
$validSelections[] = $serviceObject;
}
}
}
return $validSelections;
}
private function isServiceValidForParticipant(
mixed $selectedService,
array $availableServices,
BookingDto $bookingDto,
int $participantIndex,
): bool {
$service = $this->findServiceInAvailableServices($selectedService, $availableServices);
if (null === $service) {
return false;
}
$ageEvaluator = new ServiceAgeEvaluator();
if (!$ageEvaluator->canEvaluate($service)) {
return true;
}
return $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex);
}
/**
* Filters rentals to only include those matching the skipass duration.
*
* Rentals must have exact date matching with the selected skipass:
* - rental.dateFrom === skipass.dateFrom
* - rental.dateTo === skipass.dateTo
*
* @param array $rentals All available rental services
* @param \App\Form\Model\ParticipantDto $participant The participant with skipass selection
*
* @return array Filtered rentals matching the skipass duration
*/
private function filterRentalsBySkiPassDuration(array $rentals, $participant): array
{
$selectedSkiPass = $participant->skiPass;
// If skipass has no valid dates, return empty rentals
if (null === $selectedSkiPass->dateFrom || null === $selectedSkiPass->dateTo) {
return [];
}
return array_filter($rentals, function (Service $rental) use ($selectedSkiPass) {
// Rental must have valid dates to be considered
if (null === $rental->dateFrom || null === $rental->dateTo) {
return false;
}
// Exact date matching: rental dates must match skipass dates exactly
return $rental->dateFrom == $selectedSkiPass->dateFrom
&& $rental->dateTo == $selectedSkiPass->dateTo;
});
}
}