From e144658705fd1f35c4ebd1c19971c8e20a153df1 Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Thu, 22 Feb 2024 11:09:06 +0100 Subject: [PATCH 01/16] Improvement: Bump CI versions --- .github/workflows/end-2-end-test.yml | 6 +++--- .github/workflows/integration-test.yml | 6 +++--- .github/workflows/phpstan.yml | 6 +++--- .github/workflows/setup-di-compile.yml | 6 +++--- .github/workflows/unit-test.yml | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/end-2-end-test.yml b/.github/workflows/end-2-end-test.yml index 47c67b07bbe..66af1977d26 100644 --- a/.github/workflows/end-2-end-test.yml +++ b/.github/workflows/end-2-end-test.yml @@ -47,9 +47,9 @@ jobs: matrix: include: - PHP_VERSION: php74-fpm - MAGENTO_VERSION: 2.3.7-p3 - - PHP_VERSION: php81-fpm - MAGENTO_VERSION: 2.4.6 + MAGENTO_VERSION: 2.3.7-p4 + - PHP_VERSION: php82-fpm + MAGENTO_VERSION: 2.4.6-p4 runs-on: ubuntu-latest env: PHP_VERSION: ${{ matrix.PHP_VERSION }} diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 8d146e1245d..706780f5952 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -7,15 +7,15 @@ jobs: matrix: include: - PHP_VERSION: php73-fpm - MAGENTO_VERSION: 2.3.7-p3 + MAGENTO_VERSION: 2.3.7-p4 - PHP_VERSION: php74-fpm MAGENTO_VERSION: 2.4.0 - PHP_VERSION: php74-fpm MAGENTO_VERSION: 2.4.3-with-replacements - PHP_VERSION: php81-fpm - MAGENTO_VERSION: 2.4.6 + MAGENTO_VERSION: 2.4.6-p4 - PHP_VERSION: php82-fpm - MAGENTO_VERSION: 2.4.6 + MAGENTO_VERSION: 2.4.6-p4 runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 359a90b1204..b6bd1a8377b 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -7,13 +7,13 @@ jobs: matrix: include: - PHP_VERSION: php73-fpm - MAGENTO_VERSION: 2.3.7-p3 + MAGENTO_VERSION: 2.3.7-p4 - PHP_VERSION: php74-fpm MAGENTO_VERSION: 2.4.0 - PHP_VERSION: php81-fpm - MAGENTO_VERSION: 2.4.6 + MAGENTO_VERSION: 2.4.6-p4 - PHP_VERSION: php82-fpm - MAGENTO_VERSION: 2.4.6 + MAGENTO_VERSION: 2.4.6-p4 runs-on: ubuntu-latest steps: diff --git a/.github/workflows/setup-di-compile.yml b/.github/workflows/setup-di-compile.yml index 783ca7d27fa..2b0f6577523 100644 --- a/.github/workflows/setup-di-compile.yml +++ b/.github/workflows/setup-di-compile.yml @@ -7,15 +7,15 @@ jobs: matrix: include: - PHP_VERSION: php73-fpm - MAGENTO_VERSION: 2.3.7-p3 + MAGENTO_VERSION: 2.3.7-p4 - PHP_VERSION: php74-fpm MAGENTO_VERSION: 2.4.0 - PHP_VERSION: php74-fpm MAGENTO_VERSION: 2.4.3-with-replacements - PHP_VERSION: php81-fpm - MAGENTO_VERSION: 2.4.6 + MAGENTO_VERSION: 2.4.6-p4 - PHP_VERSION: php82-fpm - MAGENTO_VERSION: 2.4.6 + MAGENTO_VERSION: 2.4.6-p4 runs-on: ubuntu-latest steps: diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 589685e4c41..c161b6f551c 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -7,15 +7,15 @@ jobs: matrix: include: - PHP_VERSION: php73-fpm - MAGENTO_VERSION: 2.3.7-p3 + MAGENTO_VERSION: 2.3.7-p4 - PHP_VERSION: php74-fpm MAGENTO_VERSION: 2.4.0 - PHP_VERSION: php74-fpm MAGENTO_VERSION: 2.4.3-with-replacements - PHP_VERSION: php81-fpm - MAGENTO_VERSION: 2.4.6 + MAGENTO_VERSION: 2.4.6-p4 - PHP_VERSION: php82-fpm - MAGENTO_VERSION: 2.4.6 + MAGENTO_VERSION: 2.4.6-p4 runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 From 18137cdb311dda13061573c0d2a9852cf72571b6 Mon Sep 17 00:00:00 2001 From: Imanuel Bertrand Date: Mon, 26 Feb 2024 15:26:02 +0100 Subject: [PATCH 02/16] Handle null gift wrapping amount case Fixes deprecation notice and subsequent crash in Adobe Commerce Cloud --- Service/Order/Lines/Generator/MagentoGiftWrapping.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/Order/Lines/Generator/MagentoGiftWrapping.php b/Service/Order/Lines/Generator/MagentoGiftWrapping.php index 0abb9133249..3e1ba266916 100644 --- a/Service/Order/Lines/Generator/MagentoGiftWrapping.php +++ b/Service/Order/Lines/Generator/MagentoGiftWrapping.php @@ -38,7 +38,7 @@ public function process(OrderInterface $order, array $orderLines): array $extensionAttributes->getGwItemsBasePriceInclTax() : $extensionAttributes->getGwItemsPriceInclTax(); - if (abs($amount) < 0.01) { + if ($amount === null || abs($amount) < 0.01) { return $orderLines; } From 69740a6a19f3648cfab2df8ed7aefc9f857057ce Mon Sep 17 00:00:00 2001 From: Mark Rees <35261502+Sental@users.noreply.github.com> Date: Thu, 7 Mar 2024 14:09:58 +0100 Subject: [PATCH 03/16] Specify the Payment Link Url Scope Specify the Payment Link Url Scope to prevent the payment link url from using the admin url when different. --- Service/Magento/PaymentLinkUrl.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Service/Magento/PaymentLinkUrl.php b/Service/Magento/PaymentLinkUrl.php index 4f93f819d2d..b1e87c495b2 100644 --- a/Service/Magento/PaymentLinkUrl.php +++ b/Service/Magento/PaymentLinkUrl.php @@ -54,6 +54,7 @@ public function execute(int $orderId): string return $this->urlBuilder->getUrl('mollie/checkout/paymentlink', [ 'order' => $orderId, + '_scope' => $order->getStoreId() ]); } From 420c483da49dfd07cab17c95b35d962c5f8422a1 Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Thu, 7 Mar 2024 11:54:16 +0100 Subject: [PATCH 04/16] Bugfix: Create new transaction when the previous transaction is canceled --- .../PaymentLinkRedirectResultInterface.php | 5 + Controller/Checkout/PaymentLink.php | 6 ++ .../Resolver/Checkout/PaymentLinkRedirect.php | 1 + Model/Client/Orders.php | 15 ++- Service/Magento/PaymentLinkRedirect.php | 19 +++- Service/Magento/PaymentLinkRedirectResult.php | 11 +++ Service/Mollie/Order/IsPaymentLinkExpired.php | 61 ++++++++++++ Service/Mollie/Order/Transaction/Expires.php | 10 +- .../Magento/PaymentLinkRedirectFake.php | 16 +++- .../Block/Form/PaymentlinkTest.php | 2 +- .../Checkout/PaymentLinkRedirectTest.php | 30 +++++- Test/Integration/Model/Client/OrdersTest.php | 2 +- .../Mollie/Order/IsPaymentLinkExpiredTest.php | 93 +++++++++++++++++++ .../Service/Order/Transaction/ExpiresTest.php | 2 +- etc/schema.graphqls | 1 + 15 files changed, 258 insertions(+), 16 deletions(-) create mode 100644 Service/Mollie/Order/IsPaymentLinkExpired.php create mode 100644 Test/Integration/Service/Mollie/Order/IsPaymentLinkExpiredTest.php diff --git a/Api/Data/PaymentLinkRedirectResultInterface.php b/Api/Data/PaymentLinkRedirectResultInterface.php index 560aab710d5..bd302e1c955 100644 --- a/Api/Data/PaymentLinkRedirectResultInterface.php +++ b/Api/Data/PaymentLinkRedirectResultInterface.php @@ -17,4 +17,9 @@ public function isAlreadyPaid(): bool; * @return string|null */ public function getRedirectUrl(): ?string; + + /** + * @return bool + */ + public function isExpired(): bool; } diff --git a/Controller/Checkout/PaymentLink.php b/Controller/Checkout/PaymentLink.php index 3f3aaee1928..e28184ccb6b 100644 --- a/Controller/Checkout/PaymentLink.php +++ b/Controller/Checkout/PaymentLink.php @@ -60,6 +60,12 @@ public function execute() return $this->returnStatusCode(404); } + if ($result->isExpired()) { + $this->messageManager->addErrorMessage(__('Your payment link has expired.')); + + return $this->resultFactory->create(ResultFactory::TYPE_REDIRECT)->setUrl('/'); + } + if ($result->isAlreadyPaid()) { $this->messageManager->addSuccessMessage(__('Your order has already been paid.')); diff --git a/GraphQL/Resolver/Checkout/PaymentLinkRedirect.php b/GraphQL/Resolver/Checkout/PaymentLinkRedirect.php index 20444e711f6..6953a1f48a6 100644 --- a/GraphQL/Resolver/Checkout/PaymentLinkRedirect.php +++ b/GraphQL/Resolver/Checkout/PaymentLinkRedirect.php @@ -41,6 +41,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value return [ 'already_paid' => $result->isAlreadyPaid(), 'redirect_url' => $result->getRedirectUrl(), + 'is_expired' => $result->isExpired(), ]; } } diff --git a/Model/Client/Orders.php b/Model/Client/Orders.php index 82acaf41dcf..f22353a9ac2 100644 --- a/Model/Client/Orders.php +++ b/Model/Client/Orders.php @@ -225,8 +225,7 @@ public function startTransaction(OrderInterface $order, $mollieApi) $transactionId = $order->getMollieTransactionId(); if (!empty($transactionId)) { - $mollieOrder = $mollieApi->orders->get($transactionId); - return $mollieOrder->getCheckoutUrl(); + return $this->getCheckoutUrl($mollieApi, $order); } $paymentToken = $this->paymentTokenForOrder->execute($order); @@ -900,4 +899,16 @@ private function getCaptureAmount(OrderInterface $order, InvoiceInterface $invoi return $order->getBaseGrandTotal(); } + + private function getCheckoutUrl(MollieApiClient $mollieApi, OrderInterface $order): string + { + $mollieOrder = $mollieApi->orders->get($order->getMollieTransactionId()); + if ($checkoutUrl = $mollieOrder->getCheckoutUrl()) { + return $checkoutUrl; + } + + // There is no checkout URL, the transaction is either canceled or expired. Create a new transaction. + $order->setMollieTransactionId(null); + return $this->startTransaction($order, $mollieApi); + } } diff --git a/Service/Magento/PaymentLinkRedirect.php b/Service/Magento/PaymentLinkRedirect.php index 2204733bcc1..e97242dc30f 100644 --- a/Service/Magento/PaymentLinkRedirect.php +++ b/Service/Magento/PaymentLinkRedirect.php @@ -14,6 +14,7 @@ use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order; use Mollie\Payment\Model\Mollie; +use Mollie\Payment\Service\Mollie\Order\IsPaymentLinkExpired; class PaymentLinkRedirect { @@ -33,17 +34,23 @@ class PaymentLinkRedirect * @var PaymentLinkRedirectResultFactory */ private $paymentLinkRedirectResultFactory; + /** + * @var IsPaymentLinkExpired + */ + private $isPaymentLinkExpired; public function __construct( EncryptorInterface $encryptor, OrderRepositoryInterface $orderRepository, Mollie $mollie, - PaymentLinkRedirectResultFactory $paymentLinkRedirectResultFactory + PaymentLinkRedirectResultFactory $paymentLinkRedirectResultFactory, + IsPaymentLinkExpired $isPaymentLinkExpired ) { $this->encryptor = $encryptor; $this->orderRepository = $orderRepository; $this->mollie = $mollie; $this->paymentLinkRedirectResultFactory = $paymentLinkRedirectResultFactory; + $this->isPaymentLinkExpired = $isPaymentLinkExpired; } public function execute(string $orderId): PaymentLinkRedirectResult @@ -60,15 +67,25 @@ public function execute(string $orderId): PaymentLinkRedirectResult throw new NotFoundException(__('Order not found')); } + if ($this->isPaymentLinkExpired->execute($order)) { + return $this->paymentLinkRedirectResultFactory->create([ + 'redirectUrl' => null, + 'isExpired' => true, + 'alreadyPaid' => false, + ]); + } + if (in_array($order->getState(), [Order::STATE_PROCESSING, Order::STATE_COMPLETE])) { return $this->paymentLinkRedirectResultFactory->create([ 'redirectUrl' => null, + 'isExpired' => false, 'alreadyPaid' => true, ]); } return $this->paymentLinkRedirectResultFactory->create([ 'redirectUrl' => $this->mollie->startTransaction($order), + 'isExpired' => false, 'alreadyPaid' => false, ]); } diff --git a/Service/Magento/PaymentLinkRedirectResult.php b/Service/Magento/PaymentLinkRedirectResult.php index d51d62f6595..66a14d43098 100644 --- a/Service/Magento/PaymentLinkRedirectResult.php +++ b/Service/Magento/PaymentLinkRedirectResult.php @@ -20,13 +20,19 @@ class PaymentLinkRedirectResult implements PaymentLinkRedirectResultInterface * @var string|null */ private $redirectUrl; + /** + * @var bool + */ + private $isExpired; public function __construct( bool $alreadyPaid, + bool $isExpired, string $redirectUrl = null ) { $this->alreadyPaid = $alreadyPaid; $this->redirectUrl = $redirectUrl; + $this->isExpired = $isExpired; } public function isAlreadyPaid(): bool @@ -38,4 +44,9 @@ public function getRedirectUrl(): ?string { return $this->redirectUrl; } + + public function isExpired(): bool + { + return $this->isExpired; + } } diff --git a/Service/Mollie/Order/IsPaymentLinkExpired.php b/Service/Mollie/Order/IsPaymentLinkExpired.php new file mode 100644 index 00000000000..befb412cf1f --- /dev/null +++ b/Service/Mollie/Order/IsPaymentLinkExpired.php @@ -0,0 +1,61 @@ +methodCode = $methodCode; + $this->expires = $expires; + $this->timezone = $timezone; + } + + public function execute(OrderInterface $order): bool + { + $methodCode = $this->methodCode->execute($order); + $this->methodCode->getExpiresAtMethod(); + if (!$this->expires->availableForMethod($methodCode, $order->getStoreId())) { + return $this->checkWithDefaultDate($order); + } + + $expiresAt = $this->expires->atDateForMethod($methodCode, $order->getStoreId()); + + return $expiresAt < $order->getCreatedAt(); + } + + private function checkWithDefaultDate(OrderInterface $order): bool + { + $date = $this->timezone->scopeDate($order->getStoreId()); + $date = $date->add(new \DateInterval('P28D')); + + return $date->format('Y-m-d H:i:s') < $order->getCreatedAt(); + } +} diff --git a/Service/Mollie/Order/Transaction/Expires.php b/Service/Mollie/Order/Transaction/Expires.php index 1afd1723b63..0f56daefd83 100644 --- a/Service/Mollie/Order/Transaction/Expires.php +++ b/Service/Mollie/Order/Transaction/Expires.php @@ -37,13 +37,13 @@ public function __construct( $this->request = $request; } - public function availableForMethod(string $method = null, $storeId = null) + public function availableForMethod(string $method = null, $storeId = null): bool { $value = $this->getExpiresAtForMethod($method, $storeId); return (bool)$value; } - public function atDateForMethod(string $method = null, $storeId = null) + public function atDateForMethod(string $method = null, $storeId = null): string { $days = $this->getExpiresAtForMethod($method, $storeId); @@ -54,7 +54,7 @@ public function atDateForMethod(string $method = null, $storeId = null) $date = $this->timezone->scopeDate($storeId); $date->add(new \DateInterval('P' . $days . 'D')); - return $date->format('Y-m-d'); + return $date->format('Y-m-d H:i:s'); } /** @@ -62,7 +62,7 @@ public function atDateForMethod(string $method = null, $storeId = null) * @param $storeId * @return mixed */ - public function getExpiresAtForMethod(string $method = null, $storeId = null) + public function getExpiresAtForMethod(string $method = null, $storeId = null): ?string { if (!$method && $value = $this->getValueFromRequest()) { return $value; @@ -76,7 +76,7 @@ public function getExpiresAtForMethod(string $method = null, $storeId = null) /** * @return mixed */ - private function getValueFromRequest() + private function getValueFromRequest(): ?string { $payment = $this->request->getParam('payment'); diff --git a/Test/Fakes/Service/Magento/PaymentLinkRedirectFake.php b/Test/Fakes/Service/Magento/PaymentLinkRedirectFake.php index 277f04a1028..0828404884d 100644 --- a/Test/Fakes/Service/Magento/PaymentLinkRedirectFake.php +++ b/Test/Fakes/Service/Magento/PaymentLinkRedirectFake.php @@ -14,6 +14,7 @@ use Mollie\Payment\Service\Magento\PaymentLinkRedirect; use Mollie\Payment\Service\Magento\PaymentLinkRedirectResult; use Mollie\Payment\Service\Magento\PaymentLinkRedirectResultFactory; +use Mollie\Payment\Service\Mollie\Order\IsPaymentLinkExpired; class PaymentLinkRedirectFake extends PaymentLinkRedirect { @@ -30,17 +31,26 @@ public function __construct( EncryptorInterface $encryptor, OrderRepositoryInterface $orderRepository, Mollie $mollie, - PaymentLinkRedirectResultFactory $paymentLinkRedirectResultFactory + PaymentLinkRedirectResultFactory $paymentLinkRedirectResultFactory, + IsPaymentLinkExpired $isPaymentLinkExpired ) { - parent::__construct($encryptor, $orderRepository, $mollie, $paymentLinkRedirectResultFactory); + parent::__construct( + $encryptor, + $orderRepository, + $mollie, + $paymentLinkRedirectResultFactory, + $isPaymentLinkExpired + ); + $this->paymentLinkRedirectResultFactory = $paymentLinkRedirectResultFactory; } - public function fakeResponse(?string $redirectUrl, bool $alreadyPaid) + public function fakeResponse(?string $redirectUrl, bool $alreadyPaid, bool $isExpired): void { $this->result = $this->paymentLinkRedirectResultFactory->create([ 'alreadyPaid' => $alreadyPaid, 'redirectUrl' => $redirectUrl, + 'isExpired' => $isExpired, ]); } diff --git a/Test/Integration/Block/Form/PaymentlinkTest.php b/Test/Integration/Block/Form/PaymentlinkTest.php index 38ccb1fdc56..1a6dd5ea32d 100644 --- a/Test/Integration/Block/Form/PaymentlinkTest.php +++ b/Test/Integration/Block/Form/PaymentlinkTest.php @@ -23,7 +23,7 @@ public function testReturnsTheCorrectDate() $now = $this->objectManager->create(TimezoneInterface::class)->scopeDate(null); $expected = $now->add(new \DateInterval('P10D')); - $this->assertEquals($expected->format('Y-m-d'), $instance->getExpiresAt()); + $this->assertEquals($expected->format('Y-m-d H:i:s'), $instance->getExpiresAt()); } /** diff --git a/Test/Integration/GraphQL/Resolver/Checkout/PaymentLinkRedirectTest.php b/Test/Integration/GraphQL/Resolver/Checkout/PaymentLinkRedirectTest.php index e4936e6f1e3..2713c9d6741 100644 --- a/Test/Integration/GraphQL/Resolver/Checkout/PaymentLinkRedirectTest.php +++ b/Test/Integration/GraphQL/Resolver/Checkout/PaymentLinkRedirectTest.php @@ -20,7 +20,7 @@ class PaymentLinkRedirectTest extends GraphQLTestCase public function testReturnsValidResultWhenNotYetPaid(): void { $fakeInstance = $this->objectManager->get(PaymentLinkRedirectFake::class); - $fakeInstance->fakeResponse('https://www.example.com', false); + $fakeInstance->fakeResponse('https://www.example.com', false, false); $this->objectManager->addSharedInstance($fakeInstance, PaymentLinkRedirect::class); @@ -31,18 +31,20 @@ public function testReturnsValidResultWhenNotYetPaid(): void molliePaymentLinkRedirect(order: "999") { already_paid redirect_url + is_expired } } '); $this->assertSame($result['molliePaymentLinkRedirect']['redirect_url'], 'https://www.example.com'); $this->assertSame($result['molliePaymentLinkRedirect']['already_paid'], false); + $this->assertSame($result['molliePaymentLinkRedirect']['is_expired'], false); } public function testReturnsValidResultWhenAlreadyPaid(): void { $fakeInstance = $this->objectManager->get(PaymentLinkRedirectFake::class); - $fakeInstance->fakeResponse(null, true); + $fakeInstance->fakeResponse(null, true, false); $this->objectManager->addSharedInstance($fakeInstance, PaymentLinkRedirect::class); @@ -51,11 +53,35 @@ public function testReturnsValidResultWhenAlreadyPaid(): void molliePaymentLinkRedirect(order: "999") { already_paid redirect_url + is_expired } } '); $this->assertSame($result['molliePaymentLinkRedirect']['redirect_url'], null); $this->assertSame($result['molliePaymentLinkRedirect']['already_paid'], true); + $this->assertSame($result['molliePaymentLinkRedirect']['is_expired'], false); + } + + public function testReturnsValidResultWhenExpired(): void + { + $fakeInstance = $this->objectManager->get(PaymentLinkRedirectFake::class); + $fakeInstance->fakeResponse(null, false, true); + + $this->objectManager->addSharedInstance($fakeInstance, PaymentLinkRedirect::class); + + $result = $this->graphQlQuery(' + mutation { + molliePaymentLinkRedirect(order: "999") { + already_paid + redirect_url + is_expired + } + } + '); + + $this->assertSame($result['molliePaymentLinkRedirect']['redirect_url'], null); + $this->assertSame($result['molliePaymentLinkRedirect']['already_paid'], false); + $this->assertSame($result['molliePaymentLinkRedirect']['is_expired'], true); } } diff --git a/Test/Integration/Model/Client/OrdersTest.php b/Test/Integration/Model/Client/OrdersTest.php index e21547388ce..c0298c8d5e8 100644 --- a/Test/Integration/Model/Client/OrdersTest.php +++ b/Test/Integration/Model/Client/OrdersTest.php @@ -189,7 +189,7 @@ public function testStartTransactionIncludesTheExpiresAtParameter( $now = $this->objectManager->create(TimezoneInterface::class)->scopeDate(null); $expected = $now->add(new \DateInterval('P' . $days . 'D')); - $this->assertEquals($expected->format('Y-m-d'), $orderData['expiresAt']); + $this->assertEquals($expected->format('Y-m-d H:i:s'), $orderData['expiresAt']); return true; }))->willReturn($mollieOrderMock); diff --git a/Test/Integration/Service/Mollie/Order/IsPaymentLinkExpiredTest.php b/Test/Integration/Service/Mollie/Order/IsPaymentLinkExpiredTest.php new file mode 100644 index 00000000000..4efeb9b5522 --- /dev/null +++ b/Test/Integration/Service/Mollie/Order/IsPaymentLinkExpiredTest.php @@ -0,0 +1,93 @@ +loadOrder('100000001'); + $order->getPayment()->setMethod(Paymentlink::CODE); + + $date = new \DateTimeImmutable(); + $date = $date->add(new \DateInterval('P28D'))->setTime(0, 0, 0); + $order->setcreatedAt($date->format('Y-m-d H:i:s')); + + $instance = $this->objectManager->create(IsPaymentLinkExpired::class); + + $this->assertFalse($instance->execute($order)); + } + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * @return void + */ + public function testIsInvalidTheDayAfterTheDefaultExpire(): void + { + $order = $this->loadOrder('100000001'); + $order->getPayment()->setMethod(Paymentlink::CODE); + + $date = new \DateTimeImmutable(); + $date = $date->add(new \DateInterval('P29D'))->setTime(23, 59, 59); + $order->setcreatedAt($date->format('Y-m-d H:i:s')); + + $instance = $this->objectManager->create(IsPaymentLinkExpired::class); + + $this->assertTrue($instance->execute($order)); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoConfigFixture default_store payment/mollie_methods_ideal/days_before_expire 10 + * @return void + */ + public function testIsValidWhenAvailableForMethodIsSetTheDayBefore(): void + { + $order = $this->loadOrder('100000001'); + $order->getPayment()->setMethod(Paymentlink::CODE); + + $date = new \DateTimeImmutable(); + $date = $date->add(new \DateInterval('P9D'))->setTime(0, 0, 0); + $order->setcreatedAt($date->format('Y-m-d H:i:s')); + + $order->getPayment()->setAdditionalInformation(['limited_methods' => ['ideal']]); + + $instance = $this->objectManager->create(IsPaymentLinkExpired::class); + + $this->assertFalse($instance->execute($order)); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoConfigFixture default_store payment/mollie_methods_ideal/days_before_expire 10 + * @return void + */ + public function testIsValidWhenAvailableForMethodIsSetTheDayAfter(): void + { + $order = $this->loadOrder('100000001'); + $order->getPayment()->setMethod(Paymentlink::CODE); + + $date = new \DateTimeImmutable(); + $date = $date->add(new \DateInterval('P11D'))->setTime(23, 59, 59); + $order->setcreatedAt($date->format('Y-m-d H:i:s')); + + $order->getPayment()->setAdditionalInformation(['limited_methods' => ['ideal']]); + + $instance = $this->objectManager->create(IsPaymentLinkExpired::class); + + $this->assertTrue($instance->execute($order)); + } +} diff --git a/Test/Integration/Service/Order/Transaction/ExpiresTest.php b/Test/Integration/Service/Order/Transaction/ExpiresTest.php index 2770e9c83a5..3689dde8979 100644 --- a/Test/Integration/Service/Order/Transaction/ExpiresTest.php +++ b/Test/Integration/Service/Order/Transaction/ExpiresTest.php @@ -46,7 +46,7 @@ public function testReturnsTheCorrectDate() /** @var Expires $instance */ $instance = $this->objectManager->create(Expires::class); - $this->assertEquals($expected->format('Y-m-d'), $instance->atDateForMethod('ideal')); + $this->assertEquals($expected->format('Y-m-d H:i:s'), $instance->atDateForMethod('ideal')); } public function testIsAvailableWhenSetInTheRequest() diff --git a/etc/schema.graphqls b/etc/schema.graphqls index 32fdf3903a8..c68bcf9d994 100644 --- a/etc/schema.graphqls +++ b/etc/schema.graphqls @@ -115,6 +115,7 @@ type MollieApplePayValidationOutput { type MolliePaymentLinkRedirectOutput { redirect_url: String already_paid: Boolean! + is_expired: Boolean! } input PaymentMethodInput { From f43b305fd91173248336bfff44ffd4ba6ed85272 Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Mon, 11 Mar 2024 12:32:18 +0100 Subject: [PATCH 05/16] Improvement: Bump required mollie/mollie-api-php version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 06c364e255e..94f4c3df53e 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,7 @@ "magento 2" ], "require": { - "mollie/mollie-api-php": "^2.1", + "mollie/mollie-api-php": "^2.65", "magento/framework": ">=102.0.3", "magento/module-backend": ">=100.3.3", "magento/module-catalog": ">=100.3.3", From d4f7b702726eaf8cf16c67e6c59dbbe9f83ae51f Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Mon, 11 Mar 2024 11:01:19 +0100 Subject: [PATCH 06/16] Bugfix: Only create invoice once --- Api/Data/TransactionToProcessInterface.php | 11 +++++++++++ Controller/Checkout/Process.php | 2 +- .../Orders/Processors/SuccessfulPayment.php | 3 ++- Model/Queue/TransactionToProcess.php | 17 +++++++++++++++++ Queue/Handler/TransactionProcessor.php | 2 +- Service/Mollie/ProcessTransaction.php | 9 +++++---- .../Service/Mollie/ProcessTransactionFake.php | 4 ++-- 7 files changed, 39 insertions(+), 9 deletions(-) diff --git a/Api/Data/TransactionToProcessInterface.php b/Api/Data/TransactionToProcessInterface.php index c87703eb2de..2e41dcb5445 100644 --- a/Api/Data/TransactionToProcessInterface.php +++ b/Api/Data/TransactionToProcessInterface.php @@ -29,4 +29,15 @@ public function setOrderId(int $id): TransactionToProcessInterface; * @return int|null */ public function getOrderId(): ?int; + + /** + * @param string $type + * @return \Mollie\Payment\Api\Data\TransactionToProcessInterface + */ + public function setType(string $type): TransactionToProcessInterface; + + /** + * @return string|null + */ + public function getType(): ?string; } diff --git a/Controller/Checkout/Process.php b/Controller/Checkout/Process.php index ed3e0cc45f3..657d4f50e79 100644 --- a/Controller/Checkout/Process.php +++ b/Controller/Checkout/Process.php @@ -118,7 +118,7 @@ public function execute() $result = null; foreach ($orderIds as $orderId => $paymentToken) { $order = $this->orderRepository->get($orderId); - $result = $this->processTransaction->execute($orderId, $order->getMollieTransactionId()); + $result = $this->processTransaction->execute($orderId, $order->getMollieTransactionId(), 'success'); } } catch (\Exception $e) { $this->mollieHelper->addTolog('error', $e->getMessage()); diff --git a/Model/Client/Orders/Processors/SuccessfulPayment.php b/Model/Client/Orders/Processors/SuccessfulPayment.php index 42ccec81835..6d9657af86c 100644 --- a/Model/Client/Orders/Processors/SuccessfulPayment.php +++ b/Model/Client/Orders/Processors/SuccessfulPayment.php @@ -179,7 +179,8 @@ private function handleWebhookCall(OrderInterface $order, MollieOrder $mollieOrd } if ($mollieOrder->isAuthorized() && - $this->mollieHelper->getInvoiceMoment($order->getStoreId()) == InvoiceMoment::ON_AUTHORIZE + $this->mollieHelper->getInvoiceMoment($order->getStoreId()) == InvoiceMoment::ON_AUTHORIZE && + $order->getInvoiceCollection()->count() === 0 ) { $payment->setIsTransactionClosed(false); $payment->registerAuthorizationNotification($order->getBaseGrandTotal(), true); diff --git a/Model/Queue/TransactionToProcess.php b/Model/Queue/TransactionToProcess.php index a6537423ad4..166bc8d2d06 100644 --- a/Model/Queue/TransactionToProcess.php +++ b/Model/Queue/TransactionToProcess.php @@ -22,6 +22,11 @@ class TransactionToProcess implements TransactionToProcessInterface */ private $orderId = null; + /** + * @var null | string + */ + private $type = null; + public function setTransactionId(string $id): TransactionToProcessInterface { $this->transactionId = $id; @@ -45,4 +50,16 @@ public function getOrderId(): ?int { return $this->orderId; } + + public function setType(string $type): TransactionToProcessInterface + { + $this->type = $type; + + return $this; + } + + public function getType(): ?string + { + return $this->type; + } } diff --git a/Queue/Handler/TransactionProcessor.php b/Queue/Handler/TransactionProcessor.php index 91430a1fea0..9b86eac0e3d 100644 --- a/Queue/Handler/TransactionProcessor.php +++ b/Queue/Handler/TransactionProcessor.php @@ -44,7 +44,7 @@ public function execute(TransactionToProcessInterface $data): void $order = $this->orderRepository->get($data->getOrderId()); $order->setMollieTransactionId($data->getTransactionId()); - $this->mollieModel->processTransactionForOrder($order, 'webhook'); + $this->mollieModel->processTransactionForOrder($order, $data->getType()); } catch (\Throwable $throwable) { $this->config->addToLog('error', [ 'from' => 'TransactionProcessor consumer', diff --git a/Service/Mollie/ProcessTransaction.php b/Service/Mollie/ProcessTransaction.php index 1edca5fae29..8f0f570f395 100644 --- a/Service/Mollie/ProcessTransaction.php +++ b/Service/Mollie/ProcessTransaction.php @@ -64,17 +64,17 @@ public function __construct( $this->getMollieStatus = $getMollieStatus; } - public function execute(int $orderId, string $transactionId): GetMollieStatusResult + public function execute(int $orderId, string $transactionId, string $type = 'webhook'): GetMollieStatusResult { if ($this->config->processTransactionsInTheQueue()) { - $this->queueOrder($orderId, $transactionId); + $this->queueOrder($orderId, $transactionId, $type); return $this->getMollieStatus->execute($orderId); } $order = $this->orderRepository->get($orderId); $order->setMollieTransactionId($transactionId); - $result = $this->mollieModel->processTransactionForOrder($order, 'webhook'); + $result = $this->mollieModel->processTransactionForOrder($order, $type); return $this->getMollieStatusResultFactory->create([ 'status' => $result['status'], @@ -82,12 +82,13 @@ public function execute(int $orderId, string $transactionId): GetMollieStatusRes ]); } - private function queueOrder(int $orderId, string $transactionId) + private function queueOrder(int $orderId, string $transactionId, string $type): void { /** @var TransactionToProcessInterface $data */ $data = $this->transactionToProcessFactory->create(); $data->setOrderId($orderId); $data->setTransactionId($transactionId); + $data->setType($type); $this->publishTransactionToProcess->publish($data); } diff --git a/Test/Fakes/Service/Mollie/ProcessTransactionFake.php b/Test/Fakes/Service/Mollie/ProcessTransactionFake.php index d45f73009d0..b04ae277c47 100644 --- a/Test/Fakes/Service/Mollie/ProcessTransactionFake.php +++ b/Test/Fakes/Service/Mollie/ProcessTransactionFake.php @@ -30,7 +30,7 @@ public function setResponse(GetMollieStatusResult $response): void $this->response = $response; } - public function execute(int $orderId, string $transactionId): GetMollieStatusResult + public function execute(int $orderId, string $transactionId, string $type = 'webhook'): GetMollieStatusResult { $this->timesCalled++; @@ -38,6 +38,6 @@ public function execute(int $orderId, string $transactionId): GetMollieStatusRes return $this->response; } - return parent::execute($orderId, $transactionId); // TODO: Change the autogenerated stub + return parent::execute($orderId, $transactionId, $type); } } From 306a4c6fed4241961bf6b417108df47911969e2e Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Mon, 4 Mar 2024 15:45:05 +0100 Subject: [PATCH 07/16] Feature: Option to set a custom template for payment link orders --- Block/Info/Base.php | 4 +- Config.php | 18 +++ .../Sender/SendPaymentLinkConfirmation.php | 47 ++++++++ .../Order/Email/PaymentLinkOrderIdentity.php | 42 +++++++ .../Order/PaymentLinkConfirmationEmail.php | 78 +++++++++++++ .../Service/Magento/PaymentLinkUrlFake.php | 44 ++++++++ Test/Integration/Block/Info/BaseTest.php | 50 +++++++++ etc/adminhtml/methods/paymentlink.xml | 45 +++++--- etc/config.xml | 2 + etc/di.xml | 4 + etc/email_templates.xml | 1 + view/adminhtml/web/css/source/_email.less | 9 ++ .../email/payment-link-confirmation.html | 106 ++++++++++++++++++ view/frontend/web/css/source/_email.less | 25 +++++ 14 files changed, 461 insertions(+), 14 deletions(-) create mode 100644 Plugin/Sales/Model/Order/Email/Sender/SendPaymentLinkConfirmation.php create mode 100644 Service/Order/Email/PaymentLinkOrderIdentity.php create mode 100644 Service/Order/PaymentLinkConfirmationEmail.php create mode 100644 Test/Fakes/Service/Magento/PaymentLinkUrlFake.php create mode 100644 view/adminhtml/web/css/source/_email.less create mode 100644 view/frontend/email/payment-link-confirmation.html create mode 100644 view/frontend/web/css/source/_email.less diff --git a/Block/Info/Base.php b/Block/Info/Base.php index 92b87db7646..146ca83a70e 100644 --- a/Block/Info/Base.php +++ b/Block/Info/Base.php @@ -94,7 +94,9 @@ public function getExpiresAt(): ?string public function getPaymentLink($storeId = null): ?string { - if (!$this->config->addPaymentLinkMessage($storeId)) { + if (!$this->config->addPaymentLinkMessage($storeId) || + $this->config->paymentLinkUseCustomEmailTemplate($storeId) + ) { return null; } diff --git a/Config.php b/Config.php index 6613d427cfe..8bc7ff9b65b 100644 --- a/Config.php +++ b/Config.php @@ -72,6 +72,8 @@ class Config const PAYMENT_PAYMENTLINK_NEW_STATUS = 'payment/mollie_methods_paymentlink/order_status_new'; const PAYMENT_PAYMENTLINK_ADD_MESSAGE = 'payment/mollie_methods_paymentlink/add_message'; const PAYMENT_PAYMENTLINK_MESSAGE = 'payment/mollie_methods_paymentlink/message'; + const PAYMENT_PAYMENTLINK_USE_CUSTOM_EMAIL_TEMPLATE = 'payment/mollie_methods_paymentlink/use_custom_email_template'; + const PAYMENT_PAYMENTLINK_EMAIL_TEMPLATE = 'payment/mollie_methods_paymentlink/email_template'; const PAYMENT_USE_CUSTOM_PAYMENTLINK_URL = 'payment/mollie_general/use_custom_paymentlink_url'; const PAYMENT_CUSTOM_PAYMENTLINK_URL = 'payment/mollie_general/custom_paymentlink_url'; const PAYMENT_POINTOFSALE_ALLOWED_CUSTOMER_GROUPS = 'payment/mollie_methods_pointofsale/allowed_customer_groups'; @@ -530,6 +532,22 @@ public function paymentLinkMessage($storeId = null): string ); } + public function paymentLinkUseCustomEmailTemplate(int $storeId = null): string + { + return $this->getPath( + static::PAYMENT_PAYMENTLINK_USE_CUSTOM_EMAIL_TEMPLATE, + $storeId + ); + } + + public function paymentLinkEmailTemplate(int $storeId = null): string + { + return $this->getPath( + static::PAYMENT_PAYMENTLINK_EMAIL_TEMPLATE, + $storeId + ); + } + public function useCustomPaymentLinkUrl($storeId = null): bool { return $this->isSetFlag(static::PAYMENT_USE_CUSTOM_PAYMENTLINK_URL, $storeId); diff --git a/Plugin/Sales/Model/Order/Email/Sender/SendPaymentLinkConfirmation.php b/Plugin/Sales/Model/Order/Email/Sender/SendPaymentLinkConfirmation.php new file mode 100644 index 00000000000..5df2013a06e --- /dev/null +++ b/Plugin/Sales/Model/Order/Email/Sender/SendPaymentLinkConfirmation.php @@ -0,0 +1,47 @@ +config = $config; + $this->paymentLinkConfirmationEmail = $paymentLinkConfirmationEmail; + } + + public function aroundSend(OrderSender $subject, callable $proceed, Order $order, $forceSyncMode = false) + { + // When the `send()` method of the OrderSender is called, we want to call our own class instead. + // But this class is also based on the OrderSender, so we need to check if we are not already in our own class. + if ($order->getPayment()->getMethod() == 'mollie_methods_paymentlink' && + $this->config->paymentLinkUseCustomEmailTemplate((int)$order->getStoreId()) && + !($subject instanceof $this->paymentLinkConfirmationEmail) + ) { + return $this->paymentLinkConfirmationEmail->send($order); + } + + return $proceed($order, $forceSyncMode); + } +} diff --git a/Service/Order/Email/PaymentLinkOrderIdentity.php b/Service/Order/Email/PaymentLinkOrderIdentity.php new file mode 100644 index 00000000000..f162f7049c8 --- /dev/null +++ b/Service/Order/Email/PaymentLinkOrderIdentity.php @@ -0,0 +1,42 @@ +config = $config; + } + + public function isEnabled() + { + return $this->config->paymentLinkUseCustomEmailTemplate(); + } + + public function getTemplateId() + { + return $this->config->paymentLinkEmailTemplate(); + } +} diff --git a/Service/Order/PaymentLinkConfirmationEmail.php b/Service/Order/PaymentLinkConfirmationEmail.php new file mode 100644 index 00000000000..e6e4c88a48f --- /dev/null +++ b/Service/Order/PaymentLinkConfirmationEmail.php @@ -0,0 +1,78 @@ +paymentLinkUrl = $paymentLinkUrl; + } + + protected function prepareTemplate(Order $order) + { + parent::prepareTemplate($order); + + $transportObject = new DataObject($this->templateContainer->getTemplateVars()); + $transportObject->setData('mollie_payment_link', $this->paymentLinkUrl->execute((int)$order->getEntityId())); + + $this->eventManager->dispatch( + 'mollie_email_paymenlink_order_set_template_vars_before', + ['sender' => $this, 'transportObject' => $transportObject] + ); + + $this->templateContainer->setTemplateVars($transportObject->getData()); + } +} diff --git a/Test/Fakes/Service/Magento/PaymentLinkUrlFake.php b/Test/Fakes/Service/Magento/PaymentLinkUrlFake.php new file mode 100644 index 00000000000..3d4b5139232 --- /dev/null +++ b/Test/Fakes/Service/Magento/PaymentLinkUrlFake.php @@ -0,0 +1,44 @@ +url = $url; + } + + public function shouldNotBeCalled() + { + $this->shouldNotBeCalled = true; + } + + public function execute(int $orderId): string + { + if ($this->shouldNotBeCalled === true) { + throw new \Exception('This method should not be called'); + } + + if ($this->url !== null) { + return $this->url; + } + + return parent::execute($orderId); + } +} diff --git a/Test/Integration/Block/Info/BaseTest.php b/Test/Integration/Block/Info/BaseTest.php index be349cfb4b5..4c43269cd79 100644 --- a/Test/Integration/Block/Info/BaseTest.php +++ b/Test/Integration/Block/Info/BaseTest.php @@ -8,6 +8,8 @@ use Magento\Sales\Model\Order\Payment\Info; use Mollie\Payment\Block\Info\Base; +use Mollie\Payment\Service\Magento\PaymentLinkUrl; +use Mollie\Payment\Test\Fakes\Service\Magento\PaymentLinkUrlFake; use Mollie\Payment\Test\Integration\IntegrationTestCase; class BaseTest extends IntegrationTestCase @@ -65,4 +67,52 @@ public function testReturnsTheRemainderAmount() $instance->setData('info', $info); $this->assertEquals('100', $instance->getRemainderAmount()); } + + /** + * @magentoConfigFixture current_store payment/mollie_methods_paymentlink/add_message 1 + * @magentoConfigFixture current_store payment/mollie_methods_paymentlink/use_custom_email_template 0 + * @magentoConfigFixture current_store payment/mollie_methods_paymentlink/message "Click here to complete your payment" + * @return void + */ + public function testReturnsThePaymentLinkMessageWithLink(): void + { + $paymentLinkFake = $this->objectManager->get(PaymentLinkUrlFake::class); + $paymentLinkFake->setUrl('http://www.example.com/mollie/checkout/paymentlink/order/-999/'); + $this->objectManager->addSharedInstance($paymentLinkFake, PaymentLinkUrl::class); + + /** @var Info $info */ + $info = $this->objectManager->create(Info::class); + + /** @var Base $instance */ + $instance = $this->objectManager->create(Base::class); + $instance->setData('info', $info); + + $result = $instance->getPaymentLink(); + + $this->assertStringNotContainsString('%link%', $result); + } + + /** + * @magentoConfigFixture current_store payment/mollie_methods_paymentlink/add_message 1 + * @magentoConfigFixture current_store payment/mollie_methods_paymentlink/use_custom_email_template 1 + * @magentoConfigFixture current_store payment/mollie_methods_paymentlink/message "Click here to complete your payment" + * @return void + */ + public function testReturnsNothingWhenPaymentLinkMailIsActive(): void + { + $paymentLinkFake = $this->objectManager->get(PaymentLinkUrlFake::class); + $paymentLinkFake->shouldNotBeCalled(); + $this->objectManager->addSharedInstance($paymentLinkFake, PaymentLinkUrl::class); + + /** @var Info $info */ + $info = $this->objectManager->create(Info::class); + + /** @var Base $instance */ + $instance = $this->objectManager->create(Base::class); + $instance->setData('info', $info); + + $result = $instance->getPaymentLink(); + + $this->assertNull($result); + } } diff --git a/etc/adminhtml/methods/paymentlink.xml b/etc/adminhtml/methods/paymentlink.xml index 224c1f61ac8..afb3c235c2d 100644 --- a/etc/adminhtml/methods/paymentlink.xml +++ b/etc/adminhtml/methods/paymentlink.xml @@ -36,7 +36,26 @@ 1 - + + Magento\Config\Model\Config\Source\Yesno + payment/mollie_methods_paymentlink/use_custom_email_template + + 1 + + + + + Magento\Config\Model\Config\Source\Email\Template + payment/mollie_methods_paymentlink/email_template + + 1 + 1 + + + Magento\Config\Model\Config\Source\Yesno @@ -47,7 +66,7 @@ 1 - + Mollie\Payment\Model\Adminhtml\Source\NewStatus payment/mollie_methods_paymentlink/order_status_new @@ -55,7 +74,7 @@ 1 - Magento\Payment\Model\Config\Source\Allspecificcountries @@ -64,7 +83,7 @@ 1 - Magento\Directory\Model\Config\Source\Country @@ -74,7 +93,7 @@ 1 - payment/mollie_methods_paymentlink/min_order_total @@ -82,7 +101,7 @@ 1 - payment/mollie_methods_paymentlink/max_order_total @@ -90,7 +109,7 @@ 1 - payment/mollie_methods_paymentlink/payment_surcharge_type @@ -99,7 +118,7 @@ 1 - payment/mollie_methods_paymentlink/payment_surcharge_fixed_amount @@ -110,7 +129,7 @@ fixed_fee,fixed_fee_and_percentage - payment/mollie_methods_paymentlink/payment_surcharge_percentage @@ -121,7 +140,7 @@ percentage,fixed_fee_and_percentage - payment/mollie_methods_paymentlink/payment_surcharge_limit @@ -133,7 +152,7 @@ percentage,fixed_fee_and_percentage - payment/mollie_methods_paymentlink/payment_surcharge_tax_class @@ -143,7 +162,7 @@ fixed_fee,percentage,fixed_fee_and_percentage - validate-number @@ -152,7 +171,7 @@ 1 - validate-digits-range digits-range-1-365 diff --git a/etc/config.xml b/etc/config.xml index 7fc0bffaf62..7fc25c91cef 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -411,6 +411,8 @@ 1 Mollie\Payment\Model\Methods\Paymentlink Mollie: Payment Link + 0 + mollie_payment_methods_mollie_methods_paymentlink_email_template {ordernumber} order order diff --git a/etc/di.xml b/etc/di.xml index 312a24c85ca..2ab14e61249 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -365,6 +365,10 @@ + + + + diff --git a/etc/email_templates.xml b/etc/email_templates.xml index d0e3d60ebf8..2a38e61dc83 100644 --- a/etc/email_templates.xml +++ b/etc/email_templates.xml @@ -1,4 +1,5 @@