From c510cbf5902c2a9212e321103837dfa5cea5343b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Mon, 16 Mar 2026 17:10:01 +0100 Subject: [PATCH] feat: auto-booking services addresses #869cfcptv --- src/BusProNet/Model/Service.php | 3 + src/BusProNet/XmlParser/TravelParser.php | 3 + .../Booking/Create/Step2Controller.php | 15 +- .../Traits/ParticipantCardFlowTrait.php | 12 +- src/Form/Model/ParticipantDto.php | 4 + ...ticipantAdditionalServicesFieldHandler.php | 61 ++ .../Service/ParticipantBoardFieldHandler.php | 59 ++ .../ParticipantFieldOptionsProvider.php | 34 +- .../ParticipantRentalsFieldHandler.php | 59 ++ .../ParticipantSkiPassFieldHandler.php | 63 ++ src/Service/BookingService.php | 495 ++++++++++++++-- .../BusProNet/XmlParser/TravelParserTest.php | 37 +- .../Booking/ParticipantCardFlowTraitTest.php | 147 +++++ ...pantAdditionalServicesFieldHandlerTest.php | 70 +++ .../ParticipantBoardFieldHandlerTest.php | 98 ++++ ...eldOptionsProviderMandatoryServiceTest.php | 1 + .../ParticipantRentalsFieldHandlerTest.php | 107 ++++ .../ParticipantSkiPassFieldHandlerTest.php | 88 +++ tests/Service/BookingServiceBabyTest.php | 545 ++++++++++++++++++ 19 files changed, 1829 insertions(+), 72 deletions(-) create mode 100644 tests/Controller/Booking/ParticipantCardFlowTraitTest.php create mode 100644 tests/Form/Service/ParticipantAdditionalServicesFieldHandlerTest.php create mode 100644 tests/Form/Service/ParticipantBoardFieldHandlerTest.php create mode 100644 tests/Form/Service/ParticipantRentalsFieldHandlerTest.php create mode 100644 tests/Form/Service/ParticipantSkiPassFieldHandlerTest.php diff --git a/src/BusProNet/Model/Service.php b/src/BusProNet/Model/Service.php index 49b47a3..00adb5b 100644 --- a/src/BusProNet/Model/Service.php +++ b/src/BusProNet/Model/Service.php @@ -29,6 +29,9 @@ class Service #[Groups(['api:single', 'api:list'])] public bool $mandatory = false; + #[Groups(['api:single', 'api:list'])] + public bool $autoBook = false; + #[Groups(['api:single', 'api:list'])] public ?string $label = null; diff --git a/src/BusProNet/XmlParser/TravelParser.php b/src/BusProNet/XmlParser/TravelParser.php index de4e793..bfc2494 100644 --- a/src/BusProNet/XmlParser/TravelParser.php +++ b/src/BusProNet/XmlParser/TravelParser.php @@ -148,6 +148,9 @@ class TravelParser extends AbstractParser $service->id = $serviceId; $service->subType = $serviceNode->attr('unterart'); $service->mandatory = $this->stringToBool($serviceNode->attr('pflicht')); + $autoBookValue = $serviceNode->attr('automatisch_buchen'); + $service->autoBook = $this->stringToBool($autoBookValue) + || 'true' === strtolower((string) $autoBookValue); $service->dateFrom = $this->stringToDate($serviceNode->attr('termin')); $service->dateTo = $this->stringToDate($serviceNode->attr('bis')); $service->label = $this->getStringOrNullValue($serviceNode->filterXPath('//text')); diff --git a/src/Controller/Booking/Create/Step2Controller.php b/src/Controller/Booking/Create/Step2Controller.php index 6fdf70a..036899d 100644 --- a/src/Controller/Booking/Create/Step2Controller.php +++ b/src/Controller/Booking/Create/Step2Controller.php @@ -85,7 +85,7 @@ class Step2Controller extends AbstractController $this->roomAssignmentService->assignRoomsIfNeeded($bookingCreateDto); // Preselect mandatory services - $this->bookingService->preselectMandatoryServices($bookingCreateDto); + $this->bookingService->preselectDefaultServices($bookingCreateDto); // Save BookingDto to session $this->bookingService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE); @@ -149,18 +149,19 @@ class Step2Controller extends AbstractController $form = $this->createParticipantForm($bookingDto, $index); $form->handleRequest($request); + $isSubmitted = $form->isSubmitted(); // Detect dummy data fill token — render pre-filled form immediately, skipping validation $isDummyDataFill = $this ->dummyDataFillService ->isTokenMatch($bookingDto->participants[$index], $bookingDto->getMode()) ; - if (true === $form->isSubmitted() && true === $isDummyDataFill) { + if (true === $isSubmitted && true === $isDummyDataFill) { $this->dummyDataFillService->fill($bookingDto->participants[$index], $index); // Keep behavior consistent with cards view: newly filled participant data // (especially dateOfBirth) must immediately trigger mandatory service preselection. - $this->bookingService->preselectMandatoryServices($bookingDto); + $this->bookingService->preselectDefaultServices($bookingDto); $bookingDto->bookingStatus = Constants::BOOKING_STATUS_OPEN; $this->bookingService->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE); @@ -171,10 +172,16 @@ class Step2Controller extends AbstractController return $this->renderParticipantForm($form, $index, $bookingDto); } + if (true === $isSubmitted) { + // Re-run default preselection after participant form input changes (e.g. DOB). + // This ensures auto-book defaults are applied as soon as eligibility becomes known. + $this->bookingService->preselectDefaultServices($bookingDto); + } + // Collect notifications from field handlers (run during PRE_SUBMIT) $notifications = $this->collectAndClearNotifications($bookingDto); - if (true === $form->isSubmitted() && true === $form->isValid()) { + if (true === $isSubmitted && true === $form->isValid()) { // Save BookingDto to session $this->bookingService->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE); diff --git a/src/Controller/Booking/Traits/ParticipantCardFlowTrait.php b/src/Controller/Booking/Traits/ParticipantCardFlowTrait.php index 3057cae..80076c9 100644 --- a/src/Controller/Booking/Traits/ParticipantCardFlowTrait.php +++ b/src/Controller/Booking/Traits/ParticipantCardFlowTrait.php @@ -122,13 +122,19 @@ trait ParticipantCardFlowTrait int $index, string $refreshRouteName, ): Response { + $refreshFormOptions = ['validation_groups' => false]; + // Create form with validation disabled - $form = $this->createParticipantForm($bookingDto, $index, [ - 'validation_groups' => false, - ]); + $form = $this->createParticipantForm($bookingDto, $index, $refreshFormOptions); $form->handleRequest($request); + // Refresh endpoint is POST-only and always processes submitted participant data. + $this->bookingService->preselectDefaultServices($bookingDto); + + // Recreate form so the rendered state reflects any new auto-preselections. + $form = $this->createParticipantForm($bookingDto, $index, $refreshFormOptions); + // Collect notifications from field handlers $notifications = $this->collectAndClearNotifications($bookingDto); diff --git a/src/Form/Model/ParticipantDto.php b/src/Form/Model/ParticipantDto.php index 8c2d219..b4cac48 100644 --- a/src/Form/Model/ParticipantDto.php +++ b/src/Form/Model/ParticipantDto.php @@ -86,6 +86,10 @@ class ParticipantDto public array $courses = []; public array $additionalServices = []; + public array $autoBookOptOutServiceIds = []; + public array $autoBookOptOutSkiPassIds = []; + public array $autoBookOptOutBoardIds = []; + public array $autoBookOptOutRentalIds = []; // Note: Ski pass validation is conditional - see ParticipantEditDto::validateSkiPassRequired() // Babies (0-2 years) are exempt from ski pass requirement diff --git a/src/Form/Service/ParticipantAdditionalServicesFieldHandler.php b/src/Form/Service/ParticipantAdditionalServicesFieldHandler.php index d94985c..b996a15 100644 --- a/src/Form/Service/ParticipantAdditionalServicesFieldHandler.php +++ b/src/Form/Service/ParticipantAdditionalServicesFieldHandler.php @@ -7,6 +7,7 @@ namespace App\Form\Service; use App\BusProNet\Constants; use App\BusProNet\Model\Service; use App\Form\Model\BookingDto; +use App\Form\Model\ParticipantDto; use App\Form\Service\Abstract\AbstractParticipantFieldHandler; /** @@ -96,6 +97,10 @@ class ParticipantAdditionalServicesFieldHandler extends AbstractParticipantField return; } + if (false === $participant instanceof ParticipantDto) { + return; + } + // Extract current service selections from submitted data $selectedServices = $this->getFieldValue($submittedData, $this->getFieldName()) ?? []; @@ -114,10 +119,66 @@ class ParticipantAdditionalServicesFieldHandler extends AbstractParticipantField $participantIndex ); + $this->updateAutoBookOptOutServices( + $participant, + $availableServices, + $validSelections, + $bookingDto, + $participantIndex + ); + // Update participant with validated selections $participant->additionalServices = $validSelections; } + private function updateAutoBookOptOutServices( + ParticipantDto $participant, + array $availableServices, + array $validSelections, + BookingDto $bookingDto, + int $participantIndex, + ): void { + $currentlySelectedAutoBookIds = []; + foreach ($participant->additionalServices as $selectedService) { + if (false === $selectedService instanceof Service) { + continue; + } + + if (null === $selectedService->id || true === $selectedService->mandatory || false === $selectedService->autoBook) { + continue; + } + + if (false === $this->isServiceValidForParticipant($selectedService, $availableServices, $bookingDto, $participantIndex)) { + continue; + } + + $currentlySelectedAutoBookIds[] = $selectedService->id; + } + + $newlySelectedAutoBookIds = []; + foreach ($validSelections as $selectedService) { + if (null === $selectedService->id || true === $selectedService->mandatory || false === $selectedService->autoBook) { + continue; + } + + $newlySelectedAutoBookIds[] = $selectedService->id; + } + + // Add explicit user deselections to opt-out list + foreach ($currentlySelectedAutoBookIds as $serviceId) { + if (false === in_array($serviceId, $newlySelectedAutoBookIds, true) + && false === in_array($serviceId, $participant->autoBookOptOutServiceIds, true)) { + $participant->autoBookOptOutServiceIds[] = $serviceId; + } + } + + // Remove opt-out when user manually re-selects the service + $participant->autoBookOptOutServiceIds = array_values(array_filter( + $participant->autoBookOptOutServiceIds, + static fn (int $serviceId): bool => false === in_array($serviceId, $newlySelectedAutoBookIds, true) + )); + } + /** * Filters service selections to keep only those valid for the participant's age. * diff --git a/src/Form/Service/ParticipantBoardFieldHandler.php b/src/Form/Service/ParticipantBoardFieldHandler.php index 4bd88ab..f5c77ee 100644 --- a/src/Form/Service/ParticipantBoardFieldHandler.php +++ b/src/Form/Service/ParticipantBoardFieldHandler.php @@ -7,6 +7,7 @@ namespace App\Form\Service; use App\BusProNet\Constants; use App\BusProNet\Model\Service; use App\Form\Model\BookingDto; +use App\Form\Model\ParticipantDto; use App\Form\Service\Abstract\AbstractParticipantFieldHandler; /** @@ -57,6 +58,10 @@ class ParticipantBoardFieldHandler extends AbstractParticipantFieldHandler return; } + if (false === $participant instanceof ParticipantDto) { + return; + } + $selectedBoard = $this->getFieldValue($submittedData, $this->getFieldName()) ?? []; // Get available board options from travel data (includes booked options in edit mode) @@ -73,9 +78,63 @@ class ParticipantBoardFieldHandler extends AbstractParticipantFieldHandler $participantIndex ); + $this->updateAutoBookOptOutBoard( + $participant, + $availableBoard, + $validSelections, + $bookingDto, + $participantIndex + ); + $participant->board = $validSelections; } + private function updateAutoBookOptOutBoard( + ParticipantDto $participant, + array $availableBoard, + array $validSelections, + BookingDto $bookingDto, + int $participantIndex, + ): void { + $currentlySelectedAutoBookIds = []; + foreach ($participant->board as $selectedService) { + if (false === $selectedService instanceof Service) { + continue; + } + + if (null === $selectedService->id || true === $selectedService->mandatory || false === $selectedService->autoBook) { + continue; + } + + if (false === $this->isServiceValidForParticipant($selectedService, $availableBoard, $bookingDto, $participantIndex)) { + continue; + } + + $currentlySelectedAutoBookIds[] = $selectedService->id; + } + + $newlySelectedAutoBookIds = []; + foreach ($validSelections as $selectedService) { + if (null === $selectedService->id || true === $selectedService->mandatory || false === $selectedService->autoBook) { + continue; + } + + $newlySelectedAutoBookIds[] = $selectedService->id; + } + + foreach ($currentlySelectedAutoBookIds as $serviceId) { + if (false === in_array($serviceId, $newlySelectedAutoBookIds, true) + && false === in_array($serviceId, $participant->autoBookOptOutBoardIds, true)) { + $participant->autoBookOptOutBoardIds[] = $serviceId; + } + } + + $participant->autoBookOptOutBoardIds = array_values(array_filter( + $participant->autoBookOptOutBoardIds, + static fn (int $serviceId): bool => false === in_array($serviceId, $newlySelectedAutoBookIds, true) + )); + } + private function filterValidServiceSelections( array $selectedServices, array $availableServices, diff --git a/src/Form/Service/ParticipantFieldOptionsProvider.php b/src/Form/Service/ParticipantFieldOptionsProvider.php index 9823515..f381710 100644 --- a/src/Form/Service/ParticipantFieldOptionsProvider.php +++ b/src/Form/Service/ParticipantFieldOptionsProvider.php @@ -249,13 +249,26 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider $attributes = []; + // Make mandatory services readonly only when actually selected by participant. + if (true === $service->mandatory) { + $participant = $bookingDto->getParticipant($participantIndex); + $isSelected = null !== $participant + && $this->hasServiceById($participant->board, $service->id); + + if (true === $isSelected) { + $attributes['readonly'] = true; + $attributes['data-tooltip'] = 'Diese Leistung ist nicht abwählbar'; + } + } + // Add service description as data attribute for frontend use if (null !== $service->description && '' !== trim($service->description)) { $attributes['data-description'] = $service->description; } - // Make readonly if service is unavailable (intelligently handles edit mode) - if ($this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'board')) { + // Make readonly if service is unavailable (only if not already mandatory) + if (false === $service->mandatory + && $this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'board')) { $attributes['readonly'] = true; $attributes['data-tooltip'] = 'ausgebucht'; } @@ -361,13 +374,26 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider $attributes = []; + // Make mandatory services readonly only when actually selected by participant. + if (true === $service->mandatory) { + $participant = $bookingDto->getParticipant($participantIndex); + $isSelected = null !== $participant + && $this->hasServiceById($participant->rentals, $service->id); + + if (true === $isSelected) { + $attributes['readonly'] = true; + $attributes['data-tooltip'] = 'Diese Leistung ist nicht abwählbar'; + } + } + // Add service description as data attribute for frontend use if (null !== $service->description && '' !== trim($service->description)) { $attributes['data-description'] = $service->description; } - // Make readonly if service is unavailable (intelligently handles edit mode) - if ($this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'rentals')) { + // Make readonly if service is unavailable (only if not already mandatory) + if (false === $service->mandatory + && $this->shouldMakeServiceReadonly($service, $bookingDto, $participantIndex, 'rentals')) { $attributes['readonly'] = true; $attributes['data-tooltip'] = 'ausgebucht'; } diff --git a/src/Form/Service/ParticipantRentalsFieldHandler.php b/src/Form/Service/ParticipantRentalsFieldHandler.php index b62b1fb..cf75211 100644 --- a/src/Form/Service/ParticipantRentalsFieldHandler.php +++ b/src/Form/Service/ParticipantRentalsFieldHandler.php @@ -7,6 +7,7 @@ namespace App\Form\Service; use App\BusProNet\Constants; use App\BusProNet\Model\Service; use App\Form\Model\BookingDto; +use App\Form\Model\ParticipantDto; use App\Form\Service\Abstract\AbstractParticipantFieldHandler; /** @@ -65,6 +66,10 @@ class ParticipantRentalsFieldHandler extends AbstractParticipantFieldHandler return; } + if (false === $participant instanceof ParticipantDto) { + return; + } + // Store previous rental state BEFORE checking skipass $previousRentals = $participant->rentals; @@ -102,6 +107,14 @@ class ParticipantRentalsFieldHandler extends AbstractParticipantFieldHandler $participantIndex ); + $this->updateAutoBookOptOutRentals( + $participant, + $durationFilteredRentals, + $validSelections, + $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) @@ -115,6 +128,52 @@ class ParticipantRentalsFieldHandler extends AbstractParticipantFieldHandler $participant->rentals = $validSelections; } + private function updateAutoBookOptOutRentals( + ParticipantDto $participant, + array $availableRentals, + array $validSelections, + BookingDto $bookingDto, + int $participantIndex, + ): void { + $currentlySelectedAutoBookIds = []; + foreach ($participant->rentals as $selectedService) { + if (false === $selectedService instanceof Service) { + continue; + } + + if (null === $selectedService->id || true === $selectedService->mandatory || false === $selectedService->autoBook) { + continue; + } + + if (false === $this->isServiceValidForParticipant($selectedService, $availableRentals, $bookingDto, $participantIndex)) { + continue; + } + + $currentlySelectedAutoBookIds[] = $selectedService->id; + } + + $newlySelectedAutoBookIds = []; + foreach ($validSelections as $selectedService) { + if (null === $selectedService->id || true === $selectedService->mandatory || false === $selectedService->autoBook) { + continue; + } + + $newlySelectedAutoBookIds[] = $selectedService->id; + } + + foreach ($currentlySelectedAutoBookIds as $serviceId) { + if (false === in_array($serviceId, $newlySelectedAutoBookIds, true) + && false === in_array($serviceId, $participant->autoBookOptOutRentalIds, true)) { + $participant->autoBookOptOutRentalIds[] = $serviceId; + } + } + + $participant->autoBookOptOutRentalIds = array_values(array_filter( + $participant->autoBookOptOutRentalIds, + static fn (int $serviceId): bool => false === in_array($serviceId, $newlySelectedAutoBookIds, true) + )); + } + private function filterValidServiceSelections( array $selectedServices, array $availableServices, diff --git a/src/Form/Service/ParticipantSkiPassFieldHandler.php b/src/Form/Service/ParticipantSkiPassFieldHandler.php index 731ef57..ec8bf7a 100644 --- a/src/Form/Service/ParticipantSkiPassFieldHandler.php +++ b/src/Form/Service/ParticipantSkiPassFieldHandler.php @@ -7,6 +7,7 @@ namespace App\Form\Service; use App\BusProNet\Constants; use App\BusProNet\Model\Service; use App\Form\Model\BookingDto; +use App\Form\Model\ParticipantDto; use App\Form\Service\Abstract\AbstractParticipantFieldHandler; /** @@ -91,6 +92,10 @@ class ParticipantSkiPassFieldHandler extends AbstractParticipantFieldHandler return; } + if (false === $participant instanceof ParticipantDto) { + return; + } + // Extract current skipass selection from submitted data $selectedSkiPass = $this->getFieldValue($submittedData, $this->getFieldName()); @@ -102,6 +107,13 @@ class ParticipantSkiPassFieldHandler extends AbstractParticipantFieldHandler true ); + $currentlySelectedAutoBookSkiPassId = $this->getSelectedAutoBookSkiPassId( + $participant, + $availableSkipasses, + $bookingDto, + $participantIndex + ); + // For single selection, validate the selected skipass and convert ID to Service object $validSelection = null; if (null !== $selectedSkiPass) { @@ -111,10 +123,61 @@ class ParticipantSkiPassFieldHandler extends AbstractParticipantFieldHandler } } + $this->updateAutoBookOptOutSkiPasses($participant, $currentlySelectedAutoBookSkiPassId, $validSelection); + // Update participant with validated selection $participant->skiPass = $validSelection; } + private function getSelectedAutoBookSkiPassId( + ParticipantDto $participant, + array $availableSkipasses, + BookingDto $bookingDto, + int $participantIndex, + ): ?int { + $currentSkiPass = $participant->skiPass; + if (null === $currentSkiPass || null === $currentSkiPass->id) { + return null; + } + + if (true === $currentSkiPass->mandatory || false === $currentSkiPass->autoBook) { + return null; + } + + if (false === $this->isServiceValidForParticipant($currentSkiPass, $availableSkipasses, $bookingDto, $participantIndex)) { + return null; + } + + return $currentSkiPass->id; + } + + private function updateAutoBookOptOutSkiPasses( + ParticipantDto $participant, + ?int $currentlySelectedAutoBookSkiPassId, + ?Service $validSelection, + ): void { + $newlySelectedAutoBookSkiPassId = null; + if (null !== $validSelection + && null !== $validSelection->id + && false === $validSelection->mandatory + && true === $validSelection->autoBook) { + $newlySelectedAutoBookSkiPassId = $validSelection->id; + } + + if (null !== $currentlySelectedAutoBookSkiPassId + && $currentlySelectedAutoBookSkiPassId !== $newlySelectedAutoBookSkiPassId + && false === in_array($currentlySelectedAutoBookSkiPassId, $participant->autoBookOptOutSkiPassIds, true)) { + $participant->autoBookOptOutSkiPassIds[] = $currentlySelectedAutoBookSkiPassId; + } + + if (null !== $newlySelectedAutoBookSkiPassId) { + $participant->autoBookOptOutSkiPassIds = array_values(array_filter( + $participant->autoBookOptOutSkiPassIds, + static fn (int $serviceId): bool => $serviceId !== $newlySelectedAutoBookSkiPassId + )); + } + } + /** * Validates if a selected skipass is still valid for the participant. * diff --git a/src/Service/BookingService.php b/src/Service/BookingService.php index aad23ff..05d5449 100644 --- a/src/Service/BookingService.php +++ b/src/Service/BookingService.php @@ -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); } /** diff --git a/tests/BusProNet/XmlParser/TravelParserTest.php b/tests/BusProNet/XmlParser/TravelParserTest.php index 569df35..6d78b7d 100644 --- a/tests/BusProNet/XmlParser/TravelParserTest.php +++ b/tests/BusProNet/XmlParser/TravelParserTest.php @@ -32,7 +32,7 @@ class TravelParserTest extends TestCase 0,00 Frei - + Bettwäsche-Set inkl. Handtuch Das Bettwäsche-Set umfasst Spannbettlaken, Kissen- und Deckenbezug. Beim Handtuch handelt es sich um ein Duschtuch (ca. 70x140cm). 13,90 @@ -92,6 +92,7 @@ class TravelParserTest extends TestCase 'Das Bettwäsche-Set umfasst Spannbettlaken, Kissen- und Deckenbezug. Beim Handtuch handelt es sich um ein Duschtuch (ca. 70x140cm).', $bettwascheService->description ); + $this->assertTrue($bettwascheService->autoBook); // Service without description (ID 183656 - Skipass, has hinweis_stamm but no hinweis) $this->assertArrayHasKey(183656, $additionalServices); @@ -112,6 +113,7 @@ class TravelParserTest extends TestCase $busService = $transportationServices[183649]; $this->assertSame('Bus-Hinfahrt', $busService->label); $this->assertSame('Bus fährt nur bei ausreichender Teilnehmerzahl', $busService->description); + $this->assertFalse($busService->autoBook); } public function testParseServiceWithoutDescriptions(): void @@ -149,6 +151,39 @@ class TravelParserTest extends TestCase $this->assertNull($service->description); // No hinweis node should result in null description } + public function testParseServiceWithLowerCaseAutoBookFlag(): void + { + $xmlContent = ' + + + + Test Travel + 659,00 + + + Service with auto-book + 8,90 + Frei + + + + + + + + + +'; + + $crawler = new Crawler($xmlContent); + $travelNode = $crawler->filterXPath('//reise/termin')->first(); + $travel = $this->parser->parse($travelNode); + + $additionalServices = $travel->additionalServices; + $this->assertArrayHasKey(183660, $additionalServices); + $this->assertTrue($additionalServices[183660]->autoBook); + } + public function testParseServiceWithEmptyDescription(): void { $xmlContent = ' diff --git a/tests/Controller/Booking/ParticipantCardFlowTraitTest.php b/tests/Controller/Booking/ParticipantCardFlowTraitTest.php new file mode 100644 index 0000000..a4254a8 --- /dev/null +++ b/tests/Controller/Booking/ParticipantCardFlowTraitTest.php @@ -0,0 +1,147 @@ +createMock(BookingService::class); + $summaryDataService = $this->createMock(BookingSummaryDataService::class); + + $formOne = $this->createMock(FormInterface::class); + $formOne->expects($this->once()) + ->method('handleRequest'); + $formOne->method('isSubmitted') + ->willReturn(true); + + $formTwo = $this->createMock(FormInterface::class); + $formTwo->expects($this->once()) + ->method('createView') + ->willReturn(new FormView()); + + $bookingDto = $this->createEditModeBookingDto(); + $request = new Request(); + + $bookingService->expects($this->once()) + ->method('preselectDefaultServices') + ->with($bookingDto); + + $bookingService->expects($this->once()) + ->method('saveBookingDto') + ->with($request, $bookingDto, BookingDto::MODE_EDIT); + + $summaryDataService->expects($this->once()) + ->method('getSummaryData') + ->with($bookingDto) + ->willReturn(new BookingSummaryDto([], 1, 0.0, 0.0, [], [], [], null)); + + $controller = new class($bookingService, $summaryDataService, [$formOne, $formTwo]) { + use ParticipantCardFlowTrait; + + public BookingService $bookingService; + public BookingSummaryDataService $summaryDataService; + public object $participantCardService; + + /** @var FormInterface[] */ + private array $forms; + + public function __construct(BookingService $bookingService, BookingSummaryDataService $summaryDataService, array $forms) + { + $this->bookingService = $bookingService; + $this->summaryDataService = $summaryDataService; + $this->forms = $forms; + $this->participantCardService = new class() { + public function getAllCardsData(BookingDto $bookingDto): array + { + return []; + } + }; + } + + public function callHandleParticipantRefresh( + Request $request, + BookingDto $bookingDto, + int $index, + string $refreshRouteName, + ): Response { + $method = new \ReflectionMethod($this, 'handleParticipantRefresh'); + $method->setAccessible(true); + + return $method->invoke($this, $request, $bookingDto, $index, $refreshRouteName); + } + + private function htmxOobResponse(string $template, array $blocks, array $parameters): Response + { + return new Response('ok'); + } + + protected function getParameter(string $name): mixed + { + return []; + } + + private function createForm(string $type, $data = null, array $options = []): FormInterface + { + return array_shift($this->forms); + } + + private function render(string $view, array $parameters = [], ?Response $response = null): Response + { + return new Response('rendered'); + } + + protected function addFlash(string $type, mixed $message): void + { + } + + protected function redirectToRoute(string $route, array $parameters = [], int $status = 302): RedirectResponse + { + return new RedirectResponse('/'); + } + }; + + $response = $controller->callHandleParticipantRefresh( + $request, + $bookingDto, + 0, + 'app_booking_edit_step_2_participant_refresh' + ); + + $this->assertSame('ok', $response->getContent()); + } + + private function createEditModeBookingDto(): BookingDto + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-06'); + + $bookingDto = new BookingDto($travel, 157047); + $bookingDto->booking = new Booking(); // Marks DTO as edit mode + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = new \DateTimeImmutable('1990-01-01'); + $bookingDto->participants[0] = $participant; + + return $bookingDto; + } +} diff --git a/tests/Form/Service/ParticipantAdditionalServicesFieldHandlerTest.php b/tests/Form/Service/ParticipantAdditionalServicesFieldHandlerTest.php new file mode 100644 index 0000000..6b92c1e --- /dev/null +++ b/tests/Form/Service/ParticipantAdditionalServicesFieldHandlerTest.php @@ -0,0 +1,70 @@ +id = 10; + $autoBookService->subType = Constants::TOKEN_ADDITIONAL; + $autoBookService->label = 'Auto Book'; + $autoBookService->autoBook = true; + + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('+30 days'); + $travel->dateTo = new \DateTimeImmutable('+37 days'); + $travel->additionalServices = [10 => $autoBookService]; + + $bookingDto = new BookingDto($travel, 1); + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->additionalServices = [$autoBookService]; + $bookingDto->participants[0] = $participant; + + $handler = new ParticipantAdditionalServicesFieldHandler(); + $handler->processField([], $bookingDto, 0); + + $this->assertSame([10], $participant->autoBookOptOutServiceIds); + $this->assertSame([], $participant->additionalServices); + } + + public function testDeselectMandatoryAutoBookServiceDoesNotAddOptOutId(): void + { + $service = new Service(); + $service->id = 11; + $service->subType = Constants::TOKEN_ADDITIONAL; + $service->label = 'Mandatory Auto Book'; + $service->mandatory = true; + $service->autoBook = true; + + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('+30 days'); + $travel->dateTo = new \DateTimeImmutable('+37 days'); + $travel->additionalServices = [11 => $service]; + + $bookingDto = new BookingDto($travel, 1); + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->additionalServices = [$service]; + $bookingDto->participants[0] = $participant; + + $handler = new ParticipantAdditionalServicesFieldHandler(); + $handler->processField([], $bookingDto, 0); + + $this->assertSame([], $participant->autoBookOptOutServiceIds); + } +} diff --git a/tests/Form/Service/ParticipantBoardFieldHandlerTest.php b/tests/Form/Service/ParticipantBoardFieldHandlerTest.php new file mode 100644 index 0000000..04cba1c --- /dev/null +++ b/tests/Form/Service/ParticipantBoardFieldHandlerTest.php @@ -0,0 +1,98 @@ +id = 60; + $autoBookBoard->subType = Constants::TOKEN_BOARD; + $autoBookBoard->label = 'Auto Book Board'; + $autoBookBoard->autoBook = true; + + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('+30 days'); + $travel->dateTo = new \DateTimeImmutable('+37 days'); + $travel->additionalServices = [60 => $autoBookBoard]; + + $bookingDto = new BookingDto($travel, 1); + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->board = [$autoBookBoard]; + $bookingDto->participants[0] = $participant; + + $handler = new ParticipantBoardFieldHandler(); + $handler->processField([], $bookingDto, 0); + + $this->assertSame([60], $participant->autoBookOptOutBoardIds); + $this->assertSame([], $participant->board); + } + + public function testDeselectMandatoryAutoBookBoardDoesNotAddOptOutId(): void + { + $mandatoryAutoBookBoard = new Service(); + $mandatoryAutoBookBoard->id = 61; + $mandatoryAutoBookBoard->subType = Constants::TOKEN_BOARD; + $mandatoryAutoBookBoard->label = 'Mandatory Auto Book Board'; + $mandatoryAutoBookBoard->mandatory = true; + $mandatoryAutoBookBoard->autoBook = true; + + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('+30 days'); + $travel->dateTo = new \DateTimeImmutable('+37 days'); + $travel->additionalServices = [61 => $mandatoryAutoBookBoard]; + + $bookingDto = new BookingDto($travel, 1); + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->board = [$mandatoryAutoBookBoard]; + $bookingDto->participants[0] = $participant; + + $handler = new ParticipantBoardFieldHandler(); + $handler->processField([], $bookingDto, 0); + + $this->assertSame([], $participant->autoBookOptOutBoardIds); + } + + public function testReselectAutoBookBoardRemovesOptOutId(): void + { + $autoBookBoard = new Service(); + $autoBookBoard->id = 62; + $autoBookBoard->subType = Constants::TOKEN_BOARD; + $autoBookBoard->label = 'Auto Book Board'; + $autoBookBoard->autoBook = true; + + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('+30 days'); + $travel->dateTo = new \DateTimeImmutable('+37 days'); + $travel->additionalServices = [62 => $autoBookBoard]; + + $bookingDto = new BookingDto($travel, 1); + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->autoBookOptOutBoardIds = [62]; + $bookingDto->participants[0] = $participant; + + $handler = new ParticipantBoardFieldHandler(); + $handler->processField(['board' => ['62']], $bookingDto, 0); + + $this->assertSame([], $participant->autoBookOptOutBoardIds); + $this->assertCount(1, $participant->board); + $this->assertSame(62, $participant->board[0]->id); + } +} diff --git a/tests/Form/Service/ParticipantFieldOptionsProviderMandatoryServiceTest.php b/tests/Form/Service/ParticipantFieldOptionsProviderMandatoryServiceTest.php index 48e54b7..c5db354 100644 --- a/tests/Form/Service/ParticipantFieldOptionsProviderMandatoryServiceTest.php +++ b/tests/Form/Service/ParticipantFieldOptionsProviderMandatoryServiceTest.php @@ -91,6 +91,7 @@ class ParticipantFieldOptionsProviderMandatoryServiceTest extends TestCase $nonMandatoryService->available = 10; $nonMandatoryService->price = 10.0; $nonMandatoryService->mandatory = false; + $nonMandatoryService->autoBook = true; $travel = $this->createTravelWithAdditionalServices([$nonMandatoryService]); $bookingDto = new BookingDto($travel, 1); diff --git a/tests/Form/Service/ParticipantRentalsFieldHandlerTest.php b/tests/Form/Service/ParticipantRentalsFieldHandlerTest.php new file mode 100644 index 0000000..706114f --- /dev/null +++ b/tests/Form/Service/ParticipantRentalsFieldHandlerTest.php @@ -0,0 +1,107 @@ +createSkiPass(70); + $autoBookRental = $this->createRental(71, autoBook: true, mandatory: false); + $bookingDto = $this->createBookingDtoWithSkiPassAndRental($skiPass, $autoBookRental); + $participant = $bookingDto->participants[0]; + $participant->rentals = [$autoBookRental]; + + $handler = new ParticipantRentalsFieldHandler(); + $handler->processField([], $bookingDto, 0); + + $this->assertSame([71], $participant->autoBookOptOutRentalIds); + $this->assertSame([], $participant->rentals); + } + + public function testDeselectMandatoryAutoBookRentalDoesNotAddOptOutId(): void + { + $skiPass = $this->createSkiPass(72); + $mandatoryAutoBookRental = $this->createRental(73, autoBook: true, mandatory: true); + $bookingDto = $this->createBookingDtoWithSkiPassAndRental($skiPass, $mandatoryAutoBookRental); + $participant = $bookingDto->participants[0]; + $participant->rentals = [$mandatoryAutoBookRental]; + + $handler = new ParticipantRentalsFieldHandler(); + $handler->processField([], $bookingDto, 0); + + $this->assertSame([], $participant->autoBookOptOutRentalIds); + } + + public function testReselectAutoBookRentalRemovesOptOutId(): void + { + $skiPass = $this->createSkiPass(74); + $autoBookRental = $this->createRental(75, autoBook: true, mandatory: false); + $bookingDto = $this->createBookingDtoWithSkiPassAndRental($skiPass, $autoBookRental); + $participant = $bookingDto->participants[0]; + $participant->autoBookOptOutRentalIds = [75]; + + $handler = new ParticipantRentalsFieldHandler(); + $handler->processField(['rentals' => ['75']], $bookingDto, 0); + + $this->assertSame([], $participant->autoBookOptOutRentalIds); + $this->assertCount(1, $participant->rentals); + $this->assertSame(75, $participant->rentals[0]->id); + } + + private function createBookingDtoWithSkiPassAndRental(Service $skiPass, Service $rental): BookingDto + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-08'); + $travel->additionalServices = [ + $skiPass->id => $skiPass, + $rental->id => $rental, + ]; + + $bookingDto = new BookingDto($travel, 1); + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->skiPass = $skiPass; + $bookingDto->participants[0] = $participant; + + return $bookingDto; + } + + private function createSkiPass(int $id): Service + { + $service = new Service(); + $service->id = $id; + $service->subType = Constants::TOKEN_SKI_PASS; + $service->label = 'Ski Pass'; + $service->dateFrom = new \DateTimeImmutable('2030-01-01'); + $service->dateTo = new \DateTimeImmutable('2030-01-07'); + + return $service; + } + + private function createRental(int $id, bool $autoBook, bool $mandatory): Service + { + $service = new Service(); + $service->id = $id; + $service->subType = 'VER'; + $service->label = 'Rental'; + $service->autoBook = $autoBook; + $service->mandatory = $mandatory; + $service->dateFrom = new \DateTimeImmutable('2030-01-01'); + $service->dateTo = new \DateTimeImmutable('2030-01-07'); + + return $service; + } +} diff --git a/tests/Form/Service/ParticipantSkiPassFieldHandlerTest.php b/tests/Form/Service/ParticipantSkiPassFieldHandlerTest.php new file mode 100644 index 0000000..02b9c9a --- /dev/null +++ b/tests/Form/Service/ParticipantSkiPassFieldHandlerTest.php @@ -0,0 +1,88 @@ +createSkiPass(31, autoBook: true, mandatory: false); + $bookingDto = $this->createBookingDtoWithSkiPass($autoBookSkiPass); + $participant = $bookingDto->participants[0]; + $participant->skiPass = $autoBookSkiPass; + + $handler = new ParticipantSkiPassFieldHandler(); + $handler->processField([], $bookingDto, 0); + + $this->assertSame([31], $participant->autoBookOptOutSkiPassIds); + $this->assertNull($participant->skiPass); + } + + public function testDeselectMandatoryAutoBookSkiPassDoesNotAddOptOutId(): void + { + $mandatoryAutoBookSkiPass = $this->createSkiPass(32, autoBook: true, mandatory: true); + $bookingDto = $this->createBookingDtoWithSkiPass($mandatoryAutoBookSkiPass); + $participant = $bookingDto->participants[0]; + $participant->skiPass = $mandatoryAutoBookSkiPass; + + $handler = new ParticipantSkiPassFieldHandler(); + $handler->processField([], $bookingDto, 0); + + $this->assertSame([], $participant->autoBookOptOutSkiPassIds); + } + + public function testReselectAutoBookSkiPassRemovesOptOutId(): void + { + $autoBookSkiPass = $this->createSkiPass(33, autoBook: true, mandatory: false); + $bookingDto = $this->createBookingDtoWithSkiPass($autoBookSkiPass); + $participant = $bookingDto->participants[0]; + $participant->autoBookOptOutSkiPassIds = [33]; + + $handler = new ParticipantSkiPassFieldHandler(); + $handler->processField(['skiPass' => '33'], $bookingDto, 0); + + $this->assertSame([], $participant->autoBookOptOutSkiPassIds); + $this->assertNotNull($participant->skiPass); + $this->assertSame(33, $participant->skiPass?->id); + } + + private function createBookingDtoWithSkiPass(Service $skiPass): BookingDto + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('+30 days'); + $travel->dateTo = new \DateTimeImmutable('+37 days'); + $travel->additionalServices = [$skiPass->id => $skiPass]; + + $bookingDto = new BookingDto($travel, 1); + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $bookingDto->participants[0] = $participant; + + return $bookingDto; + } + + private function createSkiPass(int $id, bool $autoBook, bool $mandatory): Service + { + $service = new Service(); + $service->id = $id; + $service->subType = Constants::TOKEN_SKI_PASS; + $service->label = 'Ski Pass'; + $service->autoBook = $autoBook; + $service->mandatory = $mandatory; + $service->dateFrom = new \DateTimeImmutable('+30 days'); + $service->dateTo = new \DateTimeImmutable('+36 days'); + + return $service; + } +} diff --git a/tests/Service/BookingServiceBabyTest.php b/tests/Service/BookingServiceBabyTest.php index 576f9b3..9da9d34 100644 --- a/tests/Service/BookingServiceBabyTest.php +++ b/tests/Service/BookingServiceBabyTest.php @@ -189,6 +189,299 @@ class BookingServiceBabyTest extends TestCase $this->assertEmpty($participant->additionalServices, 'Age should be calculated at travel date, not current date'); } + public function testPreselectMandatoryServicesIncludesAutoBookServiceForEligibleParticipant(): void + { + $travel = $this->createTravelWithAutoBookServices(); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->additionalServices = []; + $bookingDto->participants[0] = $participant; + + $this->participantEligibilityService + ->method('isParticipantEligible') + ->willReturn(true); + + $this->bookingService->preselectMandatoryServices($bookingDto); + + $this->assertCount(1, $participant->additionalServices); + $this->assertSame('Auto Book Service', $participant->additionalServices[0]->label); + } + + public function testPreselectMandatoryServicesRespectsAutoBookOptOut(): void + { + $travel = $this->createTravelWithAutoBookServices(); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->additionalServices = []; + $participant->autoBookOptOutServiceIds = [2]; + $bookingDto->participants[0] = $participant; + + $this->participantEligibilityService + ->method('isParticipantEligible') + ->willReturn(true); + + $this->bookingService->preselectMandatoryServices($bookingDto); + + $this->assertEmpty($participant->additionalServices); + } + + public function testPreselectMandatoryServicesMandatoryOverridesAutoBookOptOut(): void + { + $travel = $this->createTravelWithMandatoryAndAutoBookService(); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->additionalServices = []; + $participant->autoBookOptOutServiceIds = [3]; + $bookingDto->participants[0] = $participant; + + $this->participantEligibilityService + ->method('isParticipantEligible') + ->willReturn(true); + + $this->bookingService->preselectMandatoryServices($bookingDto); + + $this->assertCount(1, $participant->additionalServices); + $this->assertSame('Mandatory And Auto Book Service', $participant->additionalServices[0]->label); + } + + public function testPreselectMandatoryServicesIncludesAutoBookSkiPassForEligibleParticipant(): void + { + $travel = $this->createTravelWithAutoBookSkiPassServices(); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $bookingDto->participants[0] = $participant; + + $this->participantEligibilityService + ->method('isParticipantEligible') + ->willReturn(true); + + $this->bookingService->preselectMandatoryServices($bookingDto); + + $this->assertNotNull($participant->skiPass); + $this->assertSame('Auto Book Ski Pass', $participant->skiPass?->label); + } + + public function testPreselectMandatoryServicesRespectsAutoBookSkiPassOptOut(): void + { + $travel = $this->createTravelWithAutoBookSkiPassServices(); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->autoBookOptOutSkiPassIds = [20]; + $bookingDto->participants[0] = $participant; + + $this->participantEligibilityService + ->method('isParticipantEligible') + ->willReturn(true); + + $this->bookingService->preselectMandatoryServices($bookingDto); + + $this->assertNull($participant->skiPass); + } + + public function testPreselectMandatoryServicesMandatorySkiPassOverridesAutoBookOptOut(): void + { + $travel = $this->createTravelWithMandatoryAndAutoBookSkiPassService(); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->autoBookOptOutSkiPassIds = [21]; + $bookingDto->participants[0] = $participant; + + $this->participantEligibilityService + ->method('isParticipantEligible') + ->willReturn(true); + + $this->bookingService->preselectMandatoryServices($bookingDto); + + $this->assertNotNull($participant->skiPass); + $this->assertSame('Mandatory And Auto Book Ski Pass', $participant->skiPass?->label); + } + + public function testPreselectMandatoryServicesIncludesAutoBookBoardForEligibleParticipant(): void + { + $travel = $this->createTravelWithAutoBookBoardServices(); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $bookingDto->participants[0] = $participant; + + $this->participantEligibilityService + ->method('isParticipantEligible') + ->willReturn(true); + + $this->bookingService->preselectMandatoryServices($bookingDto); + + $this->assertCount(1, $participant->board); + $this->assertSame('Auto Book Board', $participant->board[0]->label); + } + + public function testPreselectMandatoryServicesRespectsAutoBookBoardOptOut(): void + { + $travel = $this->createTravelWithAutoBookBoardServices(); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->autoBookOptOutBoardIds = [40]; + $bookingDto->participants[0] = $participant; + + $this->participantEligibilityService + ->method('isParticipantEligible') + ->willReturn(true); + + $this->bookingService->preselectMandatoryServices($bookingDto); + + $this->assertSame([], $participant->board); + } + + public function testPreselectMandatoryServicesMandatoryBoardOverridesAutoBookOptOut(): void + { + $travel = $this->createTravelWithMandatoryAndAutoBookBoardService(); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->autoBookOptOutBoardIds = [41]; + $bookingDto->participants[0] = $participant; + + $this->participantEligibilityService + ->method('isParticipantEligible') + ->willReturn(true); + + $this->bookingService->preselectMandatoryServices($bookingDto); + + $this->assertCount(1, $participant->board); + $this->assertSame('Mandatory And Auto Book Board', $participant->board[0]->label); + } + + public function testPreselectMandatoryServicesIncludesAutoBookRentalForMatchingSkiPass(): void + { + $travel = $this->createTravelWithAutoBookRentalServices(); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->skiPass = $travel->additionalServices[20]; + $bookingDto->participants[0] = $participant; + + $this->participantEligibilityService + ->method('isParticipantEligible') + ->willReturn(true); + + $this->bookingService->preselectMandatoryServices($bookingDto); + + $this->assertCount(1, $participant->rentals); + $this->assertSame('Auto Book Rental', $participant->rentals[0]->label); + } + + public function testPreselectMandatoryServicesRespectsAutoBookRentalOptOut(): void + { + $travel = $this->createTravelWithAutoBookRentalServices(); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->skiPass = $travel->additionalServices[20]; + $participant->autoBookOptOutRentalIds = [50]; + $bookingDto->participants[0] = $participant; + + $this->participantEligibilityService + ->method('isParticipantEligible') + ->willReturn(true); + + $this->bookingService->preselectMandatoryServices($bookingDto); + + $this->assertSame([], $participant->rentals); + } + + public function testPreselectMandatoryServicesMandatoryRentalOverridesAutoBookOptOut(): void + { + $travel = $this->createTravelWithMandatoryAndAutoBookRentalServices(); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->skiPass = $travel->additionalServices[22]; + $participant->autoBookOptOutRentalIds = [51]; + $bookingDto->participants[0] = $participant; + + $this->participantEligibilityService + ->method('isParticipantEligible') + ->willReturn(true); + + $this->bookingService->preselectMandatoryServices($bookingDto); + + $this->assertCount(1, $participant->rentals); + $this->assertSame('Mandatory And Auto Book Rental', $participant->rentals[0]->label); + } + + public function testPreselectMandatoryServicesSkipsAutoBookRentalWithoutSkiPass(): void + { + $travel = $this->createTravelWithAutoBookRentalServices(); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $bookingDto->participants[0] = $participant; + + $this->participantEligibilityService + ->method('isParticipantEligible') + ->willReturn(true); + + $this->bookingService->preselectMandatoryServices($bookingDto); + + $this->assertSame([], $participant->rentals); + } + + public function testPreselectMandatoryServicesSkipsAutoBookRentalForNonMatchingSkiPassDuration(): void + { + $travel = $this->createTravelWithAutoBookRentalServices(); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->skiPass = new Service(); + $participant->skiPass->id = 99; + $participant->skiPass->subType = Constants::TOKEN_SKI_PASS; + $participant->skiPass->dateFrom = new \DateTimeImmutable('+31 days'); + $participant->skiPass->dateTo = new \DateTimeImmutable('+35 days'); + $bookingDto->participants[0] = $participant; + + $this->participantEligibilityService + ->method('isParticipantEligible') + ->willReturn(true); + + $this->bookingService->preselectMandatoryServices($bookingDto); + + $this->assertSame([], $participant->rentals); + } + private function createTravelWithMandatoryServices(): Travel { $travel = new Travel(); @@ -199,6 +492,86 @@ class BookingServiceBabyTest extends TestCase return $travel; } + private function createTravelWithAutoBookServices(): Travel + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('+30 days'); + $travel->dateTo = new \DateTimeImmutable('+37 days'); + $travel->additionalServices = $this->createAutoBookAdditionalServices(); + + return $travel; + } + + private function createTravelWithMandatoryAndAutoBookService(): Travel + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('+30 days'); + $travel->dateTo = new \DateTimeImmutable('+37 days'); + $travel->additionalServices = $this->createMandatoryAndAutoBookAdditionalServices(); + + return $travel; + } + + private function createTravelWithAutoBookSkiPassServices(): Travel + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('+30 days'); + $travel->dateTo = new \DateTimeImmutable('+37 days'); + $travel->additionalServices = $this->createAutoBookSkiPassServices(); + + return $travel; + } + + private function createTravelWithMandatoryAndAutoBookSkiPassService(): Travel + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('+30 days'); + $travel->dateTo = new \DateTimeImmutable('+37 days'); + $travel->additionalServices = $this->createMandatoryAndAutoBookSkiPassServices(); + + return $travel; + } + + private function createTravelWithAutoBookBoardServices(): Travel + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('+30 days'); + $travel->dateTo = new \DateTimeImmutable('+37 days'); + $travel->additionalServices = $this->createAutoBookBoardServices(); + + return $travel; + } + + private function createTravelWithMandatoryAndAutoBookBoardService(): Travel + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('+30 days'); + $travel->dateTo = new \DateTimeImmutable('+37 days'); + $travel->additionalServices = $this->createMandatoryAndAutoBookBoardServices(); + + return $travel; + } + + private function createTravelWithAutoBookRentalServices(): Travel + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-08'); + $travel->additionalServices = $this->createAutoBookRentalServices(); + + return $travel; + } + + private function createTravelWithMandatoryAndAutoBookRentalServices(): Travel + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-08'); + $travel->additionalServices = $this->createMandatoryAndAutoBookRentalServices(); + + return $travel; + } + /** * @return array */ @@ -214,4 +587,176 @@ class BookingServiceBabyTest extends TestCase return [1 => $mandatoryService]; } + + /** + * @return array + */ + private function createAutoBookAdditionalServices(): array + { + $autoBookService = new Service(); + $autoBookService->id = 2; + $autoBookService->label = 'Auto Book Service'; + $autoBookService->subType = Constants::TOKEN_ADDITIONAL; + $autoBookService->mandatory = false; + $autoBookService->autoBook = true; + $autoBookService->available = 10; + $autoBookService->price = 10.0; + + return [2 => $autoBookService]; + } + + /** + * @return array + */ + private function createMandatoryAndAutoBookAdditionalServices(): array + { + $service = new Service(); + $service->id = 3; + $service->label = 'Mandatory And Auto Book Service'; + $service->subType = Constants::TOKEN_ADDITIONAL; + $service->mandatory = true; + $service->autoBook = true; + $service->available = 10; + $service->price = 10.0; + + return [3 => $service]; + } + + /** + * @return array + */ + private function createAutoBookSkiPassServices(): array + { + $service = new Service(); + $service->id = 20; + $service->label = 'Auto Book Ski Pass'; + $service->subType = Constants::TOKEN_SKI_PASS; + $service->mandatory = false; + $service->autoBook = true; + $service->available = 10; + $service->price = 59.0; + $service->dateFrom = new \DateTimeImmutable('+30 days'); + $service->dateTo = new \DateTimeImmutable('+36 days'); + + return [20 => $service]; + } + + /** + * @return array + */ + private function createMandatoryAndAutoBookSkiPassServices(): array + { + $service = new Service(); + $service->id = 21; + $service->label = 'Mandatory And Auto Book Ski Pass'; + $service->subType = Constants::TOKEN_SKI_PASS; + $service->mandatory = true; + $service->autoBook = true; + $service->available = 10; + $service->price = 59.0; + $service->dateFrom = new \DateTimeImmutable('+30 days'); + $service->dateTo = new \DateTimeImmutable('+36 days'); + + return [21 => $service]; + } + + /** + * @return array + */ + private function createAutoBookBoardServices(): array + { + $service = new Service(); + $service->id = 40; + $service->label = 'Auto Book Board'; + $service->subType = Constants::TOKEN_BOARD; + $service->mandatory = false; + $service->autoBook = true; + $service->available = 10; + $service->price = 12.0; + + return [40 => $service]; + } + + /** + * @return array + */ + private function createMandatoryAndAutoBookBoardServices(): array + { + $service = new Service(); + $service->id = 41; + $service->label = 'Mandatory And Auto Book Board'; + $service->subType = Constants::TOKEN_BOARD; + $service->mandatory = true; + $service->autoBook = true; + $service->available = 10; + $service->price = 12.0; + + return [41 => $service]; + } + + /** + * @return array + */ + private function createAutoBookRentalServices(): array + { + $serviceDateFrom = new \DateTimeImmutable('2030-01-01'); + $serviceDateTo = new \DateTimeImmutable('2030-01-07'); + + $skiPass = new Service(); + $skiPass->id = 20; + $skiPass->label = 'Matching Ski Pass'; + $skiPass->subType = Constants::TOKEN_SKI_PASS; + $skiPass->mandatory = false; + $skiPass->autoBook = false; + $skiPass->available = 10; + $skiPass->price = 59.0; + $skiPass->dateFrom = $serviceDateFrom; + $skiPass->dateTo = $serviceDateTo; + + $rental = new Service(); + $rental->id = 50; + $rental->label = 'Auto Book Rental'; + $rental->subType = 'VER'; + $rental->mandatory = false; + $rental->autoBook = true; + $rental->available = 10; + $rental->price = 39.0; + $rental->dateFrom = $serviceDateFrom; + $rental->dateTo = $serviceDateTo; + + return [20 => $skiPass, 50 => $rental]; + } + + /** + * @return array + */ + private function createMandatoryAndAutoBookRentalServices(): array + { + $serviceDateFrom = new \DateTimeImmutable('2030-01-01'); + $serviceDateTo = new \DateTimeImmutable('2030-01-07'); + + $skiPass = new Service(); + $skiPass->id = 22; + $skiPass->label = 'Matching Ski Pass Mandatory Rental'; + $skiPass->subType = Constants::TOKEN_SKI_PASS; + $skiPass->mandatory = false; + $skiPass->autoBook = false; + $skiPass->available = 10; + $skiPass->price = 59.0; + $skiPass->dateFrom = $serviceDateFrom; + $skiPass->dateTo = $serviceDateTo; + + $rental = new Service(); + $rental->id = 51; + $rental->label = 'Mandatory And Auto Book Rental'; + $rental->subType = 'VER'; + $rental->mandatory = true; + $rental->autoBook = true; + $rental->available = 10; + $rental->price = 39.0; + $rental->dateFrom = $serviceDateFrom; + $rental->dateTo = $serviceDateTo; + + return [22 => $skiPass, 51 => $rental]; + } }