diff --git a/src/BusProNet/XmlLoader/HotelLoader.php b/src/BusProNet/XmlLoader/HotelLoader.php index 28f6320..3ad1932 100644 --- a/src/BusProNet/XmlLoader/HotelLoader.php +++ b/src/BusProNet/XmlLoader/HotelLoader.php @@ -4,6 +4,7 @@ namespace App\BusProNet\XmlLoader; use App\BusProNet\Model\Hotel; use App\BusProNet\Model\Travel; +use App\Exception\HotelNotFoundException; use Psr\Cache\InvalidArgumentException; use Symfony\Component\DomCrawler\Crawler; use Symfony\Contracts\Cache\ItemInterface; @@ -47,11 +48,15 @@ class HotelLoader extends AbstractLoader return null; } - public function loadById(int $id, ?string $filename = 'hotel.xml'): ?Hotel + public function loadById(int $id, ?string $filename = 'hotel.xml'): Hotel { $hotels = $this->loadAll($filename); - return $hotels[$id] ?? null; + if (false === isset($hotels[$id])) { + throw new HotelNotFoundException($id); + } + + return $hotels[$id]; } private function loadXml(?string $filename = 'hotel.xml'): Crawler @@ -78,7 +83,13 @@ class HotelLoader extends AbstractLoader public function patchHotelDetails(Travel $travel): void { $hotelId = $travel->hotelId; - $hotel = $this->loadById($hotelId); - $travel->hotel = $hotel; + try { + $hotel = $this->loadById($hotelId); + $travel->hotel = $hotel; + } catch (HotelNotFoundException $e) { + // If hotel details can't be loaded, leave travel->hotel as null + // This allows the travel to be processed even if hotel details are missing + $travel->hotel = null; + } } } diff --git a/src/BusProNet/XmlLoader/TravelLoader.php b/src/BusProNet/XmlLoader/TravelLoader.php index 8f7df37..cc62571 100644 --- a/src/BusProNet/XmlLoader/TravelLoader.php +++ b/src/BusProNet/XmlLoader/TravelLoader.php @@ -7,6 +7,8 @@ use App\BusProNet\Model\MutableData; use App\BusProNet\Model\Travel; use App\BusProNet\Utility\DateCodeUtility; use App\BusProNet\XmlParser\TravelParser; +use App\Exception\HotelNotInTravelException; +use App\Exception\TravelNotFoundException; use League\Flysystem\FilesystemException; use League\Flysystem\FilesystemOperator; use League\Flysystem\StorageAttributes; @@ -160,14 +162,18 @@ class TravelLoader extends AbstractLoader * * Retrieves travel data from XML exports. If filename is provided, loads directly * from that file. Otherwise, uses the cached mapping to find the appropriate file. + * Validates that both travel and hotel (if specified) exist before attempting to parse. * * @param int $dateId The travel date ID to load * @param int|null $hotelId Optional hotel ID for specific hotel data * @param string|null $filename Optional filename to load from directly * - * @return Travel|null The loaded travel object or null if not found + * @return Travel The loaded travel object + * + * @throws TravelNotFoundException When travel ID is not found + * @throws HotelNotInTravelException When hotel ID exists but not for this travel */ - public function loadById(int $dateId, ?int $hotelId = null, ?string $filename = null): ?Travel + public function loadById(int $dateId, ?int $hotelId = null, ?string $filename = null): Travel { if (null !== $filename) { return $this->loadXml($dateId, $hotelId, $filename); @@ -175,8 +181,14 @@ class TravelLoader extends AbstractLoader $mapping = $this->generateFilesMap(); + // Validate travel exists if (false === isset($mapping[$dateId])) { - return null; + throw new TravelNotFoundException($dateId); + } + + // Validate hotel exists in this travel if specified + if (null !== $hotelId && false === isset($mapping[$dateId]['hotels'][$hotelId])) { + throw new HotelNotInTravelException($dateId, $hotelId); } $filename = $mapping[$dateId]['file']; @@ -194,9 +206,12 @@ class TravelLoader extends AbstractLoader * @param int|null $hotelId Optional hotel ID for specific hotel data * @param string $filename The XML filename to load from * - * @return Travel|null The loaded travel object or null if not found + * @return Travel The loaded travel object + * + * @throws TravelNotFoundException When travel ID is not found in XML + * @throws HotelNotInTravelException When hotel ID is not found in travel XML */ - private function loadXml(int $dateId, ?int $hotelId, string $filename): ?Travel + private function loadXml(int $dateId, ?int $hotelId, string $filename): Travel { try { $xml = $this->xmlExport->read($filename); @@ -205,12 +220,20 @@ class TravelLoader extends AbstractLoader $travelNode = $crawler->filterXPath(sprintf('//reise/termin[@idbuspro="%d"]', $dateId)); if (0 === $travelNode->count()) { - return null; + throw new TravelNotFoundException($dateId); + } + + // Validate hotel exists in travel XML if specified + if (null !== $hotelId) { + $hotelNode = $travelNode->filterXPath(sprintf('.//hotel[@idbuspro="%d"]', $hotelId)); + if (0 === $hotelNode->count()) { + throw new HotelNotInTravelException($dateId, $hotelId); + } } return $this->travelParser->parse($travelNode->first(), $hotelId); } catch (FilesystemException $e) { - return null; + throw new TravelNotFoundException($dateId, $e); } } diff --git a/src/Controller/Booking/CreateInitController.php b/src/Controller/Booking/CreateInitController.php new file mode 100644 index 0000000..80f9a5c --- /dev/null +++ b/src/Controller/Booking/CreateInitController.php @@ -0,0 +1,60 @@ + '\d+', 'hotelId' => '\d+'])] + public function init(Request $request, int $dateId, int $hotelId): Response + { + try { + // Clear any existing booking session to ensure fresh start + $this->bookingService->clearBookingSession($request); + + // Create fresh booking session with the provided parameters + $this->bookingService->startFreshBooking($request, $dateId, $hotelId); + + // Redirect to step 1 of the booking flow + return $this->redirectToRoute('app_booking_create_step_1'); + } catch (TravelNotFoundException $e) { + throw $this->createNotFoundException(sprintf('Travel not found for date ID %d', $dateId)); + } catch (HotelNotFoundException $e) { + throw $this->createNotFoundException(sprintf('Hotel not found for hotel ID %d', $hotelId)); + } catch (HotelNotInTravelException $e) { + throw $this->createNotFoundException(sprintf('Hotel ID %d is not available for travel ID %d', $hotelId, $dateId)); + } catch (NoRoomsAvailableException $e) { + throw $this->createNotFoundException('No rooms available for this travel.'); + } + } +} diff --git a/src/Controller/Booking/CreateStep1Controller.php b/src/Controller/Booking/CreateStep1Controller.php index d9fb319..bc60751 100644 --- a/src/Controller/Booking/CreateStep1Controller.php +++ b/src/Controller/Booking/CreateStep1Controller.php @@ -36,8 +36,6 @@ class CreateStep1Controller extends AbstractController 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.'); } @@ -93,8 +91,7 @@ class CreateStep1Controller extends AbstractController try { $bookingCreateDto = $this->bookingService->getOrCreateBookingCreateDto($request); } catch (NoRoomsAvailableException $e) { - // For HTMX requests, return a simple error message - return new Response('