loadBookingCreateDto($this->bookingSessionService, $request); } catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $exception) { return $this->createBookingCreateFailureResponse($exception, false); } // Validate step access if (null !== $redirect = $this->validateStepAccess($bookingCreateDto, 4)) { return $redirect; } $user = $this->getUser(); $newsletterTargetEmail = $this->bookingNewsletterOptIn->resolveApplicantEmail($bookingCreateDto, $user instanceof User ? $user : null); $newsletterOptInVisible = null !== $newsletterTargetEmail; $companions = $this->bookingNewsletterOptIn->resolveCompanions($bookingCreateDto, $newsletterTargetEmail); $companionNewsletterOptInVisible = [] !== $companions; $form = $this->createForm(BookingCreateStep4Type::class, $bookingCreateDto, [ 'show_newsletter_opt_in' => $newsletterOptInVisible, 'show_companion_newsletter_opt_in' => $companionNewsletterOptInVisible, 'newsletter_target_email' => $newsletterTargetEmail, ]); $form->handleRequest($request); if (true === $form->isSubmitted() && true === $form->isValid()) { try { $this->bookingService->applyCreateBookingStatusRules($bookingCreateDto); // Submit final booking (already validated in Step 3) $bookingResponse = $this->apiClient->createBooking($bookingCreateDto); // Surface any insurance corrections made during payload building (e.g. a // stale price tier reassigned/cleared right before submission) foreach ($this->participantFormSupportService->collectAndClearNotifications($bookingCreateDto) as $notification) { $this->addFlash($notification['type'], $notification['message']); } if ($bookingResponse instanceof Notification) { $this->handleApiError( $this->logger, 'Booking creation failed - API notification', ['message' => $bookingResponse->message], $bookingResponse->message ?? 'Ein Fehler ist aufgetreten. Bitte versuchen es erneut.', ); return $this->renderStepForm($bookingCreateDto, $form, $newsletterOptInVisible, $newsletterTargetEmail, $companionNewsletterOptInVisible); } if (false === $bookingResponse->isBookingSuccessful()) { $errorMessage = 'Buchung konnte nicht erstellt werden.'; if (null !== $bookingResponse->message && '' !== trim($bookingResponse->message)) { $errorMessage .= ' '.$bookingResponse->message; } $this->handleApiError( $this->logger, 'Booking creation unsuccessful', ['status' => $bookingResponse->status, 'message' => $bookingResponse->message], $errorMessage, ); return $this->renderStepForm($bookingCreateDto, $form, $newsletterOptInVisible, $newsletterTargetEmail, $companionNewsletterOptInVisible); } $newsletterOptInSelected = $newsletterOptInVisible && $form->has('newsletterOptIn') && true === $form->get('newsletterOptIn')->getData(); if (true === $newsletterOptInSelected) { $this->bookingNewsletterOptIn->subscribeApplicant( $newsletterTargetEmail, $this->bookingNewsletterOptIn->resolveApplicantParticipant($bookingCreateDto), ); } $companionOptInSelected = $companionNewsletterOptInVisible && $form->has('companionNewsletterOptIn') && true === $form->get('companionNewsletterOptIn')->getData(); if (true === $companionOptInSelected) { $this->bookingNewsletterOptIn->subscribeCompanions( $companions, $this->bookingNewsletterOptIn->resolveRequestedByName($bookingCreateDto), ); } // Success: Store booking data in flash for conversion tracking $bookingCreateContext = $this->createContextFactory->createWithParticipantPrices($bookingCreateDto); $this->addFlash('booking_number', $bookingResponse->bookingNumber); $this->addFlash('booking_total', $bookingCreateContext->summaryData->vouchers->payableAmount); $this->addFlash('booking_travel_name', $bookingCreateDto->travel->label); $this->clearTravelDataCache($bookingCreateDto); $this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_CREATE); $this->logger->info('Booking successfully created.', [ 'date_id' => $bookingCreateDto->travel->id, 'hotel_id' => $bookingCreateDto->hotelId, 'booking_number' => $bookingResponse->bookingNumber, ]); return $this->redirectToRoute('app_booking_create_success'); } catch (TimeoutException $e) { $this->handleApiError( $this->logger, 'Booking creation timeout', [ 'exception' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ], 'Die Anfrage hat zu lange gedauert. Bitte versuche es erneut.', ); } catch (\Exception $e) { $this->handleApiError( $this->logger, 'Booking creation exception', [ 'exception' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ], 'Ein technischer Fehler ist aufgetreten.', ); } } return $this->renderStepForm($bookingCreateDto, $form, $newsletterOptInVisible, $newsletterTargetEmail, $companionNewsletterOptInVisible); } /** * Renders the step 4 form with standard template variables. */ /** * @param FormInterface $form */ private function renderStepForm( BookingDto $bookingCreateDto, FormInterface $form, bool $newsletterOptInVisible, ?string $newsletterTargetEmail, bool $companionNewsletterOptInVisible, ): Response { $bookingCreateContext = $this->createContextFactory->createWithParticipantPrices($bookingCreateDto); return $this->render('booking/create/step_4.html.twig', [ 'bookingCreateContext' => $bookingCreateContext, 'form' => $form, 'newsletterOptInVisible' => $newsletterOptInVisible, 'newsletterTargetEmail' => $newsletterTargetEmail, 'companionNewsletterOptInVisible' => $companionNewsletterOptInVisible, ]); } /** * Clears travel data and availability cache after successful booking. */ private function clearTravelDataCache(BookingDto $bookingDto): void { $dateId = $bookingDto->travel->id; $hotelId = $bookingDto->hotelId; // Clear availability cache $this->cache->delete(sprintf('availability_%d', $dateId)); // 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)); } }