feat: prevent booking flow without available rooms
This commit is contained in:
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller\Booking;
|
namespace App\Controller\Booking;
|
||||||
|
|
||||||
|
use App\Exception\NoRoomsAvailableException;
|
||||||
use App\Form\BookingCreateStep1Type;
|
use App\Form\BookingCreateStep1Type;
|
||||||
use App\Service\BookingService;
|
use App\Service\BookingService;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
@@ -32,7 +33,13 @@ class CreateStep1Controller extends AbstractController
|
|||||||
#[Route('/bookings/create', name: 'app_booking_create_step_1')]
|
#[Route('/bookings/create', name: 'app_booking_create_step_1')]
|
||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
{
|
{
|
||||||
$bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request);
|
try {
|
||||||
|
$bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request);
|
||||||
|
} catch (NoRoomsAvailableException $e) {
|
||||||
|
$this->addFlash('error', 'Leider sind für diese Reise aktuell keine Zimmer verfügbar.');
|
||||||
|
// TODO: Redirect to travel listing or hotel details page
|
||||||
|
throw $this->createNotFoundException('No rooms available for this travel.');
|
||||||
|
}
|
||||||
|
|
||||||
// Get or create baseline snapshot for change detection
|
// Get or create baseline snapshot for change detection
|
||||||
$oldRoomSelectionSnapshot = $this->bookingService->getOrCreateBaselineSnapshot($request, $bookingCreateDto);
|
$oldRoomSelectionSnapshot = $this->bookingService->getOrCreateBaselineSnapshot($request, $bookingCreateDto);
|
||||||
@@ -83,7 +90,12 @@ class CreateStep1Controller extends AbstractController
|
|||||||
#[Route('/bookings/create/room-summary', name: 'app_booking_create_step_1_room_summary', methods: ['POST'])]
|
#[Route('/bookings/create/room-summary', name: 'app_booking_create_step_1_room_summary', methods: ['POST'])]
|
||||||
public function roomSummary(Request $request): Response
|
public function roomSummary(Request $request): Response
|
||||||
{
|
{
|
||||||
$bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request);
|
try {
|
||||||
|
$bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request);
|
||||||
|
} catch (NoRoomsAvailableException $e) {
|
||||||
|
// For HTMX requests, return a simple error message
|
||||||
|
return new Response('<div class="text-red-700 p-4">Keine Zimmer verfügbar</div>', 400);
|
||||||
|
}
|
||||||
|
|
||||||
// Process the form to update the DTO with the latest room selection
|
// Process the form to update the DTO with the latest room selection
|
||||||
$form = $this->createForm(BookingCreateStep1Type::class, $bookingCreateDto, [
|
$form = $this->createForm(BookingCreateStep1Type::class, $bookingCreateDto, [
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Controller\Booking;
|
namespace App\Controller\Booking;
|
||||||
|
|
||||||
use App\Controller\Traits\HtmxControllerTrait;
|
use App\Controller\Traits\HtmxControllerTrait;
|
||||||
|
use App\Exception\NoRoomsAvailableException;
|
||||||
use App\Form\BookingCreateStep2Type;
|
use App\Form\BookingCreateStep2Type;
|
||||||
use App\Form\Model\BookingCreateDto;
|
use App\Form\Model\BookingCreateDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
@@ -38,9 +39,15 @@ class CreateStep2Controller extends AbstractController
|
|||||||
#[Route('/bookings/create/participants', name: 'app_booking_create_step_2')]
|
#[Route('/bookings/create/participants', name: 'app_booking_create_step_2')]
|
||||||
public function participants(Request $request): Response
|
public function participants(Request $request): Response
|
||||||
{
|
{
|
||||||
$bookingCreateDto = $this
|
try {
|
||||||
->bookingService
|
$bookingCreateDto = $this
|
||||||
->getOrCreateBookingCreateDto($request);
|
->bookingService
|
||||||
|
->getOrCreateBookingCreateDto($request);
|
||||||
|
} catch (NoRoomsAvailableException $e) {
|
||||||
|
$this->addFlash('error', 'Leider sind für diese Reise aktuell keine Zimmer verfügbar.');
|
||||||
|
// TODO: Redirect to travel listing or hotel details page
|
||||||
|
throw $this->createNotFoundException('No rooms available for this travel.');
|
||||||
|
}
|
||||||
|
|
||||||
// Enrich with fresh availability data
|
// Enrich with fresh availability data
|
||||||
$this->enrichWithFreshAvailabilities($bookingCreateDto);
|
$this->enrichWithFreshAvailabilities($bookingCreateDto);
|
||||||
@@ -93,7 +100,12 @@ class CreateStep2Controller extends AbstractController
|
|||||||
#[Route('/bookings/create/participants/refresh', name: 'app_booking_create_step_2_refresh', methods: ['POST'])]
|
#[Route('/bookings/create/participants/refresh', name: 'app_booking_create_step_2_refresh', methods: ['POST'])]
|
||||||
public function refreshParticipantForm(Request $request): Response
|
public function refreshParticipantForm(Request $request): Response
|
||||||
{
|
{
|
||||||
$bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request);
|
try {
|
||||||
|
$bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request);
|
||||||
|
} catch (NoRoomsAvailableException $e) {
|
||||||
|
// For HTMX requests, return a simple error message
|
||||||
|
return new Response('<div class="text-red-700 p-4">Keine Zimmer verfügbar</div>', 400);
|
||||||
|
}
|
||||||
|
|
||||||
// Enrich with fresh availability data
|
// Enrich with fresh availability data
|
||||||
$this->enrichWithFreshAvailabilities($bookingCreateDto);
|
$this->enrichWithFreshAvailabilities($bookingCreateDto);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Controller\Booking;
|
namespace App\Controller\Booking;
|
||||||
|
|
||||||
|
use App\Exception\NoRoomsAvailableException;
|
||||||
use App\Service\BookingService;
|
use App\Service\BookingService;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -31,7 +32,13 @@ class CreateStep3Controller extends AbstractController
|
|||||||
#[Route('/bookings/create/confirm', name: 'app_booking_create_step_3')]
|
#[Route('/bookings/create/confirm', name: 'app_booking_create_step_3')]
|
||||||
public function confirm(Request $request): Response
|
public function confirm(Request $request): Response
|
||||||
{
|
{
|
||||||
$bookingCreateDto = $this->bookingCreateService->getOrCreateBookingCreateDto($request);
|
try {
|
||||||
|
$bookingCreateDto = $this->bookingCreateService->getOrCreateBookingCreateDto($request);
|
||||||
|
} catch (NoRoomsAvailableException $e) {
|
||||||
|
$this->addFlash('error', 'Leider sind für diese Reise aktuell keine Zimmer verfügbar.');
|
||||||
|
// TODO: Redirect to travel listing or hotel details page
|
||||||
|
throw $this->createNotFoundException('No rooms available for this travel.');
|
||||||
|
}
|
||||||
|
|
||||||
// Validate step access
|
// Validate step access
|
||||||
$this->validateStepAccess($bookingCreateDto, 3);
|
$this->validateStepAccess($bookingCreateDto, 3);
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Exception;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when attempting to create a booking but no rooms are available.
|
||||||
|
*
|
||||||
|
* This exception is used to prevent users from entering the booking flow
|
||||||
|
* when there are no available rooms for the selected travel dates.
|
||||||
|
*/
|
||||||
|
class NoRoomsAvailableException extends HttpException
|
||||||
|
{
|
||||||
|
public function __construct(int $dateId, int $hotelId, ?\Throwable $previous = null)
|
||||||
|
{
|
||||||
|
$message = sprintf(
|
||||||
|
'Leider sind für diese Reise aktuell keine Zimmer verfügbar (Travel ID: %d, Hotel ID: %d)',
|
||||||
|
$dateId,
|
||||||
|
$hotelId
|
||||||
|
);
|
||||||
|
|
||||||
|
parent::__construct(400, $message, $previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ namespace App\Service;
|
|||||||
|
|
||||||
use App\BusProNet\Model\Room;
|
use App\BusProNet\Model\Room;
|
||||||
use App\BusProNet\Model\Travel;
|
use App\BusProNet\Model\Travel;
|
||||||
|
use App\Exception\NoRoomsAvailableException;
|
||||||
use App\Form\Model\BookingCreateDto;
|
use App\Form\Model\BookingCreateDto;
|
||||||
use App\Form\Model\RoomSelectionDto;
|
use App\Form\Model\RoomSelectionDto;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -78,6 +79,11 @@ class BookingService
|
|||||||
$roomsIdsAndQuantities = $this->processRoomQuantities($request);
|
$roomsIdsAndQuantities = $this->processRoomQuantities($request);
|
||||||
$availableRooms = $travelData->getAvailableRooms();
|
$availableRooms = $travelData->getAvailableRooms();
|
||||||
|
|
||||||
|
// Prevent booking flow entry when no rooms are available
|
||||||
|
if (empty($availableRooms)) {
|
||||||
|
throw new NoRoomsAvailableException($dateId, $hotelId);
|
||||||
|
}
|
||||||
|
|
||||||
$roomSelections = array_map(
|
$roomSelections = array_map(
|
||||||
fn (Room $room) => $this->createRoomSelection($room, $roomsIdsAndQuantities),
|
fn (Room $room) => $this->createRoomSelection($room, $roomsIdsAndQuantities),
|
||||||
$availableRooms
|
$availableRooms
|
||||||
|
|||||||
Reference in New Issue
Block a user