diff --git a/config/services.yaml b/config/services.yaml index a746fbe..0669ef9 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -155,11 +155,11 @@ services: - '@App\Form\Service\ParticipantPurchaseVoucherFieldHandler' - '@App\Form\Service\ParticipantPromoVoucherFieldHandler' - # Participant Status Rules + # Booking Status Rules App\BusProNet\Service\StatusRule\ChaperonServiceStatusRule: ~ - # Participant Status Rule Registry - App\BusProNet\Service\ParticipantStatusRuleRegistry: + # Booking Status Rule Registry + App\BusProNet\Service\BookingStatusRuleRegistry: arguments: $rules: - '@App\BusProNet\Service\StatusRule\ChaperonServiceStatusRule' diff --git a/src/BusProNet/DataProcessor/BookingPayloadBuilder.php b/src/BusProNet/DataProcessor/BookingPayloadBuilder.php index f3f9ade..3a61c30 100644 --- a/src/BusProNet/DataProcessor/BookingPayloadBuilder.php +++ b/src/BusProNet/DataProcessor/BookingPayloadBuilder.php @@ -6,7 +6,6 @@ namespace App\BusProNet\DataProcessor; use App\BusProNet\Constants; use App\BusProNet\Model\Booking; -use App\BusProNet\Service\ParticipantStatusRuleRegistry; use App\Form\Model\BookingDto; /** @@ -17,9 +16,10 @@ use App\Form\Model\BookingDto; */ class BookingPayloadBuilder { + private const DEFAULT_PARTICIPANT_STATUS = 'F'; + public function __construct( private readonly ServiceMappingCollector $mappingCollector, - private readonly ParticipantStatusRuleRegistry $statusRuleRegistry, ) { } @@ -302,7 +302,7 @@ class BookingPayloadBuilder foreach ($bookingDto->participants as $index => $participant) { $participantData = [ '@id' => $index + 1, - 'status' => $this->statusRuleRegistry->evaluateStatus($participant), + 'status' => self::DEFAULT_PARTICIPANT_STATUS, 'name' => $participant->lastName, 'vorname' => $participant->firstName, 'geschlecht' => $participant->gender ?? '', diff --git a/src/BusProNet/Service/BookingStatusRuleRegistry.php b/src/BusProNet/Service/BookingStatusRuleRegistry.php new file mode 100644 index 0000000..3d3abcb --- /dev/null +++ b/src/BusProNet/Service/BookingStatusRuleRegistry.php @@ -0,0 +1,58 @@ +sortedRules = $this->sortRulesByPriority($rules); + } + + public function evaluateStatus(BookingDto $bookingDto): string + { + foreach ($this->sortedRules as $rule) { + if (true === $rule->evaluate($bookingDto)) { + return $rule->getStatus(); + } + } + + return self::DEFAULT_STATUS; + } + + /** + * @param BookingStatusRuleInterface[] $rules + * + * @return BookingStatusRuleInterface[] + */ + private function sortRulesByPriority(array $rules): array + { + usort($rules, static fn ( + BookingStatusRuleInterface $a, + BookingStatusRuleInterface $b, + ): int => $b->getPriority() <=> $a->getPriority()); + + return $rules; + } +} diff --git a/src/BusProNet/Service/Contract/BookingStatusRuleInterface.php b/src/BusProNet/Service/Contract/BookingStatusRuleInterface.php new file mode 100644 index 0000000..23f8e27 --- /dev/null +++ b/src/BusProNet/Service/Contract/BookingStatusRuleInterface.php @@ -0,0 +1,36 @@ +sortedRules = $this->sortRulesByPriority($rules); - } - - /** - * Evaluates all rules to determine the participant's status. - * - * Rules are evaluated in priority order (highest first). The first rule - * that matches determines the status. Returns 'F' if no rules match. - * - * @param ParticipantDto $participant The participant to evaluate - * - * @return string The determined status code - */ - public function evaluateStatus(ParticipantDto $participant): string - { - foreach ($this->sortedRules as $rule) { - if (true === $rule->evaluate($participant)) { - return $rule->getStatus(); - } - } - - return self::DEFAULT_STATUS; - } - - /** - * Sorts rules by priority in descending order. - * - * @param ParticipantStatusRuleInterface[] $rules The rules to sort - * - * @return ParticipantStatusRuleInterface[] The sorted rules - */ - private function sortRulesByPriority(array $rules): array - { - usort($rules, static fn ( - ParticipantStatusRuleInterface $a, - ParticipantStatusRuleInterface $b, - ): int => $b->getPriority() <=> $a->getPriority()); - - return $rules; - } -} diff --git a/src/BusProNet/Service/StatusRule/ChaperonServiceStatusRule.php b/src/BusProNet/Service/StatusRule/ChaperonServiceStatusRule.php index b3cd341..28917a1 100644 --- a/src/BusProNet/Service/StatusRule/ChaperonServiceStatusRule.php +++ b/src/BusProNet/Service/StatusRule/ChaperonServiceStatusRule.php @@ -5,26 +5,37 @@ declare(strict_types=1); namespace App\BusProNet\Service\StatusRule; use App\BusProNet\Model\Service; -use App\BusProNet\Service\Contract\ParticipantStatusRuleInterface; +use App\BusProNet\Service\Contract\BookingStatusRuleInterface; +use App\Form\Model\BookingDto; use App\Form\Model\ParticipantDto; /** - * Status rule for participants with chaperon (Begleitperson) services. + * Status rule for bookings with chaperon (Begleitperson) services. * - * Assigns 'Option' status to participants who have selected any service - * containing 'Begleitperson' in its label. This applies to accompanying - * persons whose booking confirmation may depend on the main participant. + * Assigns 'Option' status to bookings where any participant has selected any + * service containing 'Begleitperson' in its label. */ -class ChaperonServiceStatusRule implements ParticipantStatusRuleInterface +class ChaperonServiceStatusRule implements BookingStatusRuleInterface { private const STATUS = 'O'; private const PRIORITY = 100; private const SEARCH_TERM = 'begleitperson'; /** - * @see ParticipantStatusRuleInterface::evaluate() + * @see BookingStatusRuleInterface::evaluate() */ - public function evaluate(ParticipantDto $participant): bool + public function evaluate(BookingDto $bookingDto): bool + { + foreach ($bookingDto->participants as $participant) { + if (true === $this->participantHasChaperonService($participant)) { + return true; + } + } + + return false; + } + + private function participantHasChaperonService(ParticipantDto $participant): bool { // Check array services $arrayServices = [ @@ -58,7 +69,7 @@ class ChaperonServiceStatusRule implements ParticipantStatusRuleInterface } /** - * @see ParticipantStatusRuleInterface::getStatus() + * @see BookingStatusRuleInterface::getStatus() */ public function getStatus(): string { @@ -66,7 +77,7 @@ class ChaperonServiceStatusRule implements ParticipantStatusRuleInterface } /** - * @see ParticipantStatusRuleInterface::getPriority() + * @see BookingStatusRuleInterface::getPriority() */ public function getPriority(): int { @@ -74,7 +85,7 @@ class ChaperonServiceStatusRule implements ParticipantStatusRuleInterface } /** - * @see ParticipantStatusRuleInterface::getDescription() + * @see BookingStatusRuleInterface::getDescription() */ public function getDescription(): string { diff --git a/src/Controller/Booking/Create/Step2Controller.php b/src/Controller/Booking/Create/Step2Controller.php index 036899d..fd5a668 100644 --- a/src/Controller/Booking/Create/Step2Controller.php +++ b/src/Controller/Booking/Create/Step2Controller.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Controller\Booking\Create; -use App\BusProNet\Constants; use App\Controller\Booking\Traits\BookingCreateTrait; use App\Controller\Booking\Traits\BookingExceptionHandlerTrait; use App\Controller\Booking\Traits\ParticipantCardFlowTrait; @@ -86,6 +85,7 @@ class Step2Controller extends AbstractController // Preselect mandatory services $this->bookingService->preselectDefaultServices($bookingCreateDto); + $this->bookingService->applyCreateBookingStatusRules($bookingCreateDto); // Save BookingDto to session $this->bookingService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE); @@ -162,8 +162,7 @@ class Step2Controller extends AbstractController // Keep behavior consistent with cards view: newly filled participant data // (especially dateOfBirth) must immediately trigger mandatory service preselection. $this->bookingService->preselectDefaultServices($bookingDto); - - $bookingDto->bookingStatus = Constants::BOOKING_STATUS_OPEN; + $this->bookingService->applyCreateBookingStatusRules($bookingDto); $this->bookingService->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE); // Recreate form with filled DTO so the view shows the dummy data @@ -176,6 +175,7 @@ class Step2Controller extends AbstractController // 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); + $this->bookingService->applyCreateBookingStatusRules($bookingDto); } // Collect notifications from field handlers (run during PRE_SUBMIT) diff --git a/src/Controller/Booking/Create/Step3Controller.php b/src/Controller/Booking/Create/Step3Controller.php index ff5092f..ef5a00e 100644 --- a/src/Controller/Booking/Create/Step3Controller.php +++ b/src/Controller/Booking/Create/Step3Controller.php @@ -70,6 +70,8 @@ class Step3Controller extends AbstractController } try { + $this->bookingService->applyCreateBookingStatusRules($bookingCreateDto); + // Validate booking data with API by submitting an inquiry booking $inquiryResponse = $this->apiClient->createBookingInquiry($bookingCreateDto); diff --git a/src/Controller/Booking/Create/Step4Controller.php b/src/Controller/Booking/Create/Step4Controller.php index 99144c7..c3689ab 100644 --- a/src/Controller/Booking/Create/Step4Controller.php +++ b/src/Controller/Booking/Create/Step4Controller.php @@ -86,6 +86,8 @@ class Step4Controller extends AbstractController if (true === $form->isSubmitted() && true === $form->isValid()) { try { + $this->bookingService->applyCreateBookingStatusRules($bookingCreateDto); + // Submit final booking (already validated in Step 3) $bookingResponse = $this->apiClient->createBooking($bookingCreateDto); diff --git a/src/Controller/Booking/Traits/ParticipantCardFlowTrait.php b/src/Controller/Booking/Traits/ParticipantCardFlowTrait.php index 80076c9..203624f 100644 --- a/src/Controller/Booking/Traits/ParticipantCardFlowTrait.php +++ b/src/Controller/Booking/Traits/ParticipantCardFlowTrait.php @@ -131,6 +131,9 @@ trait ParticipantCardFlowTrait // Refresh endpoint is POST-only and always processes submitted participant data. $this->bookingService->preselectDefaultServices($bookingDto); + if (BookingDto::MODE_CREATE === $bookingDto->getMode()) { + $this->bookingService->applyCreateBookingStatusRules($bookingDto); + } // Recreate form so the rendered state reflects any new auto-preselections. $form = $this->createParticipantForm($bookingDto, $index, $refreshFormOptions); diff --git a/src/Form/Model/BookingDto.php b/src/Form/Model/BookingDto.php index 05eadbc..aa5fe9a 100644 --- a/src/Form/Model/BookingDto.php +++ b/src/Form/Model/BookingDto.php @@ -54,7 +54,7 @@ class BookingDto /** * Booking status code for API submission. - * Values: 'F' (Final/Frei), 'A' (Anfrage/Inquiry). + * Values: 'F' (Fest), 'O' (Option), 'A' (Anfrage/Inquiry). */ public string $bookingStatus = 'F'; diff --git a/src/Service/BookingService.php b/src/Service/BookingService.php index 05d5449..a7b4e13 100644 --- a/src/Service/BookingService.php +++ b/src/Service/BookingService.php @@ -8,6 +8,7 @@ use App\BusProNet\Constants; use App\BusProNet\Model\Room; use App\BusProNet\Model\Service; use App\BusProNet\Model\Travel; +use App\BusProNet\Service\BookingStatusRuleRegistry; use App\BusProNet\XmlLoader\AgencyLoader; use App\Exception\BookingSessionNotFoundException; use App\Exception\NoRoomsAvailableException; @@ -31,6 +32,7 @@ class BookingService private readonly TravelDataService $travelDataService, private readonly BookingPriceCalculatorService $priceCalculator, private readonly ParticipantEligibilityService $participantEligibilityService, + private readonly BookingStatusRuleRegistry $bookingStatusRuleRegistry, private readonly AgencyLoader $agencyLoader, #[Autowire('%default_booking_status%')] private readonly string $defaultBookingStatus, @@ -982,6 +984,20 @@ class BookingService } } + /** + * Applies booking-level status rules in create flow. + * + * Inquiry bookings always win and are never overridden. + */ + public function applyCreateBookingStatusRules(BookingDto $bookingDto): void + { + if (Constants::BOOKING_STATUS_INQUIRY === $bookingDto->bookingStatus) { + return; + } + + $bookingDto->bookingStatus = $this->bookingStatusRuleRegistry->evaluateStatus($bookingDto); + } + /** * Ensures the booking DTO has the correct number of participant objects. * diff --git a/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php b/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php index ff2b98b..b7b5f97 100644 --- a/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php +++ b/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php @@ -18,7 +18,6 @@ use App\BusProNet\Model\Pickup; use App\BusProNet\Model\Room; use App\BusProNet\Model\Service; use App\BusProNet\Model\Travel; -use App\BusProNet\Service\ParticipantStatusRuleRegistry; use App\Form\Model\BookingDto; use App\Form\Model\ParticipantDto; use App\Service\BookingPriceCalculatorService; @@ -53,8 +52,7 @@ class BookingDataProcessorTest extends TestCase // Create the new dependencies $mappingCollector = new ServiceMappingCollector(); $serviceProcessor = new ParticipantServiceProcessor(new NullLogger()); - $statusRuleRegistry = new ParticipantStatusRuleRegistry([]); - $payloadBuilder = new BookingPayloadBuilder($mappingCollector, $statusRuleRegistry); + $payloadBuilder = new BookingPayloadBuilder($mappingCollector); $personalDataSynchronizer = new PersonalDataSynchronizer(); $this->processor = new BookingDataProcessor( diff --git a/tests/BusProNet/Service/BookingStatusRuleRegistryTest.php b/tests/BusProNet/Service/BookingStatusRuleRegistryTest.php new file mode 100644 index 0000000..91abf00 --- /dev/null +++ b/tests/BusProNet/Service/BookingStatusRuleRegistryTest.php @@ -0,0 +1,111 @@ +createMock(BookingStatusRuleInterface::class); + $rule->method('evaluate')->willReturn(false); + $rule->method('getPriority')->willReturn(100); + + $registry = new BookingStatusRuleRegistry([$rule]); + $bookingDto = new BookingDto($this->createMockTravel(), 1); + + $this->assertSame('F', $registry->evaluateStatus($bookingDto)); + } + + public function testReturnsMatchingRuleStatus(): void + { + $rule = $this->createMock(BookingStatusRuleInterface::class); + $rule->method('evaluate')->willReturn(true); + $rule->method('getStatus')->willReturn('O'); + $rule->method('getPriority')->willReturn(100); + + $registry = new BookingStatusRuleRegistry([$rule]); + $bookingDto = new BookingDto($this->createMockTravel(), 1); + + $this->assertSame('O', $registry->evaluateStatus($bookingDto)); + } + + public function testHigherPriorityRuleTakesPrecedence(): void + { + $lowPriorityRule = $this->createMock(BookingStatusRuleInterface::class); + $lowPriorityRule->method('evaluate')->willReturn(true); + $lowPriorityRule->method('getStatus')->willReturn('L'); + $lowPriorityRule->method('getPriority')->willReturn(50); + + $highPriorityRule = $this->createMock(BookingStatusRuleInterface::class); + $highPriorityRule->method('evaluate')->willReturn(true); + $highPriorityRule->method('getStatus')->willReturn('H'); + $highPriorityRule->method('getPriority')->willReturn(100); + + $registry = new BookingStatusRuleRegistry([$lowPriorityRule, $highPriorityRule]); + $bookingDto = new BookingDto($this->createMockTravel(), 1); + + $this->assertSame('H', $registry->evaluateStatus($bookingDto)); + } + + public function testEmptyRulesArrayReturnsDefaultStatus(): void + { + $registry = new BookingStatusRuleRegistry([]); + $bookingDto = new BookingDto($this->createMockTravel(), 1); + + $this->assertSame('F', $registry->evaluateStatus($bookingDto)); + } + + public function testFirstMatchingRuleWinsForSamePriority(): void + { + $firstMatchingRule = $this->createMock(BookingStatusRuleInterface::class); + $firstMatchingRule->method('evaluate')->willReturn(true); + $firstMatchingRule->method('getStatus')->willReturn('A'); + $firstMatchingRule->method('getPriority')->willReturn(100); + + $secondMatchingRule = $this->createMock(BookingStatusRuleInterface::class); + $secondMatchingRule->method('evaluate')->willReturn(true); + $secondMatchingRule->method('getStatus')->willReturn('B'); + $secondMatchingRule->method('getPriority')->willReturn(100); + + $registry = new BookingStatusRuleRegistry([$firstMatchingRule, $secondMatchingRule]); + $bookingDto = new BookingDto($this->createMockTravel(), 1); + + $this->assertSame('A', $registry->evaluateStatus($bookingDto)); + } + + public function testNonMatchingHighPriorityRuleDoesNotBlockLowerPriority(): void + { + $highPriorityRule = $this->createMock(BookingStatusRuleInterface::class); + $highPriorityRule->method('evaluate')->willReturn(false); + $highPriorityRule->method('getStatus')->willReturn('H'); + $highPriorityRule->method('getPriority')->willReturn(100); + + $lowPriorityRule = $this->createMock(BookingStatusRuleInterface::class); + $lowPriorityRule->method('evaluate')->willReturn(true); + $lowPriorityRule->method('getStatus')->willReturn('L'); + $lowPriorityRule->method('getPriority')->willReturn(50); + + $registry = new BookingStatusRuleRegistry([$highPriorityRule, $lowPriorityRule]); + $bookingDto = new BookingDto($this->createMockTravel(), 1); + + $this->assertSame('L', $registry->evaluateStatus($bookingDto)); + } + + private function createMockTravel(): \App\BusProNet\Model\Travel + { + $travel = new \App\BusProNet\Model\Travel(); + $travel->id = 1; + $travel->hotelId = 1; + $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-08'); + + return $travel; + } +} diff --git a/tests/BusProNet/Service/ParticipantStatusRuleRegistryTest.php b/tests/BusProNet/Service/ParticipantStatusRuleRegistryTest.php deleted file mode 100644 index 9724d3a..0000000 --- a/tests/BusProNet/Service/ParticipantStatusRuleRegistryTest.php +++ /dev/null @@ -1,129 +0,0 @@ -createMock(ParticipantStatusRuleInterface::class); - $rule->method('evaluate')->willReturn(false); - $rule->method('getPriority')->willReturn(100); - - $registry = new ParticipantStatusRuleRegistry([$rule]); - $participant = new ParticipantDto(); - - $this->assertSame('F', $registry->evaluateStatus($participant)); - } - - public function testReturnsMatchingRuleStatus(): void - { - $rule = $this->createMock(ParticipantStatusRuleInterface::class); - $rule->method('evaluate')->willReturn(true); - $rule->method('getStatus')->willReturn('O'); - $rule->method('getPriority')->willReturn(100); - - $registry = new ParticipantStatusRuleRegistry([$rule]); - $participant = new ParticipantDto(); - - $this->assertSame('O', $registry->evaluateStatus($participant)); - } - - public function testHigherPriorityRuleTakesPrecedence(): void - { - $lowPriorityRule = $this->createMock(ParticipantStatusRuleInterface::class); - $lowPriorityRule->method('evaluate')->willReturn(true); - $lowPriorityRule->method('getStatus')->willReturn('L'); - $lowPriorityRule->method('getPriority')->willReturn(50); - - $highPriorityRule = $this->createMock(ParticipantStatusRuleInterface::class); - $highPriorityRule->method('evaluate')->willReturn(true); - $highPriorityRule->method('getStatus')->willReturn('H'); - $highPriorityRule->method('getPriority')->willReturn(100); - - // Register low priority first, high priority second - $registry = new ParticipantStatusRuleRegistry([$lowPriorityRule, $highPriorityRule]); - $participant = new ParticipantDto(); - - // High priority rule should be evaluated first and match - $this->assertSame('H', $registry->evaluateStatus($participant)); - } - - public function testEmptyRulesArrayReturnsDefaultStatus(): void - { - $registry = new ParticipantStatusRuleRegistry([]); - $participant = new ParticipantDto(); - - $this->assertSame('F', $registry->evaluateStatus($participant)); - } - - public function testFirstMatchingRuleWins(): void - { - $firstMatchingRule = $this->createMock(ParticipantStatusRuleInterface::class); - $firstMatchingRule->method('evaluate')->willReturn(true); - $firstMatchingRule->method('getStatus')->willReturn('A'); - $firstMatchingRule->method('getPriority')->willReturn(100); - - $secondMatchingRule = $this->createMock(ParticipantStatusRuleInterface::class); - $secondMatchingRule->method('evaluate')->willReturn(true); - $secondMatchingRule->method('getStatus')->willReturn('B'); - $secondMatchingRule->method('getPriority')->willReturn(100); - - $registry = new ParticipantStatusRuleRegistry([$firstMatchingRule, $secondMatchingRule]); - $participant = new ParticipantDto(); - - // First rule with same priority registered first should win - $this->assertSame('A', $registry->evaluateStatus($participant)); - } - - public function testNonMatchingHighPriorityRuleDoesNotBlockLowerPriority(): void - { - $highPriorityRule = $this->createMock(ParticipantStatusRuleInterface::class); - $highPriorityRule->method('evaluate')->willReturn(false); - $highPriorityRule->method('getStatus')->willReturn('H'); - $highPriorityRule->method('getPriority')->willReturn(100); - - $lowPriorityRule = $this->createMock(ParticipantStatusRuleInterface::class); - $lowPriorityRule->method('evaluate')->willReturn(true); - $lowPriorityRule->method('getStatus')->willReturn('L'); - $lowPriorityRule->method('getPriority')->willReturn(50); - - $registry = new ParticipantStatusRuleRegistry([$highPriorityRule, $lowPriorityRule]); - $participant = new ParticipantDto(); - - // High priority doesn't match, so low priority rule should be used - $this->assertSame('L', $registry->evaluateStatus($participant)); - } - - public function testMultipleRulesWithVariousPrioritiesAreSortedCorrectly(): void - { - $lowRule = $this->createMock(ParticipantStatusRuleInterface::class); - $lowRule->method('evaluate')->willReturn(true); - $lowRule->method('getStatus')->willReturn('LOW'); - $lowRule->method('getPriority')->willReturn(10); - - $mediumRule = $this->createMock(ParticipantStatusRuleInterface::class); - $mediumRule->method('evaluate')->willReturn(true); - $mediumRule->method('getStatus')->willReturn('MED'); - $mediumRule->method('getPriority')->willReturn(50); - - $highRule = $this->createMock(ParticipantStatusRuleInterface::class); - $highRule->method('evaluate')->willReturn(true); - $highRule->method('getStatus')->willReturn('HIGH'); - $highRule->method('getPriority')->willReturn(100); - - // Register in random order - $registry = new ParticipantStatusRuleRegistry([$mediumRule, $lowRule, $highRule]); - $participant = new ParticipantDto(); - - // Highest priority rule should match first - $this->assertSame('HIGH', $registry->evaluateStatus($participant)); - } -} diff --git a/tests/BusProNet/Service/StatusRule/ChaperonServiceStatusRuleTest.php b/tests/BusProNet/Service/StatusRule/ChaperonServiceStatusRuleTest.php index 4d3ec92..ddf47d7 100644 --- a/tests/BusProNet/Service/StatusRule/ChaperonServiceStatusRuleTest.php +++ b/tests/BusProNet/Service/StatusRule/ChaperonServiceStatusRuleTest.php @@ -5,7 +5,9 @@ declare(strict_types=1); namespace App\Tests\BusProNet\Service\StatusRule; use App\BusProNet\Model\Service; +use App\BusProNet\Model\Travel; use App\BusProNet\Service\StatusRule\ChaperonServiceStatusRule; +use App\Form\Model\BookingDto; use App\Form\Model\ParticipantDto; use PHPUnit\Framework\TestCase; @@ -20,99 +22,99 @@ class ChaperonServiceStatusRuleTest extends TestCase public function testReturnsTrueWhenBegleitpersonInAdditionalServices(): void { - $participant = new ParticipantDto(); + $bookingDto = $this->createBookingDtoWithParticipant(); $service = new Service(); $service->label = 'Skibegleitperson Kurs'; - $participant->additionalServices = [$service]; + $bookingDto->participants[0]->additionalServices = [$service]; - $this->assertTrue($this->rule->evaluate($participant)); + $this->assertTrue($this->rule->evaluate($bookingDto)); } public function testReturnsTrueWhenBegleitpersonInCourses(): void { - $participant = new ParticipantDto(); + $bookingDto = $this->createBookingDtoWithParticipant(); $service = new Service(); - $service->label = 'Begleitperson Anfängerkurs'; - $participant->courses = [$service]; + $service->label = 'Begleitperson Anfaengerkurs'; + $bookingDto->participants[0]->courses = [$service]; - $this->assertTrue($this->rule->evaluate($participant)); + $this->assertTrue($this->rule->evaluate($bookingDto)); } public function testReturnsTrueWhenBegleitpersonInBoard(): void { - $participant = new ParticipantDto(); + $bookingDto = $this->createBookingDtoWithParticipant(); $service = new Service(); $service->label = 'Halbpension Begleitperson'; - $participant->board = [$service]; + $bookingDto->participants[0]->board = [$service]; - $this->assertTrue($this->rule->evaluate($participant)); + $this->assertTrue($this->rule->evaluate($bookingDto)); } public function testReturnsTrueWhenBegleitpersonInRentals(): void { - $participant = new ParticipantDto(); + $bookingDto = $this->createBookingDtoWithParticipant(); $service = new Service(); $service->label = 'Skiset Begleitperson'; - $participant->rentals = [$service]; + $bookingDto->participants[0]->rentals = [$service]; - $this->assertTrue($this->rule->evaluate($participant)); + $this->assertTrue($this->rule->evaluate($bookingDto)); } public function testReturnsTrueWhenBegleitpersonInSkiPass(): void { - $participant = new ParticipantDto(); + $bookingDto = $this->createBookingDtoWithParticipant(); $service = new Service(); $service->label = 'Skipass Begleitperson 6 Tage'; - $participant->skiPass = $service; + $bookingDto->participants[0]->skiPass = $service; - $this->assertTrue($this->rule->evaluate($participant)); + $this->assertTrue($this->rule->evaluate($bookingDto)); } public function testReturnsTrueWhenBegleitpersonInVeg(): void { - $participant = new ParticipantDto(); + $bookingDto = $this->createBookingDtoWithParticipant(); $service = new Service(); $service->label = 'Vegetarisch Begleitperson'; - $participant->veg = $service; + $bookingDto->participants[0]->veg = $service; - $this->assertTrue($this->rule->evaluate($participant)); + $this->assertTrue($this->rule->evaluate($bookingDto)); + } + + public function testReturnsTrueWhenSecondParticipantMatches(): void + { + $bookingDto = $this->createBookingDtoWithParticipantCount(2); + $service = new Service(); + $service->label = 'Begleitperson Service'; + $bookingDto->participants[1]->additionalServices = [$service]; + + $this->assertTrue($this->rule->evaluate($bookingDto)); } public function testReturnsFalseWhenNoBegleitpersonService(): void { - $participant = new ParticipantDto(); + $bookingDto = $this->createBookingDtoWithParticipant(); $service = new Service(); $service->label = 'Skipass 6 Tage Erwachsene'; - $participant->skiPass = $service; + $bookingDto->participants[0]->skiPass = $service; - $this->assertFalse($this->rule->evaluate($participant)); + $this->assertFalse($this->rule->evaluate($bookingDto)); } - public function testReturnsFalseWithEmptyServices(): void + public function testReturnsFalseWithEmptyParticipants(): void { - $participant = new ParticipantDto(); + $bookingDto = new BookingDto($this->createTravel(), 1); - $this->assertFalse($this->rule->evaluate($participant)); + $this->assertFalse($this->rule->evaluate($bookingDto)); } public function testCaseInsensitiveMatch(): void { - $participant = new ParticipantDto(); + $bookingDto = $this->createBookingDtoWithParticipant(); $service = new Service(); $service->label = 'BEGLEITPERSON Kurs'; - $participant->additionalServices = [$service]; + $bookingDto->participants[0]->additionalServices = [$service]; - $this->assertTrue($this->rule->evaluate($participant)); - } - - public function testMixedCaseMatch(): void - { - $participant = new ParticipantDto(); - $service = new Service(); - $service->label = 'beGleitPerson Kurs'; - $participant->courses = [$service]; - - $this->assertTrue($this->rule->evaluate($participant)); + $this->assertTrue($this->rule->evaluate($bookingDto)); } public function testReturnsStatusO(): void @@ -133,28 +135,32 @@ class ChaperonServiceStatusRuleTest extends TestCase $this->assertNotEmpty($description); } - public function testHandlesNullLabel(): void + private function createBookingDtoWithParticipant(): BookingDto { - $participant = new ParticipantDto(); - $service = new Service(); - $service->label = null; - $participant->additionalServices = [$service]; - - $this->assertFalse($this->rule->evaluate($participant)); + return $this->createBookingDtoWithParticipantCount(1); } - public function testMultipleServicesWithOneBegleitperson(): void + private function createBookingDtoWithParticipantCount(int $count): BookingDto { - $participant = new ParticipantDto(); + $bookingDto = new BookingDto($this->createTravel(), 1); - $normalService = new Service(); - $normalService->label = 'Skipass 6 Tage'; + for ($i = 0; $i < $count; ++$i) { + $participant = new ParticipantDto(); + $participant->index = $i; + $bookingDto->participants[$i] = $participant; + } - $begleitpersonService = new Service(); - $begleitpersonService->label = 'Skipass Begleitperson'; + return $bookingDto; + } - $participant->additionalServices = [$normalService, $begleitpersonService]; + private function createTravel(): Travel + { + $travel = new Travel(); + $travel->id = 1; + $travel->hotelId = 1; + $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-08'); - $this->assertTrue($this->rule->evaluate($participant)); + return $travel; } } diff --git a/tests/Service/BookingServiceBabyTest.php b/tests/Service/BookingServiceBabyTest.php index 9da9d34..4ac9b67 100644 --- a/tests/Service/BookingServiceBabyTest.php +++ b/tests/Service/BookingServiceBabyTest.php @@ -7,6 +7,7 @@ namespace App\Tests\Service; use App\BusProNet\Constants; use App\BusProNet\Model\Service; use App\BusProNet\Model\Travel; +use App\BusProNet\Service\BookingStatusRuleRegistry; use App\BusProNet\XmlLoader\AgencyLoader; use App\Form\Model\BookingDto; use App\Form\Model\ParticipantDto; @@ -26,12 +27,15 @@ class BookingServiceBabyTest extends TestCase $travelDataService = $this->createMock(TravelDataService::class); $priceCalculator = $this->createMock(BookingPriceCalculatorService::class); $this->participantEligibilityService = $this->createMock(ParticipantEligibilityService::class); + $bookingStatusRuleRegistry = $this->createMock(BookingStatusRuleRegistry::class); + $bookingStatusRuleRegistry->method('evaluateStatus')->willReturn('F'); $agencyLoader = $this->createMock(AgencyLoader::class); $this->bookingService = new BookingService( $travelDataService, $priceCalculator, $this->participantEligibilityService, + $bookingStatusRuleRegistry, $agencyLoader, 'F' // default booking status ); diff --git a/tests/Service/BookingServiceStatusTest.php b/tests/Service/BookingServiceStatusTest.php index 7266177..91ca037 100644 --- a/tests/Service/BookingServiceStatusTest.php +++ b/tests/Service/BookingServiceStatusTest.php @@ -5,8 +5,10 @@ declare(strict_types=1); namespace App\Tests\Service; use App\BusProNet\Constants; +use App\BusProNet\Model\Service; use App\BusProNet\Model\Room; use App\BusProNet\Model\Travel; +use App\BusProNet\Service\BookingStatusRuleRegistry; use App\BusProNet\XmlLoader\AgencyLoader; use App\Exception\NoRoomsAvailableException; use App\Service\BookingPriceCalculatorService; @@ -28,12 +30,15 @@ class BookingServiceStatusTest extends TestCase $this->travelDataService = $this->createMock(TravelDataService::class); $priceCalculator = $this->createMock(BookingPriceCalculatorService::class); $participantEligibility = $this->createMock(ParticipantEligibilityService::class); + $bookingStatusRuleRegistry = $this->createMock(BookingStatusRuleRegistry::class); + $bookingStatusRuleRegistry->method('evaluateStatus')->willReturn('F'); $agencyLoader = $this->createMock(AgencyLoader::class); $this->bookingService = new BookingService( $this->travelDataService, $priceCalculator, $participantEligibility, + $bookingStatusRuleRegistry, $agencyLoader, 'F' // default booking status ); @@ -167,6 +172,65 @@ class BookingServiceStatusTest extends TestCase $this->assertSame('A', $bookingDto->bookingStatus); } + public function testApplyCreateBookingStatusRulesSetsOptionFromRegistry(): void + { + $bookingStatusRuleRegistry = $this->createMock(BookingStatusRuleRegistry::class); + $bookingStatusRuleRegistry->method('evaluateStatus')->willReturn('O'); + + $bookingService = new BookingService( + $this->travelDataService, + $this->createMock(BookingPriceCalculatorService::class), + $this->createMock(ParticipantEligibilityService::class), + $bookingStatusRuleRegistry, + $this->createMock(AgencyLoader::class), + 'F' + ); + + $travel = new Travel(); + $travel->id = 123; + $travel->hotelId = 456; + $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-06'); + + $bookingDto = new \App\Form\Model\BookingDto($travel, 456); + $bookingDto->bookingStatus = 'F'; + $participant = new \App\Form\Model\ParticipantDto(); + $participant->additionalServices = [new Service()]; + $bookingDto->participants = [$participant]; + + $bookingService->applyCreateBookingStatusRules($bookingDto); + + $this->assertSame('O', $bookingDto->bookingStatus); + } + + public function testApplyCreateBookingStatusRulesKeepsInquiryStatus(): void + { + $bookingStatusRuleRegistry = $this->createMock(BookingStatusRuleRegistry::class); + $bookingStatusRuleRegistry->expects($this->never())->method('evaluateStatus'); + + $bookingService = new BookingService( + $this->travelDataService, + $this->createMock(BookingPriceCalculatorService::class), + $this->createMock(ParticipantEligibilityService::class), + $bookingStatusRuleRegistry, + $this->createMock(AgencyLoader::class), + 'F' + ); + + $travel = new Travel(); + $travel->id = 123; + $travel->hotelId = 456; + $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-06'); + + $bookingDto = new \App\Form\Model\BookingDto($travel, 456); + $bookingDto->bookingStatus = 'A'; + + $bookingService->applyCreateBookingStatusRules($bookingDto); + + $this->assertSame('A', $bookingDto->bookingStatus); + } + /** * @param array $roomsConfig */