From 6cc5886073271405581aaad2772f22968a0f64b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Mon, 20 Oct 2025 15:29:33 +0200 Subject: [PATCH] feat: automatically assign rooms only for unique room type selection --- composer.lock | 24 +- .../Booking/Create/Step2Controller.php | 5 +- src/Service/ParticipantCardDataService.php | 9 + src/Service/RoomAssignmentService.php | 31 +++ .../ParticipantCardDataServiceTest.php | 33 ++- tests/Service/RoomAssignmentServiceTest.php | 214 ++++++++++++++++++ 6 files changed, 302 insertions(+), 14 deletions(-) create mode 100644 tests/Service/RoomAssignmentServiceTest.php diff --git a/composer.lock b/composer.lock index 29ddb16..d8deb92 100644 --- a/composer.lock +++ b/composer.lock @@ -10450,28 +10450,28 @@ }, { "name": "webmozart/assert", - "version": "1.11.0", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + "reference": "541057574806f942c94662b817a50f63f7345360" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/541057574806f942c94662b817a50f63f7345360", + "reference": "541057574806f942c94662b817a50f63f7345360", "shasum": "" }, "require": { "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", "php": "^7.2 || ^8.0" }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" }, "type": "library", "extra": { @@ -10502,9 +10502,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" + "source": "https://github.com/webmozarts/assert/tree/1.12.0" }, - "time": "2022-06-03T18:03:27+00:00" + "time": "2025-10-20T12:43:39+00:00" } ], "packages-dev": [ diff --git a/src/Controller/Booking/Create/Step2Controller.php b/src/Controller/Booking/Create/Step2Controller.php index 420c7d6..02a89bb 100644 --- a/src/Controller/Booking/Create/Step2Controller.php +++ b/src/Controller/Booking/Create/Step2Controller.php @@ -254,6 +254,10 @@ class Step2Controller extends AbstractController /** * Automatically assigns participants to rooms if they don't have room assignments yet. + * + * Note: Auto-assignment only occurs when exactly one room type is selected. + * With multiple room types, users must manually select rooms to avoid UX issues + * with having to unselect preassigned rooms in individual participant forms. */ private function autoAssignRoomsIfNeeded(BookingDto $bookingCreateDto): void { @@ -270,5 +274,4 @@ class Step2Controller extends AbstractController $this->roomAssignmentService->assignParticipantsToRooms($bookingCreateDto); } } - } diff --git a/src/Service/ParticipantCardDataService.php b/src/Service/ParticipantCardDataService.php index d691943..e848226 100644 --- a/src/Service/ParticipantCardDataService.php +++ b/src/Service/ParticipantCardDataService.php @@ -103,6 +103,9 @@ class ParticipantCardDataService /** * Calculate and format individual participant price. + * + * Returns a dash (-) when the price is zero and no room is assigned, + * indicating incomplete configuration rather than a zero-cost booking. */ private function getFormattedPrice(BookingDto $bookingDto, int $index): string { @@ -110,6 +113,12 @@ class ParticipantCardDataService $price = $prices[$index] ?? 0.0; + // Display dash when price is zero and no room assigned (incomplete configuration) + $participant = $bookingDto->participants[$index] ?? null; + if (0.0 === $price && (null === $participant || null === $participant->assignedRoomId)) { + return '-'; + } + return number_format($price, 2, ',', '.').' €'; } } diff --git a/src/Service/RoomAssignmentService.php b/src/Service/RoomAssignmentService.php index 65c78ea..281a7f3 100644 --- a/src/Service/RoomAssignmentService.php +++ b/src/Service/RoomAssignmentService.php @@ -14,6 +14,30 @@ use App\Form\Model\BookingDto; */ class RoomAssignmentService { + /** + * Determines if automatic room assignment should be performed. + * + * Auto-assignment is only performed when exactly one room type is selected to avoid + * UX issues with individual participant forms. With multiple room types, users should + * manually select rooms to avoid having to unselect preassigned rooms. + * + * @param BookingDto $dto The booking DTO containing room selections + * + * @return bool True if auto-assignment should proceed, false otherwise + */ + public function shouldAutoAssignRooms(BookingDto $dto): bool + { + $selectedRoomTypeCount = 0; + + foreach ($dto->roomSelections as $roomSelection) { + if (null !== $roomSelection->quantity && $roomSelection->quantity > 0) { + ++$selectedRoomTypeCount; + } + } + + return 1 === $selectedRoomTypeCount; + } + /** * Automatically assigns participants to rooms based on selected room quantities and capacities. * @@ -26,10 +50,17 @@ class RoomAssignmentService * - 2x "Doppelzimmer" (capacity 2) = participants 0-1 → room A, participants 2-3 → room A * - 1x "3-Bett-Zimmer" (capacity 3) = participants 4-6 → room B * + * Note: Only performs assignment if shouldAutoAssignRooms() returns true. + * * @param BookingDto $dto The booking DTO containing room selections and participants */ public function assignParticipantsToRooms(BookingDto $dto): void { + // Skip auto-assignment if multiple room types selected + if (false === $this->shouldAutoAssignRooms($dto)) { + return; + } + $participantIndex = 0; $selectedRooms = $dto->getSelectedRooms(); $availableRooms = $dto->travel->getAvailableRooms(); diff --git a/tests/Service/ParticipantCardDataServiceTest.php b/tests/Service/ParticipantCardDataServiceTest.php index 6524215..7374ce1 100644 --- a/tests/Service/ParticipantCardDataServiceTest.php +++ b/tests/Service/ParticipantCardDataServiceTest.php @@ -167,7 +167,7 @@ class ParticipantCardDataServiceTest extends TestCase $this->assertEquals('Unbekanntes Zimmer', $result['roomName']); } - public function testGetCardDataWithZeroPrice(): void + public function testGetCardDataWithZeroPriceAndNoRoom(): void { $travel = new Travel(); $travel->rooms = []; @@ -175,6 +175,7 @@ class ParticipantCardDataServiceTest extends TestCase $participant = new ParticipantDto(); $participant->firstName = 'Max'; $participant->lastName = 'Mustermann'; + $participant->assignedRoomId = null; // No room assigned $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$participant]; @@ -185,6 +186,36 @@ class ParticipantCardDataServiceTest extends TestCase $result = $this->service->getCardData($bookingDto, 0); + // When no room assigned and price is zero, display dash (incomplete configuration) + $this->assertEquals('-', $result['price']); + } + + public function testGetCardDataWithZeroPriceButRoomAssigned(): void + { + // Create test room + $room = new Room(); + $room->id = 1; + $room->label = 'Doppelzimmer'; + $room->price = 0.0; + + $travel = new Travel(); + $travel->rooms = [$room]; + + $participant = new ParticipantDto(); + $participant->firstName = 'Max'; + $participant->lastName = 'Mustermann'; + $participant->assignedRoomId = 1; // Room is assigned + + $bookingDto = new BookingDto($travel, 1); + $bookingDto->participants = [$participant]; + + $this->priceCalculator + ->method('calculateAllParticipantIndividualPrices') + ->willReturn([0.0]); + + $result = $this->service->getCardData($bookingDto, 0); + + // When room is assigned but price is zero, display formatted zero price $this->assertEquals('0,00 €', $result['price']); } diff --git a/tests/Service/RoomAssignmentServiceTest.php b/tests/Service/RoomAssignmentServiceTest.php new file mode 100644 index 0000000..6e9c195 --- /dev/null +++ b/tests/Service/RoomAssignmentServiceTest.php @@ -0,0 +1,214 @@ +service = new RoomAssignmentService(); + } + + /** + * @test + */ + public function shouldAutoAssignRoomsReturnsTrueForSingleRoomType(): void + { + $bookingDto = $this->createBookingDtoWithRoomSelections([ + ['roomId' => 1, 'quantity' => 3], + ]); + + $result = $this->service->shouldAutoAssignRooms($bookingDto); + + self::assertTrue($result, 'Should auto-assign when exactly one room type is selected'); + } + + /** + * @test + */ + public function shouldAutoAssignRoomsReturnsFalseForMultipleRoomTypes(): void + { + $bookingDto = $this->createBookingDtoWithRoomSelections([ + ['roomId' => 1, 'quantity' => 2], + ['roomId' => 2, 'quantity' => 1], + ]); + + $result = $this->service->shouldAutoAssignRooms($bookingDto); + + self::assertFalse($result, 'Should not auto-assign when multiple room types are selected'); + } + + /** + * @test + */ + public function shouldAutoAssignRoomsReturnsFalseForNoRoomSelections(): void + { + $bookingDto = $this->createBookingDtoWithRoomSelections([]); + + $result = $this->service->shouldAutoAssignRooms($bookingDto); + + self::assertFalse($result, 'Should not auto-assign when no rooms are selected'); + } + + /** + * @test + */ + public function shouldAutoAssignRoomsIgnoresZeroQuantityRooms(): void + { + $bookingDto = $this->createBookingDtoWithRoomSelections([ + ['roomId' => 1, 'quantity' => 2], + ['roomId' => 2, 'quantity' => 0], + ['roomId' => 3, 'quantity' => null], + ]); + + $result = $this->service->shouldAutoAssignRooms($bookingDto); + + self::assertTrue($result, 'Should treat zero/null quantity as not selected'); + } + + /** + * @test + */ + public function assignParticipantsToRoomsWorksWithSingleRoomType(): void + { + $bookingDto = $this->createBookingDtoWithRoomSelections([ + ['roomId' => 100, 'quantity' => 2, 'capacity' => 2], + ]); + + // Create 4 participants (2 rooms × 2 capacity) + $bookingDto->participants = [ + new ParticipantDto(), + new ParticipantDto(), + new ParticipantDto(), + new ParticipantDto(), + ]; + + // Mock available room + $room = new Room(); + $room->id = 100; + $room->minPax = 2; + $room->available = 10; + $room->status = Constants::STATUS_AVAILABLE; + + $bookingDto->travel->rooms = [$room]; + + $this->service->assignParticipantsToRooms($bookingDto); + + // Verify all participants got assigned + self::assertSame(100, $bookingDto->participants[0]->assignedRoomId); + self::assertSame(100, $bookingDto->participants[1]->assignedRoomId); + self::assertSame(100, $bookingDto->participants[2]->assignedRoomId); + self::assertSame(100, $bookingDto->participants[3]->assignedRoomId); + } + + /** + * @test + */ + public function assignParticipantsToRoomsSkipsAssignmentWithMultipleRoomTypes(): void + { + $bookingDto = $this->createBookingDtoWithRoomSelections([ + ['roomId' => 100, 'quantity' => 1, 'capacity' => 2], + ['roomId' => 200, 'quantity' => 1, 'capacity' => 2], + ]); + + // Create 4 participants (2 rooms × 2 capacity) + $bookingDto->participants = [ + new ParticipantDto(), + new ParticipantDto(), + new ParticipantDto(), + new ParticipantDto(), + ]; + + // Mock available rooms + $room1 = new Room(); + $room1->id = 100; + $room1->minPax = 2; + $room1->available = 10; + $room1->status = Constants::STATUS_AVAILABLE; + + $room2 = new Room(); + $room2->id = 200; + $room2->minPax = 2; + $room2->available = 10; + $room2->status = Constants::STATUS_AVAILABLE; + + $bookingDto->travel->rooms = [$room1, $room2]; + + $this->service->assignParticipantsToRooms($bookingDto); + + // Verify NO participants got assigned (conditional logic skipped assignment) + self::assertNull($bookingDto->participants[0]->assignedRoomId); + self::assertNull($bookingDto->participants[1]->assignedRoomId); + self::assertNull($bookingDto->participants[2]->assignedRoomId); + self::assertNull($bookingDto->participants[3]->assignedRoomId); + } + + /** + * @test + */ + public function assignParticipantsToRoomsHandlesMixedRoomCapacities(): void + { + $bookingDto = $this->createBookingDtoWithRoomSelections([ + ['roomId' => 100, 'quantity' => 1, 'capacity' => 2], + ]); + + // Create participants + $bookingDto->participants = [ + new ParticipantDto(), + new ParticipantDto(), + ]; + + // Mock available room with capacity 2 + $room = new Room(); + $room->id = 100; + $room->minPax = 2; + $room->available = 10; + $room->status = Constants::STATUS_AVAILABLE; + + $bookingDto->travel->rooms = [$room]; + + $this->service->assignParticipantsToRooms($bookingDto); + + // Both participants assigned to same room + self::assertSame(100, $bookingDto->participants[0]->assignedRoomId); + self::assertSame(100, $bookingDto->participants[1]->assignedRoomId); + } + + private function createBookingDtoWithRoomSelections(array $selections): BookingDto + { + $travel = new Travel(); + $travel->rooms = []; + + $bookingDto = new BookingDto($travel, 1); + $bookingDto->roomSelections = []; + + foreach ($selections as $selection) { + $roomSelectionDto = new RoomSelectionDto(); + $roomSelectionDto->roomId = $selection['roomId']; + $roomSelectionDto->quantity = $selection['quantity']; + if (isset($selection['capacity'])) { + $roomSelectionDto->capacity = $selection['capacity']; + } + + $bookingDto->roomSelections[] = $roomSelectionDto; + } + + return $bookingDto; + } +}