wip: dynamic availability of services
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Utility\DirectionMapper;
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
|
||||
/**
|
||||
* Calculates dynamic service availability based on current booking selections.
|
||||
*
|
||||
* This service tracks how many participants have selected each service within
|
||||
* the current booking session and calculates remaining availability for
|
||||
* display to other participants. This prevents overbooking within a single
|
||||
* booking workflow.
|
||||
*/
|
||||
class ServiceAvailabilityCalculator
|
||||
{
|
||||
/**
|
||||
* Calculate remaining availability for all services based on current participant selections.
|
||||
*
|
||||
* @param BookingCreateDto $bookingDto The booking data with participant selections
|
||||
* @param int $currentParticipantIndex The index of the participant currently filling the form
|
||||
*
|
||||
* @return array<int, int> Array mapping service IDs to remaining availability counts
|
||||
*/
|
||||
public function calculateRemainingAvailability(BookingCreateDto $bookingDto, int $currentParticipantIndex): array
|
||||
{
|
||||
$serviceUsage = $this->calculateServiceUsage($bookingDto, $currentParticipantIndex);
|
||||
$remainingAvailability = [];
|
||||
|
||||
// Get all services from the travel data
|
||||
$allServices = $this->getAllServicesFromTravel($bookingDto);
|
||||
|
||||
foreach ($allServices as $service) {
|
||||
$originalAvailability = $service->available ?? null;
|
||||
$usedCount = $serviceUsage[$service->id] ?? 0;
|
||||
|
||||
// Only track services that have availability limits set
|
||||
if (null !== $originalAvailability && $originalAvailability > 0) {
|
||||
$remaining = max(0, $originalAvailability - $usedCount);
|
||||
$remainingAvailability[$service->id] = $remaining;
|
||||
}
|
||||
}
|
||||
|
||||
return $remainingAvailability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter services array to only include those with remaining availability.
|
||||
*
|
||||
* @param array $services Array of Service objects to filter
|
||||
* @param BookingCreateDto $bookingDto The booking data with participant selections
|
||||
* @param int $participantIndex The index of the participant currently filling the form
|
||||
*
|
||||
* @return array Filtered array containing only available services
|
||||
*/
|
||||
public function filterAvailableServices(array $services, BookingCreateDto $bookingDto, int $participantIndex): array
|
||||
{
|
||||
$remainingAvailability = $this->calculateRemainingAvailability($bookingDto, $participantIndex);
|
||||
|
||||
return array_filter($services, function (Service $service) use ($remainingAvailability) {
|
||||
// If service has no availability limit set, treat as unlimited
|
||||
if (null === $service->available || $service->available <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For services with availability limits, check remaining availability
|
||||
return ($remainingAvailability[$service->id] ?? $service->available) > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific service is available for the current participant.
|
||||
*
|
||||
* @param int $serviceId The ID of the service to check
|
||||
* @param BookingCreateDto $bookingDto The booking data with participant selections
|
||||
* @param int $participantIndex The index of the participant currently filling the form
|
||||
*
|
||||
* @return bool True if the service has remaining availability
|
||||
*/
|
||||
public function isServiceAvailable(int $serviceId, BookingCreateDto $bookingDto, int $participantIndex): bool
|
||||
{
|
||||
$remainingAvailability = $this->calculateRemainingAvailability($bookingDto, $participantIndex);
|
||||
|
||||
return ($remainingAvailability[$serviceId] ?? 0) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate how many times each service has been selected by other participants.
|
||||
*
|
||||
* @param BookingCreateDto $bookingDto The booking data with participant selections
|
||||
* @param int $currentParticipantIndex The index of the participant currently filling the form
|
||||
*
|
||||
* @return array<int, int> Array mapping service IDs to usage counts
|
||||
*/
|
||||
private function calculateServiceUsage(BookingCreateDto $bookingDto, int $currentParticipantIndex): array
|
||||
{
|
||||
$serviceUsage = [];
|
||||
|
||||
foreach ($bookingDto->participants as $index => $participant) {
|
||||
// Skip the current participant to avoid counting their potential selections
|
||||
if ($index === $currentParticipantIndex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Count service selections for this participant
|
||||
$this->countParticipantServiceUsage($participant, $serviceUsage);
|
||||
}
|
||||
|
||||
return $serviceUsage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count service usage for a single participant and add to the usage array.
|
||||
*
|
||||
* @param mixed $participant The participant DTO object
|
||||
* @param array $serviceUsage Reference to the service usage array to update
|
||||
*/
|
||||
private function countParticipantServiceUsage($participant, array &$serviceUsage): void
|
||||
{
|
||||
// Board service (single selection)
|
||||
if (isset($participant->board) && $participant->board instanceof Service) {
|
||||
$serviceUsage[$participant->board->id] = ($serviceUsage[$participant->board->id] ?? 0) + 1;
|
||||
}
|
||||
|
||||
// Ski pass service (single selection)
|
||||
if (isset($participant->skiPass) && $participant->skiPass instanceof Service) {
|
||||
$serviceUsage[$participant->skiPass->id] = ($serviceUsage[$participant->skiPass->id] ?? 0) + 1;
|
||||
}
|
||||
|
||||
// Transportation services (single selection each)
|
||||
if (isset($participant->transportationOutbound) && $participant->transportationOutbound instanceof Service) {
|
||||
$serviceUsage[$participant->transportationOutbound->id] = ($serviceUsage[$participant->transportationOutbound->id] ?? 0) + 1;
|
||||
}
|
||||
|
||||
if (isset($participant->transportationInbound) && $participant->transportationInbound instanceof Service) {
|
||||
$serviceUsage[$participant->transportationInbound->id] = ($serviceUsage[$participant->transportationInbound->id] ?? 0) + 1;
|
||||
}
|
||||
|
||||
// Courses (multiple selection)
|
||||
if (isset($participant->courses) && is_array($participant->courses)) {
|
||||
foreach ($participant->courses as $course) {
|
||||
if ($course instanceof Service) {
|
||||
$serviceUsage[$course->id] = ($serviceUsage[$course->id] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Additional services (multiple selection)
|
||||
if (isset($participant->additionalServices) && is_array($participant->additionalServices)) {
|
||||
foreach ($participant->additionalServices as $additionalService) {
|
||||
if ($additionalService instanceof Service) {
|
||||
$serviceUsage[$additionalService->id] = ($serviceUsage[$additionalService->id] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rentals (multiple selection)
|
||||
if (isset($participant->rentals) && is_array($participant->rentals)) {
|
||||
foreach ($participant->rentals as $rental) {
|
||||
if ($rental instanceof Service) {
|
||||
$serviceUsage[$rental->id] = ($serviceUsage[$rental->id] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all services from the travel data for availability calculation.
|
||||
*
|
||||
* @param BookingCreateDto $bookingDto The booking data containing travel information
|
||||
*
|
||||
* @return array<Service> Array of all available services
|
||||
*/
|
||||
private function getAllServicesFromTravel(BookingCreateDto $bookingDto): array
|
||||
{
|
||||
$allServices = [];
|
||||
|
||||
// Get transportation services
|
||||
$transportationServices = array_merge(
|
||||
$bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL) ?? [],
|
||||
$bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::INBOUND_TRAVEL) ?? []
|
||||
);
|
||||
$allServices = array_merge($allServices, $transportationServices);
|
||||
|
||||
// Get additional services by category using proper constants
|
||||
$additionalServiceTokens = [
|
||||
Constants::TOKEN_COURSES,
|
||||
Constants::TOKEN_ADDITIONAL,
|
||||
Constants::TOKEN_RENTALS,
|
||||
Constants::TOKEN_SKI_PASS,
|
||||
Constants::TOKEN_BOARD,
|
||||
];
|
||||
|
||||
foreach ($additionalServiceTokens as $token) {
|
||||
$categoryServices = $bookingDto->travel->getAdditionalServicesBySubTypes($token) ?? [];
|
||||
$allServices = array_merge($allServices, $categoryServices);
|
||||
}
|
||||
|
||||
return $allServices;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user