From b872f2a5dbb0c553845ed548bc94ed6ae73af138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Tue, 25 Nov 2025 16:43:41 +0100 Subject: [PATCH] feat: prevent selection of baby rooms without regular rooms --- src/Form/Model/BookingDto.php | 13 +- src/Validator/Constraints/RoomSelection.php | 21 +++ .../Constraints/RoomSelectionValidator.php | 65 +++++++ .../RoomSelectionValidatorTest.php | 159 ++++++++++++++++++ 4 files changed, 247 insertions(+), 11 deletions(-) create mode 100644 src/Validator/Constraints/RoomSelection.php create mode 100644 src/Validator/Constraints/RoomSelectionValidator.php create mode 100644 tests/Validator/Constraints/RoomSelectionValidatorTest.php diff --git a/src/Form/Model/BookingDto.php b/src/Form/Model/BookingDto.php index 90d1ceb..fd3905a 100644 --- a/src/Form/Model/BookingDto.php +++ b/src/Form/Model/BookingDto.php @@ -7,6 +7,7 @@ namespace App\Form\Model; use App\BusProNet\Constants; use App\BusProNet\Model\Booking; use App\BusProNet\Model\Travel; +use App\Validator\Constraints as AppAssert; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -18,6 +19,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; * in participant DTOs regardless of mode, ensuring consistent data structure and * simplifying pricing calculations, field handlers, and template rendering. */ +#[AppAssert\RoomSelection(groups: ['booking_create_step_1'])] class BookingDto { public const MODE_CREATE = 'create'; @@ -175,17 +177,6 @@ class BookingDto return $room?->label; } - #[Assert\Callback(callback: 'validateRoomSelection', groups: ['booking_create_step_1'])] - public function validateRoomSelection(ExecutionContextInterface $context): void - { - $selectedRooms = $this->getSelectedRooms(); - - if (0 === count($selectedRooms)) { - $context->buildViolation('Bitte mindestens ein Zimmer/Bett auswählen') - ->addViolation(); - } - } - #[Assert\Callback] public function validateBankAccount(ExecutionContextInterface $context): void { diff --git a/src/Validator/Constraints/RoomSelection.php b/src/Validator/Constraints/RoomSelection.php new file mode 100644 index 0000000..cc5b9ed --- /dev/null +++ b/src/Validator/Constraints/RoomSelection.php @@ -0,0 +1,21 @@ +getSelectedRooms(); + + // Check that at least one room is selected + if (0 === count($selectedRooms)) { + $this->context->buildViolation($constraint->noRoomSelectedMessage) + ->addViolation(); + + return; + } + + // Check that not only Baby rooms are selected + $this->assertNotOnlyBabyRooms($value, $constraint); + } + + private function assertNotOnlyBabyRooms(BookingDto $bookingDto, RoomSelection $constraint): void + { + $availableRooms = $bookingDto->travel->getAvailableRooms(); + $selectedRooms = $bookingDto->getSelectedRooms(); + + $hasRegularRoom = false; + $hasBabyRoom = false; + + foreach ($selectedRooms as $roomSelection) { + $room = $availableRooms[$roomSelection->roomId] ?? null; + + if (null === $room) { + continue; + } + + if (RoomSelection::BABY_ROOM_CODE === $room->code) { + $hasBabyRoom = true; + } else { + $hasRegularRoom = true; + } + } + + if ($hasBabyRoom && !$hasRegularRoom) { + $this->context->buildViolation($constraint->onlyBabyRoomsMessage) + ->addViolation(); + } + } +} diff --git a/tests/Validator/Constraints/RoomSelectionValidatorTest.php b/tests/Validator/Constraints/RoomSelectionValidatorTest.php new file mode 100644 index 0000000..619d236 --- /dev/null +++ b/tests/Validator/Constraints/RoomSelectionValidatorTest.php @@ -0,0 +1,159 @@ +createBookingDto([]); + + $this->validator->validate($bookingDto, new RoomSelection()); + + $this->buildViolation('Bitte mindestens ein Zimmer/Bett auswählen') + ->assertRaised(); + } + + public function testRegularRoomSelectedPassesValidation(): void + { + $bookingDto = $this->createBookingDto([ + ['id' => 1, 'code' => '2erDW', 'quantity' => 1], + ]); + + $this->validator->validate($bookingDto, new RoomSelection()); + + $this->assertNoViolation(); + } + + public function testMultipleRegularRoomsSelectedPassesValidation(): void + { + $bookingDto = $this->createBookingDto([ + ['id' => 1, 'code' => '2erDW', 'quantity' => 1], + ['id' => 2, 'code' => '4erDW', 'quantity' => 2], + ]); + + $this->validator->validate($bookingDto, new RoomSelection()); + + $this->assertNoViolation(); + } + + public function testOnlyBabyRoomSelectedFailsValidation(): void + { + $bookingDto = $this->createBookingDto([ + ['id' => 1, 'code' => 'Baby', 'quantity' => 1], + ]); + + $this->validator->validate($bookingDto, new RoomSelection()); + + $this->buildViolation('Baby-Zimmer können nur in Kombination mit regulären Zimmern gebucht werden.') + ->assertRaised(); + } + + public function testMultipleBabyRoomsOnlySelectedFailsValidation(): void + { + $bookingDto = $this->createBookingDto([ + ['id' => 1, 'code' => 'Baby', 'quantity' => 2], + ]); + + $this->validator->validate($bookingDto, new RoomSelection()); + + $this->buildViolation('Baby-Zimmer können nur in Kombination mit regulären Zimmern gebucht werden.') + ->assertRaised(); + } + + public function testBabyRoomWithRegularRoomPassesValidation(): void + { + $bookingDto = $this->createBookingDto([ + ['id' => 1, 'code' => '2erDW', 'quantity' => 1], + ['id' => 2, 'code' => 'Baby', 'quantity' => 1], + ]); + + $this->validator->validate($bookingDto, new RoomSelection()); + + $this->assertNoViolation(); + } + + public function testBabyRoomWithMultipleRegularRoomsPassesValidation(): void + { + $bookingDto = $this->createBookingDto([ + ['id' => 1, 'code' => '2erDW', 'quantity' => 1], + ['id' => 2, 'code' => '4erDW', 'quantity' => 2], + ['id' => 3, 'code' => 'Baby', 'quantity' => 1], + ]); + + $this->validator->validate($bookingDto, new RoomSelection()); + + $this->assertNoViolation(); + } + + public function testRoomWithZeroQuantityIsIgnored(): void + { + $bookingDto = $this->createBookingDto([ + ['id' => 1, 'code' => '2erDW', 'quantity' => 0], + ['id' => 2, 'code' => 'Baby', 'quantity' => 1], + ]); + + $this->validator->validate($bookingDto, new RoomSelection()); + + $this->buildViolation('Baby-Zimmer können nur in Kombination mit regulären Zimmern gebucht werden.') + ->assertRaised(); + } + + public function testRoomWithNullQuantityIsIgnored(): void + { + $bookingDto = $this->createBookingDto([ + ['id' => 1, 'code' => '2erDW', 'quantity' => null], + ['id' => 2, 'code' => 'Baby', 'quantity' => 1], + ]); + + $this->validator->validate($bookingDto, new RoomSelection()); + + $this->buildViolation('Baby-Zimmer können nur in Kombination mit regulären Zimmern gebucht werden.') + ->assertRaised(); + } + + /** + * @param array $roomData + */ + private function createBookingDto(array $roomData): BookingDto + { + $travel = new Travel(); + $travel->rooms = []; + + $roomSelections = []; + + foreach ($roomData as $data) { + $room = new Room(); + $room->id = $data['id']; + $room->code = $data['code']; + $room->available = 10; + $room->status = 'Frei'; + $travel->rooms[] = $room; + + $roomSelection = new RoomSelectionDto(); + $roomSelection->roomId = $data['id']; + $roomSelection->quantity = $data['quantity']; + $roomSelections[] = $roomSelection; + } + + $bookingDto = new BookingDto($travel, 1); + $bookingDto->roomSelections = $roomSelections; + + return $bookingDto; + } +}