diff --git a/config/services.yaml b/config/services.yaml index 730a2f8..1665381 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -211,12 +211,14 @@ services: - '@App\Form\Service\ParticipantPromoVoucherFieldHandler' # Booking Status Rules + App\BusProNet\Service\StatusRule\Selection1473StatusRule: ~ App\BusProNet\Service\StatusRule\ChaperonServiceStatusRule: ~ # Booking Status Rule Registry App\BusProNet\Service\BookingStatusRuleRegistry: arguments: $rules: + - '@App\BusProNet\Service\StatusRule\Selection1473StatusRule' - '@App\BusProNet\Service\StatusRule\ChaperonServiceStatusRule' App\Service\CmsDataProvider: diff --git a/src/BusProNet/Constants.php b/src/BusProNet/Constants.php index 6ed30e0..49aacfb 100644 --- a/src/BusProNet/Constants.php +++ b/src/BusProNet/Constants.php @@ -57,7 +57,7 @@ final class Constants // Booking status codes from buchungstatusmoeglich attribute public const BOOKING_STATUS_FREE = 'F'; public const BOOKING_STATUS_INQUIRY = 'A'; - public const BOOKING_STATUS_OPEN = 'O'; // Optionsbuchung + public const BOOKING_STATUS_OPTION = 'O'; // Optionsbuchung // Payment methods public const PAYMENT_METHOD_TRANSFER = 'transfer'; diff --git a/src/BusProNet/Model/Travel.php b/src/BusProNet/Model/Travel.php index 0fdbe6f..1aa07e5 100644 --- a/src/BusProNet/Model/Travel.php +++ b/src/BusProNet/Model/Travel.php @@ -218,6 +218,28 @@ class Travel return $services; } + /** + * Checks whether any CRM selection group contains the given selection ID. + * + * This scans all parsed groups and their selections, regardless of group. + */ + public function hasSelectionId(int $selectionId): bool + { + foreach ($this->selectionGroups as $selectionGroup) { + if (null === $selectionGroup->selections) { + continue; + } + + foreach ($selectionGroup->selections as $selection) { + if ($selectionId === $selection->id) { + return true; + } + } + } + + return false; + } + /** * Retrieves rooms filtered by their IDs. * diff --git a/src/BusProNet/Service/StatusRule/Selection1473StatusRule.php b/src/BusProNet/Service/StatusRule/Selection1473StatusRule.php new file mode 100644 index 0000000..62712e3 --- /dev/null +++ b/src/BusProNet/Service/StatusRule/Selection1473StatusRule.php @@ -0,0 +1,41 @@ +travel->hasSelectionId(self::SELECTION_ID); + } + + public function getStatus(): string + { + return self::STATUS; + } + + public function getPriority(): int + { + return self::PRIORITY; + } + + public function getDescription(): string + { + return 'Assigns Option status when selection 1473 is present'; + } +} diff --git a/src/Service/BookingConfigurator.php b/src/Service/BookingConfigurator.php index 008bf17..8480df1 100644 --- a/src/Service/BookingConfigurator.php +++ b/src/Service/BookingConfigurator.php @@ -41,6 +41,7 @@ class BookingConfigurator * * Handles three booking status types: * - 'Frei': Regular booking with availability checks + * - 'Option': Create-flow override from specific rules * - 'Anfrage': Inquiry booking, allows booking even with 0 availability */ public function startFreshBooking(Request $request, int $dateId, int $hotelId, ?int $agencyId = null): BookingDto @@ -83,6 +84,7 @@ class BookingConfigurator ? $this->agencyLoader->loadById($agencyId)?->code : null; $bookingCreateDto->bookingStatus = $bookingStatus; + $this->applyCreateBookingStatusRules($bookingCreateDto); $this->bookingSessionService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE); @@ -587,13 +589,13 @@ class BookingConfigurator return $this->defaultBookingStatus; } - // Fall back to allowed statuses in order of preference: F → O → A + // Fall back to allowed statuses in order of preference: F → O(option) → A if ($travelData->isBookingStatusAllowed(Constants::BOOKING_STATUS_FREE)) { return Constants::BOOKING_STATUS_FREE; } - if ($travelData->isBookingStatusAllowed(Constants::BOOKING_STATUS_OPEN)) { - return Constants::BOOKING_STATUS_OPEN; + if ($travelData->isBookingStatusAllowed(Constants::BOOKING_STATUS_OPTION)) { + return Constants::BOOKING_STATUS_OPTION; } if ($travelData->isBookingStatusAllowed(Constants::BOOKING_STATUS_INQUIRY)) { diff --git a/tests/BusProNet/Model/TravelTest.php b/tests/BusProNet/Model/TravelTest.php index 6da2bfb..eda33c0 100644 --- a/tests/BusProNet/Model/TravelTest.php +++ b/tests/BusProNet/Model/TravelTest.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace App\Tests\BusProNet\Model; +use App\BusProNet\Model\CrmSelection; +use App\BusProNet\Model\CrmSelectionGroup; use App\BusProNet\Model\Service; use App\BusProNet\Model\Travel; use PHPUnit\Framework\TestCase; @@ -109,4 +111,44 @@ class TravelTest extends TestCase $this->assertSame('BUS', $result[0]->subType); $this->assertSame('PKW', $result[1]->subType); } + + public function testHasSelectionIdReturnsTrueWhenSelectionExists(): void + { + $travel = new Travel(); + $travel->selectionGroups = [ + 10 => $this->createSelectionGroup(10, [1473, 2001]), + 11 => $this->createSelectionGroup(11, [3001]), + ]; + + $this->assertTrue($travel->hasSelectionId(1473)); + } + + public function testHasSelectionIdReturnsFalseWhenSelectionDoesNotExist(): void + { + $travel = new Travel(); + $travel->selectionGroups = [ + 10 => $this->createSelectionGroup(10, [2001]), + 11 => $this->createSelectionGroup(11, [3001]), + ]; + + $this->assertFalse($travel->hasSelectionId(1473)); + } + + /** + * @param list $selectionIds + */ + private function createSelectionGroup(int $groupId, array $selectionIds): CrmSelectionGroup + { + $group = new CrmSelectionGroup(); + $group->id = $groupId; + $group->selections = []; + + foreach ($selectionIds as $selectionId) { + $selection = new CrmSelection(); + $selection->id = $selectionId; + $group->selections[] = $selection; + } + + return $group; + } } diff --git a/tests/BusProNet/Service/BookingStatusRuleRegistryTest.php b/tests/BusProNet/Service/BookingStatusRuleRegistryTest.php index bbd27cd..c610ee6 100644 --- a/tests/BusProNet/Service/BookingStatusRuleRegistryTest.php +++ b/tests/BusProNet/Service/BookingStatusRuleRegistryTest.php @@ -7,6 +7,7 @@ namespace App\Tests\BusProNet\Service; use App\BusProNet\Model\Travel; use App\BusProNet\Service\BookingStatusRuleRegistry; use App\BusProNet\Service\Contract\BookingStatusRuleInterface; +use App\BusProNet\Service\StatusRule\Selection1473StatusRule; use App\Form\Model\BookingDto; use PHPUnit\Framework\TestCase; @@ -99,6 +100,22 @@ class BookingStatusRuleRegistryTest extends TestCase $this->assertSame('L', $registry->evaluateStatus($bookingDto)); } + public function testSelection1473RuleWinsOverLowerPriorityRule(): void + { + $lowerPriorityRule = $this->createMock(BookingStatusRuleInterface::class); + $lowerPriorityRule->method('evaluate')->willReturn(true); + $lowerPriorityRule->method('getStatus')->willReturn('X'); + $lowerPriorityRule->method('getPriority')->willReturn(100); + + $registry = new BookingStatusRuleRegistry([ + $lowerPriorityRule, + new Selection1473StatusRule(), + ]); + $bookingDto = new BookingDto($this->createMockTravelWithSelection1473(), 1); + + $this->assertSame('O', $registry->evaluateStatus($bookingDto)); + } + private function createMockTravel(): Travel { $travel = new Travel(); @@ -109,4 +126,32 @@ class BookingStatusRuleRegistryTest extends TestCase return $travel; } + + private function createMockTravelWithSelection1473(): Travel + { + $travel = $this->createMockTravel(); + $travel->selectionGroups = [ + 1 => $this->createSelectionGroup(1, [1473]), + ]; + + return $travel; + } + + /** + * @param list $selectionIds + */ + private function createSelectionGroup(int $groupId, array $selectionIds): \App\BusProNet\Model\CrmSelectionGroup + { + $group = new \App\BusProNet\Model\CrmSelectionGroup(); + $group->id = $groupId; + $group->selections = []; + + foreach ($selectionIds as $selectionId) { + $selection = new \App\BusProNet\Model\CrmSelection(); + $selection->id = $selectionId; + $group->selections[] = $selection; + } + + return $group; + } } diff --git a/tests/BusProNet/Service/StatusRule/Selection1473StatusRuleTest.php b/tests/BusProNet/Service/StatusRule/Selection1473StatusRuleTest.php new file mode 100644 index 0000000..bbfc0bd --- /dev/null +++ b/tests/BusProNet/Service/StatusRule/Selection1473StatusRuleTest.php @@ -0,0 +1,86 @@ +rule = new Selection1473StatusRule(); + } + + public function testEvaluatesTrueWhenSelection1473Exists(): void + { + $bookingDto = new BookingDto($this->createTravelWithSelectionIds([1473]), 1); + + $this->assertTrue($this->rule->evaluate($bookingDto)); + } + + public function testEvaluatesFalseWhenSelection1473DoesNotExist(): void + { + $bookingDto = new BookingDto($this->createTravelWithSelectionIds([2001]), 1); + + $this->assertFalse($this->rule->evaluate($bookingDto)); + } + + public function testReturnsStatusO(): void + { + $this->assertSame('O', $this->rule->getStatus()); + } + + public function testReturnsPriority200(): void + { + $this->assertSame(200, $this->rule->getPriority()); + } + + public function testReturnsDescription(): void + { + $this->assertSame('Assigns Option status when selection 1473 is present', $this->rule->getDescription()); + } + + /** + * @param list $selectionIds + */ + private function createTravelWithSelectionIds(array $selectionIds): Travel + { + $travel = new Travel(); + $travel->id = 1; + $travel->hotelId = 1; + $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-08'); + $travel->selectionGroups = [ + 1 => $this->createSelectionGroup(1, $selectionIds), + ]; + + return $travel; + } + + /** + * @param list $selectionIds + */ + private function createSelectionGroup(int $groupId, array $selectionIds): CrmSelectionGroup + { + $group = new CrmSelectionGroup(); + $group->id = $groupId; + $group->selections = []; + + foreach ($selectionIds as $selectionId) { + $selection = new CrmSelection(); + $selection->id = $selectionId; + $group->selections[] = $selection; + } + + return $group; + } +} diff --git a/tests/Service/BookingConfiguratorStatusTest.php b/tests/Service/BookingConfiguratorStatusTest.php index 3e701f5..c312820 100644 --- a/tests/Service/BookingConfiguratorStatusTest.php +++ b/tests/Service/BookingConfiguratorStatusTest.php @@ -6,11 +6,14 @@ namespace App\Tests\Service; use App\Form\Model\BookingDto; use App\Form\Model\ParticipantDto; +use App\BusProNet\Model\CrmSelection; +use App\BusProNet\Model\CrmSelectionGroup; 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\Service\StatusRule\Selection1473StatusRule; use App\BusProNet\XmlLoader\AgencyLoader; use App\Exception\NoRoomsAvailableException; use App\Service\BookingConfigurator; @@ -174,6 +177,57 @@ class BookingConfiguratorStatusTest extends TestCase $this->assertSame('A', $bookingDto->bookingStatus); } + public function testStartFreshBookingWithSelection1473UsesOptionStatus(): void + { + $bookingService = new BookingConfigurator( + $this->createMock(BookingSessionManager::class), + $this->travelDataService, + $this->createMock(ParticipantEligibilityChecker::class), + new BookingStatusRuleRegistry([ + new Selection1473StatusRule(), + ]), + $this->createMock(AgencyLoader::class), + 'F' + ); + + $travel = $this->createTravelWithRooms( + [ + ['status' => Constants::STATUS_AVAILABLE, 'available' => 5], + ], + [$this->createSelectionGroupWithSelectionId(1473)] + ); + + $this->travelDataService + ->method('getTravelData') + ->willReturn($travel); + + $request = $this->createRequestWithSession(); + + $bookingDto = $bookingService->startFreshBooking($request, 123, 456); + + $this->assertSame('O', $bookingDto->bookingStatus); + } + + public function testStartFreshBookingWithInquiryRoomsAndSelection1473KeepsInquiryStatus(): void + { + $travel = $this->createTravelWithRooms( + [ + ['status' => Constants::STATUS_ON_REQUEST, 'available' => 5], + ], + [$this->createSelectionGroupWithSelectionId(1473)] + ); + + $this->travelDataService + ->method('getTravelData') + ->willReturn($travel); + + $request = $this->createRequestWithSession(); + + $bookingDto = $this->bookingService->startFreshBooking($request, 123, 456); + + $this->assertSame('A', $bookingDto->bookingStatus); + } + public function testApplyCreateBookingStatusRulesSetsOptionFromRegistry(): void { $bookingStatusRuleRegistry = $this->createMock(BookingStatusRuleRegistry::class); @@ -236,7 +290,7 @@ class BookingConfiguratorStatusTest extends TestCase /** * @param array $roomsConfig */ - private function createTravelWithRooms(array $roomsConfig): Travel + private function createTravelWithRooms(array $roomsConfig, array $selectionGroups = []): Travel { $travel = new Travel(); $travel->id = 123; @@ -244,6 +298,7 @@ class BookingConfiguratorStatusTest extends TestCase $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); $travel->dateTo = new \DateTimeImmutable('2030-01-06'); $travel->allowedBookingStatus = []; // Empty means no API restrictions + $travel->selectionGroups = $selectionGroups; $rooms = []; foreach ($roomsConfig as $index => $config) { @@ -265,6 +320,24 @@ class BookingConfiguratorStatusTest extends TestCase return $travel; } + /** + * @param list $selectionIds + */ + private function createSelectionGroupWithSelectionId(int $groupId, array $selectionIds = [1473]): CrmSelectionGroup + { + $group = new CrmSelectionGroup(); + $group->id = $groupId; + $group->selections = []; + + foreach ($selectionIds as $selectionId) { + $selection = new CrmSelection(); + $selection->id = $selectionId; + $group->selections[] = $selection; + } + + return $group; + } + private function createRequestWithSession(): Request { $session = new Session(new MockArraySessionStorage());