feat: voucher purchase flow
This commit is contained in:
1 parent
a628e96d06
commit
f3dd122678
70 files changed
+3672
-122
No files matched your search
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Voucher;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
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 ApiClient $apiClient,
|
||||
private readonly ValidatorInterface $validator,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
parent::__construct($sessionManager, $thumbnailResolver);
|
||||
}
|
||||
|
||||
#[Route('/gutschein/auswahl', 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('/gutschein/auswahl/aktualisieren', 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('/gutschein/auswahl/vorschau', 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('/gutschein/vorschau.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->apiClient->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));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user