From 86086bd42e6877b6e54a22c03ed54b0283558450 Mon Sep 17 00:00:00 2001 From: Frederik Rommel <15031079+rommelfreddy@users.noreply.github.com> Date: Fri, 13 Sep 2024 18:45:51 +0200 Subject: [PATCH] RATESWSX-306: installment: prevent fatal error message in checkout on unreachable gateway (#65) --- CHANGELOG.md | 2 + .../DependencyInjection/subscriber.xml | 8 ++- .../Subscriber/BuildPaymentSubscriber.php | 21 ++++-- .../Subscriber/CheckoutSubscriber.php | 69 ++++++++++++++++--- .../RequestBuilderFailedSubscriber.php | 2 +- .../InstallmentPaymentHandler.php | 2 +- .../Event/RequestBuilderFailedEvent.php | 14 +++- .../Service/Request/AbstractRequest.php | 14 ++-- .../snippet/de_DE/messages.de-DE.json | 1 + .../snippet/en_GB/messages.en-GB.json | 1 + .../checkout/ratepay/installment.html.twig | 4 +- 11 files changed, 110 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 826c55f9..1e75a76c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## WIP +* RATESWX-306: installment: prevent fatal error message in checkout on unreachable gateway + ## Version 7.0.1 - Released on 2024-06-07 RATESWSX-303: fix admin-session logout endless redirect & make admin-session urls more unified diff --git a/src/Components/InstallmentCalculator/DependencyInjection/subscriber.xml b/src/Components/InstallmentCalculator/DependencyInjection/subscriber.xml index 927f6c1f..c966af9e 100644 --- a/src/Components/InstallmentCalculator/DependencyInjection/subscriber.xml +++ b/src/Components/InstallmentCalculator/DependencyInjection/subscriber.xml @@ -16,8 +16,12 @@ - - + + + + + + diff --git a/src/Components/InstallmentCalculator/Subscriber/BuildPaymentSubscriber.php b/src/Components/InstallmentCalculator/Subscriber/BuildPaymentSubscriber.php index 039c0e7b..62ebf1c0 100644 --- a/src/Components/InstallmentCalculator/Subscriber/BuildPaymentSubscriber.php +++ b/src/Components/InstallmentCalculator/Subscriber/BuildPaymentSubscriber.php @@ -31,13 +31,16 @@ use Ratepay\RpayPayments\Util\RequestHelper; use RuntimeException; use Shopware\Core\Checkout\Payment\PaymentMethodEntity; +use Shopware\Core\Framework\Adapter\Translation\AbstractTranslator; use Shopware\Core\Framework\Validation\DataBag\DataBag; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Throwable; class BuildPaymentSubscriber implements EventSubscriberInterface { public function __construct( - private readonly InstallmentService $installmentService + private readonly InstallmentService $installmentService, + private readonly AbstractTranslator $translator ) { } @@ -54,7 +57,9 @@ public function buildPayment(BuildEvent $event): void /** @var PaymentRequestData $requestData */ $requestData = $event->getRequestData(); - if (MethodHelper::isInstallmentMethod($requestData->getTransaction()->getPaymentMethod()->getHandlerIdentifier())) { + $paymentMethod = $requestData->getTransaction()->getPaymentMethod(); + + if (MethodHelper::isInstallmentMethod($paymentMethod->getHandlerIdentifier())) { /** @var Payment $paymentObject */ $paymentObject = $event->getBuildData(); @@ -71,9 +76,15 @@ public function buildPayment(BuildEvent $event): void ); $calcContext->setTotalAmount($paymentObject->getAmount()); $calcContext->setOrder($requestData->getOrder()); - $calcContext->setPaymentMethod($requestData->getTransaction()->getPaymentMethod()); - - $plan = $this->installmentService->getInstallmentPlanData($calcContext); + $calcContext->setPaymentMethod($paymentMethod); + + try { + $plan = $this->installmentService->getInstallmentPlanData($calcContext); + } catch (Throwable $exception) { + throw new RuntimeException($this->translator->trans('checkout.error.RATEPAY_INSTALLMENT_CAN_NOT_BE_LOADED', [ + '%method%' => $paymentMethod->getTranslated()['name'] ?? $paymentMethod->getName(), + ]), $exception->getCode(), previous: $exception); + } if (PlanHasher::isPlanEqualWithHash($requestedInstallment->get('hash'), $plan)) { throw new Exception('the hash value of the calculated plan does not match the given hash'); diff --git a/src/Components/InstallmentCalculator/Subscriber/CheckoutSubscriber.php b/src/Components/InstallmentCalculator/Subscriber/CheckoutSubscriber.php index f41977e7..367f4afc 100644 --- a/src/Components/InstallmentCalculator/Subscriber/CheckoutSubscriber.php +++ b/src/Components/InstallmentCalculator/Subscriber/CheckoutSubscriber.php @@ -11,19 +11,29 @@ namespace Ratepay\RpayPayments\Components\InstallmentCalculator\Subscriber; +use Psr\Log\LoggerInterface; use Ratepay\RpayPayments\Components\Checkout\Event\PaymentDataExtensionBuilt; use Ratepay\RpayPayments\Components\InstallmentCalculator\Model\InstallmentCalculatorContext; use Ratepay\RpayPayments\Components\InstallmentCalculator\Service\InstallmentService; use Ratepay\RpayPayments\Util\MethodHelper; +use Shopware\Core\Checkout\Cart\Error\Error; +use Shopware\Core\Checkout\Cart\Error\GenericCartError; use Shopware\Core\Checkout\Cart\SalesChannel\CartService; use Shopware\Core\Checkout\Order\OrderEntity; +use Shopware\Core\Framework\Adapter\Translation\AbstractTranslator; +use Shopware\Core\Framework\Struct\ArrayStruct; +use Shopware\Storefront\Page\Account\Order\AccountEditOrderPageLoadedEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface; +use Throwable; class CheckoutSubscriber implements EventSubscriberInterface { public function __construct( private readonly InstallmentService $installmentService, - private readonly CartService $cartService + private readonly CartService $cartService, + private readonly LoggerInterface $logger, + private readonly AbstractTranslator $translator ) { } @@ -31,6 +41,7 @@ public static function getSubscribedEvents(): array { return [ PaymentDataExtensionBuilt::class => 'buildCheckoutExtension', + AccountEditOrderPageLoadedEvent::class => ['onAccountEditOrderPageLoaded', 300], ]; } @@ -45,20 +56,62 @@ public function buildCheckoutExtension(PaymentDataExtensionBuilt $event): void $calcContext = (new InstallmentCalculatorContext($salesChannelContext, '', null)) ->setPaymentMethodId($paymentMethod->getId()) ->setOrder($order); + $cart = $this->cartService->getCart($salesChannelContext->getToken(), $salesChannelContext); if (!$order instanceof OrderEntity) { - $calcContext->setTotalAmount($this->cartService->getCart($salesChannelContext->getToken(), $salesChannelContext)->getPrice()->getTotalPrice()); + $calcContext->setTotalAmount($cart->getPrice()->getTotalPrice()); } - $installmentCalculator = $this->installmentService->getInstallmentCalculatorData($calcContext); + try { + $installmentCalculator = $this->installmentService->getInstallmentCalculatorData($calcContext); - $calcContext->setCalculationType($installmentCalculator['defaults']['type']); - $calcContext->setCalculationValue($installmentCalculator['defaults']['value']); + $calcContext->setCalculationType($installmentCalculator['defaults']['type']); + $calcContext->setCalculationValue($installmentCalculator['defaults']['value']); - $vars = $this->installmentService->getInstallmentPlanTwigVars($calcContext); - $vars['calculator'] = $installmentCalculator; + $vars = $this->installmentService->getInstallmentPlanTwigVars($calcContext); + $vars['calculator'] = $installmentCalculator; + $extension->offsetSet('installment', $vars); + } catch (Throwable $exception) { + $this->logger->error('Ratepay installment can not be loaded. ' . $exception->getMessage(), [ + 'order_id' => $order?->getId(), + 'payment_method_id' => $calcContext->getPaymentMethodId(), + 'total_amount' => $calcContext->getTotalAmount(), + 'calculation_type' => $calcContext->getCalculationType(), + 'calculation_value' => $calcContext->getCalculationValue(), + ]); - $extension->offsetSet('installment', $vars); + if (!$order instanceof OrderEntity) { + $cart->addErrors(new GenericCartError( + 'RATEPAY::INSTALLMENT_CAN_NOT_BE_LOADED', + 'error.RATEPAY_INSTALLMENT_CAN_NOT_BE_LOADED', + [ + 'method' => $paymentMethod->getTranslated()['name'] ?? $paymentMethod->getName(), + ], + Error::LEVEL_ERROR, + true, + false, + true + )); + } + } + } + } + + public function onAccountEditOrderPageLoaded(AccountEditOrderPageLoadedEvent $event): void + { + $paymentMethod = $event->getSalesChannelContext()->getPaymentMethod(); + + if (MethodHelper::isInstallmentMethod($paymentMethod->getHandlerIdentifier())) { + /** @var ArrayStruct|null $ratepayData */ + $ratepayData = $event->getPage()->getExtension('ratepay'); + if ($ratepayData?->get('installment') === null) { + $session = $event->getRequest()->getSession(); + if ($session instanceof FlashBagAwareSessionInterface) { + $session->getFlashbag()->add('danger', $this->translator->trans('checkout.error.RATEPAY_INSTALLMENT_CAN_NOT_BE_LOADED', [ + '%method%' => $paymentMethod->getTranslated()['name'] ?? $paymentMethod->getName(), + ])); + } + } } } } diff --git a/src/Components/Logging/Subscriber/RequestBuilderFailedSubscriber.php b/src/Components/Logging/Subscriber/RequestBuilderFailedSubscriber.php index b4253fdc..3cffdf10 100755 --- a/src/Components/Logging/Subscriber/RequestBuilderFailedSubscriber.php +++ b/src/Components/Logging/Subscriber/RequestBuilderFailedSubscriber.php @@ -32,7 +32,7 @@ public static function getSubscribedEvents(): array public function onRequestBuilderFailed(RequestBuilderFailedEvent $event): void { // $requestData = $event->getRequestData(); - $exception = $event->getException(); + $exception = $event->getThrowable(); $this->fileLogger->error('RequestBuilder failed', [ 'message' => $exception->getMessage(), 'trace' => $exception->getTraceAsString(), diff --git a/src/Components/PaymentHandler/InstallmentPaymentHandler.php b/src/Components/PaymentHandler/InstallmentPaymentHandler.php index afb75fd0..64ea0393 100644 --- a/src/Components/PaymentHandler/InstallmentPaymentHandler.php +++ b/src/Components/PaymentHandler/InstallmentPaymentHandler.php @@ -62,7 +62,7 @@ public function getValidationDefinitions(DataBag $requestDataBag, $baseData): ar $ratepayData = RequestHelper::getRatepayData($requestDataBag); $installmentData = $ratepayData->get('installment'); - if ($installmentData->get('paymentType') && $installmentData->get('paymentType') === 'DIRECT-DEBIT') { + if ($installmentData && $installmentData->get('paymentType') && $installmentData->get('paymentType') === 'DIRECT-DEBIT') { $validations = array_merge($validations, $this->getDebitConstraints($baseData)); } diff --git a/src/Components/RatepayApi/Event/RequestBuilderFailedEvent.php b/src/Components/RatepayApi/Event/RequestBuilderFailedEvent.php index 7b382ed0..6d34b736 100644 --- a/src/Components/RatepayApi/Event/RequestBuilderFailedEvent.php +++ b/src/Components/RatepayApi/Event/RequestBuilderFailedEvent.php @@ -11,23 +11,31 @@ namespace Ratepay\RpayPayments\Components\RatepayApi\Event; -use Exception; use Ratepay\RpayPayments\Components\RatepayApi\Dto\AbstractRequestData; use Symfony\Contracts\EventDispatcher\Event; +use Throwable; class RequestBuilderFailedEvent extends Event { public function __construct( - private readonly Exception $exception, + private readonly Throwable $exception, private readonly AbstractRequestData $requestData ) { } - public function getException(): Exception + public function getThrowable(): Throwable { return $this->exception; } + /** + * @deprecated use getThrowable + */ + public function getException(): Throwable + { + return $this->getThrowable(); + } + public function getRequestData(): AbstractRequestData { return $this->requestData; diff --git a/src/Components/RatepayApi/Service/Request/AbstractRequest.php b/src/Components/RatepayApi/Service/Request/AbstractRequest.php index 6d369bfa..bb724f00 100644 --- a/src/Components/RatepayApi/Service/Request/AbstractRequest.php +++ b/src/Components/RatepayApi/Service/Request/AbstractRequest.php @@ -12,7 +12,6 @@ namespace Ratepay\RpayPayments\Components\RatepayApi\Service\Request; use InvalidArgumentException; -use RatePAY\Exception\ExceptionAbstract; use RatePAY\Exception\RequestException; use RatePAY\Model\Request\SubModel\Content; use RatePAY\Model\Request\SubModel\Head; @@ -28,6 +27,7 @@ use Ratepay\RpayPayments\Components\RatepayApi\Factory\HeadFactory; use Ratepay\RpayPayments\Exception\RatepayException; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Throwable; abstract class AbstractRequest { @@ -113,10 +113,10 @@ final public function doRequest(AbstractRequestData $requestData): RequestBuilde $this->_initRequest($requestData); - $head = $this->_getRequestHead($requestData); - $content = $this->_getRequestContent($requestData); - try { + $head = $this->_getRequestHead($requestData); + $content = $this->_getRequestContent($requestData); + if ($this->isRequestBlockedByFeatureFlag($requestData)) { throw new RequestException('Request has been blocked by feature flag.'); } @@ -126,9 +126,9 @@ final public function doRequest(AbstractRequestData $requestData): RequestBuilde if ($this->_subType) { $requestBuilder = $requestBuilder->subtype($this->_subType); } - } catch (ExceptionAbstract $exception) { - $this->eventDispatcher->dispatch(new RequestBuilderFailedEvent($exception, $requestData)); - throw new RatepayException($exception->getMessage(), $exception->getCode(), $exception); + } catch (Throwable $throwable) { + $this->eventDispatcher->dispatch(new RequestBuilderFailedEvent($throwable, $requestData)); + throw new RatepayException($throwable->getMessage(), $throwable->getCode(), $throwable); } $this->eventDispatcher->dispatch(new RequestDoneEvent($requestData, $requestBuilder)); diff --git a/src/Resources/snippet/de_DE/messages.de-DE.json b/src/Resources/snippet/de_DE/messages.de-DE.json index 68e8aa49..f21c14f1 100644 --- a/src/Resources/snippet/de_DE/messages.de-DE.json +++ b/src/Resources/snippet/de_DE/messages.de-DE.json @@ -11,6 +11,7 @@ "error.VIOLATION::METHOD_NOT_AVAILABLE": "Leider ist eine Bezahlung mit der gewählten Zahlungsart nicht möglich.", "error.VIOLATION::RP_MISSING_BANK_ACCOUNT_HOLDER": "Bitte geben Sie den Kontoinhaber an.", "error.VIOLATION::RP_INVALID_BANK_ACCOUNT_HOLDER": "Bitte geben Sie einen gültigen Kontoinhaber an.", + "checkout.error.RATEPAY_INSTALLMENT_CAN_NOT_BE_LOADED": "Leider kann der Ratenrechner der Zahlungsart %method% nicht geladen und damit die Zahlungsmethode nicht zur Verfügung gestellt werden. Bitte laden Sie die Seite neu, versuchen Sie es später erneut, oder wählen eine andere Zahlungsart.", "ratepay": { "storefront": { "admin-order": { diff --git a/src/Resources/snippet/en_GB/messages.en-GB.json b/src/Resources/snippet/en_GB/messages.en-GB.json index b21af6e0..4628b018 100644 --- a/src/Resources/snippet/en_GB/messages.en-GB.json +++ b/src/Resources/snippet/en_GB/messages.en-GB.json @@ -11,6 +11,7 @@ "error.VIOLATION::METHOD_NOT_AVAILABLE": "Unfortunately, it is not possible to use the selected payment method.", "error.VIOLATION::RP_MISSING_BANK_ACCOUNT_HOLDER": "Please provide a bank account owner.", "error.VIOLATION::RP_INVALID_BANK_ACCOUNT_HOLDER": "Please provide a valid bank account owner.", + "checkout.error.RATEPAY_INSTALLMENT_CAN_NOT_BE_LOADED": "Unfortunately the instalment calculator for the payment method %method% could not be loaded and so the payment method could not be provided to you. Please reload the page, try again later, or choose another payment method.", "ratepay": { "storefront": { "admin-order": { diff --git a/src/Resources/views/storefront/page/checkout/ratepay/installment.html.twig b/src/Resources/views/storefront/page/checkout/ratepay/installment.html.twig index 8283bc6b..7f06c5ae 100644 --- a/src/Resources/views/storefront/page/checkout/ratepay/installment.html.twig +++ b/src/Resources/views/storefront/page/checkout/ratepay/installment.html.twig @@ -9,5 +9,7 @@ {% block ratepay_checkout_fields %} {{ parent() }} - {% sw_include '@RatepayPaments/storefront/page/checkout/ratepay/common/installment-calculator.html.twig' %} + {% if page.extensions.ratepay.installment.calculator %} + {% sw_include '@RatepayPaments/storefront/page/checkout/ratepay/common/installment-calculator.html.twig' %} + {% endif %} {% endblock %}