feat: correct determination of inquiry status and conditional voucher field display

This commit is contained in:
Björn Fromme
2025-11-25 16:41:25 +01:00
parent f33e55f201
commit ecc4741f56
19 changed files with 468 additions and 193 deletions
+75 -21
View File
@@ -5,7 +5,6 @@ namespace App\Service;
use App\BusProNet\Constants;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\Travel;
use App\Exception\BookingNotPossibleException;
use App\Exception\BookingSessionNotFoundException;
use App\Exception\NoRoomsAvailableException;
use App\Form\Model\BookingDto;
@@ -192,7 +191,6 @@ class BookingService
* Handles three booking status types:
* - 'Frei': Regular booking with availability checks
* - 'Anfrage': Inquiry booking, allows booking even with 0 availability
* - 'Buchungsstop': Booking stopped, no bookings allowed
*/
public function startFreshBooking(Request $request, int $dateId, int $hotelId, ?int $agencyId = null): BookingDto
{
@@ -201,30 +199,23 @@ class BookingService
throw new NotFoundHttpException(sprintf('Travel data not found for date ID %d and hotel ID %d', $dateId, $hotelId));
}
// Handle Buchungsstop - no bookings allowed at all
if ('Buchungsstop' === $travelData->status) {
throw new BookingNotPossibleException($dateId, $hotelId);
// Fetch and patch availability data (includes allowedBookingStatus from API)
$availabilities = $this->travelDataService->getAvailabilityData($dateId);
if (null !== $availabilities) {
$this->travelDataService->patchAvailabilities($travelData, $availabilities);
}
// Determine booking status based on travel status
$isInquiryBooking = 'Anfrage' === $travelData->status;
$bookingStatus = $isInquiryBooking ? 'A' : 'F';
// Get available rooms
// Get bookable rooms (Frei or Anfrage with available > 0)
$availableRooms = $travelData->getAvailableRooms();
// 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)) {
// No bookable rooms means booking is not possible
if ([] === $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 Constants::STATUS_AVAILABLE === $room->status;
});
}
// Determine initial booking status based on room availability
// This is for early UI decisions (e.g., voucher visibility), final verdict is in step 3
$bookingStatus = $this->determineInitialBookingStatus($travelData);
// Create room selections with zero quantities (user will set these in step 1)
$roomSelections = array_map(
@@ -334,11 +325,11 @@ class BookingService
}
/**
* Groups available rooms by selection type ('by_pax' or 'by_room').
* Groups available rooms by selection type ('by_pax' or 'by_room') and sorts them by maxPax.
*
* @param array<int, Room> $rooms Rooms indexed by room ID
*
* @return array{by_pax: array<int, Room>, by_room: array<int, Room>}
* @return array{by_pax: array<int, Room>, by_room: array<int, Room>} Rooms grouped and sorted by maxPax (ascending)
*/
public function groupRoomsBySelectionType(array $rooms): array
{
@@ -354,6 +345,10 @@ class BookingService
}
}
// Sort each group by maxPax (ascending order - smallest capacity first)
uasort($groups[Room::SELECTION_TYPE_BY_PAX], fn (Room $a, Room $b): int => $a->maxPax <=> $b->maxPax);
uasort($groups[Room::SELECTION_TYPE_BY_ROOM], fn (Room $a, Room $b): int => $a->maxPax <=> $b->maxPax);
return $groups;
}
@@ -491,4 +486,63 @@ class BookingService
$participant->additionalServices = $currentSelections;
}
}
/**
* Determines the initial booking status based on travel configuration.
*
* This method implements a multi-stage check to determine if a booking
* should start as inquiry ('A') or final ('F') booking:
* 1. Checks if final bookings are allowed via buchungstatusmoeglich attribute from API
* 2. Checks if only inquiry rooms are available (no Frei rooms with available > 0)
*
* @param Travel $travelData The travel data to evaluate
*
* @return string 'A' for inquiry booking, 'F' for final booking
*/
private function determineInitialBookingStatus(Travel $travelData): string
{
// Check 1: Allowed booking status from buchungstatusmoeglich attribute (from availability API)
// Only applies if the API provided status restrictions
if ([] !== $travelData->allowedBookingStatus
&& false === $travelData->isBookingStatusAllowed(Constants::BOOKING_STATUS_FREE)) {
return 'A';
}
// Check 2: Only inquiry rooms available (all rooms with available > 0 have status 'Anfrage')
if ($travelData->requiresInquiryBooking()) {
return 'A';
}
return 'F';
}
/**
* Updates booking status based on selected room requirements.
*
* Checks if any selected room has inquiry-only status. If so, forces
* the entire booking to inquiry mode. This override happens after room
* selection in Step 1 and respects the business rule: if ANY room requires
* inquiry, the whole booking becomes an inquiry.
*
* @param BookingDto $bookingDto The booking DTO to update
*/
public function updateBookingStatusFromRoomSelection(BookingDto $bookingDto): void
{
// Skip if already inquiry - no need to check
if ('A' === $bookingDto->bookingStatus) {
return;
}
// Check selected rooms for inquiry-only status
foreach ($bookingDto->getSelectedRooms() as $roomSelection) {
$room = $bookingDto->travel->getRoomById($roomSelection->roomId);
if ($room
&& Constants::STATUS_ON_REQUEST === $room->status
&& $room->available > 0) {
$bookingDto->bookingStatus = 'A';
return;
}
}
}
}