220 lines
8.0 KiB
PHP
220 lines
8.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Controller\Voucher;
|
|
|
|
use App\BusProNet\Client\VoucherClient;
|
|
use App\BusProNet\Exception\ApiClientException;
|
|
use App\BusProNet\Model\Notification;
|
|
use App\Exception\VoucherSessionNotFoundException;
|
|
use App\Exception\VoucherShopUnavailableException;
|
|
use App\Form\Model\VoucherPurchaseDto;
|
|
use App\Form\VoucherPurchaseStep1Type;
|
|
use App\Service\VoucherPurchaseSessionManager;
|
|
use App\Service\VoucherTemplateProvider;
|
|
use App\Service\VoucherThumbnailResolver;
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Component\Form\FormInterface;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
|
|
|
/**
|
|
* Step 1: design, von/für and value, with a PDF preview rendered by BusPro.
|
|
*/
|
|
class Step1Controller extends AbstractVoucherPurchaseController
|
|
{
|
|
public function __construct(
|
|
VoucherPurchaseSessionManager $sessionManager,
|
|
VoucherThumbnailResolver $thumbnailResolver,
|
|
private readonly VoucherTemplateProvider $templateProvider,
|
|
private readonly VoucherClient $voucherClient,
|
|
private readonly ValidatorInterface $validator,
|
|
private readonly LoggerInterface $logger,
|
|
) {
|
|
parent::__construct($sessionManager, $thumbnailResolver);
|
|
}
|
|
|
|
#[Route('/vouchers/create/design', name: 'app_voucher_step_1')]
|
|
public function index(Request $request): Response
|
|
{
|
|
$dto = $this->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<VoucherPurchaseDto>
|
|
*
|
|
* @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<VoucherPurchaseDto> $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));
|
|
}
|
|
}
|