feat: prevent selection of baby rooms without regular rooms
This commit is contained in:
@@ -7,6 +7,7 @@ namespace App\Form\Model;
|
|||||||
use App\BusProNet\Constants;
|
use App\BusProNet\Constants;
|
||||||
use App\BusProNet\Model\Booking;
|
use App\BusProNet\Model\Booking;
|
||||||
use App\BusProNet\Model\Travel;
|
use App\BusProNet\Model\Travel;
|
||||||
|
use App\Validator\Constraints as AppAssert;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
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
|
* in participant DTOs regardless of mode, ensuring consistent data structure and
|
||||||
* simplifying pricing calculations, field handlers, and template rendering.
|
* simplifying pricing calculations, field handlers, and template rendering.
|
||||||
*/
|
*/
|
||||||
|
#[AppAssert\RoomSelection(groups: ['booking_create_step_1'])]
|
||||||
class BookingDto
|
class BookingDto
|
||||||
{
|
{
|
||||||
public const MODE_CREATE = 'create';
|
public const MODE_CREATE = 'create';
|
||||||
@@ -175,17 +177,6 @@ class BookingDto
|
|||||||
return $room?->label;
|
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]
|
#[Assert\Callback]
|
||||||
public function validateBankAccount(ExecutionContextInterface $context): void
|
public function validateBankAccount(ExecutionContextInterface $context): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Validator\Constraints;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
|
#[\Attribute]
|
||||||
|
class RoomSelection extends Constraint
|
||||||
|
{
|
||||||
|
public const BABY_ROOM_CODE = 'Baby';
|
||||||
|
|
||||||
|
public string $noRoomSelectedMessage = 'Bitte mindestens ein Zimmer/Bett auswählen';
|
||||||
|
public string $onlyBabyRoomsMessage = 'Baby-Zimmer können nur in Kombination mit regulären Zimmern gebucht werden.';
|
||||||
|
|
||||||
|
public function getTargets(): array|string
|
||||||
|
{
|
||||||
|
return static::CLASS_CONSTRAINT;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Validator\Constraints;
|
||||||
|
|
||||||
|
use App\Form\Model\BookingDto;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||||
|
|
||||||
|
class RoomSelectionValidator extends ConstraintValidator
|
||||||
|
{
|
||||||
|
public function validate(mixed $value, Constraint $constraint): void
|
||||||
|
{
|
||||||
|
if (!$constraint instanceof RoomSelection) {
|
||||||
|
throw new UnexpectedTypeException($constraint, RoomSelection::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$value instanceof BookingDto) {
|
||||||
|
throw new UnexpectedTypeException($value, BookingDto::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedRooms = $value->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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Validator\Constraints;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Room;
|
||||||
|
use App\BusProNet\Model\Travel;
|
||||||
|
use App\Form\Model\BookingDto;
|
||||||
|
use App\Form\Model\RoomSelectionDto;
|
||||||
|
use App\Validator\Constraints\RoomSelection;
|
||||||
|
use App\Validator\Constraints\RoomSelectionValidator;
|
||||||
|
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||||
|
|
||||||
|
class RoomSelectionValidatorTest extends ConstraintValidatorTestCase
|
||||||
|
{
|
||||||
|
protected function createValidator(): RoomSelectionValidator
|
||||||
|
{
|
||||||
|
return new RoomSelectionValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNoRoomSelectedFailsValidation(): void
|
||||||
|
{
|
||||||
|
$bookingDto = $this->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<array{id: int, code: string, quantity: int|null}> $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user