sessionManager->find($request); if (null === $dto) { return $this->createSessionExpiredResponse($request); } try { $form = $this->createStepForm($dto); } catch (VoucherShopUnavailableException) { return $this->createShopUnavailableResponse($request); } $form->handleRequest($request); if (true === $form->isSubmitted() && true === $form->isValid()) { $dto->currentStep = 2; $this->sessionManager->save($request, $dto); return $this->redirectToRoute('app_voucher_step_2'); } return $this->renderStep($dto, $form); } /** * Takes over the current input without validating it and refreshes the summary. */ #[Route('/vouchers/create/refresh', name: 'app_voucher_step_1_refresh', methods: ['POST'])] public function refresh(Request $request): Response { $dto = $this->sessionManager->find($request); if (null === $dto) { return $this->createSessionExpiredResponse($request); } try { $form = $this->createStepForm($dto, false); } catch (VoucherShopUnavailableException) { return $this->createShopUnavailableResponse($request); } $form->handleRequest($request); $this->sessionManager->save($request, $dto); return $this->htmxOobResponse('voucher/step_1.html.twig', ['voucher_summary'], [ ...$this->createStepContext($dto), 'form' => $form->createView(), ]); } /** * Takes over the current input like refresh() and opens the preview modal, whose frame loads * the PDF from previewPdf(). */ #[Route('/vouchers/create/preview', name: 'app_voucher_preview', methods: ['POST'])] public function preview(Request $request): Response { $dto = $this->sessionManager->find($request); if (null === $dto) { return $this->createSessionExpiredResponse($request); } try { $form = $this->createStepForm($dto, false); } catch (VoucherShopUnavailableException) { return $this->createShopUnavailableResponse($request); } $form->handleRequest($request); $this->sessionManager->save($request, $dto); return $this->render('voucher/modal_preview.html.twig', [ 'previewable' => $this->isPreviewable($dto), // busts the browser cache of the frame, the URL is otherwise the same for every input 'previewVersion' => bin2hex(random_bytes(4)), ]); } #[Route('/vouchers/create/preview.pdf', name: 'app_voucher_preview_pdf', methods: ['GET'])] public function previewPdf(Request $request): Response { try { $dto = $this->sessionManager->get($request); } catch (VoucherSessionNotFoundException) { return $this->createPreviewErrorResponse('Deine Sitzung ist abgelaufen. Bitte starte den Gutscheinkauf neu.'); } if (false === $this->isPreviewable($dto)) { return $this->createPreviewErrorResponse('Für die Vorschau bitte zuerst Motiv, Von, Für und Gutscheinwert angeben.'); } // Read-only from here on: release the session lock, so the page's other requests do not // wait the seconds BusPro takes to render the PDF. $request->getSession()->save(); try { $preview = $this->voucherClient->getVoucherPreview($dto); } catch (ApiClientException $e) { $this->logger->error('Voucher preview failed', ['error' => $e->getMessage()]); return $this->createPreviewErrorResponse('Die Vorschau ist gerade nicht verfügbar. Bitte versuche es später noch einmal.'); } if ($preview instanceof Notification) { $this->logger->warning('Voucher preview rejected', ['code' => $preview->code, 'message' => $preview->message]); return $this->createPreviewErrorResponse($preview->message ?? 'Die Vorschau ist gerade nicht verfügbar.'); } $response = new Response($preview->pdf, Response::HTTP_OK, ['Content-Type' => 'application/pdf']); $response->headers->set('Content-Disposition', $response->headers->makeDisposition( ResponseHeaderBag::DISPOSITION_INLINE, 'gutschein-vorschau.pdf' )); $response->setPrivate(); $response->headers->addCacheControlDirective('no-store'); return $response; } /** * @return FormInterface * * @throws VoucherShopUnavailableException */ private function createStepForm(VoucherPurchaseDto $dto, bool $validate = true): FormInterface { $templates = $this->templateProvider->getTemplates(); $options = [ 'templates' => $templates->templates, 'min_amount' => $dto->minAmount, 'max_amount' => $dto->maxAmount, ]; if (false === $validate) { $options['validation_groups'] = false; } return $this->createForm(VoucherPurchaseStep1Type::class, $dto, $options); } /** * @param FormInterface $form */ private function renderStep(VoucherPurchaseDto $dto, FormInterface $form): Response { $templates = $this->templateProvider->getTemplates(); return $this->render('voucher/step_1.html.twig', [ ...$this->createStepContext($dto), 'form' => $form, 'thumbnails' => $this->thumbnailResolver->resolveAll($templates->templates), 'previewable' => $this->isPreviewable($dto), ]); } /** * The preview takes what step 1 accepts: BusPro refuses it without the names (604/605), and * this keeps the button and the route to one rule, the step's own validation group. */ private function isPreviewable(VoucherPurchaseDto $dto): bool { return 0 === \count($this->validator->validate($dto, null, ['voucher_step_1'])); } private function createShopUnavailableResponse(Request $request): Response { $this->addFlash('error', 'Gutscheine können gerade nicht gekauft werden. Bitte versuche es später noch einmal.'); return $this->htmxRedirect($request, $this->generateUrl('app_voucher_error')); } private function createPreviewErrorResponse(string $message): Response { return $this->render('voucher/preview_error.html.twig', ['message' => $message], new Response('', Response::HTTP_UNPROCESSABLE_ENTITY)); } }