sessionManager->find($request); if (null === $dto) { // also the second of two submits: the first one cleared the session before redirecting return $this->createSessionExpiredResponse($request); } if (null !== $redirect = $this->validateStepAccess($dto, 3)) { return $redirect; } $form = $this->createForm(VoucherPurchaseStep3Type::class, $dto, [ 'amount' => $dto->amount ?? 0, ]); if (false === $request->isMethod(Request::METHOD_POST)) { $dto->termsAccepted = false; $dto->purchaseConfirmed = false; return $this->runDryRun($request, $dto, $form); } $form->handleRequest($request); if (true === $form->isSubmitted() && true === $form->isValid()) { return $this->purchase($request, $dto, $form); } return $this->renderStep($dto, $form, true); } /** * @param FormInterface $form */ private function runDryRun(Request $request, VoucherPurchaseDto $dto, FormInterface $form): Response { try { $response = $this->voucherClient->createVoucherSaleInquiry($dto); } catch (\Exception $e) { $this->logger->error('Voucher sale inquiry failed', ['exception' => $e->getMessage()]); $this->addFlash('error', 'Der Gutscheinkauf konnte gerade nicht geprüft werden. Bitte versuche es später noch einmal.'); return $this->renderStep($dto, $form, false); } if ($response instanceof Notification) { return $this->handleRejection($request, $dto, $form, $response, 'Voucher sale inquiry rejected'); } if (false === $response->isInquiryValid()) { $this->logger->error('Voucher sale inquiry unsuccessful', ['status' => $response->status]); $this->addFlash('error', 'Der Gutschein kann gerade nicht gekauft werden. Bitte versuche es später noch einmal.'); return $this->renderStep($dto, $form, false); } return $this->renderStep($dto, $form, true); } /** * @param FormInterface $form */ private function purchase(Request $request, VoucherPurchaseDto $dto, FormInterface $form): Response { try { $response = $this->voucherClient->createVoucherSale($dto); } catch (TimeoutException $e) { // The purchase may have gone through on BusPro's side, so a blind retry could buy twice $this->logger->error('Voucher sale timeout', ['exception' => $e->getMessage()]); $this->addFlash('error', 'Die Anfrage hat zu lange gedauert. Möglicherweise wurde der Gutschein trotzdem gekauft. Bitte wende dich an den Kundenservice, bevor du es erneut versuchst.'); return $this->renderStep($dto, $form, false); } catch (\Exception $e) { $this->logger->error('Voucher sale exception', ['exception' => $e->getMessage()]); $this->addFlash('error', 'Ein technischer Fehler ist aufgetreten. Bitte versuche es später noch einmal.'); return $this->renderStep($dto, $form, true); } if ($response instanceof Notification) { return $this->handleRejection($request, $dto, $form, $response, 'Voucher sale rejected'); } if (false === $response->isSaleSuccessful()) { $this->logger->error('Voucher sale unsuccessful', ['status' => $response->status]); $this->addFlash('error', 'Der Gutschein konnte nicht gekauft werden. Bitte versuche es später noch einmal.'); return $this->renderStep($dto, $form, false); } // Cleared before the redirect, so a second submit finds no purchase and cannot buy twice $this->sessionManager->clear($request); // The redemption code stays out of the log and the page: the voucher is only paid for once // the transfer arrives, and BusPro mails it then. $this->logger->info('Voucher successfully sold.', [ 'voucher_id' => $response->voucherId, 'voucher_number' => $response->voucherNumber, 'invoice_number' => $response->invoiceNumber, 'template_id' => $dto->template?->id, 'amount' => $dto->amount, ]); $this->addFlash('voucher_number', $response->voucherNumber); $this->addFlash('voucher_invoice_number', $response->invoiceNumber); $this->addFlash('voucher_total', $response->price ?? (float) $dto->amount); $this->addFlash('voucher_label', $dto->template?->label); $this->addFlash('voucher_email', $response->email ?? $dto->email); return $this->redirectToRoute('app_voucher_success'); } /** * Sends the visitor to the step that owns the complaint, or keeps them here without a buy * button when the complaint is about nothing they can change. * * @param FormInterface $form */ private function handleRejection( Request $request, VoucherPurchaseDto $dto, FormInterface $form, Notification $notification, string $logMessage, ): Response { $step = $this->rejectionMapper->stepFor($notification); $this->logger->warning($logMessage, [ 'code' => $notification->code, 'message' => $notification->message, 'step' => $step, ]); $this->addFlash('error', $notification->message ?? 'Der Gutschein kann so nicht gekauft werden.'); if (VoucherSaleRejectionMapper::STEP_OVERVIEW === $step) { return $this->renderStep($dto, $form, false); } $dto->currentStep = $step; $this->sessionManager->save($request, $dto); return $this->redirectToStep($step); } /** * @param FormInterface $form */ private function renderStep(VoucherPurchaseDto $dto, FormInterface $form, bool $purchasable): Response { return $this->render('voucher/step_3.html.twig', [ ...$this->createStepContext($dto), 'form' => $form, 'purchasable' => $purchasable, ]); } }