createTravel([$this->createRoom(1409, 'Einzelzimmer', minPax: 1)]); $bookingDto = $this->createBookingDto($travel); $bookingService = $this->createMock(BookingConfigurator::class); $bookingService->expects($this->once()) ->method('reconcileRoomSelections') ->with($bookingDto) ->willReturn([new RoomSelectionAdjustment(1408, 'Doppelzimmer', 2, 0)]); $bookingService->expects($this->never())->method('preselectDefaultServices'); $bookingSessionService = $this->createMock(BookingSessionManager::class); $bookingSessionService->method('getOrCreateBookingCreateDto')->willReturn($bookingDto); $bookingSessionService->expects($this->once()) ->method('saveBookingDto') ->with($request, $bookingDto, BookingDto::MODE_CREATE); $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->once()) ->method('error') ->with( 'Room selection exceeds what the current travel data still offers.', [ 'travelId' => 4711, 'travelCode' => 'TRV-4711', 'hotelId' => 1, 'rooms' => [[ 'roomId' => 1408, 'label' => 'Doppelzimmer', 'requestedQuantity' => 2, 'bookableQuantity' => 0, ]], ] ); $controller = $this->createController($bookingService, $bookingSessionService, $logger); $response = $controller->index($request); $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertSame('/app_booking_create_step_1', $response->getTargetUrl()); $this->assertSame( [['error', 'Dein gewähltes Zimmer ist leider nicht mehr verfügbar. Bitte wähle dein Zimmer erneut.']], $controller->flashes ); } public function testSendsCustomerBackToStep1WithTheCappedQuantityPrefilled(): void { $request = Request::create('/bookings/create/participants', 'GET'); $travel = $this->createTravel([$this->createRoom(1408, 'Doppelzimmer', minPax: 2)]); $bookingDto = $this->createBookingDto($travel); $bookingDto->roomSelections = [ 1408 => $this->createRoomSelection(1408, 'Doppelzimmer', quantity: 1, capacity: 2), ]; $bookingService = $this->createMock(BookingConfigurator::class); $bookingService->expects($this->once()) ->method('reconcileRoomSelections') ->with($bookingDto) ->willReturn([new RoomSelectionAdjustment(1408, 'Doppelzimmer', 3, 1)]); $bookingSessionService = $this->createMock(BookingSessionManager::class); $bookingSessionService->method('getOrCreateBookingCreateDto')->willReturn($bookingDto); $bookingSessionService->expects($this->once()) ->method('saveBookingDto') ->with($request, $bookingDto, BookingDto::MODE_CREATE); $controller = $this->createController( $bookingService, $bookingSessionService, $this->createStub(LoggerInterface::class) ); $response = $controller->index($request); $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertSame('/app_booking_create_step_1', $response->getTargetUrl()); $this->assertSame( [['error', 'Für dein gewähltes Zimmer sind nicht mehr genügend Plätze verfügbar. Wir haben deine Auswahl angepasst, bitte überprüfe sie.']], $controller->flashes ); $this->assertSame(1, $bookingDto->roomSelections[1408]->quantity, 'Step 1 reopens on the capped selection'); } public function testSendsCustomerToTheErrorPageWhenTheTravelSoldOutCompletely(): void { $request = Request::create('/bookings/create/participants', 'GET'); $bookingDto = $this->createBookingDto($this->createTravel([])); $bookingService = $this->createMock(BookingConfigurator::class); $bookingService->expects($this->once()) ->method('reconcileRoomSelections') ->with($bookingDto) ->willReturn([new RoomSelectionAdjustment(1408, 'Doppelzimmer', 2, 0)]); $bookingSessionService = $this->createMock(BookingSessionManager::class); $bookingSessionService->method('getOrCreateBookingCreateDto')->willReturn($bookingDto); $bookingSessionService->expects($this->once()) ->method('saveBookingDto') ->with($request, $bookingDto, BookingDto::MODE_CREATE); $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->once()) ->method('error') ->with('Travel has no bookable rooms left while a booking is in progress.', $this->anything()); $controller = $this->createController($bookingService, $bookingSessionService, $logger); $response = $controller->index($request); $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertSame('/app_booking_create_error', $response->getTargetUrl()); $this->assertSame( [['error', 'Für diese Reise sind leider keine Zimmer mehr verfügbar.']], $controller->flashes ); } public function testContinuesWhenOnlyUnselectedRoomsWereDropped(): void { $request = Request::create('/bookings/create/participants', 'GET'); $travel = $this->createTravel([$this->createRoom(1409, 'Einzelzimmer', minPax: 3)]); $bookingDto = $this->createBookingDto($travel); $bookingDto->roomSelections = [ 1409 => $this->createRoomSelection(1409, 'Einzelzimmer', quantity: 2, capacity: 3), ]; $bookingService = $this->createMock(BookingConfigurator::class); $bookingService->expects($this->once()) ->method('reconcileRoomSelections') ->with($bookingDto) ->willReturn([]); $bookingSessionService = $this->createMock(BookingSessionManager::class); $bookingSessionService->method('getOrCreateBookingCreateDto')->willReturn($bookingDto); $bookingSessionService->expects($this->exactly(2)) ->method('saveBookingDto') ->with($request, $bookingDto, BookingDto::MODE_CREATE); $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->never())->method('error'); $controller = $this->createController($bookingService, $bookingSessionService, $logger); $response = $controller->index($request); $this->assertSame([], $controller->flashes); $this->assertInstanceOf(Response::class, $response); $this->assertCount(6, $bookingDto->participants, 'Two rooms for three people each yield six participants'); } public function testEndsASplitTheTravelDateDoesNotOffer(): void { $bookingDto = $this->createSplitBookingDto($this->createTravel([$this->createRoom(1409, 'Einzelzimmer', minPax: 1)])); $applicant = $bookingDto->applicant; $controller = $this->createControllerForFullRun($bookingDto); $controller->index(Request::create('/bookings/create/participants', 'GET')); $this->assertTrue($bookingDto->applicantTravels); $this->assertNull($bookingDto->applicant); $this->assertSame($applicant, $bookingDto->participants[0]); } public function testKeepsASplitTheTravelDateOffers(): void { $travel = $this->createTravel([$this->createRoom(1409, 'Einzelzimmer', minPax: 1)]); $selection = new CrmSelection(); $selection->id = 1488; $group = new CrmSelectionGroup(); $group->id = 10; $group->selections = [$selection]; $travel->selectionGroups = [10 => $group]; $bookingDto = $this->createSplitBookingDto($travel); $applicant = $bookingDto->applicant; $controller = $this->createControllerForFullRun($bookingDto); $controller->index(Request::create('/bookings/create/participants', 'GET')); $this->assertFalse($bookingDto->applicantTravels); $this->assertSame($applicant, $bookingDto->applicant); $this->assertNotSame($applicant, $bookingDto->participants[0]); } private function createSplitBookingDto(Travel $travel): BookingDto { $bookingDto = $this->createBookingDto($travel); $bookingDto->roomSelections = [ 1409 => $this->createRoomSelection(1409, 'Einzelzimmer', quantity: 1, capacity: 1), ]; $bookingDto->applicantTravels = false; $bookingDto->applicant = new ParticipantDto(); return $bookingDto; } private function createControllerForFullRun(BookingDto $bookingDto): TestableStep2Controller { $bookingService = $this->createStub(BookingConfigurator::class); $bookingService->method('reconcileRoomSelections')->willReturn([]); $bookingSessionService = $this->createStub(BookingSessionManager::class); $bookingSessionService->method('getOrCreateBookingCreateDto')->willReturn($bookingDto); return $this->createController($bookingService, $bookingSessionService, $this->createStub(LoggerInterface::class)); } private function createController( BookingConfigurator $bookingService, BookingSessionManager $bookingSessionService, LoggerInterface $logger, ): TestableStep2Controller { $form = $this->createStub(FormInterface::class); $form->method('isSubmitted')->willReturn(false); return new TestableStep2Controller( $bookingService, $bookingSessionService, $this->createStub(BookingCreateContextFactory::class), $this->createStub(TravelDataProvider::class), $this->createStub(RoomAssigner::class), $this->createStub(ParticipantDataPrefiller::class), $logger, $form, ); } private function createBookingDto(Travel $travel): BookingDto { $bookingDto = new BookingDto($travel, 1); $bookingDto->currentStep = 2; return $bookingDto; } /** * @param array $rooms */ private function createTravel(array $rooms): Travel { $travel = new Travel(); $travel->id = 4711; $travel->code = 'TRV-4711'; $travel->rooms = $rooms; return $travel; } private function createRoom(int $id, string $label, int $minPax): Room { $room = new Room(); $room->id = $id; $room->label = $label; $room->available = 4; $room->minPax = $minPax; $room->status = Constants::STATUS_AVAILABLE; return $room; } private function createRoomSelection(int $id, string $label, int $quantity, int $capacity): RoomSelectionDto { $selection = new RoomSelectionDto(); $selection->id = $id; $selection->label = $label; $selection->quantity = $quantity; $selection->capacity = $capacity; return $selection; } } final class TestableStep2Controller extends Step2Controller { /** * @var array */ public array $flashes = []; /** * @var array */ public array $renderParameters = []; /** * @var FormInterface */ private readonly FormInterface $form; /** * @param FormInterface $form */ public function __construct( BookingConfigurator $bookingService, BookingSessionManager $bookingSessionService, BookingCreateContextFactory $createContextFactory, TravelDataProvider $travelDataService, RoomAssigner $roomAssignmentService, ParticipantDataPrefiller $prepopulationService, LoggerInterface $logger, FormInterface $form, ) { $this->form = $form; parent::__construct( $bookingService, $bookingSessionService, $createContextFactory, $travelDataService, $roomAssignmentService, $prepopulationService, $logger, ); } protected function getUser(): ?UserInterface { return null; } /** * @return FormInterface */ protected function createForm(string $type, mixed $data = null, array $options = []): FormInterface { return $this->form; } protected function addFlash(string $type, mixed $message): void { $this->flashes[] = [$type, $message]; } /** * @param array $parameters */ protected function redirectToRoute(string $route, array $parameters = [], int $status = 302): RedirectResponse { return new RedirectResponse('/'.$route, $status); } /** * @param array $parameters */ protected function render(string $view, array $parameters = [], ?Response $response = null): Response { $this->renderParameters = $parameters; return new Response(''); } }