diff --git a/src/BusProNet/DataProcessor/BookingDataProcessor.php b/src/BusProNet/DataProcessor/BookingDataProcessor.php index 3b2200f..a8fdc4a 100644 --- a/src/BusProNet/DataProcessor/BookingDataProcessor.php +++ b/src/BusProNet/DataProcessor/BookingDataProcessor.php @@ -671,7 +671,7 @@ class BookingDataProcessor $payload = [ 'buchungsart' => $bookingType, - 'status' => 'F', + 'status' => $bookingDto->bookingStatus, 'idreise' => $bookingDto->travel->id, 'idpartner' => $bookingDto->travel->hotelId, 'idagentur' => $bookingDto->agencyId, diff --git a/src/BusProNet/Model/Travel.php b/src/BusProNet/Model/Travel.php index 4fb16e8..2bf2688 100644 --- a/src/BusProNet/Model/Travel.php +++ b/src/BusProNet/Model/Travel.php @@ -46,6 +46,13 @@ class Travel #[Groups(['api:list', 'api:single'])] public ?string $type = null; + /** + * Travel booking status from BusProNet API. + * Possible values: 'Frei', 'Anfrage', 'Buchungsstop' + */ + #[Groups(['api:single'])] + public ?string $status = null; + #[Groups(['api:list', 'api:single'])] public ?float $priceFrom = null; diff --git a/src/BusProNet/XmlParser/TravelParser.php b/src/BusProNet/XmlParser/TravelParser.php index 4191f29..c7322d2 100644 --- a/src/BusProNet/XmlParser/TravelParser.php +++ b/src/BusProNet/XmlParser/TravelParser.php @@ -54,6 +54,7 @@ class TravelParser extends AbstractParser $travel->dateTo = $this->stringToDate($node->attr('bis')); $travel->code = $node->attr('code'); $travel->type = $node->attr('reiseart'); + $travel->status = $this->getStringOrNullValue($node->filterXPath('//status_hin')); $travel->priceFrom = $this->stringToFloat($this->getStringOrNullValue($node->filterXPath('//abpreis'))); $travel->selectionGroups = $this->getSelectionGroups($node->filterXPath('//selektiongruppe')); $travel->additionalServices = $this diff --git a/src/Controller/Booking/CreateInitController.php b/src/Controller/Booking/CreateInitController.php index be18554..2163391 100644 --- a/src/Controller/Booking/CreateInitController.php +++ b/src/Controller/Booking/CreateInitController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Controller\Booking; use App\BusProNet\XmlLoader\AgencyLoader; +use App\Exception\BookingNotPossibleException; use App\Exception\HotelNotFoundException; use App\Exception\HotelNotInTravelException; use App\Exception\NoRoomsAvailableException; @@ -64,6 +65,8 @@ class CreateInitController extends AbstractController 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.'); + } catch (BookingNotPossibleException $e) { + throw $this->createNotFoundException('Booking is not possible for this travel (Buchungsstop).'); } } diff --git a/src/Exception/BookingNotPossibleException.php b/src/Exception/BookingNotPossibleException.php new file mode 100644 index 0000000..14892ab --- /dev/null +++ b/src/Exception/BookingNotPossibleException.php @@ -0,0 +1,24 @@ +status) { + throw new BookingNotPossibleException($dateId, $hotelId); + } + + // Determine booking status based on travel status + $isInquiryBooking = 'Anfrage' === $travelData->status; + $bookingStatus = $isInquiryBooking ? 'A' : 'F'; + + // Get available rooms $availableRooms = $travelData->getAvailableRooms(); - // Prevent booking flow entry when no rooms are available - if (empty($availableRooms)) { + // For regular bookings (Frei), prevent entry when no rooms are available + // For inquiry bookings (Anfrage), allow even with 0 availability + if (false === $isInquiryBooking && empty($availableRooms)) { throw new NoRoomsAvailableException($dateId, $hotelId); } + // For inquiry bookings with 0 availability, get all rooms ignoring availability count + if ($isInquiryBooking && empty($availableRooms)) { + $availableRooms = array_filter($travelData->rooms, function (Room $room) { + return \App\BusProNet\Constants::STATUS_AVAILABLE === $room->status; + }); + } + // Create room selections with zero quantities (user will set these in step 1) $roomSelections = array_map( fn (Room $room) => $this->createRoomSelection($room, []), @@ -191,6 +215,7 @@ class BookingService $bookingCreateDto->roomSelections = $roomSelections; $bookingCreateDto->currentStep = 1; $bookingCreateDto->agencyId = $agencyId; + $bookingCreateDto->bookingStatus = $bookingStatus; $this->saveBookingCreateDto($request, $bookingCreateDto); diff --git a/templates/booking/create_step_1.html.twig b/templates/booking/create_step_1.html.twig index 53f9f04..d54b215 100644 --- a/templates/booking/create_step_1.html.twig +++ b/templates/booking/create_step_1.html.twig @@ -2,7 +2,20 @@ {% block content %} {% include '_partials/_flashes.html.twig' %} -

Neue Buchung

+

Neue Buchung

+ {% if bookingCreateDto.bookingStatus == 'A' %} +
+
+ + + +
+

Buchung auf Anfrage

+

Diese Reise kann nur auf Anfrage gebucht werden. Deine Buchung wird nach Eingang geprüft und du erhältst eine Bestätigung.

+
+
+
+ {% endif %}

diff --git a/templates/booking/create_step_4.html.twig b/templates/booking/create_step_4.html.twig index dcbf27b..bf6b1a2 100644 --- a/templates/booking/create_step_4.html.twig +++ b/templates/booking/create_step_4.html.twig @@ -7,6 +7,20 @@

Neue Buchung

+ {% if bookingCreateDto.bookingStatus == 'A' %} +
+
+ + + +
+

Achtung: Buchung auf Anfrage

+

Diese Buchung erfolgt auf Anfrage. Nach der Absendung wird deine Anfrage geprüft. Du erhältst anschließend eine verbindliche Buchungsbestätigung oder eine Absage per E-Mail.

+
+
+
+ {% endif %} +

Buchung bestätigen

diff --git a/tests/BusProNet/XmlParser/TravelParserTest.php b/tests/BusProNet/XmlParser/TravelParserTest.php index 30caf67..ffd556f 100644 --- a/tests/BusProNet/XmlParser/TravelParserTest.php +++ b/tests/BusProNet/XmlParser/TravelParserTest.php @@ -184,4 +184,103 @@ class TravelParserTest extends TestCase $this->assertSame('Service with empty description', $service->label); $this->assertNull($service->description); // Empty hinweis should result in null description } + + public function testParseTravelStatusFrei(): void + { + $xmlContent = ' + + + + Test Travel + 659,00 + Frei + + + + + + + +'; + + $crawler = new Crawler($xmlContent); + $travelNode = $crawler->filterXPath('//reise/termin')->first(); + $travel = $this->parser->parse($travelNode); + + $this->assertSame('Frei', $travel->status); + } + + public function testParseTravelStatusAnfrage(): void + { + $xmlContent = ' + + + + Test Travel + 659,00 + Anfrage + + + + + + + +'; + + $crawler = new Crawler($xmlContent); + $travelNode = $crawler->filterXPath('//reise/termin')->first(); + $travel = $this->parser->parse($travelNode); + + $this->assertSame('Anfrage', $travel->status); + } + + public function testParseTravelStatusBuchungsstop(): void + { + $xmlContent = ' + + + + Test Travel + 659,00 + Buchungsstop + + + + + + + +'; + + $crawler = new Crawler($xmlContent); + $travelNode = $crawler->filterXPath('//reise/termin')->first(); + $travel = $this->parser->parse($travelNode); + + $this->assertSame('Buchungsstop', $travel->status); + } + + public function testParseTravelWithoutStatus(): void + { + $xmlContent = ' + + + + Test Travel + 659,00 + + + + + + + +'; + + $crawler = new Crawler($xmlContent); + $travelNode = $crawler->filterXPath('//reise/termin')->first(); + $travel = $this->parser->parse($travelNode); + + $this->assertNull($travel->status); // No status_hin node should result in null + } } diff --git a/tests/Service/BookingServiceStatusTest.php b/tests/Service/BookingServiceStatusTest.php new file mode 100644 index 0000000..218f80a --- /dev/null +++ b/tests/Service/BookingServiceStatusTest.php @@ -0,0 +1,164 @@ +travelDataService = $this->createMock(TravelDataService::class); + $priceCalculator = $this->createMock(BookingPriceCalculatorService::class); + $participantEligibility = $this->createMock(ParticipantEligibilityService::class); + + $this->bookingService = new BookingService( + $this->travelDataService, + $priceCalculator, + $participantEligibility + ); + } + + public function testStartFreshBookingWithFreiStatus(): void + { + $travel = $this->createTravelWithStatus('Frei', 5); + + $this->travelDataService + ->method('getTravelData') + ->willReturn($travel); + + $request = $this->createRequestWithSession(); + + $bookingDto = $this->bookingService->startFreshBooking($request, 123, 456); + + $this->assertSame('F', $bookingDto->bookingStatus); + $this->assertCount(1, $bookingDto->roomSelections); + } + + public function testStartFreshBookingWithFreiStatusAndNoRoomsThrowsException(): void + { + $travel = $this->createTravelWithStatus('Frei', 0); + + $this->travelDataService + ->method('getTravelData') + ->willReturn($travel); + + $request = $this->createRequestWithSession(); + + $this->expectException(NoRoomsAvailableException::class); + + $this->bookingService->startFreshBooking($request, 123, 456); + } + + public function testStartFreshBookingWithAnfrageStatus(): void + { + $travel = $this->createTravelWithStatus('Anfrage', 5); + + $this->travelDataService + ->method('getTravelData') + ->willReturn($travel); + + $request = $this->createRequestWithSession(); + + $bookingDto = $this->bookingService->startFreshBooking($request, 123, 456); + + $this->assertSame('A', $bookingDto->bookingStatus); + $this->assertCount(1, $bookingDto->roomSelections); + } + + public function testStartFreshBookingWithAnfrageStatusAndNoRoomsAllowsBooking(): void + { + $travel = $this->createTravelWithStatus('Anfrage', 0); + + $this->travelDataService + ->method('getTravelData') + ->willReturn($travel); + + $request = $this->createRequestWithSession(); + + $bookingDto = $this->bookingService->startFreshBooking($request, 123, 456); + + $this->assertSame('A', $bookingDto->bookingStatus); + $this->assertCount(1, $bookingDto->roomSelections); + } + + public function testStartFreshBookingWithBuchungsstopThrowsException(): void + { + $travel = $this->createTravelWithStatus('Buchungsstop', 5); + + $this->travelDataService + ->method('getTravelData') + ->willReturn($travel); + + $request = $this->createRequestWithSession(); + + $this->expectException(BookingNotPossibleException::class); + $this->expectExceptionMessage('Für diese Reise ist aktuell keine Buchung möglich'); + + $this->bookingService->startFreshBooking($request, 123, 456); + } + + public function testStartFreshBookingWithBuchungsstopAndNoRoomsThrowsException(): void + { + $travel = $this->createTravelWithStatus('Buchungsstop', 0); + + $this->travelDataService + ->method('getTravelData') + ->willReturn($travel); + + $request = $this->createRequestWithSession(); + + $this->expectException(BookingNotPossibleException::class); + + $this->bookingService->startFreshBooking($request, 123, 456); + } + + private function createTravelWithStatus(string $status, int $availability): Travel + { + $travel = new Travel(); + $travel->id = 123; + $travel->hotelId = 456; + $travel->status = $status; + $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-06'); + + $room = new Room(); + $room->id = 1; + $room->label = 'Test Room'; + $room->price = 100.0; + $room->available = $availability; + $room->status = \App\BusProNet\Constants::STATUS_AVAILABLE; + $room->minPax = 2; + $room->category = 'A'; + $room->boardId = 1; + + $travel->rooms = [1 => $room]; + + return $travel; + } + + private function createRequestWithSession(): Request + { + $session = new Session(new MockArraySessionStorage()); + $request = new Request(); + $request->setSession($session); + + return $request; + } +} \ No newline at end of file