From beb96fe160a720dad4dff9a4f7042e583321d132 Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Mon, 24 Apr 2023 08:49:01 +0200 Subject: [PATCH 01/10] Bugfix: Make sure the transaction ID keeps set on the order #630 --- Service/OrderLockService.php | 7 +++++ .../Service/OrderLockServiceTest.php | 26 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 Test/Integration/Service/OrderLockServiceTest.php diff --git a/Service/OrderLockService.php b/Service/OrderLockService.php index 166b12a26f2..9325aa9eac7 100644 --- a/Service/OrderLockService.php +++ b/Service/OrderLockService.php @@ -58,12 +58,19 @@ public function execute(OrderInterface $order, callable $callback) $connection = $this->resourceConnection->getConnection('sales'); $connection->beginTransaction(); + // Save this value, so we can restore it after the order has been saved. + $mollieTransactionId = $order->getMollieTransactionId(); + // The order repository uses caching to make sure it only loads the order once, but in this case we want // the latest version of the order, so we need to make sure we get a new instance of the repository. /** @var OrderRepositoryInterface $orderRepository */ $orderRepository = $this->orderRepositoryFactory->create(); $order = $orderRepository->get($order->getEntityId()); + // Restore the transaction ID as it might not be set on the saved order yet. + // This is required further down the process. + $order->setMollieTransactionId($mollieTransactionId); + try { $result = $callback($order); $orderRepository->save($order); diff --git a/Test/Integration/Service/OrderLockServiceTest.php b/Test/Integration/Service/OrderLockServiceTest.php new file mode 100644 index 00000000000..a6137d53b8f --- /dev/null +++ b/Test/Integration/Service/OrderLockServiceTest.php @@ -0,0 +1,26 @@ +loadOrderById('100000001'); + $order->setMollieTransactionId('test_value'); + + /** @var OrderLockService $instance */ + $instance = $this->objectManager->create(OrderLockService::class); + + $instance->execute($order, function ($order) { + $this->assertEquals('test_value', $order->getMollieTransactionId()); + }); + } +} From 0cd7cb4de69c394f57df6ad315273fce7ee62bb2 Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Mon, 24 Apr 2023 09:50:11 +0200 Subject: [PATCH 02/10] Feature: Add BCC option to second chance emails #627 --- Config.php | 10 ++++ Service/Order/SecondChanceEmail.php | 5 ++ .../Service/Order/SecondChanceEmailTest.php | 50 +++++++++++++++++++ etc/adminhtml/system.xml | 9 ++++ 4 files changed, 74 insertions(+) create mode 100644 Test/Integration/Service/Order/SecondChanceEmailTest.php diff --git a/Config.php b/Config.php index 43074487863..6e94b71a81a 100644 --- a/Config.php +++ b/Config.php @@ -39,6 +39,7 @@ class Config const GENERAL_REDIRECT_WHEN_TRANSACTION_FAILS_TO = 'payment/mollie_general/redirect_when_transaction_fails_to'; const GENERAL_SECOND_CHANCE_EMAIL_TEMPLATE = 'payment/mollie_general/second_chance_email_template'; const GENERAL_SECOND_CHANCE_DELAY = 'payment/mollie_general/second_chance_email_delay'; + const GENERAL_SECOND_CHANCE_SEND_BCC_TO = 'payment/mollie_general/second_chance_send_bcc_to'; const GENERAL_SECOND_CHANCE_USE_PAYMENT_METHOD = 'payment/mollie_general/second_chance_use_payment_method'; const GENERAL_TYPE = 'payment/mollie_general/type'; const GENERAL_USE_BASE_CURRENCY = 'payment/mollie_general/currency'; @@ -308,6 +309,15 @@ public function secondChanceEmailDelay($storeId = null) return $this->getPath(static::GENERAL_SECOND_CHANCE_DELAY, $storeId); } + /** + * @param null|int|string $storeId + * @return string|null + */ + public function secondChanceSendBccTo(int $storeId = null): ?string + { + return $this->getPath(static::GENERAL_SECOND_CHANCE_SEND_BCC_TO, $storeId); + } + /** * @param null|int|string $storeId * @return string|null diff --git a/Service/Order/SecondChanceEmail.php b/Service/Order/SecondChanceEmail.php index 93d6208c4dd..8c3afb1a3fc 100644 --- a/Service/Order/SecondChanceEmail.php +++ b/Service/Order/SecondChanceEmail.php @@ -111,6 +111,11 @@ public function send(OrderInterface $order) $builder->setTemplateOptions(['area' => 'frontend', 'store' => $storeId]); $this->setFrom($builder, $storeId); $builder->addTo($order->getCustomerEmail(), $customerName); + + if ($bcc = $this->config->secondChanceSendBccTo($storeId)) { + $builder->addBcc(explode(',', $bcc)); + } + $templateVars = new DataObject($this->getTemplateVars($order)); $this->eventManager->dispatch('mollie_second_change_email_before_send', ['variables' => $templateVars]); $builder->setTemplateVars($templateVars->toArray()); diff --git a/Test/Integration/Service/Order/SecondChanceEmailTest.php b/Test/Integration/Service/Order/SecondChanceEmailTest.php new file mode 100644 index 00000000000..9b9e007b260 --- /dev/null +++ b/Test/Integration/Service/Order/SecondChanceEmailTest.php @@ -0,0 +1,50 @@ +getOrder(); + + /** @var SecondChanceEmail $instance */ + $instance = $this->objectManager->create(SecondChanceEmail::class); + $instance->send($order); + + /** @var TransportBuilderMock $transportBuilder */ + $transportBuilder = $this->objectManager->get(TransportBuilderMock::class); + + $bcc = $transportBuilder->getSentMessage()->getHeaders()['Bcc']; + + $this->assertStringContainsString('example@mollie.com', $bcc); + $this->assertStringContainsString('example2@mollie.com', $bcc); + } + + public function getOrder(): OrderInterface + { + $order = $this->loadOrderById('100000001'); + + // This is required for the payment token. + /** @var CartInterface $cart */ + $cart = $this->objectManager->create(Quote::class); + $cart->load('test01', 'reserved_order_id'); + $order->setQuoteId($cart->getId()); + + return $order; + } +} diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 5a028e95ca9..c23e7cdc7fe 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -353,6 +353,15 @@ + + + payment/mollie_general/second_chance_send_bcc_to + Optional Comma separated list of emailaddresses. Leave empty to disable.]]> + validate-emails + + 1 + + From 4c5bd6e1125ca94f4c8c309ce18e8d98d392af2f Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Mon, 24 Apr 2023 10:19:44 +0200 Subject: [PATCH 03/10] Bugfix: Fix invalid formats with date of birth entries --- Service/Order/TransactionPart/DateOfBirth.php | 17 +++- .../Order/TransactionPart/DateOfBirthTest.php | 95 +++++++++++++++++-- 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/Service/Order/TransactionPart/DateOfBirth.php b/Service/Order/TransactionPart/DateOfBirth.php index 01813d258f6..e62a4006e4a 100644 --- a/Service/Order/TransactionPart/DateOfBirth.php +++ b/Service/Order/TransactionPart/DateOfBirth.php @@ -10,12 +10,25 @@ class DateOfBirth implements TransactionPartInterface { public function process(OrderInterface $order, $apiMethod, array $transaction) { + if (!$order->getPayment() || $order->getPayment()->getMethod() != 'mollie_methods_in3') { + return $transaction; + } + if ($apiMethod == Payments::CHECKOUT_TYPE) { return $transaction; } - if ($order->getCustomerDob()) { - $date = \DateTime::createFromFormat('Y-m-d H:i:s', $order->getCustomerDob()); + if (!$order->getCustomerDob()) { + return $transaction; + } + + $date = \DateTime::createFromFormat('Y-m-d', $order->getCustomerDob()); + if ($date) { + $transaction['consumerDateOfBirth'] = $date->format('Y-m-d'); + } + + $date = \DateTime::createFromFormat('Y-m-d 00:00:00', $order->getCustomerDob()); + if ($date) { $transaction['consumerDateOfBirth'] = $date->format('Y-m-d'); } diff --git a/Test/Integration/Service/Order/TransactionPart/DateOfBirthTest.php b/Test/Integration/Service/Order/TransactionPart/DateOfBirthTest.php index 5175cae0da0..dd427cf3bb5 100644 --- a/Test/Integration/Service/Order/TransactionPart/DateOfBirthTest.php +++ b/Test/Integration/Service/Order/TransactionPart/DateOfBirthTest.php @@ -3,6 +3,7 @@ namespace Mollie\Payment\Test\Integration\Service\Order\TransactionPart; use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderPaymentInterface; use Mollie\Payment\Model\Client\Orders; use Mollie\Payment\Model\Client\Payments; use Mollie\Payment\Service\Order\TransactionPart\DateOfBirth; @@ -10,6 +11,28 @@ class DateOfBirthTest extends IntegrationTestCase { + public function testDoesNothingWhenPaymentMethodIsNotIn3(): void + { + /** @var DateOfBirth $instance */ + $instance = $this->objectManager->create(DateOfBirth::class); + + /** @var OrderInterface $order */ + $order = $this->objectManager->create(OrderInterface::class); + + $order->setPayment($this->objectManager->create(OrderPaymentInterface::class)); + $order->getPayment()->setMethod('mollie_methods_ideal'); + + $order->setCustomerDob('2016-11-19 00:00:00'); + + $transaction = $instance->process( + $order, + Orders::CHECKOUT_TYPE, + [] + ); + + $this->assertArrayNotHasKey('consumerDateOfBirth', $transaction); + } + public function testDoesNothingWhenPaymentsApiIsUsed(): void { $transaction = []; @@ -17,8 +40,12 @@ public function testDoesNothingWhenPaymentsApiIsUsed(): void /** @var DateOfBirth $instance */ $instance = $this->objectManager->create(DateOfBirth::class); + $order = $this->objectManager->create(OrderInterface::class); + $order->setPayment($this->objectManager->create(OrderPaymentInterface::class)); + $order->getPayment()->setMethod('mollie_methods_in3'); + $newTransaction = $instance->process( - $this->objectManager->create(OrderInterface::class), + $order, Payments::CHECKOUT_TYPE, $transaction ); @@ -28,13 +55,17 @@ public function testDoesNothingWhenPaymentsApiIsUsed(): void public function testDoesNothingWhenCustomerDobIsNotSet(): void { - $transaction = ['consumerDateOfBirth' => null]; + $transaction = []; /** @var DateOfBirth $instance */ $instance = $this->objectManager->create(DateOfBirth::class); + $order = $this->objectManager->create(OrderInterface::class); + $order->setPayment($this->objectManager->create(OrderPaymentInterface::class)); + $order->getPayment()->setMethod('mollie_methods_in3'); + $newTransaction = $instance->process( - $this->objectManager->create(OrderInterface::class), + $order, Orders::CHECKOUT_TYPE, $transaction ); @@ -44,24 +75,72 @@ public function testDoesNothingWhenCustomerDobIsNotSet(): void public function testFormatsTheDateCorrect(): void { - $transaction = ['consumerDateOfBirth' => null]; - /** @var DateOfBirth $instance */ $instance = $this->objectManager->create(DateOfBirth::class); /** @var OrderInterface $order */ $order = $this->objectManager->create(OrderInterface::class); + + $order->setPayment($this->objectManager->create(OrderPaymentInterface::class)); + $order->getPayment()->setMethod('mollie_methods_in3'); + $order->setCustomerDob('2016-11-19 00:00:00'); - $newTransaction = $instance->process( + $transaction = $instance->process( $order, Orders::CHECKOUT_TYPE, - $transaction + [] ); $this->assertSame( '2016-11-19', - $newTransaction['consumerDateOfBirth'] + $transaction['consumerDateOfBirth'] + ); + } + + public function testFormatsTheDateCorrectWhenNoTimeIsAvailable(): void + { + /** @var DateOfBirth $instance */ + $instance = $this->objectManager->create(DateOfBirth::class); + + /** @var OrderInterface $order */ + $order = $this->objectManager->create(OrderInterface::class); + + $order->setPayment($this->objectManager->create(OrderPaymentInterface::class)); + $order->getPayment()->setMethod('mollie_methods_in3'); + + $order->setCustomerDob('2016-11-19'); + + $transaction = $instance->process( + $order, + Orders::CHECKOUT_TYPE, + [] + ); + + $this->assertSame( + '2016-11-19', + $transaction['consumerDateOfBirth'] + ); + } + + public function testDoesNothingWhenTheDateCantBeParsed(): void + { + /** @var DateOfBirth $instance */ + $instance = $this->objectManager->create(DateOfBirth::class); + + /** @var OrderInterface $order */ + $order = $this->objectManager->create(OrderInterface::class); + $order->setCustomerDob('nope'); + + $transaction = $instance->process( + $order, + Orders::CHECKOUT_TYPE, + [] + ); + + $this->assertArrayNotHasKey( + 'consumerDateOfBirth', + $transaction ); } } From 4e00135b7481a55c1cedfade5a754ebb4852710f Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Mon, 24 Apr 2023 16:16:00 +0200 Subject: [PATCH 04/10] Bugfix: Catch customer-data.js error --- Test/End-2-end/cypress/support/e2e.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Test/End-2-end/cypress/support/e2e.js b/Test/End-2-end/cypress/support/e2e.js index d1dd1353e81..af5bd7d5035 100644 --- a/Test/End-2-end/cypress/support/e2e.js +++ b/Test/End-2-end/cypress/support/e2e.js @@ -18,3 +18,10 @@ import './commands' // Alternatively you can use CommonJS syntax: // require('./commands') + +Cypress.on('uncaught:exception', (error, runnable) => { + // This is often thrown in Magento 2.4.6, but it doesn't seem to affect the test + if (error.message.indexOf('Cannot read properties of undefined (reading \'remove\')') !== -1) { + return false + } +}) From 51ba801624983ece0f5c735f53c4c088ff872bd1 Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Mon, 24 Apr 2023 16:24:56 +0200 Subject: [PATCH 05/10] Feature: Only send the phonenumber when the payment method is In3 --- Service/Order/TransactionPart/PhoneNumber.php | 5 +++ .../Order/TransactionPart/PhoneNumberTest.php | 34 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Service/Order/TransactionPart/PhoneNumber.php b/Service/Order/TransactionPart/PhoneNumber.php index c9e15ad5f78..db6c56e2cbb 100644 --- a/Service/Order/TransactionPart/PhoneNumber.php +++ b/Service/Order/TransactionPart/PhoneNumber.php @@ -4,6 +4,7 @@ use Magento\Sales\Api\Data\OrderInterface; use Mollie\Payment\Model\Client\Payments; +use Mollie\Payment\Model\Methods\In3; use Mollie\Payment\Service\Order\TransactionPartInterface; class PhoneNumber implements TransactionPartInterface @@ -260,6 +261,10 @@ class PhoneNumber implements TransactionPartInterface public function process(OrderInterface $order, $apiMethod, array $transaction) { + if ($order->getPayment()->getMethod() != In3::CODE) { + return $transaction; + } + if ($apiMethod == Payments::CHECKOUT_TYPE) { return $transaction; } diff --git a/Test/Integration/Service/Order/TransactionPart/PhoneNumberTest.php b/Test/Integration/Service/Order/TransactionPart/PhoneNumberTest.php index 5c14833997e..2f022eaa8df 100644 --- a/Test/Integration/Service/Order/TransactionPart/PhoneNumberTest.php +++ b/Test/Integration/Service/Order/TransactionPart/PhoneNumberTest.php @@ -4,6 +4,7 @@ use Magento\Sales\Api\Data\OrderAddressInterface; use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderPaymentInterface; use Mollie\Payment\Model\Client\Orders; use Mollie\Payment\Model\Client\Payments; use Mollie\Payment\Service\Order\TransactionPart\PhoneNumber; @@ -11,6 +12,24 @@ class PhoneNumberTest extends IntegrationTestCase { + public function testDoesNothingWhenPaymentMethodIsNotIn3(): void + { + /** @var PhoneNumber $instance */ + $instance = $this->objectManager->create(PhoneNumber::class); + + $order = $this->objectManager->create(OrderInterface::class); + $order->setPayment($this->objectManager->create(OrderPaymentInterface::class)); + $order->getPayment()->setMethod('mollie_methods_ideal'); + + $transaction = $instance->process( + $order, + Payments::CHECKOUT_TYPE, + ['billingAddress' => []] + ); + + $this->assertArrayNotHasKey('phone', $transaction['billingAddress']); + } + public function testDoesNothingWhenPaymentsApiIsUsed(): void { $transaction = []; @@ -18,8 +37,12 @@ public function testDoesNothingWhenPaymentsApiIsUsed(): void /** @var PhoneNumber $instance */ $instance = $this->objectManager->create(PhoneNumber::class); + $order = $this->objectManager->create(OrderInterface::class); + $order->setPayment($this->objectManager->create(OrderPaymentInterface::class)); + $order->getPayment()->setMethod('mollie_methods_in3'); + $newTransaction = $instance->process( - $this->objectManager->create(OrderInterface::class), + $order, Payments::CHECKOUT_TYPE, $transaction ); @@ -41,6 +64,9 @@ public function testConvertsPhoneNumbersToTheCorrectFormat( string $expected ): void { $order = $this->loadOrder('100000001'); + $order->setPayment($this->objectManager->create(OrderPaymentInterface::class)); + $order->getPayment()->setMethod('mollie_methods_in3'); + $billingAddress = $order->getBillingAddress(); $billingAddress->setCountryId($countryCode); $billingAddress->setTelephone($phoneNumber); @@ -64,6 +90,9 @@ public function testConvertsPhoneNumbersToTheCorrectFormat( public function testDoesNotAddThePhoneNumberWhenItsEmpty(): void { $order = $this->loadOrder('100000001'); + $order->setPayment($this->objectManager->create(OrderPaymentInterface::class)); + $order->getPayment()->setMethod('mollie_methods_in3'); + $billingAddress = $order->getBillingAddress(); $billingAddress->setCountryId('NL'); $billingAddress->setTelephone(''); @@ -87,6 +116,9 @@ public function testDoesNotAddThePhoneNumberWhenItsEmpty(): void public function testHandlesNullAsPhonenumber(): void { $order = $this->loadOrder('100000001'); + $order->setPayment($this->objectManager->create(OrderPaymentInterface::class)); + $order->getPayment()->setMethod('mollie_methods_in3'); + $billingAddress = $order->getBillingAddress(); $billingAddress->setCountryId('NL'); $billingAddress->setData(OrderAddressInterface::TELEPHONE, null); From c9bc31d1a99a71bff508ef64e58cc21a12a16693 Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Mon, 1 May 2023 09:41:04 +0200 Subject: [PATCH 06/10] Bugfix: Handle empty redirect url --- Service/Mollie/Order/RedirectUrl.php | 23 ++++++++++++++++++- .../Service/Mollie/Order/RedirectUrlTest.php | 22 ++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Service/Mollie/Order/RedirectUrl.php b/Service/Mollie/Order/RedirectUrl.php index c9200f6a307..ee0546dd913 100644 --- a/Service/Mollie/Order/RedirectUrl.php +++ b/Service/Mollie/Order/RedirectUrl.php @@ -2,6 +2,7 @@ namespace Mollie\Payment\Service\Mollie\Order; +use Magento\Framework\Message\ManagerInterface; use Magento\Framework\UrlInterface; use Magento\Sales\Api\Data\OrderInterface; use Mollie\Payment\Config; @@ -22,12 +23,19 @@ class RedirectUrl */ private $url; + /** + * @var ManagerInterface + */ + private $messageManager; + public function __construct( Config $config, - UrlInterface $url + UrlInterface $url, + ManagerInterface $messageManager ) { $this->config = $config; $this->url = $url; + $this->messageManager = $messageManager; } public function execute(Mollie $methodInstance, OrderInterface $order): string @@ -46,6 +54,19 @@ public function execute(Mollie $methodInstance, OrderInterface $order): string return $this->url->getUrl('checkout/onepage/success/'); } + if (!$redirectUrl) { + $this->config->addToLog( + 'error', + 'RedirectUrl: No redirect url found for order ' . $order->getIncrementId() + ); + + $this->messageManager->addErrorMessage( + __('Something went wrong while trying to redirect you to Mollie. Please try again later.') + ); + + return $this->url->getUrl('checkout/cart'); + } + return $redirectUrl; } } diff --git a/Test/Integration/Service/Mollie/Order/RedirectUrlTest.php b/Test/Integration/Service/Mollie/Order/RedirectUrlTest.php index 2825b1fb17d..155e410fc76 100644 --- a/Test/Integration/Service/Mollie/Order/RedirectUrlTest.php +++ b/Test/Integration/Service/Mollie/Order/RedirectUrlTest.php @@ -2,6 +2,7 @@ namespace Mollie\Payment\Test\Integration\Service\Mollie\Order; +use Magento\Framework\Message\ManagerInterface; use Magento\Sales\Api\Data\OrderInterface; use Mollie\Payment\Model\Methods\ApplePay; use Mollie\Payment\Model\Methods\Creditcard; @@ -64,4 +65,25 @@ public function testRedirectsToSuccessPageForCreditCard(): void $this->assertStringContainsString('checkout/onepage/success', $result); } + + public function testRedirectsToTheCartWhenNoUrlIsAvailable(): void + { + $mollieMock = $this->createMock(Ideal::class); + $mollieMock->method('startTransaction')->willReturn(null); // Should be empty + + /** @var ManagerInterface $messageManager */ + $messageManager = $this->objectManager->get(ManagerInterface::class); + + /** @var RedirectUrl $instance */ + $instance = $this->objectManager->get(RedirectUrl::class); + $result = $instance->execute($mollieMock, $this->objectManager->create(OrderInterface::class)); + + $this->assertSame(1, $messageManager->getMessages()->getCount()); + $this->assertEquals( + 'Something went wrong while trying to redirect you to Mollie. Please try again later.', + $messageManager->getMessages()->getItemsByType('error')[0]->getText() + ); + + $this->assertStringContainsString('checkout/cart', $result); + } } From e28d08b1281e306334a59ed90c168ee652216a40 Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Mon, 8 May 2023 09:45:47 +0200 Subject: [PATCH 07/10] Improvement: Better error message when something went wrong during placing transaction --- Controller/Checkout/Process.php | 6 +++--- Test/Fakes/Model/Methods/IdealFake.php | 4 ++-- Test/Integration/Controller/Checkout/ProcessTest.php | 2 +- Test/Integration/Controller/Checkout/WebhookTest.php | 2 +- i18n/de_DE.csv | 2 +- i18n/en_US.csv | 2 +- i18n/es_ES.csv | 2 +- i18n/fr_FR.csv | 2 +- i18n/nl_NL.csv | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Controller/Checkout/Process.php b/Controller/Checkout/Process.php index e3759e1f762..eef4f591e95 100644 --- a/Controller/Checkout/Process.php +++ b/Controller/Checkout/Process.php @@ -122,7 +122,7 @@ public function execute() return $this->_redirect($redirect->getData('path')); } catch (\Exception $e) { $this->mollieHelper->addTolog('error', $e->getMessage()); - $this->messageManager->addErrorMessage(__('Something went wrong.')); + $this->messageManager->addErrorMessage(__('Transaction failed. Please verify your billing information and payment method, and try again.')); return $this->_redirect($this->redirectOnError->getUrl()); } } @@ -157,7 +157,7 @@ protected function handleNonSuccessResult(array $result, array $orderIds): Respo protected function addResultMessage(array $result) { if (!isset($result['status'])) { - $this->messageManager->addErrorMessage(__('Something went wrong.')); + $this->messageManager->addErrorMessage(__('Transaction failed. Please verify your billing information and payment method, and try again.')); return; } @@ -171,7 +171,7 @@ protected function addResultMessage(array $result) return; } - $this->messageManager->addErrorMessage(__('Something went wrong.')); + $this->messageManager->addErrorMessage(__('Transaction failed. Please verify your billing information and payment method, and try again.')); } /** diff --git a/Test/Fakes/Model/Methods/IdealFake.php b/Test/Fakes/Model/Methods/IdealFake.php index b81c76fdc6e..b9fa4960104 100644 --- a/Test/Fakes/Model/Methods/IdealFake.php +++ b/Test/Fakes/Model/Methods/IdealFake.php @@ -9,7 +9,7 @@ class IdealFake extends Ideal { public function startTransaction(Order $order) { - throw new \Exception('[TEST] Something went wrong'); + throw new \Exception('[TEST] Transaction failed. Please verify your billing information and payment method, and try again.'); } -} \ No newline at end of file +} diff --git a/Test/Integration/Controller/Checkout/ProcessTest.php b/Test/Integration/Controller/Checkout/ProcessTest.php index 036f3849f49..7d609109cc3 100644 --- a/Test/Integration/Controller/Checkout/ProcessTest.php +++ b/Test/Integration/Controller/Checkout/ProcessTest.php @@ -26,7 +26,7 @@ public function testDoesRedirectsToCartWhenNoIdProvided() public function testRedirectsToCartOnException() { $mollieModel = $this->createMock(Mollie::class); - $mollieModel->method('processTransaction')->willThrowException(new \Exception('[TEST] Something went wrong')); + $mollieModel->method('processTransaction')->willThrowException(new \Exception('[TEST] Transaction failed. Please verify your billing information and payment method, and try again.')); $this->_objectManager->addSharedInstance($mollieModel, Mollie::class); diff --git a/Test/Integration/Controller/Checkout/WebhookTest.php b/Test/Integration/Controller/Checkout/WebhookTest.php index 8caa9f95421..818993f5275 100644 --- a/Test/Integration/Controller/Checkout/WebhookTest.php +++ b/Test/Integration/Controller/Checkout/WebhookTest.php @@ -12,7 +12,7 @@ public function testSetsTheStatusCodeTo503WhenTheOrderProcessFails() { $mollieModel = $this->createMock(Mollie::class); $mollieModel->method('getOrderIdsByTransactionId')->willReturn([123]); - $mollieModel->method('processTransaction')->willThrowException(new \Exception('[TEST] Something went wrong')); + $mollieModel->method('processTransaction')->willThrowException(new \Exception('[TEST] Transaction failed. Please verify your billing information and payment method, and try again.')); $this->_objectManager->addSharedInstance($mollieModel, Mollie::class); diff --git a/i18n/de_DE.csv b/i18n/de_DE.csv index 20ed665aa2a..a7cf05eca9e 100644 --- a/i18n/de_DE.csv +++ b/i18n/de_DE.csv @@ -18,7 +18,7 @@ "Invalid return, missing order id.","Ungültige Ausgabe, fehlende Bestellnummer." "Invalid return from Mollie.","Ungültige Ausgabe von Mollie." "There was an error checking the transaction status.","Bei der Überprüfung des Transaktionsstatus ist ein Fehler aufgetreten." -"Something went wrong.","Etwas ist schiefgegangen." +"Transaction failed. Please verify your billing information and payment method, and try again.","Transaktion fehlgeschlagen. Bitte überprüfen Sie Ihre Rechnungsinformationen und Zahlungsmethode, und versuchen Sie es erneut." "Payment canceled, please try again.","Zahlung abgebrochen. Bitte versuchen Sie es noch einmal." "Payment of type %1 has been rejected. Decision is based on order and outcome of risk assessment.","Die Zahlung vom Typ %1 wurde abgelehnt. Die Entscheidung basiert auf der Bestellung und dem Ergebnis der Risikobewertung." "Payment Method not found","Zahlungsmethode nicht gefunden" diff --git a/i18n/en_US.csv b/i18n/en_US.csv index 63e4d55c790..136762c9a08 100644 --- a/i18n/en_US.csv +++ b/i18n/en_US.csv @@ -18,7 +18,7 @@ "Invalid return, missing order id.","Invalid return, missing order id." "Invalid return from Mollie.","Invalid return from Mollie." "There was an error checking the transaction status.","There was an error checking the transaction status." -"Something went wrong.","Something went wrong." +"Transaction failed. Please verify your billing information and payment method, and try again.","Transaction failed. Please verify your billing information and payment method, and try again." "Payment canceled, please try again.","Payment canceled, please try again." "Payment of type %1 has been rejected. Decision is based on order and outcome of risk assessment.","Payment of type %1 has been rejected. Decision is based on order and outcome of risk assessment." "Payment Method not found","Payment Method not found" diff --git a/i18n/es_ES.csv b/i18n/es_ES.csv index e23fba36d0d..18966b5ec74 100644 --- a/i18n/es_ES.csv +++ b/i18n/es_ES.csv @@ -18,7 +18,7 @@ "Invalid return, missing order id.","Devolución no válida, falta la ID del pedido." "Invalid return from Mollie.","Devolución no válida de Mollie." "There was an error checking the transaction status.","Se ha producido un error al comprobar el estado de la transacción." -"Something went wrong.","Algo ha salido mal." +"Transaction failed. Please verify your billing information and payment method, and try again.","Transacción fallida. Por favor, verifica tu información de facturación y método de pago, e inténtalo de nuevo." "Payment canceled, please try again.","Pago cancelado, inténtelo de nuevo." "Payment of type %1 has been rejected. Decision is based on order and outcome of risk assessment.","El pago del tipo %1 ha sido rechazado. La decisión se basa en el pedido y el resultado de la evaluación de riesgos." "Payment Method not found","Método de pago no encontrado" diff --git a/i18n/fr_FR.csv b/i18n/fr_FR.csv index 7a8d700aa82..4ed6a443f96 100644 --- a/i18n/fr_FR.csv +++ b/i18n/fr_FR.csv @@ -18,7 +18,7 @@ "Invalid return, missing order id.","Retour non valide, ID de commande manquant." "Invalid return from Mollie.","Retour non valide de Mollie." "There was an error checking the transaction status.","Une erreur s'est produite lors de la vérification du statut de la transaction." -"Something went wrong.","Une erreur s'est produite." +"Transaction failed. Please verify your billing information and payment method, and try again.","Échec de la transaction. Veuillez vérifier vos informations de facturation et votre méthode de paiement, puis réessayez." "Payment canceled, please try again.","Paiement annulé, veuillez réessayer." "Payment of type %1 has been rejected. Decision is based on order and outcome of risk assessment.","Le paiement de type %1 a été rejeté. La décision s'appuie sur la commande et la conclusion de l'évaluation des risques." "Payment Method not found","Aucune méthode de paiement n'a été trouvée" diff --git a/i18n/nl_NL.csv b/i18n/nl_NL.csv index 94665457be3..444023e8ce6 100644 --- a/i18n/nl_NL.csv +++ b/i18n/nl_NL.csv @@ -18,7 +18,7 @@ "Invalid return, missing order id.","Ongeldige respons, ontbrekende bestelling-ID" "Invalid return from Mollie.","Ongeldige respons van Mollie." "There was an error checking the transaction status.","Er is een fout opgetreden bij het controleren van de transactiestatus." -"Something went wrong.","Er is iets misgegaan." +"Transaction failed. Please verify your billing information and payment method, and try again.","Transactie mislukt. Controleer uw factuurgegevens en betaalmethode, en probeer het opnieuw." "Payment canceled, please try again.","Betaling geannuleerd, probeer het opnieuw." "Payment of type %1 has been rejected. Decision is based on order and outcome of risk assessment.","Betaling van het type %1 is geweigerd. De beslissing is gebaseerd op de bestelling en het resultaat van de risicobeoordeling." "Payment Method not found","Betaalmethode niet gevonden" From a369eeb872be060775cc145bc4be5d4bf535ea42 Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Mon, 8 May 2023 11:00:49 +0200 Subject: [PATCH 08/10] Improvement: Allow to skip metadata validation --- Service/Mollie/ValidateMetadata.php | 16 +++++++++++++++- .../Service/Mollie/ValidateMetadataTest.php | 12 ++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Service/Mollie/ValidateMetadata.php b/Service/Mollie/ValidateMetadata.php index ecf62234d13..2aef2465133 100644 --- a/Service/Mollie/ValidateMetadata.php +++ b/Service/Mollie/ValidateMetadata.php @@ -13,17 +13,31 @@ class ValidateMetadata */ private $config; + /** + * @var bool + */ + private $skipValidation = false; + public function __construct( Config $config ) { $this->config = $config; } + public function skipValidation(): void + { + $this->skipValidation = true; + } + /** * @throws LocalizedException */ - public function execute(\stdClass $metadata, OrderInterface $order): void + public function execute(\stdClass $metadata = null, OrderInterface $order): void { + if ($this->skipValidation) { + return; + } + if (isset($metadata->order_id)) { $this->validateSingleOrder($metadata, $order); return; diff --git a/Test/Integration/Service/Mollie/ValidateMetadataTest.php b/Test/Integration/Service/Mollie/ValidateMetadataTest.php index e471b0312f4..50e0fad5d81 100644 --- a/Test/Integration/Service/Mollie/ValidateMetadataTest.php +++ b/Test/Integration/Service/Mollie/ValidateMetadataTest.php @@ -49,6 +49,18 @@ public function testThrowsExceptionWhenNoMetadataIsSet(): void $instance->execute($metadata, $this->getOrder()); } + public function testValidationCanBeSkipped(): void + { + /** @var ValidateMetadata $instance */ + $instance = $this->objectManager->create(ValidateMetadata::class); + + $instance->skipValidation(); + + $instance->execute(null, $this->getOrder()); + + $this->expectNotToPerformAssertions(); + } + public function getOrder(): OrderInterface { /** @var OrderInterface $order */ From 43b37ec4418eab84ecd4b175dc20b6d7243f8bfe Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Mon, 8 May 2023 15:33:13 +0200 Subject: [PATCH 09/10] Bugfix: Ignore expired orders if there are multiple transactions --- Api/Data/TransactionToOrderInterface.php | 12 ++ .../Orders/Processors/ExpiredProcessor.php | 66 +++++++++++ Model/Client/Payments.php | 19 ++- Model/Client/Payments/ProcessTransaction.php | 10 ++ .../Processors/ExpiredStatusProcessor.php | 64 +++++++++++ Model/TransactionToOrder.php | 19 +++ Service/Order/ExpiredOrderToTransaction.php | 69 +++++++++++ .../Processors/ExpiredProcessorTest.php | 107 +++++++++++++++++ .../Processors/ExpiredStatusProcessorTest.php | 108 ++++++++++++++++++ Test/Integration/MolliePaymentBuilder.php | 5 + .../Order/ExpiredOrderToTransactionTest.php | 43 +++++++ etc/db_schema.xml | 1 + etc/db_schema_whitelist.json | 3 +- etc/di.xml | 5 +- 14 files changed, 528 insertions(+), 3 deletions(-) create mode 100644 Model/Client/Orders/Processors/ExpiredProcessor.php create mode 100644 Model/Client/Payments/Processors/ExpiredStatusProcessor.php create mode 100644 Service/Order/ExpiredOrderToTransaction.php create mode 100644 Test/Integration/Model/Client/Orders/Processors/ExpiredProcessorTest.php create mode 100644 Test/Integration/Model/Client/Payments/Processors/ExpiredStatusProcessorTest.php create mode 100644 Test/Integration/Service/Order/ExpiredOrderToTransactionTest.php diff --git a/Api/Data/TransactionToOrderInterface.php b/Api/Data/TransactionToOrderInterface.php index ed33d70c76e..b28a6965e8e 100644 --- a/Api/Data/TransactionToOrderInterface.php +++ b/Api/Data/TransactionToOrderInterface.php @@ -10,6 +10,7 @@ interface TransactionToOrderInterface extends ExtensibleDataInterface { public const TRANSACTION_ID = 'transaction_id'; public const ORDER_ID = 'order_id'; + public const SKIPPED = 'skipped'; public const CREATED_AT = 'created_at'; /** @@ -45,6 +46,17 @@ public function getCreatedAt(): ?string; */ public function setCreatedAt(string $created_at): \Mollie\Payment\Api\Data\TransactionToOrderInterface; + /** + * @return string|null + */ + public function getSkipped(): ?int; + + /** + * @param int $skipped + * @return \Mollie\Payment\Api\Data\TransactionToOrderInterface + */ + public function setSkipped(int $skipped): \Mollie\Payment\Api\Data\TransactionToOrderInterface; + /** * @return \Mollie\Payment\Api\Data\TransactionToOrderExtensionInterface|null */ diff --git a/Model/Client/Orders/Processors/ExpiredProcessor.php b/Model/Client/Orders/Processors/ExpiredProcessor.php new file mode 100644 index 00000000000..e5c53d2bb12 --- /dev/null +++ b/Model/Client/Orders/Processors/ExpiredProcessor.php @@ -0,0 +1,66 @@ +cancelledProcessor = $cancelledProcessor; + $this->processTransactionResponseFactory = $processTransactionResponseFactory; + $this->expiredOrderToTransaction = $expiredOrderToTransaction; + } + + public function process( + OrderInterface $magentoOrder, + Order $mollieOrder, + string $type, + ProcessTransactionResponse $response + ): ?ProcessTransactionResponse { + if ($this->shouldCancelProcessing($magentoOrder)) { + return $this->processTransactionResponseFactory->create([ + 'success' => false, + 'status' => $mollieOrder->status, + 'order_id' => $magentoOrder->getEntityId(), + 'type' => $type + ]); + } + + return $this->cancelledProcessor->process($magentoOrder, $mollieOrder, $type, $response); + } + + private function shouldCancelProcessing(OrderInterface $order): bool + { + if (!$this->expiredOrderToTransaction->hasMultipleTransactions($order)) { + return false; + } + + $this->expiredOrderToTransaction->markTransactionAsSkipped($order->getMollieTransactionId()); + return true; + } +} diff --git a/Model/Client/Payments.php b/Model/Client/Payments.php index d75d7f9f3fc..598a57c9714 100644 --- a/Model/Client/Payments.php +++ b/Model/Client/Payments.php @@ -26,6 +26,7 @@ use Mollie\Payment\Service\Order\OrderAmount; use Mollie\Payment\Service\Order\CancelOrder; use Mollie\Payment\Service\Order\OrderCommentHistory; +use Mollie\Payment\Service\Order\ExpiredOrderToTransaction; use Mollie\Payment\Service\Order\SaveAdditionalInformationDetails; use Mollie\Payment\Service\Order\SendOrderEmails; use Mollie\Payment\Service\Order\Transaction; @@ -125,6 +126,11 @@ class Payments extends AbstractModel */ private $saveAdditionalInformationDetails; + /** + * @var ExpiredOrderToTransaction + */ + private $expiredOrderToTransaction; + /** * Payments constructor. * @@ -146,6 +152,7 @@ class Payments extends AbstractModel * @param ProcessTransaction $processTransaction * @param ValidateMetadata $validateMetadata * @param SaveAdditionalInformationDetails $saveAdditionalInformationDetails + * @param ExpiredOrderToTransaction $expiredOrderToTransaction */ public function __construct( OrderRepository $orderRepository, @@ -165,7 +172,8 @@ public function __construct( LinkTransactionToOrder $linkTransactionToOrder, ProcessTransaction $processTransaction, ValidateMetadata $validateMetadata, - SaveAdditionalInformationDetails $saveAdditionalInformationDetails + SaveAdditionalInformationDetails $saveAdditionalInformationDetails, + ExpiredOrderToTransaction $expiredOrderToTransaction ) { $this->orderRepository = $orderRepository; $this->checkoutSession = $checkoutSession; @@ -185,6 +193,7 @@ public function __construct( $this->processTransaction = $processTransaction; $this->validateMetadata = $validateMetadata; $this->saveAdditionalInformationDetails = $saveAdditionalInformationDetails; + $this->expiredOrderToTransaction = $expiredOrderToTransaction; } /** @@ -422,6 +431,14 @@ public function processTransaction(Order $order, $mollieApi, $type = 'webhook', $this->mollieHelper->addTolog('success', $msg); return $msg; } + if ($status == 'expired') { + if ($this->expiredOrderToTransaction->hasMultipleTransactions($order)) { + $this->expiredOrderToTransaction->markTransactionAsSkipped($transactionId); + $msg = ['success' => false, 'status' => $status, 'order_id' => $orderId, 'type' => $type]; + $this->mollieHelper->addTolog('success', $msg); + return $msg; + } + } if ($status == 'canceled' || $status == 'failed' || $status == 'expired') { if ($type == 'webhook') { $this->cancelOrder->execute($order, $status); diff --git a/Model/Client/Payments/ProcessTransaction.php b/Model/Client/Payments/ProcessTransaction.php index f432eaa2a6b..a5be68abbfa 100644 --- a/Model/Client/Payments/ProcessTransaction.php +++ b/Model/Client/Payments/ProcessTransaction.php @@ -113,6 +113,16 @@ public function execute( ); } + if ($status == 'expired') { + return $this->paymentProcessors->process( + 'expired', + $magentoOrder, + $molliePayment, + $type, + $defaultResponse + ); + } + if ($status == 'canceled' || $status == 'failed' || $status == 'expired') { return $this->paymentProcessors->process( 'failed', diff --git a/Model/Client/Payments/Processors/ExpiredStatusProcessor.php b/Model/Client/Payments/Processors/ExpiredStatusProcessor.php new file mode 100644 index 00000000000..9998256fb8f --- /dev/null +++ b/Model/Client/Payments/Processors/ExpiredStatusProcessor.php @@ -0,0 +1,64 @@ +expiredOrderToTransaction = $expiredOrderToTransaction; + $this->failedStatusProcessor = $failedStatusProcessor; + $this->processTransactionResponseFactory = $processTransactionResponseFactory; + } + + public function process( + OrderInterface $magentoOrder, + Payment $molliePayment, + string $type, + ProcessTransactionResponse $response + ): ?ProcessTransactionResponse { + if ($this->shouldCancelProcessing($magentoOrder)) { + return $this->processTransactionResponseFactory->create([ + 'success' => false, + 'status' => $molliePayment->status, + 'order_id' => $magentoOrder->getEntityId(), + 'type' => $type + ]); + } + + return $this->failedStatusProcessor->process($magentoOrder, $molliePayment, $type, $response); + } + + private function shouldCancelProcessing(OrderInterface $order): bool + { + if (!$this->expiredOrderToTransaction->hasMultipleTransactions($order)) { + return false; + } + + $this->expiredOrderToTransaction->markTransactionAsSkipped($order->getMollieTransactionId()); + return true; + } +} diff --git a/Model/TransactionToOrder.php b/Model/TransactionToOrder.php index 977aa61562a..2c61ccbed6d 100644 --- a/Model/TransactionToOrder.php +++ b/Model/TransactionToOrder.php @@ -73,6 +73,25 @@ public function setCreatedAt(string $created_at): TransactionToOrderInterface return $this->setData(self::CREATED_AT, $created_at); } + /** + * Get skipped + * @return int|null + */ + public function getSkipped(): ?int + { + return (int)$this->getData(self::SKIPPED); + } + + /** + * Set skipped + * @param int $skipped + * @return TransactionToOrderInterface + */ + public function setSkipped(int $skipped): TransactionToOrderInterface + { + return $this->setData(self::SKIPPED, $skipped); + } + /** * Retrieve existing extension attributes object or create a new one. * @return TransactionToOrderExtensionInterface|null diff --git a/Service/Order/ExpiredOrderToTransaction.php b/Service/Order/ExpiredOrderToTransaction.php new file mode 100644 index 00000000000..f24e79b397d --- /dev/null +++ b/Service/Order/ExpiredOrderToTransaction.php @@ -0,0 +1,69 @@ +transactionToOrderRepository = $transactionToOrderRepository; + $this->criteriaBuilderFactory = $criteriaBuilderFactory; + } + + public function hasMultipleTransactions(OrderInterface $order): bool + { + $criteria = $this->criteriaBuilderFactory->create(); + $criteria->addFilter('skipped', '0'); + $criteria->addFilter('order_id', $order->getEntityId()); + + $result = $this->transactionToOrderRepository->getList($criteria->create()); + + return $result->getTotalCount() > 1; + } + + public function getByTransactionId(string $transactionId): TransactionToOrderInterface + { + $criteria = $this->criteriaBuilderFactory->create(); + $criteria->addFilter('transaction_id', $transactionId); + + $result = $this->transactionToOrderRepository->getList($criteria->create()); + + $items = $result->getItems(); + + if (empty($items)) { + throw new NoSuchEntityException(__("Transaction with ID %1 not found", $transactionId)); + } + + return array_shift($items); + } + + /** + * A transaction can be skipped if there are multiple transactions for a single order, and this transaction + * is expired. In that case, we don't want to cancel the order, but we do want to mark the transaction as skipped. + * When the next transaction is also expired, and there are no other transactions left, we will cancel the order. + */ + public function markTransactionAsSkipped(string $transactionId): void + { + $transaction = $this->getByTransactionId($transactionId); + $transaction->setSkipped(true); + + $this->transactionToOrderRepository->save($transaction); + } +} diff --git a/Test/Integration/Model/Client/Orders/Processors/ExpiredProcessorTest.php b/Test/Integration/Model/Client/Orders/Processors/ExpiredProcessorTest.php new file mode 100644 index 00000000000..c3c00eea740 --- /dev/null +++ b/Test/Integration/Model/Client/Orders/Processors/ExpiredProcessorTest.php @@ -0,0 +1,107 @@ +loadOrderById('100000001'); + $order->setMollieTransactionId($transactionId); + + /** @var TransactionToOrderInterface $transactionToOrder */ + $transactionToOrder = $this->objectManager->create(TransactionToOrderInterface::class); + $transactionToOrder->setOrderId($order->getEntityId()); + $transactionToOrder->setTransactionId($transactionId); + $this->objectManager->get(TransactionToOrderRepositoryInterface::class)->save($transactionToOrder); + + /** @var ExpiredProcessor $instance */ + $instance = $this->objectManager->get(ExpiredProcessor::class); + $instance->process( + $order, + $this->getMollieOrder(), + 'webhook', + $this->objectManager->create(ProcessTransactionResponse::class, [ + 'success' => true, + 'status' => 'test', + 'order_id' => '-01', + 'type' => 'webhook', + ]) + ); + + $this->assertEquals(Order::STATE_CANCELED, $order->getState()); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testMarksTransactionAsSkippedWhenThereAreMultipleTransactions(): void + { + $transaction1 = uniqid(); + $transaction2 = uniqid(); + + $order = $this->loadOrderById('100000001'); + $order->setMollieTransactionId($transaction1); + + /** @var TransactionToOrderInterface $transactionToOrder1 */ + $transactionToOrder1 = $this->objectManager->create(TransactionToOrderInterface::class); + $transactionToOrder1->setOrderId($order->getEntityId()); + $transactionToOrder1->setTransactionId($transaction1); + $this->objectManager->get(TransactionToOrderRepositoryInterface::class)->save($transactionToOrder1); + + /** @var TransactionToOrderInterface $transactionToOrder2 */ + $transactionToOrder2 = $this->objectManager->create(TransactionToOrderInterface::class); + $transactionToOrder2->setOrderId($order->getEntityId()); + $transactionToOrder2->setTransactionId($transaction2); + $this->objectManager->get(TransactionToOrderRepositoryInterface::class)->save($transactionToOrder2); + + /** @var ExpiredProcessor $instance */ + $instance = $this->objectManager->get(ExpiredProcessor::class); + $instance->process( + $order, + $this->getMollieOrder(), + 'webhook', + $this->objectManager->create(ProcessTransactionResponse::class, [ + 'success' => true, + 'status' => 'test', + 'order_id' => '-01', + 'type' => 'webhook', + ]) + ); + + /** @var OrderToTransaction $transactionToOrder */ + $transactionToOrder = $this->objectManager->get(ExpiredOrderToTransaction::class); + $transaction = $transactionToOrder->getByTransactionId($transaction1); + + $this->assertEquals(1, $transaction->getSkipped()); + $this->assertEquals(Order::STATE_PROCESSING, $order->getState()); + } + + private function getMollieOrder(): \Mollie\Api\Resources\Order + { + /** @var MollieOrderBuilder $orderBuilder */ + $orderBuilder = $this->objectManager->create(MollieOrderBuilder::class); + $orderBuilder->setAmount(100, 'USD'); + $orderBuilder->setStatus('expired'); + + return $orderBuilder->build(); + } +} diff --git a/Test/Integration/Model/Client/Payments/Processors/ExpiredStatusProcessorTest.php b/Test/Integration/Model/Client/Payments/Processors/ExpiredStatusProcessorTest.php new file mode 100644 index 00000000000..90246cd9b8a --- /dev/null +++ b/Test/Integration/Model/Client/Payments/Processors/ExpiredStatusProcessorTest.php @@ -0,0 +1,108 @@ +loadOrderById('100000001'); + $order->setMollieTransactionId($transactionId); + + /** @var TransactionToOrderInterface $transactionToOrder */ + $transactionToOrder = $this->objectManager->create(TransactionToOrderInterface::class); + $transactionToOrder->setOrderId($order->getEntityId()); + $transactionToOrder->setTransactionId($transactionId); + $this->objectManager->get(TransactionToOrderRepositoryInterface::class)->save($transactionToOrder); + + /** @var ExpiredStatusProcessor $instance */ + $instance = $this->objectManager->get(ExpiredStatusProcessor::class); + $instance->process( + $order, + $this->getMolliePayment(), + 'webhook', + $this->objectManager->create(ProcessTransactionResponse::class, [ + 'success' => true, + 'status' => 'test', + 'order_id' => '-01', + 'type' => 'webhook', + ]) + ); + + $this->assertEquals(Order::STATE_CANCELED, $order->getState()); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testMarksTransactionAsSkippedWhenThereAreMultipleTransactions(): void + { + $transaction1 = uniqid(); + $transaction2 = uniqid(); + + $order = $this->loadOrderById('100000001'); + $order->setMollieTransactionId($transaction1); + + /** @var TransactionToOrderInterface $transactionToOrder1 */ + $transactionToOrder1 = $this->objectManager->create(TransactionToOrderInterface::class); + $transactionToOrder1->setOrderId($order->getEntityId()); + $transactionToOrder1->setTransactionId($transaction1); + $this->objectManager->get(TransactionToOrderRepositoryInterface::class)->save($transactionToOrder1); + + /** @var TransactionToOrderInterface $transactionToOrder2 */ + $transactionToOrder2 = $this->objectManager->create(TransactionToOrderInterface::class); + $transactionToOrder2->setOrderId($order->getEntityId()); + $transactionToOrder2->setTransactionId($transaction2); + $this->objectManager->get(TransactionToOrderRepositoryInterface::class)->save($transactionToOrder2); + + /** @var ExpiredStatusProcessor $instance */ + $instance = $this->objectManager->get(ExpiredStatusProcessor::class); + $instance->process( + $order, + $this->getMolliePayment(), + 'webhook', + $this->objectManager->create(ProcessTransactionResponse::class, [ + 'success' => true, + 'status' => 'test', + 'order_id' => '-01', + 'type' => 'webhook', + ]) + ); + + /** @var ExpiredOrderToTransaction $transactionToOrder */ + $transactionToOrder = $this->objectManager->get(ExpiredOrderToTransaction::class); + $transaction = $transactionToOrder->getByTransactionId($transaction1); + + $this->assertEquals(1, $transaction->getSkipped()); + $this->assertEquals(Order::STATE_PROCESSING, $order->getState()); + } + + private function getMolliePayment(): Payment + { + /** @var MolliePaymentBuilder $orderBuilder */ + $orderBuilder = $this->objectManager->create(MolliePaymentBuilder::class); + $orderBuilder->setAmount(100, 'USD'); + $orderBuilder->setStatus('expired'); + + return $orderBuilder->build(); + } +} diff --git a/Test/Integration/MolliePaymentBuilder.php b/Test/Integration/MolliePaymentBuilder.php index 9f7ceca66f4..c43c5f79af5 100644 --- a/Test/Integration/MolliePaymentBuilder.php +++ b/Test/Integration/MolliePaymentBuilder.php @@ -34,6 +34,11 @@ public function setAmount(float $value, $currency = 'EUR'): void $this->payment->amount->currency = $currency; } + public function setStatus(string $status): void + { + $this->payment->status = $status; + } + public function build(): Payment { return $this->payment; diff --git a/Test/Integration/Service/Order/ExpiredOrderToTransactionTest.php b/Test/Integration/Service/Order/ExpiredOrderToTransactionTest.php new file mode 100644 index 00000000000..b22f83d419b --- /dev/null +++ b/Test/Integration/Service/Order/ExpiredOrderToTransactionTest.php @@ -0,0 +1,43 @@ +loadOrderById('100000001'); + + /** @var TransactionToOrderInterface $transactionToOrder1 */ + $transactionToOrder1 = $this->objectManager->create(TransactionToOrderInterface::class); + $transactionToOrder1->setOrderId($order->getEntityId()); + $transactionToOrder1->setTransactionId($transaction1); + $transactionToOrder1->setSkipped(true); + $this->objectManager->get(TransactionToOrderRepositoryInterface::class)->save($transactionToOrder1); + + /** @var TransactionToOrderInterface $transactionToOrder2 */ + $transactionToOrder2 = $this->objectManager->create(TransactionToOrderInterface::class); + $transactionToOrder2->setOrderId($order->getEntityId()); + $transactionToOrder2->setTransactionId($transaction2); + $transactionToOrder1->setSkipped(false); + $this->objectManager->get(TransactionToOrderRepositoryInterface::class)->save($transactionToOrder2); + + /** @var ExpiredOrderToTransaction $instance */ + $instance = $this->objectManager->create(ExpiredOrderToTransaction::class); + + $this->assertFalse($instance->hasMultipleTransactions($order)); + } +} diff --git a/etc/db_schema.xml b/etc/db_schema.xml index 9d1b6ea8dc0..f2c763834b2 100644 --- a/etc/db_schema.xml +++ b/etc/db_schema.xml @@ -116,6 +116,7 @@ + diff --git a/etc/db_schema_whitelist.json b/etc/db_schema_whitelist.json index 1688dab146d..57851c49868 100644 --- a/etc/db_schema_whitelist.json +++ b/etc/db_schema_whitelist.json @@ -83,6 +83,7 @@ "entity_id": true, "transaction_id": true, "order_id": true, + "skipped": true, "created_at": true }, "constaint": { @@ -158,4 +159,4 @@ "PRIMARY": true } } -} \ No newline at end of file +} diff --git a/etc/di.xml b/etc/di.xml index ed1237983ed..73bf94c6543 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -284,7 +284,7 @@ Mollie\Payment\Model\Client\Orders\Processors\CancelledProcessor - Mollie\Payment\Model\Client\Orders\Processors\CancelledProcessor + Mollie\Payment\Model\Client\Orders\Processors\ExpiredProcessor Mollie\Payment\Model\Client\Orders\Processors\SuccessfulPayment @@ -302,6 +302,9 @@ Mollie\Payment\Model\Client\Payments\Processors\FailedStatusProcessor + + Mollie\Payment\Model\Client\Payments\Processors\ExpiredStatusProcessor + Mollie\Payment\Model\Client\Payments\Processors\SendEmailForBanktransfer From 06f0e4da0e8c11b42e41d476f3b8687ae7bfce39 Mon Sep 17 00:00:00 2001 From: Marvin Besselsen Date: Tue, 9 May 2023 19:31:44 +0200 Subject: [PATCH 10/10] Version bump --- composer.json | 2 +- etc/config.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 4dd639557a2..9d5a879084d 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "mollie/magento2", "description": "Mollie Payment Module for Magento 2", - "version": "2.24.1", + "version": "2.25.0", "keywords": [ "mollie", "payment", diff --git a/etc/config.xml b/etc/config.xml index 2d6c13aec6b..a6d9d42b751 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -3,7 +3,7 @@ - v2.24.1 + v2.25.0 0 0 test