fix: refresh live room availability during create flow

This commit is contained in:
Björn Fromme committed 2026-07-14 11:25:23 +02:00
1 parent a73e8a593f
commit cdce7d57e0
26 files changed
+1976 -54

No files matched your search

+6 -1
View File
@@ -4,6 +4,7 @@ namespace App\Controller\Api;
use App\BusProNet\ApiClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use App\BusProNet\Model\Travel;
use App\BusProNet\Utility\DateCodeUtility;
use App\Service\TravelDataProvider;
@@ -91,7 +92,11 @@ class TravelController extends AbstractController
try {
$result = $this->apiClient->getHotelAvailability($dateId, $hotelId, $dateTo);
return $this->json($result);
if ($result instanceof Notification) {
return $this->json(['error' => $result->message, 'type' => 'api_error']);
}
return $this->json($result->getItems(), Response::HTTP_OK, [], ['groups' => ['api:list', 'api:single']]);
} catch (ApiClientException $e) {
return $this->json(['error' => $e->getMessage(), 'type' => 'api_error']);
}
@@ -8,6 +8,8 @@ use App\Exception\BookingSessionNotFoundException;
use App\Exception\HotelNotInTravelException;
use App\Exception\TravelNotFoundException;
use App\Form\Model\BookingDto;
use App\Htmx\HxTrait;
use App\Service\BookingAvailabilityRefreshResult;
use App\Service\BookingSessionManager;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -20,6 +22,8 @@ use Symfony\Component\HttpFoundation\Response;
*/
abstract class AbstractBookingCreateController extends AbstractController
{
use HxTrait;
protected function loadBookingCreateDto(BookingSessionManager $bookingSessionService, Request $request): BookingDto
{
return $bookingSessionService->getOrCreateBookingCreateDto($request);
@@ -91,4 +95,53 @@ abstract class AbstractBookingCreateController extends AbstractController
return $this->redirectToRoute($route);
}
protected function redirectIfRoomSelectionsStale(
Request $request,
BookingDto $bookingDto,
BookingSessionManager $bookingSessionService,
bool $forceRefresh = false,
?\Closure $onRefreshFailure = null,
): ?Response {
$result = $bookingSessionService->refreshCreateAvailability($request, $bookingDto, $forceRefresh);
if (true === $forceRefresh && false === $result->availabilityRefreshed) {
if (null !== $onRefreshFailure) {
return $onRefreshFailure();
}
$this->addFlash('error', 'Die Verfügbarkeit konnte gerade nicht aktualisiert werden. Bitte versuche es erneut.');
return $this->htmxRedirect($request, $this->generateUrl('app_booking_create_error'));
}
if ($redirect = $this->redirectIfNoRoomsAvailable($request, $result)) {
return $redirect;
}
if (false === $result->roomSelectionQuantityReset) {
if (true === $forceRefresh && true === $result->roomSelectionSnapshotChanged) {
$this->addFlash('warning', 'Die Zimmerpreise haben sich aktualisiert. Bitte überprüfe deine Buchung erneut.');
return $this->htmxRedirect($request, $this->generateUrl('app_booking_create_step_4'));
}
return null;
}
$this->addFlash('error', 'Mindestens eine Zimmerauswahl ist nicht mehr verfügbar. Bitte wähle die Zimmer erneut.');
return $this->htmxRedirect($request, $this->generateUrl('app_booking_create_step_1'));
}
protected function redirectIfNoRoomsAvailable(Request $request, BookingAvailabilityRefreshResult $result): ?Response
{
if (true === $result->hasAvailableRooms) {
return null;
}
$this->addFlash('error', 'Leider sind für diese Reise aktuell keine Zimmer verfügbar.');
return $this->htmxRedirect($request, $this->generateUrl('app_booking_create_error'));
}
}
@@ -55,6 +55,19 @@ class Step1Controller extends AbstractBookingCreateController
return $redirect;
}
$availabilityRefresh = $this->bookingSessionService->refreshCreateAvailability($request, $bookingCreateDto);
if ($availabilityRefresh->roomSelectionSnapshotChanged) {
$oldRoomSelectionSnapshot = $this->bookingSessionService->saveBaselineSnapshot($request, $bookingCreateDto);
}
if ($availabilityRefresh->roomSelectionQuantityReset) {
$this->addFlash('warning', 'Mindestens eine Zimmerauswahl ist nicht mehr verfügbar und wurde zurückgesetzt.');
}
if ($redirect = $this->redirectIfNoRoomsAvailable($request, $availabilityRefresh)) {
return $redirect;
}
$form = $this->createForm(BookingCreateStep1Type::class, $bookingCreateDto, [
'validation_groups' => ['booking_create_step_1'],
]);
@@ -18,7 +18,6 @@ use App\Service\BookingCreateContextFactory;
use App\Service\BookingSessionManager;
use App\Service\ParticipantDataPrefiller;
use App\Service\RoomAssigner;
use App\Service\TravelDataProvider;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@@ -35,7 +34,6 @@ class Step2Controller extends AbstractBookingCreateController
private readonly BookingConfigurator $bookingService,
private readonly BookingSessionManager $bookingSessionService,
private readonly BookingCreateContextFactory $createContextFactory,
private readonly TravelDataProvider $travelDataService,
private readonly RoomAssigner $roomAssignmentService,
private readonly ParticipantDataPrefiller $prepopulationService,
) {
@@ -58,8 +56,11 @@ class Step2Controller extends AbstractBookingCreateController
return $redirect;
}
// Enrich with fresh availability data
$this->travelDataService->enrichWithFreshAvailabilities($bookingCreateDto->travel);
if ($redirect = $this->redirectIfRoomSelectionsStale($request, $bookingCreateDto, $this->bookingSessionService)) {
return $redirect;
}
$this->bookingService->updateBookingStatusFromRoomSelection($bookingCreateDto);
// Ensure correct number of participants first, then prepopulate the applicant if needed.
$this->ensureCorrectNumberOfParticipants($bookingCreateDto);
@@ -13,8 +13,6 @@ use App\Service\BookingSessionManager;
use App\Service\ParticipantDataPrefiller;
use App\Service\ParticipantFormSupport;
use App\Service\RoomPricingCalculator;
use App\Service\TravelDataProvider;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -23,7 +21,7 @@ use Symfony\Component\Routing\Attribute\Route;
/**
* Handles participant editing and refresh actions within booking step 2.
*/
class Step2ParticipantController extends AbstractController
class Step2ParticipantController extends AbstractBookingCreateController
{
use HxTrait;
@@ -31,7 +29,6 @@ class Step2ParticipantController extends AbstractController
private readonly BookingConfigurator $bookingService,
private readonly BookingSessionManager $bookingSessionService,
private readonly BookingCreateContextFactory $createContextFactory,
private readonly TravelDataProvider $travelDataService,
private readonly ParticipantDataPrefiller $prepopulationService,
private readonly ParticipantFormSupport $participantFormSupportService,
) {
@@ -51,48 +48,53 @@ class Step2ParticipantController extends AbstractController
if ($result instanceof Response) {
return $result;
}
$bookingDto = $result;
$bookingCreateDto = $result;
$this->participantFormSupportService->ensureParticipantExists($bookingDto, $index);
$this->participantFormSupportService->ensureParticipantExists($bookingCreateDto, $index);
$this->travelDataService->enrichWithFreshAvailabilities($bookingDto->travel);
if ($redirect = $this->redirectIfRoomSelectionsStale($request, $bookingCreateDto, $this->bookingSessionService)) {
return $redirect;
}
$form = $this->createParticipantForm($bookingDto, $index);
$this->bookingService->updateBookingStatusFromRoomSelection($bookingCreateDto);
$this->bookingSessionService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
$form = $this->createParticipantForm($bookingCreateDto, $index);
$form->handleRequest($request);
$isSubmitted = $form->isSubmitted();
$isDummyDataFill = $this
->prepopulationService
->isDummyDataFillRequested($bookingDto->participants[$index], $bookingDto->getMode())
->isDummyDataFillRequested($bookingCreateDto->participants[$index], $bookingCreateDto->getMode())
;
if (true === $isSubmitted && true === $isDummyDataFill) {
$this->prepopulationService->fillDummyParticipant($bookingDto->participants[$index], $index);
$this->prepopulationService->fillDummyParticipant($bookingCreateDto->participants[$index], $index);
$this->bookingService->preselectDefaultServices($bookingDto);
$this->bookingService->applyCreateBookingStatusRules($bookingDto);
$this->bookingSessionService->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE);
$this->bookingService->preselectDefaultServices($bookingCreateDto);
$this->bookingService->applyCreateBookingStatusRules($bookingCreateDto);
$this->bookingSessionService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
$form = $this->createParticipantForm($bookingDto, $index);
$form = $this->createParticipantForm($bookingCreateDto, $index);
return $this->renderParticipantForm($form, $index, $bookingDto);
return $this->renderParticipantForm($form, $index, $bookingCreateDto);
}
if (true === $isSubmitted) {
$this->bookingService->preselectDefaultServices($bookingDto);
$this->bookingService->applyCreateBookingStatusRules($bookingDto);
$this->bookingService->preselectDefaultServices($bookingCreateDto);
$this->bookingService->applyCreateBookingStatusRules($bookingCreateDto);
}
$notifications = $this->participantFormSupportService->collectAndClearNotifications($bookingDto);
$notifications = $this->participantFormSupportService->collectAndClearNotifications($bookingCreateDto);
if (true === $isSubmitted && true === $form->isValid()) {
$this->bookingSessionService->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE);
$this->bookingSessionService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
$this->addNotificationsAsFlashMessages($notifications);
return $this->redirectToRoute('app_booking_create_step_2');
}
return $this->renderParticipantForm($form, $index, $bookingDto);
return $this->renderParticipantForm($form, $index, $bookingCreateDto);
}
/**
@@ -114,7 +116,11 @@ class Step2ParticipantController extends AbstractController
$this->participantFormSupportService->ensureParticipantExists($bookingDto, $index);
$this->travelDataService->enrichWithFreshAvailabilities($bookingDto->travel);
if ($redirect = $this->redirectIfRoomSelectionsStale($request, $bookingDto, $this->bookingSessionService)) {
return $redirect;
}
$this->bookingService->updateBookingStatusFromRoomSelection($bookingDto);
return $this->handleParticipantRefresh(
$request,
@@ -61,6 +61,10 @@ class Step3Controller extends AbstractBookingCreateController
return $redirect;
}
if ($redirect = $this->redirectIfRoomSelectionsStale($request, $bookingCreateDto, $this->bookingSessionService)) {
return $redirect;
}
$form = $this->createForm(BookingCreateStep3Type::class, $bookingCreateDto);
$form->handleRequest($request);
@@ -62,6 +62,10 @@ class Step4Controller extends AbstractBookingCreateController
return $redirect;
}
if ($redirect = $this->redirectIfRoomSelectionsStale($request, $bookingCreateDto, $this->bookingSessionService)) {
return $redirect;
}
$newsletterTargetEmail = $this->resolveNewsletterTargetEmail($bookingCreateDto);
$newsletterOptInVisible = null !== $newsletterTargetEmail;
@@ -72,6 +76,20 @@ class Step4Controller extends AbstractBookingCreateController
$form->handleRequest($request);
if (true === $form->isSubmitted() && true === $form->isValid()) {
if ($redirect = $this->redirectIfRoomSelectionsStale(
$request,
$bookingCreateDto,
$this->bookingSessionService,
forceRefresh: true,
onRefreshFailure: function () use ($bookingCreateDto, $form, $newsletterOptInVisible, $newsletterTargetEmail): Response {
$this->addFlash('error', 'Die Verfügbarkeit konnte gerade nicht aktualisiert werden. Bitte versuche es erneut.');
return $this->renderStepForm($bookingCreateDto, $form, $newsletterOptInVisible, $newsletterTargetEmail);
},
)) {
return $redirect;
}
try {
$this->bookingService->applyCreateBookingStatusRules($bookingCreateDto);
@@ -238,5 +256,14 @@ class Step4Controller extends AbstractBookingCreateController
// Clear travel data cache (both local and remote variants)
$this->cache->delete(sprintf('travel_unified_%d_%d_local', $dateId, $hotelId));
$this->cache->delete(sprintf('travel_unified_%d_%d_remote', $dateId, $hotelId));
if (null !== $bookingDto->travel->dateTo) {
$this->cache->delete(sprintf(
'hotel_availability_%d_%d_%s',
$dateId,
$hotelId,
$bookingDto->travel->dateTo->format('Ymd')
));
}
}
}