getOrCreateBookingCreateDto($this->bookingService, $request); if ($result instanceof Response) { return $result; } $bookingCreateDto = $result; // Validate step access if ($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 { // Validate booking data with API by submitting an inquiry booking $inquiryResponse = $this->apiClient->createBookingInquiry($bookingCreateDto); if ($inquiryResponse instanceof Notification) { return $this->handleApiError( 'Booking inquiry failed', ['message' => $inquiryResponse->message], $inquiryResponse->message ?? 'Ein Fehler ist aufgetreten. Bitte versuche es erneut oder wende dich an den Kundenservice.', $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->bookingService->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; } return $this->handleApiError( 'Booking inquiry validation failed', ['status' => $inquiryResponse->status, 'message' => $inquiryResponse->message], $errorMessage, $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)) { return $this->handleApiError( 'Price mismatch detected - payload incomplete', [ 'apiTotal' => $apiTotal, 'calculatedSubtotal' => $calculatedSubtotal, 'promoGoodwillDiscount' => $promoGoodwillDiscount, 'apiAppliedDiscount' => $apiAppliedDiscount, 'expectedTotal' => $expectedTotal, 'difference' => abs($apiTotal - $expectedTotal), ], 'Preisabweichung festgestellt. Bitte wende dich an den Kundenservice.', $bookingCreateDto, $form ); } // Validation successful - proceed to confirmation step $bookingCreateDto->currentStep = 4; $this->bookingService->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) { return $this->handleApiError( '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.', $bookingCreateDto, $form ); } catch (\Exception $e) { return $this->handleApiError( 'Booking inquiry exception', [ 'exception' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ], 'Ein allgemeiner Fehler ist aufgetreten. Bitte wende dich an den Kundenservice.', $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 { $result = $this->getOrCreateBookingCreateDto($this->bookingService, $request); if ($result instanceof Response) { return $result; } $bookingCreateDto = $result; $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. */ private function renderStepForm(BookingDto $bookingCreateDto, FormInterface $form): Response { // Get complete summary data (pricing, rooms, CMS data) $summaryData = $this->summaryDataService->getSummaryData($bookingCreateDto); return $this->render('booking/create/step_3.html.twig', [ 'bookingCreateDto' => $bookingCreateDto, 'form' => $form->createView(), 'summaryData' => $summaryData, ]); } /** * 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 ('nicht möglich' !== $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); } }