loadBookingCreateDto($this->bookingSessionService, $request); } catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $exception) { return $this->createBookingCreateFailureResponse($exception, false); } // Validate step access if (null !== $redirect = $this->validateStepAccess($bookingCreateDto, 3)) { return $redirect; } $form = $this->createForm(BookingCreateStep3Type::class, $bookingCreateDto); $form->handleRequest($request); if (true === $form->isSubmitted() && true === $form->isValid()) { // Notify user if goodwill vouchers cannot be redeemed for inquiry bookings if ($bookingCreateDto->isInquiryBooking() && $bookingCreateDto->hasGoodwillVouchers()) { $this->addFlash('warning', 'Hinweis: Bei Anfragebuchungen können keine Kulanz-Gutscheine eingelöst werden. Kauf- und Aktionsgutscheine werden berücksichtigt.'); } try { $this->bookingService->applyCreateBookingStatusRules($bookingCreateDto); // Validate booking data with API by submitting an inquiry booking $inquiryResponse = $this->apiClient->createBookingInquiry($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 ($inquiryResponse instanceof Notification) { $this->handleApiError( $this->logger, 'Booking inquiry failed', ['message' => $inquiryResponse->message], $inquiryResponse->message ?? 'Ein Fehler ist aufgetreten. Bitte versuche es erneut oder wende dich an den Kundenservice.', ); return $this->renderStepForm($bookingCreateDto, $form); } if (false === $inquiryResponse->isInquiryValid()) { // Check if API suggests inquiry booking instead of showing error if ($this->shouldFallbackToInquiryMode($inquiryResponse)) { // Auto-switch to inquiry mode $bookingCreateDto->bookingStatus = 'A'; $bookingCreateDto->currentStep = 4; $this->bookingSessionService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE); $message = 'Buchung konnte nicht validiert werden.'; if (null !== $inquiryResponse->message && '' !== trim($inquiryResponse->message)) { $message = $inquiryResponse->message; } $this->addFlash('info', $message); return $this->redirectToRoute('app_booking_create_step_4'); } // Not a fallback scenario - show validation error $errorMessage = 'Buchung konnte nicht validiert werden.'; if (null !== $inquiryResponse->message && '' !== trim($inquiryResponse->message)) { $errorMessage .= ' '.$inquiryResponse->message; } $this->handleApiError( $this->logger, 'Booking inquiry validation failed', ['status' => $inquiryResponse->status, 'message' => $inquiryResponse->message], $errorMessage, ); return $this->renderStepForm($bookingCreateDto, $form); } // Validate price match (rounded to cent precision to avoid floating-point errors) // API gesamtpreis includes: // - Promotional/goodwill voucher discounts (negative price items with art=AKTION/KULANZGUTSCHEIN) // - API-applied automatic discounts (e.g., Gruppenrabatt with art=ERM) // - NOT purchase vouchers (those reduce restzahlung, not gesamtpreis) $apiTotal = round($inquiryResponse->totalPrice ?? 0.0, 2); $calculatedSubtotal = round($this->priceCalculator->calculateGrandTotal($bookingCreateDto), 2); $promoGoodwillDiscount = round($inquiryResponse->getVoucherDiscountFromPrices(), 2); $apiAppliedDiscount = round($inquiryResponse->getApiAppliedDiscountTotal(), 2); $expectedTotal = round($calculatedSubtotal - $promoGoodwillDiscount - $apiAppliedDiscount, 2); if ((int) round($apiTotal * 100) !== (int) round($expectedTotal * 100)) { $diagnostics = $this->priceMismatchDiagnostics->buildDiagnostics($bookingCreateDto, $inquiryResponse); $deltaBreakdown = $diagnostics['deltaBreakdown'] ?? []; $this->handleApiError( $this->logger, 'Price mismatch detected - payload incomplete', [ 'apiTotal' => $apiTotal, 'calculatedSubtotal' => $calculatedSubtotal, 'promoGoodwillDiscount' => $promoGoodwillDiscount, 'apiAppliedDiscount' => $apiAppliedDiscount, 'expectedTotal' => $expectedTotal, 'difference' => abs($apiTotal - $expectedTotal), 'deltaTotal' => $deltaBreakdown['deltaTotal'] ?? null, 'deltaRoom' => $deltaBreakdown['deltaRoom'] ?? null, 'deltaService' => $deltaBreakdown['deltaService'] ?? null, 'deltaInsurance' => $deltaBreakdown['deltaInsurance'] ?? null, 'diagnostics' => $diagnostics, ], 'Preisabweichung festgestellt. Bitte wende dich an den Kundenservice.', ); return $this->renderStepForm($bookingCreateDto, $form); } // Validation successful - proceed to confirmation step $bookingCreateDto->currentStep = 4; $this->bookingSessionService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE); if ($inquiryResponse->message) { $this->addFlash('info', $inquiryResponse->message); } return $this->redirectToRoute('app_booking_create_step_4'); } catch (TimeoutException $e) { $this->handleApiError( $this->logger, 'Booking inquiry timeout', [ 'exception' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ], 'Die Anfrage hat zu lange gedauert. Bitte versuche es erneut oder wende dich an den Kundenservice.', ); } catch (\Exception $e) { $this->handleApiError( $this->logger, 'Booking inquiry exception', [ 'exception' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ], 'Ein allgemeiner Fehler ist aufgetreten. Bitte wende dich an den Kundenservice.', ); } return $this->renderStepForm($bookingCreateDto, $form); } return $this->renderStepForm($bookingCreateDto, $form); } /** * Handles HTMX refresh when payment method changes. */ #[Route('/bookings/create/payment/refresh', name: 'app_booking_create_step_3_refresh', methods: ['POST'])] public function refresh(Request $request): Response { try { $bookingCreateDto = $this->loadBookingCreateDto($this->bookingSessionService, $request); } catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $exception) { return $this->createBookingCreateFailureResponse($exception, true); } $form = $this->createForm(BookingCreateStep3Type::class, $bookingCreateDto, [ 'validation_groups' => false, ]); $form->handleRequest($request); return $this->renderStepForm($bookingCreateDto, $form); } /** * Renders the step 3 form with standard template variables. */ /** @param FormInterface $form */ private function renderStepForm(BookingDto $bookingCreateDto, FormInterface $form): Response { $context = $this->createContextFactory->create( $bookingCreateDto, RoomPricingCalculator::PRICING_MODE_ASSIGNMENT ); return $this->render('booking/create/step_3.html.twig', [ 'bookingCreateContext' => $context, 'form' => $form, ]); } /** * Determines if validation failure should trigger inquiry mode fallback. * * Checks if the API response indicates that the booking should proceed as * an inquiry rather than showing an error. This handles scenarios where * availability changes between booking initialization and validation. * * @param BookingResponse $response The API response * * @return bool True if should fallback to inquiry mode, false if should show error */ private function shouldFallbackToInquiryMode(BookingResponse $response): bool { // Check for "nicht möglich" status + inquiry suggestion in message if (BookingResponse::BOOKING_IMPOSSIBLE !== $response->status) { return false; } $message = $response->message ?? ''; if ('' === $message) { return false; } // Check if message suggests inquiry booking (case-insensitive) return 1 === preg_match('/anfrage/i', $message); } }