feat: auto-booking services
addresses #869cfcptv
This commit is contained in:
1 parent
6d9265de0f
commit
c510cbf590
19 files changed
+1829
-72
No files matched your search
+435
-60
@@ -6,12 +6,14 @@ namespace App\Service;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Room;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\XmlLoader\AgencyLoader;
|
||||
use App\Exception\BookingSessionNotFoundException;
|
||||
use App\Exception\NoRoomsAvailableException;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Service\ServiceAgeEvaluator;
|
||||
use App\Form\Model\RoomSelectionDto;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
@@ -460,72 +462,445 @@ class BookingService
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-selects mandatory services for all participants in the booking DTO.
|
||||
* Pre-selects default services for all participants.
|
||||
*
|
||||
* This method ensures that mandatory services are selected before form rendering
|
||||
* and pricing calculations, resolving timing issues where mandatory services
|
||||
* were only selected during form rendering via choice_attr callbacks.
|
||||
*
|
||||
* Mandatory services are only preselected for eligible participants - those who
|
||||
* have at least one skipass available for their age. Ineligible participants are
|
||||
* skipped to prevent their mandatory services from being included in pricing.
|
||||
*
|
||||
* @param BookingDto $bookingDto The booking DTO to update with mandatory services
|
||||
* Mandatory and auto-book rules are handled in dedicated methods:
|
||||
* mandatory first, auto-book second.
|
||||
*/
|
||||
public function preselectDefaultServices(BookingDto $bookingDto): void
|
||||
{
|
||||
$additionalServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL);
|
||||
$mandatoryAdditionalServices = array_filter(
|
||||
$additionalServices,
|
||||
static fn (Service $service): bool => true === $service->mandatory
|
||||
);
|
||||
$autoBookAdditionalServices = array_filter(
|
||||
$additionalServices,
|
||||
// Services that are both mandatory and auto-book belong to mandatory bucket only.
|
||||
static fn (Service $service): bool => true === $service->autoBook && false === $service->mandatory
|
||||
);
|
||||
|
||||
$skiPassServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true);
|
||||
$mandatorySkiPassServices = array_filter(
|
||||
$skiPassServices,
|
||||
static fn (Service $service): bool => true === $service->mandatory
|
||||
);
|
||||
$autoBookSkiPassServices = array_filter(
|
||||
$skiPassServices,
|
||||
// Services that are both mandatory and auto-book belong to mandatory bucket only.
|
||||
static fn (Service $service): bool => true === $service->autoBook && false === $service->mandatory
|
||||
);
|
||||
|
||||
$boardServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD, true);
|
||||
$mandatoryBoardServices = array_filter(
|
||||
$boardServices,
|
||||
static fn (Service $service): bool => true === $service->mandatory
|
||||
);
|
||||
$autoBookBoardServices = array_filter(
|
||||
$boardServices,
|
||||
// Services that are both mandatory and auto-book belong to mandatory bucket only.
|
||||
static fn (Service $service): bool => true === $service->autoBook && false === $service->mandatory
|
||||
);
|
||||
|
||||
$rentalServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true);
|
||||
$mandatoryRentalServices = array_filter(
|
||||
$rentalServices,
|
||||
static fn (Service $service): bool => true === $service->mandatory
|
||||
);
|
||||
$autoBookRentalServices = array_filter(
|
||||
$rentalServices,
|
||||
// Services that are both mandatory and auto-book belong to mandatory bucket only.
|
||||
static fn (Service $service): bool => true === $service->autoBook && false === $service->mandatory
|
||||
);
|
||||
|
||||
$ageEvaluator = new ServiceAgeEvaluator();
|
||||
|
||||
foreach ($bookingDto->participants as $participantIndex => $participant) {
|
||||
if (false === $this->canPreselectServicesForParticipant($bookingDto, $participantIndex, $participant)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->preselectMandatoryAdditionalServices(
|
||||
$participant,
|
||||
$mandatoryAdditionalServices,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
);
|
||||
$this->preselectAutoBookAdditionalServices(
|
||||
$participant,
|
||||
$autoBookAdditionalServices,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
);
|
||||
$this->preselectMandatorySkiPass(
|
||||
$participant,
|
||||
$mandatorySkiPassServices,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
);
|
||||
$this->preselectAutoBookSkiPass(
|
||||
$participant,
|
||||
$autoBookSkiPassServices,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
);
|
||||
$this->preselectMandatoryBoardServices(
|
||||
$participant,
|
||||
$mandatoryBoardServices,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
);
|
||||
$this->preselectAutoBookBoardServices(
|
||||
$participant,
|
||||
$autoBookBoardServices,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
);
|
||||
$this->preselectMandatoryRentals(
|
||||
$participant,
|
||||
$mandatoryRentalServices,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
);
|
||||
$this->preselectAutoBookRentals(
|
||||
$participant,
|
||||
$autoBookRentalServices,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use preselectDefaultServices() instead.
|
||||
*/
|
||||
public function preselectMandatoryServices(BookingDto $bookingDto): void
|
||||
{
|
||||
$additionalServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL);
|
||||
$mandatoryServices = array_filter($additionalServices, fn ($service) => true === $service->mandatory);
|
||||
$this->preselectDefaultServices($bookingDto);
|
||||
}
|
||||
|
||||
// Pre-select mandatory services for each participant
|
||||
foreach ($bookingDto->participants as $participantIndex => $participant) {
|
||||
if (null === $participant->dateOfBirth) {
|
||||
continue; // Skip participants without age information
|
||||
}
|
||||
|
||||
// Skip babies - they only get services with explicit age ranges (handled by field filtering)
|
||||
$age = $participant->getAge($bookingDto->travel->dateFrom);
|
||||
if (null !== $age && $age <= Constants::BABY_MAX_AGE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip ineligible participants (no skipasses available for their age)
|
||||
if (false === $this->participantEligibilityService->isParticipantEligible($bookingDto, $participantIndex)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get age-appropriate mandatory services for this participant
|
||||
$ageAppropriateServices = array_filter($mandatoryServices, function ($service) use ($bookingDto, $participant) {
|
||||
if (null === $service->ageFrom && null === $service->ageTo) {
|
||||
return true; // No age restrictions
|
||||
}
|
||||
|
||||
// participant's age at travel date is relevant
|
||||
$age = $participant->getAge($bookingDto->travel->dateFrom);
|
||||
|
||||
if (null !== $service->ageFrom && $age < $service->ageFrom) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null !== $service->ageTo && $age > $service->ageTo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Add mandatory services to current selections
|
||||
$currentSelections = $participant->additionalServices ?? [];
|
||||
$currentServiceIds = array_map(fn ($service) => $service->id, $currentSelections);
|
||||
|
||||
foreach ($ageAppropriateServices as $mandatoryService) {
|
||||
if (false === in_array($mandatoryService->id, $currentServiceIds, true)) {
|
||||
$currentSelections[] = $mandatoryService;
|
||||
}
|
||||
}
|
||||
|
||||
$participant->additionalServices = $currentSelections;
|
||||
private function canPreselectServicesForParticipant(
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
ParticipantDto $participant,
|
||||
): bool {
|
||||
if (null === $participant->dateOfBirth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$age = $participant->getAge($bookingDto->travel->dateFrom);
|
||||
if (null !== $age && $age <= Constants::BABY_MAX_AGE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->participantEligibilityService->isParticipantEligible($bookingDto, $participantIndex);
|
||||
}
|
||||
|
||||
private function preselectMandatoryAdditionalServices(
|
||||
ParticipantDto $participant,
|
||||
array $mandatoryAdditionalServices,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
ServiceAgeEvaluator $ageEvaluator,
|
||||
): void {
|
||||
$eligibleServices = $this->getEligibleServicesForParticipant(
|
||||
$mandatoryAdditionalServices,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
);
|
||||
|
||||
$this->appendAdditionalServices($participant, $eligibleServices, false);
|
||||
}
|
||||
|
||||
private function preselectAutoBookAdditionalServices(
|
||||
ParticipantDto $participant,
|
||||
array $autoBookAdditionalServices,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
ServiceAgeEvaluator $ageEvaluator,
|
||||
): void {
|
||||
$eligibleServices = $this->getEligibleServicesForParticipant(
|
||||
$autoBookAdditionalServices,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
);
|
||||
|
||||
$this->appendAdditionalServices($participant, $eligibleServices, true);
|
||||
}
|
||||
|
||||
private function preselectMandatorySkiPass(
|
||||
ParticipantDto $participant,
|
||||
array $mandatorySkiPassServices,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
ServiceAgeEvaluator $ageEvaluator,
|
||||
): void {
|
||||
if (null !== $participant->skiPass) {
|
||||
return;
|
||||
}
|
||||
|
||||
$eligibleServices = $this->getEligibleServicesForParticipant(
|
||||
$mandatorySkiPassServices,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
);
|
||||
|
||||
foreach ($eligibleServices as $service) {
|
||||
$participant->skiPass = $service;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private function preselectAutoBookSkiPass(
|
||||
ParticipantDto $participant,
|
||||
array $autoBookSkiPassServices,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
ServiceAgeEvaluator $ageEvaluator,
|
||||
): void {
|
||||
if (null !== $participant->skiPass) {
|
||||
return;
|
||||
}
|
||||
|
||||
$eligibleServices = $this->getEligibleServicesForParticipant(
|
||||
$autoBookSkiPassServices,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
);
|
||||
|
||||
foreach ($eligibleServices as $service) {
|
||||
if (null !== $service->id
|
||||
&& true === in_array($service->id, $participant->autoBookOptOutSkiPassIds, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$participant->skiPass = $service;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private function preselectMandatoryBoardServices(
|
||||
ParticipantDto $participant,
|
||||
array $mandatoryBoardServices,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
ServiceAgeEvaluator $ageEvaluator,
|
||||
): void {
|
||||
$eligibleServices = $this->getEligibleServicesForParticipant(
|
||||
$mandatoryBoardServices,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
);
|
||||
|
||||
$this->appendBoardServices($participant, $eligibleServices, false);
|
||||
}
|
||||
|
||||
private function preselectAutoBookBoardServices(
|
||||
ParticipantDto $participant,
|
||||
array $autoBookBoardServices,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
ServiceAgeEvaluator $ageEvaluator,
|
||||
): void {
|
||||
$eligibleServices = $this->getEligibleServicesForParticipant(
|
||||
$autoBookBoardServices,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
);
|
||||
|
||||
$this->appendBoardServices($participant, $eligibleServices, true);
|
||||
}
|
||||
|
||||
private function preselectMandatoryRentals(
|
||||
ParticipantDto $participant,
|
||||
array $mandatoryRentalServices,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
ServiceAgeEvaluator $ageEvaluator,
|
||||
): void {
|
||||
if (null === $participant->skiPass) {
|
||||
return;
|
||||
}
|
||||
|
||||
$eligibleServices = $this->getEligibleServicesForParticipant(
|
||||
$mandatoryRentalServices,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
);
|
||||
|
||||
$matchingDurationServices = $this->getRentalsMatchingSkiPassDuration($eligibleServices, $participant);
|
||||
|
||||
$this->appendRentalServices($participant, $matchingDurationServices, false);
|
||||
}
|
||||
|
||||
private function preselectAutoBookRentals(
|
||||
ParticipantDto $participant,
|
||||
array $autoBookRentalServices,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
ServiceAgeEvaluator $ageEvaluator,
|
||||
): void {
|
||||
if (null === $participant->skiPass) {
|
||||
return;
|
||||
}
|
||||
|
||||
$eligibleServices = $this->getEligibleServicesForParticipant(
|
||||
$autoBookRentalServices,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
);
|
||||
|
||||
$matchingDurationServices = $this->getRentalsMatchingSkiPassDuration($eligibleServices, $participant);
|
||||
|
||||
$this->appendRentalServices($participant, $matchingDurationServices, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Service[] $services
|
||||
*
|
||||
* @return Service[]
|
||||
*/
|
||||
private function getEligibleServicesForParticipant(
|
||||
array $services,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
ServiceAgeEvaluator $ageEvaluator,
|
||||
): array {
|
||||
return array_filter(
|
||||
$services,
|
||||
fn (Service $service): bool => $this->isServiceAvailableForParticipant(
|
||||
$service,
|
||||
$bookingDto,
|
||||
$participantIndex,
|
||||
$ageEvaluator
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Service[] $services
|
||||
*/
|
||||
private function appendAdditionalServices(ParticipantDto $participant, array $services, bool $respectOptOut): void
|
||||
{
|
||||
$currentSelections = $participant->additionalServices ?? [];
|
||||
$currentServiceIds = array_map(static fn (Service $service): ?int => $service->id, $currentSelections);
|
||||
|
||||
foreach ($services as $service) {
|
||||
if (null === $service->id || true === in_array($service->id, $currentServiceIds, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (true === $respectOptOut
|
||||
&& true === in_array($service->id, $participant->autoBookOptOutServiceIds, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$currentSelections[] = $service;
|
||||
}
|
||||
|
||||
$participant->additionalServices = $currentSelections;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Service[] $services
|
||||
*/
|
||||
private function appendBoardServices(ParticipantDto $participant, array $services, bool $respectOptOut): void
|
||||
{
|
||||
$currentSelections = $participant->board ?? [];
|
||||
$currentServiceIds = array_map(static fn (Service $service): ?int => $service->id, $currentSelections);
|
||||
|
||||
foreach ($services as $service) {
|
||||
if (null === $service->id || true === in_array($service->id, $currentServiceIds, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (true === $respectOptOut
|
||||
&& true === in_array($service->id, $participant->autoBookOptOutBoardIds, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$currentSelections[] = $service;
|
||||
}
|
||||
|
||||
$participant->board = $currentSelections;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Service[] $services
|
||||
*/
|
||||
private function appendRentalServices(ParticipantDto $participant, array $services, bool $respectOptOut): void
|
||||
{
|
||||
$currentSelections = $participant->rentals ?? [];
|
||||
$currentServiceIds = array_map(static fn (Service $service): ?int => $service->id, $currentSelections);
|
||||
|
||||
foreach ($services as $service) {
|
||||
if (null === $service->id || true === in_array($service->id, $currentServiceIds, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (true === $respectOptOut
|
||||
&& true === in_array($service->id, $participant->autoBookOptOutRentalIds, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$currentSelections[] = $service;
|
||||
}
|
||||
|
||||
$participant->rentals = $currentSelections;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Service[] $rentals
|
||||
*
|
||||
* @return Service[]
|
||||
*/
|
||||
private function getRentalsMatchingSkiPassDuration(array $rentals, ParticipantDto $participant): array
|
||||
{
|
||||
$selectedSkiPass = $participant->skiPass;
|
||||
if (null === $selectedSkiPass || null === $selectedSkiPass->dateFrom || null === $selectedSkiPass->dateTo) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter($rentals, static function (Service $rental) use ($selectedSkiPass): bool {
|
||||
if (null === $rental->dateFrom || null === $rental->dateTo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $rental->dateFrom == $selectedSkiPass->dateFrom
|
||||
&& $rental->dateTo == $selectedSkiPass->dateTo;
|
||||
});
|
||||
}
|
||||
|
||||
private function isServiceAvailableForParticipant(
|
||||
Service $service,
|
||||
BookingDto $bookingDto,
|
||||
int $participantIndex,
|
||||
ServiceAgeEvaluator $ageEvaluator,
|
||||
): bool
|
||||
{
|
||||
if (false === $ageEvaluator->canEvaluate($service)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user