diff --git a/src/BusProNet/XmlLoader/TravelLoader.php b/src/BusProNet/XmlLoader/TravelLoader.php index cc62571..63972a9 100644 --- a/src/BusProNet/XmlLoader/TravelLoader.php +++ b/src/BusProNet/XmlLoader/TravelLoader.php @@ -314,7 +314,7 @@ class TravelLoader extends AbstractLoader $travelData = $this->loadById($travelId); $booking->travelData = $travelData; - if (null === $travelData || null === $travelData->dateFrom) { + if (null === $travelData->dateFrom) { continue; } @@ -322,7 +322,7 @@ class TravelLoader extends AbstractLoader $travelDate = $travelData->dateFrom; $travelCode = $travelData->code; - // generate url to travelinfo page five days before travel begins + // generate url to travel info page five days before travel begins if ($travelDate->modify('-5 days') < $now) { $booking->travelInfoUrl = sprintf( '%s/%s', diff --git a/src/Controller/Booking/BookingCreateTrait.php b/src/Controller/Booking/BookingCreateTrait.php index 5000401..abf9f44 100644 --- a/src/Controller/Booking/BookingCreateTrait.php +++ b/src/Controller/Booking/BookingCreateTrait.php @@ -17,16 +17,20 @@ use Symfony\Component\HttpFoundation\RedirectResponse; trait BookingCreateTrait { /** - * Validates step access and redirects if necessary. + * Validates step access and returns redirect response if necessary. + * + * @return RedirectResponse|null Returns redirect response if validation fails, null if access is allowed */ - private function validateStepAccess(BookingCreateDto $bookingCreateDto, int $expectedStep): void + private function validateStepAccess(BookingCreateDto $bookingCreateDto, int $expectedStep): ?RedirectResponse { // Allow access to current step or any previous step if ($expectedStep > $bookingCreateDto->currentStep) { $this->addFlash('error', 'Bitte erst die vorherigen Schritte abschließen.'); - $this->redirectToCurrentStep($bookingCreateDto); + return $this->redirectToCurrentStep($bookingCreateDto); } + + return null; } /** @@ -34,18 +38,12 @@ trait BookingCreateTrait */ private function redirectToCurrentStep(BookingCreateDto $bookingCreateDto): RedirectResponse { - $routeParams = [ - 'date_id' => $bookingCreateDto->travel->id, - 'hotel_id' => $bookingCreateDto->travel->hotelId, - ]; - $route = match ($bookingCreateDto->currentStep) { - 1 => 'app_booking_create_step_1', 2 => 'app_booking_create_step_2', 3 => 'app_booking_create_step_3', default => 'app_booking_create_step_1', }; - return $this->redirectToRoute($route, $routeParams); + return $this->redirectToRoute($route); } } diff --git a/src/Controller/Booking/BookingExceptionHandlerTrait.php b/src/Controller/Booking/BookingExceptionHandlerTrait.php new file mode 100644 index 0000000..64b2321 --- /dev/null +++ b/src/Controller/Booking/BookingExceptionHandlerTrait.php @@ -0,0 +1,66 @@ +getOrCreateBookingCreateDto($request); + } catch (BookingSessionNotFoundException $e) { + $this->addFlash('error', 'Ihre Buchungssitzung ist abgelaufen. Bitte starten Sie eine neue Buchung.'); + return $this->redirectToRoute('app_booking_create_error'); + } catch (TravelNotFoundException $e) { + $this->addFlash('error', 'Die angeforderte Reise wurde nicht gefunden.'); + return $this->redirectToRoute('app_booking_create_error'); + } catch (HotelNotFoundException $e) { + $this->addFlash('error', 'Das angeforderte Hotel wurde nicht gefunden.'); + return $this->redirectToRoute('app_booking_create_error'); + } catch (HotelNotInTravelException $e) { + $this->addFlash('error', 'Das Hotel ist für diese Reise nicht verfügbar.'); + return $this->redirectToRoute('app_booking_create_error'); + } catch (NoRoomsAvailableException $e) { + $this->addFlash('error', 'Für diese Reise sind aktuell keine Zimmer verfügbar.'); + return $this->redirectToRoute('app_booking_create_error'); + } + } + + /** + * Safely retrieves booking DTO for HTMX requests with lightweight error responses. + * + * Returns empty 400 responses for HTMX requests when exceptions occur, + * allowing the frontend to handle errors appropriately. + */ + protected function getOrCreateBookingCreateDtoForHtmx(BookingService $bookingService, Request $request): mixed + { + try { + return $bookingService->getOrCreateBookingCreateDto($request); + } catch (BookingSessionNotFoundException | TravelNotFoundException | HotelNotFoundException | HotelNotInTravelException | NoRoomsAvailableException $e) { + return new Response('', 400); + } + } +} \ No newline at end of file diff --git a/src/Controller/Booking/CreateInitController.php b/src/Controller/Booking/CreateInitController.php index 80f9a5c..1e18bd5 100644 --- a/src/Controller/Booking/CreateInitController.php +++ b/src/Controller/Booking/CreateInitController.php @@ -57,4 +57,16 @@ class CreateInitController extends AbstractController throw $this->createNotFoundException('No rooms available for this travel.'); } } + + /** + * Displays user-friendly error messages for booking initialization failures. + * + * This endpoint provides a centralized location for displaying booking errors + * with appropriate error messages and guidance for users. + */ + #[Route('/bookings/create/error', name: 'app_booking_create_error')] + public function error(Request $request): Response + { + return $this->render('booking/create_error.html.twig'); + } } diff --git a/src/Controller/Booking/CreateStep1Controller.php b/src/Controller/Booking/CreateStep1Controller.php index bc60751..b171f4f 100644 --- a/src/Controller/Booking/CreateStep1Controller.php +++ b/src/Controller/Booking/CreateStep1Controller.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Controller\Booking; -use App\Exception\NoRoomsAvailableException; use App\Form\BookingCreateStep1Type; use App\Service\BookingService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -21,6 +20,7 @@ use Symfony\Component\Routing\Attribute\Route; class CreateStep1Controller extends AbstractController { use BookingCreateTrait; + use BookingExceptionHandlerTrait; public function __construct( private readonly BookingService $bookingService, @@ -33,11 +33,11 @@ class CreateStep1Controller extends AbstractController #[Route('/bookings/create', name: 'app_booking_create_step_1')] public function index(Request $request): Response { - try { - $bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request); - } catch (NoRoomsAvailableException $e) { - throw $this->createNotFoundException('No rooms available for this travel.'); + $result = $this->getOrCreateBookingCreateDto($this->bookingService, $request); + if ($result instanceof Response) { + return $result; } + $bookingCreateDto = $result; // Get or create baseline snapshot for change detection $oldRoomSelectionSnapshot = $this->bookingService->getOrCreateBaselineSnapshot($request, $bookingCreateDto); @@ -45,7 +45,9 @@ class CreateStep1Controller extends AbstractController $summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto); // Validate step access - allow step 1 or redirect to current step - $this->validateStepAccess($bookingCreateDto, 1); + if ($redirect = $this->validateStepAccess($bookingCreateDto, 1)) { + return $redirect; + } $form = $this->createForm(BookingCreateStep1Type::class, $bookingCreateDto, [ 'validation_groups' => ['booking_create_step_1'], @@ -88,11 +90,11 @@ class CreateStep1Controller extends AbstractController #[Route('/bookings/create/room-summary', name: 'app_booking_create_step_1_room_summary', methods: ['POST'])] public function roomSummary(Request $request): Response { - try { - $bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request); - } catch (NoRoomsAvailableException $e) { - throw $this->createNotFoundException('No rooms available for this travel.'); + $result = $this->getOrCreateBookingCreateDto($this->bookingService, $request); + if ($result instanceof Response) { + return $result; } + $bookingCreateDto = $result; // Process the form to update the DTO with the latest room selection $form = $this->createForm(BookingCreateStep1Type::class, $bookingCreateDto, [ diff --git a/src/Controller/Booking/CreateStep2Controller.php b/src/Controller/Booking/CreateStep2Controller.php index b095a70..8a05ac5 100644 --- a/src/Controller/Booking/CreateStep2Controller.php +++ b/src/Controller/Booking/CreateStep2Controller.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace App\Controller\Booking; use App\Controller\Traits\HtmxControllerTrait; -use App\Exception\NoRoomsAvailableException; use App\Form\BookingCreateStep2Type; use App\Form\Model\BookingCreateDto; use App\Form\Model\ParticipantDto; @@ -25,6 +24,7 @@ use Symfony\Component\Routing\Attribute\Route; class CreateStep2Controller extends AbstractController { use BookingCreateTrait; + use BookingExceptionHandlerTrait; use HtmxControllerTrait; public function __construct( @@ -39,21 +39,19 @@ class CreateStep2Controller extends AbstractController #[Route('/bookings/create/participants', name: 'app_booking_create_step_2')] public function participants(Request $request): Response { - 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.'); + $result = $this->getOrCreateBookingCreateDto($this->bookingService, $request); + if ($result instanceof Response) { + return $result; } + $bookingCreateDto = $result; // Enrich with fresh availability data $this->enrichWithFreshAvailabilities($bookingCreateDto); // Validate step access - $this->validateStepAccess($bookingCreateDto, 2); + if ($redirect = $this->validateStepAccess($bookingCreateDto, 2)) { + return $redirect; + } // Ensure correct number of participants $this->ensureCorrectNumberOfParticipants($bookingCreateDto); @@ -100,12 +98,11 @@ class CreateStep2Controller extends AbstractController #[Route('/bookings/create/participants/refresh', name: 'app_booking_create_step_2_refresh', methods: ['POST'])] public function refreshParticipantForm(Request $request): Response { - try { - $bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request); - } catch (NoRoomsAvailableException $e) { - // For HTMX requests, return a simple error message - return new Response('
{{ flash_message }}
+ {% endfor %} + + {% if app.flashes('error') is empty %} +Es ist ein Fehler beim Starten der Buchung aufgetreten. Bitte versuchen Sie es erneut.
+ {% endif %} +