diff --git a/Components/Builder/NotificationBuilder.php b/Components/Builder/NotificationBuilder.php index 978d717e..b47ef3bb 100644 --- a/Components/Builder/NotificationBuilder.php +++ b/Components/Builder/NotificationBuilder.php @@ -68,6 +68,7 @@ public function fromParams($params): Notification } $notification->setOrder($order); + $notification->setOrderId($order->getId()); if (isset($params['pspReference'])) { $notification->setPspReference($params['pspReference']); diff --git a/Components/IncomingNotificationManager.php b/Components/IncomingNotificationManager.php index b1ad831a..e098955a 100644 --- a/Components/IncomingNotificationManager.php +++ b/Components/IncomingNotificationManager.php @@ -5,6 +5,7 @@ namespace AdyenPayment\Components; use AdyenPayment\Components\Builder\NotificationBuilder; +use AdyenPayment\Exceptions\DuplicateNotificationException; use AdyenPayment\Exceptions\InvalidParameterException; use AdyenPayment\Exceptions\OrderNotFoundException; use AdyenPayment\Models\Feedback\TextNotificationItemFeedback; @@ -18,20 +19,10 @@ */ class IncomingNotificationManager { - /** - * @var LoggerInterface - */ - private $logger; - - /** - * @var NotificationBuilder - */ - private $notificationBuilder; - - /** - * @var ModelManager - */ - private $entityManager; + private LoggerInterface $logger; + private NotificationBuilder $notificationBuilder; + private ModelManager $entityManager; + private NotificationManager $notificationManager; /** * IncomingNotificationManager constructor. @@ -39,11 +30,13 @@ class IncomingNotificationManager public function __construct( LoggerInterface $logger, NotificationBuilder $notificationBuilder, - ModelManager $entityManager + ModelManager $entityManager, + NotificationManager $notificationManager ) { $this->logger = $logger; $this->notificationBuilder = $notificationBuilder; $this->entityManager = $entityManager; + $this->notificationManager = $notificationManager; } /** @@ -60,6 +53,9 @@ public function convertNotifications(array $textNotifications): void $notification = $this->notificationBuilder->fromParams( json_decode($textNotificationItem->getTextNotification(), true) ); + + $this->notificationManager->guardDuplicate($notification); + $this->entityManager->persist($notification); } } catch (InvalidParameterException $exception) { @@ -70,10 +66,14 @@ public function convertNotifications(array $textNotifications): void $this->logger->warning( $exception->getMessage().' '.$textNotificationItem->getTextNotification() ); + } catch (DuplicateNotificationException $exception) { + $this->logger->notice( + $exception->getMessage() + ); } $this->entityManager->remove($textNotificationItem); + $this->entityManager->flush(); } - $this->entityManager->flush(); } /** diff --git a/Components/NotificationManager.php b/Components/NotificationManager.php index fa012c17..b4ecf8f9 100644 --- a/Components/NotificationManager.php +++ b/Components/NotificationManager.php @@ -4,6 +4,7 @@ namespace AdyenPayment\Components; +use AdyenPayment\Exceptions\DuplicateNotificationException; use AdyenPayment\Models\Enum\NotificationStatus; use AdyenPayment\Models\Notification; use Doctrine\ORM\EntityRepository; @@ -98,4 +99,22 @@ public function getLastNotificationForPspReference(string $pspReference) return; } } + + public function guardDuplicate(Notification $notification): void + { + $record = $this->notificationRepository->findOneBy([ + 'orderId' => $notification->getOrderId(), + 'pspReference' => $notification->getPspReference(), + 'paymentMethod' => $notification->getPaymentMethod(), + 'success' => $notification->isSuccess(), + 'eventCode' => $notification->getEventCode(), + 'merchantAccountCode' => $notification->getMerchantAccountCode(), + 'amountValue' => $notification->getAmountValue(), + 'amountCurrency' => $notification->getAmountCurrency(), + ]); + + if ($record instanceof Notification) { + throw DuplicateNotificationException::withNotification($record); + } + } } diff --git a/Components/NotificationProcessor.php b/Components/NotificationProcessor.php index fc4274f1..7268efd9 100644 --- a/Components/NotificationProcessor.php +++ b/Components/NotificationProcessor.php @@ -5,6 +5,7 @@ namespace AdyenPayment\Components; use AdyenPayment\Components\NotificationProcessor\NotificationProcessorInterface; +use AdyenPayment\Exceptions\DuplicateNotificationException; use AdyenPayment\Exceptions\NoNotificationProcessorFoundException; use AdyenPayment\Exceptions\OrderNotFoundException; use AdyenPayment\Models\Enum\NotificationStatus; @@ -26,21 +27,10 @@ class NotificationProcessor * @var NotificationProcessorInterface[] */ private $processors; - - /** - * @var LoggerInterface - */ - private $logger; - - /** - * @var ModelManager - */ - private $modelManager; - - /** - * @var ContainerAwareEventManager - */ - private $eventManager; + private LoggerInterface $logger; + private ModelManager $modelManager; + private ContainerAwareEventManager $eventManager; + private NotificationManager $notificationManager; /** * NotificationProcessor constructor. @@ -50,11 +40,13 @@ class NotificationProcessor public function __construct( LoggerInterface $logger, ModelManager $modelManager, - ContainerAwareEventManager $eventManager + ContainerAwareEventManager $eventManager, + NotificationManager $notificationManager ) { $this->logger = $logger; $this->modelManager = $modelManager; $this->eventManager = $eventManager; + $this->notificationManager = $notificationManager; } /** @@ -68,6 +60,10 @@ public function processMany(Traversable $notifications): \Generator foreach ($notifications as $notification) { try { yield from $this->process($notification); + } catch (DuplicateNotificationException $exception) { + $this->logger->notice( + $exception->getMessage() + ); } catch (NoNotificationProcessorFoundException $exception) { $this->logger->notice( 'No notification processor found', @@ -105,6 +101,8 @@ public function processMany(Traversable $notifications): \Generator */ private function process(Notification $notification): \Generator { + $this->notificationManager->guardDuplicate($notification); + $processors = $this->findProcessors($notification); if (empty($processors)) { diff --git a/Controllers/Frontend/DisableRecurringToken.php b/Controllers/Frontend/DisableRecurringToken.php index 9ff33d1c..370f89fb 100644 --- a/Controllers/Frontend/DisableRecurringToken.php +++ b/Controllers/Frontend/DisableRecurringToken.php @@ -15,11 +15,13 @@ class Shopware_Controllers_Frontend_DisableRecurringToken extends Enlight_Contro { private ApiJsonResponse $frontendJsonResponse; private DisableTokenRequestHandlerInterface $disableTokenRequestHandler; + private Shopware_Components_Snippet_Manager $snippets; public function preDispatch(): void { $this->frontendJsonResponse = $this->get(FrontendJsonResponse::class); $this->disableTokenRequestHandler = $this->get(DisableTokenRequestHandler::class); + $this->snippets = $this->get('snippets'); } public function disabledAction(): void @@ -29,7 +31,11 @@ public function disabledAction(): void $this->frontendJsonResponse->sendJsonBadRequestResponse( $this->Front(), $this->Response(), - 'Invalid method.' + $this->snippets->getNamespace('adyen/checkout/error')->get( + 'disableTokenInvalidMethodMessage', + 'Invalid method.', + true + ) ); return; @@ -40,7 +46,11 @@ public function disabledAction(): void $this->frontendJsonResponse->sendJsonBadRequestResponse( $this->Front(), $this->Response(), - 'Missing recurring token param.' + $this->snippets->getNamespace('adyen/checkout/error')->get( + 'disableTokenMissingRecurringTokenMessage', + 'Missing recurring token param.', + true + ) ); return; @@ -48,12 +58,11 @@ public function disabledAction(): void $result = $this->disableTokenRequestHandler->disableToken($recurringToken, Shopware()->Shop()); if (!$result->isSuccess()) { - $this->frontendJsonResponse->sendJsonResponse( + $this->frontendJsonResponse->sendJsonBadRequestResponse( $this->Front(), $this->Response(), - JsonResponse::create( - ['error' => true, 'message' => $result->message()], - Response::HTTP_BAD_REQUEST + $this->snippets->getNamespace('adyen/checkout/error')->get( + $result->message() ) ); diff --git a/Enricher/Payment/PaymentMethodEnricher.php b/Enricher/Payment/PaymentMethodEnricher.php index 21b67139..dc2b2388 100644 --- a/Enricher/Payment/PaymentMethodEnricher.php +++ b/Enricher/Payment/PaymentMethodEnricher.php @@ -26,7 +26,7 @@ public function __invoke(array $shopwareMethod, PaymentMethod $paymentMethod): a { return array_merge($shopwareMethod, [ 'enriched' => true, - 'additionaldescription' => $this->enrichAdditionalDescription($paymentMethod), + 'additionaldescription' => $this->enrichAdditionalDescription($shopwareMethod, $paymentMethod), 'image' => $this->imageLogoProvider->provideByType($paymentMethod->adyenType()->type()), 'isStoredPayment' => $paymentMethod->isStoredPayment(), 'isAdyenPaymentMethod' => true, @@ -37,19 +37,23 @@ public function __invoke(array $shopwareMethod, PaymentMethod $paymentMethod): a ); } - private function enrichAdditionalDescription(PaymentMethod $adyenMethod): string + private function enrichAdditionalDescription(array $shopwareMethod, PaymentMethod $adyenMethod): string { - $description = $this->snippets - ->getNamespace('adyen/method/description') - ->get($adyenMethod->adyenType()->type()) ?? ''; + $additionalDescription = $shopwareMethod['additionaldescription'] ?? ''; + + if ('' === $additionalDescription) { + $additionalDescription = $this->snippets + ->getNamespace('adyen/method/description') + ->get($shopwareMethod['attribute']['adyen_type'] ?? '') ?? ''; + } if (!$adyenMethod->isStoredPayment()) { - return $description; + return $additionalDescription; } return sprintf( '%s%s: %s', - ($description ? $description.' ' : ''), + ($additionalDescription ? $additionalDescription.' ' : ''), $this->snippets ->getNamespace('adyen/checkout/payment') ->get('CardNumberEndingOn', 'Card number ending on', true), diff --git a/Exceptions/DuplicateNotificationException.php b/Exceptions/DuplicateNotificationException.php new file mode 100644 index 00000000..8edf5474 --- /dev/null +++ b/Exceptions/DuplicateNotificationException.php @@ -0,0 +1,28 @@ +getId(), + $notification->getOrderId(), + $notification->getPspReference(), + $notification->getStatus(), + $notification->getPaymentMethod(), + $notification->getEventCode(), + $notification->isSuccess(), + $notification->getMerchantAccountCode(), + $notification->getAmountValue(), + $notification->getAmountCurrency() + )); + } +} diff --git a/Resources/frontend/js/jquery.adyen-disable-payment.js b/Resources/frontend/js/jquery.adyen-disable-payment.js index 8bdf54a8..b815f5f1 100644 --- a/Resources/frontend/js/jquery.adyen-disable-payment.js +++ b/Resources/frontend/js/jquery.adyen-disable-payment.js @@ -23,15 +23,36 @@ * CSS classes selector to clear the error elements */ errorClassSelector: '.alert.is--error.is--rounded.is--adyen-error', + /** + * @var string modalSelector + * CSS classes selector to use as confirmation modal content. + */ + modalSelector: '.adyenDisableTokenConfirmationModal', + /** + * @var string modalConfirmButtonSelector + * CSS classes selector for the disable-confirm button + */ + modalConfirmButtonSelector: '.disableConfirm', + /** + * @var string modalCancelButtonSelector + * CSS classes selector for the disable-cancel button + */ + modalCancelButtonSelector: '.disableCancel', /** * @var string errorMessageClass * CSS classes for the error message element */ - errorMessageClass: 'alert--content' + errorMessageClass: 'alert--content', + /** + * @var string modalErrorContainerSelector + * CSS classes for the error message container in the modal + */ + modalErrorContainerSelector: '.modal-error-container', }, init: function () { var me = this; me.applyDataAttributes(); + me.modalContent = $(me.opts.modalSelector).html() || ''; me.$el.on('click', $.proxy(me.enableDisableButtonClick, me)); }, enableDisableButtonClick: function () { @@ -39,7 +60,30 @@ if (0 === me.opts.adyenStoredMethodId.length) { return; } + if('' === me.modalContent){ + return; + } + me.modal = $.modal.open(me.modalContent, { + showCloseButton: true, + closeOnOverlay: false, + additionalClass: 'adyen-modal disable-token-confirmation' + }); + me.buttonConfirm = $(me.opts.modalConfirmButtonSelector); + me.buttonConfirm.on('click', $.proxy(me.runDisableTokenCall, me)); + me.buttonCancel = $(me.opts.modalCancelButtonSelector); + me.buttonCancel.on('click', $.proxy(me.closeModal, me)); + }, + closeModal: function () { + var me = this; + if(!me.modal){ + return; + } + me.modal.close(); + }, + runDisableTokenCall: function () { + var me = this; $.loadingIndicator.open(); + $.loadingIndicator.loader.$loader.addClass('over-modal'); $.post({ url: me.opts.adyenDisableTokenUrl, dataType: 'json', @@ -58,7 +102,7 @@ $(me.opts.errorClassSelector).remove(); var error = $('
').addClass(me.opts.errorClass); error.append($('
').addClass(me.opts.errorMessageClass).html(message)); - me.$el.parent().append(error); + $(me.opts.modalErrorContainerSelector).append(error); } }); })(jQuery); diff --git a/Resources/frontend/js/jquery.adyen-payment-selection.js b/Resources/frontend/js/jquery.adyen-payment-selection.js index d1f8190b..df54ef18 100644 --- a/Resources/frontend/js/jquery.adyen-payment-selection.js +++ b/Resources/frontend/js/jquery.adyen-payment-selection.js @@ -10,7 +10,7 @@ adyenOrderTotal: '', adyenOrderCurrency: '', resetSessionUrl: '', - adyenConfigAjaxUrl: '/frontend/adyenconfig/index', + adyenConfigAjaxUrl: '', /** * Fallback environment variable * diff --git a/Resources/frontend/less/all.less b/Resources/frontend/less/all.less index 6df53d6b..cab1f7fd 100644 --- a/Resources/frontend/less/all.less +++ b/Resources/frontend/less/all.less @@ -27,6 +27,25 @@ .adyen-modal .content { padding: 20px; + + .modal-error-container { + margin-top: 2em; + } +} + +.over-modal { + z-index: 7000; +} + +.disable-token-confirmation { + height: auto; + max-height: 18em; + + .buttons-container { + display: flex; + justify-content: space-evenly; + margin-top: 2em; + } } .alert.is--adyen-error { diff --git a/Resources/services/components.xml b/Resources/services/components.xml index 1512826a..c4d2492a 100644 --- a/Resources/services/components.xml +++ b/Resources/services/components.xml @@ -10,6 +10,7 @@ + diff --git a/Resources/services/managers.xml b/Resources/services/managers.xml index dfbd4728..788e9ebd 100644 --- a/Resources/services/managers.xml +++ b/Resources/services/managers.xml @@ -20,6 +20,7 @@ + diff --git a/Resources/services/shopware.xml b/Resources/services/shopware.xml index 490cb309..d53aae49 100644 --- a/Resources/services/shopware.xml +++ b/Resources/services/shopware.xml @@ -7,5 +7,9 @@ + + + + diff --git a/Resources/services/subscribers.xml b/Resources/services/subscribers.xml index a05c526f..3bd2d050 100644 --- a/Resources/services/subscribers.xml +++ b/Resources/services/subscribers.xml @@ -66,6 +66,7 @@ + diff --git a/Resources/services/validators.xml b/Resources/services/validators.xml index 4311d685..07c15785 100644 --- a/Resources/services/validators.xml +++ b/Resources/services/validators.xml @@ -30,4 +30,4 @@ service('models').getRepository('Shopware\\Models\\Shop\\Shop') - \ No newline at end of file + diff --git a/Resources/views/frontend/checkout/adyen_configuration.tpl b/Resources/views/frontend/checkout/adyen_configuration.tpl index ca36ef3a..6cd0cca3 100644 --- a/Resources/views/frontend/checkout/adyen_configuration.tpl +++ b/Resources/views/frontend/checkout/adyen_configuration.tpl @@ -1,4 +1,5 @@
diff --git a/Resources/views/frontend/register/payment_fieldset.tpl b/Resources/views/frontend/register/payment_fieldset.tpl index 795d6fe9..412bc07a 100644 --- a/Resources/views/frontend/register/payment_fieldset.tpl +++ b/Resources/views/frontend/register/payment_fieldset.tpl @@ -34,3 +34,21 @@ {/if} {/block} {/block} + +{block name="frontend_register_payment_fieldset"} + {$smarty.block.parent} +
+
+

{s name='adyenDisableTokenConfirmationMessage'}Are you sure to remove the stored payment method?{/s}

+
+ + +
+ +
+
+{/block} diff --git a/Shopware/Provider/CheckoutBasketProvider.php b/Shopware/Provider/CheckoutBasketProvider.php new file mode 100644 index 00000000..48562b8d --- /dev/null +++ b/Shopware/Provider/CheckoutBasketProvider.php @@ -0,0 +1,26 @@ +container = $container; + $this->view = new \Enlight_View_Default($engine); + $this->init(); + + parent::__construct(); + } + + public function __invoke($mergeProportional = true): array + { + return $this->getBasket($mergeProportional); + } +} diff --git a/Shopware/Provider/CheckoutBasketProviderInterface.php b/Shopware/Provider/CheckoutBasketProviderInterface.php new file mode 100644 index 00000000..3014f088 --- /dev/null +++ b/Shopware/Provider/CheckoutBasketProviderInterface.php @@ -0,0 +1,10 @@ +configuration = $configuration; $this->paymentMethodService = $paymentMethodService; @@ -37,6 +40,7 @@ public function __construct( $this->enrichedPaymentMeanProvider = $enrichedPaymentMeanProvider; $this->paymentMethodOptionsBuilder = $paymentMethodOptionsBuilder; $this->paymentMeanCollectionSerializer = $paymentMeanCollectionSerializer; + $this->checkoutBasketProvider = $checkoutBasketProvider; } public static function getSubscribedEvents(): array @@ -63,7 +67,8 @@ private function checkBasketAmount(\Enlight_Controller_Action $subject): void return; } - $basket = $subject->View()->sBasket; + $basket = ($this->checkoutBasketProvider)(); + if (!$basket) { return; } diff --git a/plugin.xml b/plugin.xml index 9b9b3666..4e10edab 100644 --- a/plugin.xml +++ b/plugin.xml @@ -4,7 +4,7 @@ - 3.7.0 + 3.8.0 Adyen Adyen https://adyen.com @@ -327,4 +327,18 @@ * HTML-Bereinigung beim Checkout entfernt (unter Verwendung von JSON-Antwort anstelle des HTML-Datenattributs), sodass das Design an der Zahlungsmethode angepasst werden kann. + + + * USP - Confirm modal + * Fix: Duplicate Webhook Notifications + * Fix: Mismatch of Basket Amount for guest and user accounts on payment + * Feature: Allow additional description for payment methods (with fallback to translations) + + + * USP - Confirm modal + * Fix: Duplicate Webhook Notifications + * Fix: Mismatch of Basket Amount for guest and user accounts on payment + * Feature: Allow additional description for payment methods (with fallback to translations) + + diff --git a/tests/Unit/Enricher/Payment/PaymentMethodEnricherTest.php b/tests/Unit/Enricher/Payment/PaymentMethodEnricherTest.php index d46adb1d..0ecade90 100644 --- a/tests/Unit/Enricher/Payment/PaymentMethodEnricherTest.php +++ b/tests/Unit/Enricher/Payment/PaymentMethodEnricherTest.php @@ -47,7 +47,7 @@ public function it_will_enrich_a_payment_method_without_stored_method_data(): vo { $shopwareMethod = [ 'id' => 'shopware-method-id', - 'additionaldescription' => '', + 'additionaldescription' => $description = 'Adyen Method', 'image' => '', ]; $paymentMethod = PaymentMethod::fromRaw($rawData = [ @@ -78,7 +78,10 @@ public function it_will_enrich_a_payment_method_without_stored_method_data(): vo /** @test */ public function it_will_enrich_a_payment_method_with_stored_method_data(): void { - $shopwareMethod = ['id' => $shopwareMethodId = 'shopware-method-id']; + $shopwareMethod = [ + 'id' => $shopwareMethodId = 'shopware-method-id', + 'additionaldescription' => $description = 'Stored Method', + ]; $paymentMethod = PaymentMethod::fromRaw($rawData = [ 'id' => $storedMethodId = 'stored_method_id', 'name' => $storedMethodName = 'stored method name', @@ -87,7 +90,7 @@ public function it_will_enrich_a_payment_method_with_stored_method_data(): void 'lastFour' => $lastFour = '1234', ]); $snippetsNamespace = $this->prophesize(\Enlight_Components_Snippet_Namespace::class); - $snippetsNamespace->get($paymentMethod->adyenType()->type())->willReturn($description = 'Stored Method'); + $snippetsNamespace->get($paymentMethod->adyenType()->type())->willReturn($description); $snippetsNamespace->get('CardNumberEndingOn', $text = 'Card number ending on', true)->willReturn($text); $this->snippets->getNamespace('adyen/method/description')->willReturn($snippetsNamespace); $this->snippets->getNamespace('adyen/checkout/payment')->willReturn($snippetsNamespace); diff --git a/tests/Unit/Exceptions/DuplicateNotificationExceptionTest.php b/tests/Unit/Exceptions/DuplicateNotificationExceptionTest.php new file mode 100644 index 00000000..9b1231ce --- /dev/null +++ b/tests/Unit/Exceptions/DuplicateNotificationExceptionTest.php @@ -0,0 +1,49 @@ +exception = new DuplicateNotificationException(); + } + + /** @test */ + public function is_a_runtime_exception(): void + { + self::assertInstanceOf(\RuntimeException::class, $this->exception); + } + + /** @test */ + public function it_can_be_constructed_with_a_notification(): void + { + $notification = new Notification(); + $notification + ->setId($id = 1) + ->setOrderId($orderId = 2) + ->setPspReference($pspReference = 'PSP_REF_1') + ->setStatus('received') + ->setPaymentMethod('mc') + ->setEventCode('AUTHORISATION') + ->setSuccess(true) + ->setMerchantAccountCode('Adyen-test') + ->setAmountValue(4598.0000) + ->setAmountCurrency('EUR'); + + $exception = DuplicateNotificationException::withNotification($notification); + + self::assertInstanceOf(DuplicateNotificationException::class, $exception); + self::assertEquals('Duplicate notification is not handled. Notification with id: "1", orderId: "2", pspReference: "PSP_REF_1", status: "received", paymentMethod: "mc", eventCode: "AUTHORISATION", success: "1", merchantAccountCode: "Adyen-test", amountValue: "4598", amountCurrency: "EUR"', + $exception->getMessage() + ); + } +}