wip: dynamic availability of services

This commit is contained in:
Björn Fromme
2025-09-02 20:03:22 +02:00
parent 21054f0c45
commit 435e3a79e8
4 changed files with 638 additions and 19 deletions
@@ -12,6 +12,7 @@ use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Abstract\AbstractFieldOptionsProvider;
use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory;
use App\Service\ServiceAvailabilityCalculator;
/**
* Provides dynamic field options for participant form fields.
@@ -40,10 +41,13 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
* keeps all field configuration logic centralized and makes it easy to add
* new dynamic fields.
*
* @param ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory Factory for creating room choice loaders
* @param ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory Factory for creating room choice loaders
* @param ServiceAvailabilityCalculator $serviceAvailabilityCalculator Service for calculating dynamic availability
*/
public function __construct(private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory)
{
public function __construct(
private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory,
private readonly ServiceAvailabilityCalculator $serviceAvailabilityCalculator,
) {
parent::__construct();
}
@@ -93,8 +97,12 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
'multiple' => true,
'expanded' => true,
'required' => false,
'choices' => $this->filterServicesByAgeConstraints(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES),
'choices' => $this->filterServicesByAvailability(
$this->filterServicesByAgeConstraints(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES),
$bookingDto,
$participantIndex
),
$bookingDto,
$participantIndex
),
@@ -108,8 +116,12 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
'multiple' => true,
'expanded' => true,
'required' => false,
'choices' => $this->filterServicesByAgeConstraints(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL),
'choices' => $this->filterServicesByAvailability(
$this->filterServicesByAgeConstraints(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL),
$bookingDto,
$participantIndex
),
$bookingDto,
$participantIndex
),
@@ -140,8 +152,12 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
'multiple' => true,
'expanded' => true,
'required' => false,
'choices' => $this->filterServicesByAgeConstraints(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD),
'choices' => $this->filterServicesByAvailability(
$this->filterServicesByAgeConstraints(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD),
$bookingDto,
$participantIndex
),
$bookingDto,
$participantIndex
),
@@ -155,8 +171,12 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
'multiple' => true,
'expanded' => true,
'required' => false,
'choices' => $this->filterServicesByAgeConstraints(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true, true),
'choices' => $this->filterServicesByAvailability(
$this->filterServicesByAgeConstraints(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true, true),
$bookingDto,
$participantIndex
),
$bookingDto,
$participantIndex
),
@@ -170,8 +190,12 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
'multiple' => false,
'expanded' => true,
'required' => true,
'choices' => $this->filterServicesByAgeConstraints(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true, true),
'choices' => $this->filterServicesByAvailability(
$this->filterServicesByAgeConstraints(
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true, true),
$bookingDto,
$participantIndex
),
$bookingDto,
$participantIndex
),
@@ -194,7 +218,11 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
// Outbound Transportation
$this->fieldOptionProviders['transportationOutbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
'label' => 'Hinfahrt',
'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL),
'choices' => $this->filterServicesByAvailability(
$bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL),
$bookingDto,
$participantIndex
),
'choice_label' => fn (Service $service) => $this->formatTransportationServiceLabel($service),
'choice_value' => 'id',
'expanded' => true,
@@ -210,7 +238,11 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
// Inbound Transportation
$this->fieldOptionProviders['transportationInbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [
'label' => 'Rückfahrt',
'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::INBOUND_TRAVEL),
'choices' => $this->filterServicesByAvailability(
$bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::INBOUND_TRAVEL),
$bookingDto,
$participantIndex
),
'choice_label' => fn (Service $service) => $this->formatTransportationServiceLabel($service),
'choice_value' => 'id',
'expanded' => true,
@@ -390,6 +422,29 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
return $this->formatServiceLabelWithPrice($parkingService);
}
/**
* Filters services based on remaining availability in the current booking session.
*
* Removes services that have been fully booked by other participants in
* the current booking session. This prevents overbooking within a single
* booking workflow while maintaining accurate availability counts.
*
* @param array $services Array of Service objects to filter
* @param BookingDtoInterface $bookingDto The booking DTO containing participant data
* @param int $participantIndex Index of the participant currently selecting services
*
* @return array Filtered array containing only services with remaining availability
*/
private function filterServicesByAvailability(array $services, BookingDtoInterface $bookingDto, int $participantIndex): array
{
if (!$bookingDto instanceof BookingCreateDto) {
// For non-create workflows, return all services (no availability tracking needed)
return $services;
}
return $this->serviceAvailabilityCalculator->filterAvailableServices($services, $bookingDto, $participantIndex);
}
/**
* Filters services based on participant's age constraints.
*
@@ -397,11 +452,11 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
* If no age evaluator is configured or participant has no birth date,
* returns empty array to be handled by field visibility conditions.
*
* @param array $services Services to filter
* @param BookingDtoInterface $bookingDto Booking data containing participant info
* @param int $participantIndex Index of participant to evaluate
* @param array $services Array of Service objects to filter
* @param BookingDtoInterface $bookingDto The booking DTO containing participant data
* @param int $participantIndex Index of the participant to evaluate
*
* @return array Filtered services array
* @return array Filtered array of available services
*/
private function filterServicesByAgeConstraints(array $services, BookingDtoInterface $bookingDto, int $participantIndex): array
{