From bc35ee4439c836787ecc5bde17686a2fc681785b Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Thu, 25 Jan 2024 11:02:01 +0100 Subject: [PATCH 1/4] Feature: Support for payment links in PWAs --- Block/Info/Base.php | 21 ++---- Config.php | 15 ++++ Controller/Checkout/PaymentLink.php | 39 ++-------- .../Resolver/Checkout/PaymentLinkRedirect.php | 46 ++++++++++++ Service/Magento/PaymentLinkRedirect.php | 75 +++++++++++++++++++ Service/Magento/PaymentLinkRedirectResult.php | 39 ++++++++++ Service/Magento/PaymentLinkUrl.php | 70 +++++++++++++++++ .../Magento/PaymentLinkRedirectFake.php | 55 ++++++++++++++ .../Checkout/PaymentLinkRedirectTest.php | 61 +++++++++++++++ .../Service/Magento/PaymentLinkUrlTest.php | 68 +++++++++++++++++ etc/adminhtml/di.xml | 2 +- etc/adminhtml/system.xml | 19 +++++ etc/schema.graphqls | 8 ++ .../templates/form/mollie_paymentlink.phtml | 12 --- 14 files changed, 471 insertions(+), 59 deletions(-) create mode 100644 GraphQL/Resolver/Checkout/PaymentLinkRedirect.php create mode 100644 Service/Magento/PaymentLinkRedirect.php create mode 100644 Service/Magento/PaymentLinkRedirectResult.php create mode 100644 Service/Magento/PaymentLinkUrl.php create mode 100644 Test/Fakes/Service/Magento/PaymentLinkRedirectFake.php create mode 100644 Test/Integration/GraphQL/Resolver/Checkout/PaymentLinkRedirectTest.php create mode 100644 Test/Integration/Service/Magento/PaymentLinkUrlTest.php diff --git a/Block/Info/Base.php b/Block/Info/Base.php index b2a00ff1b34..92b87db7646 100644 --- a/Block/Info/Base.php +++ b/Block/Info/Base.php @@ -6,10 +6,8 @@ namespace Mollie\Payment\Block\Info; -use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Registry; -use Magento\Framework\UrlInterface; use Magento\Payment\Block\Info; use Magento\Framework\View\Element\Template\Context; use Magento\Framework\Stdlib\DateTime; @@ -21,6 +19,7 @@ use Mollie\Payment\Model\Methods\Klarnapaylater; use Mollie\Payment\Model\Methods\Klarnapaynow; use Mollie\Payment\Model\Methods\Klarnasliceit; +use Mollie\Payment\Service\Magento\PaymentLinkUrl; class Base extends Info { @@ -44,18 +43,14 @@ class Base extends Info * @var PriceCurrencyInterface */ private $price; - /** - * @var EncryptorInterface - */ - private $encryptor; /** * @var Config */ private $config; /** - * @var UrlInterface + * @var PaymentLinkUrl */ - private $urlBuilder; + private $paymentLinkUrl; public function __construct( Context $context, @@ -63,17 +58,15 @@ public function __construct( MollieHelper $mollieHelper, Registry $registry, PriceCurrencyInterface $price, - EncryptorInterface $encryptor, - UrlInterface $urlBuilder + PaymentLinkUrl $paymentLinkUrl ) { parent::__construct($context); $this->mollieHelper = $mollieHelper; $this->timezone = $context->getLocaleDate(); $this->registry = $registry; $this->price = $price; - $this->encryptor = $encryptor; $this->config = $config; - $this->urlBuilder = $urlBuilder; + $this->paymentLinkUrl = $paymentLinkUrl; } public function getCheckoutType(): ?string @@ -114,9 +107,7 @@ public function getPaymentLink($storeId = null): ?string public function getPaymentLinkUrl(): string { - return $this->urlBuilder->getUrl('mollie/checkout/paymentlink', [ - 'order' => base64_encode($this->encryptor->encrypt($this->getInfo()->getParentId())), - ]); + return $this->paymentLinkUrl->execute((int)$this->getInfo()->getParentId()); } public function getCheckoutUrl(): ?string diff --git a/Config.php b/Config.php index a9f35f4d4c7..3a7292fc2e8 100644 --- a/Config.php +++ b/Config.php @@ -71,6 +71,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_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'; const PAYMENT_VOUCHER_CATEGORY = 'payment/mollie_methods_voucher/category'; const PAYMENT_VOUCHER_CUSTOM_ATTRIBUTE = 'payment/mollie_methods_voucher/custom_attribute'; @@ -513,6 +515,19 @@ public function paymentLinkMessage($storeId = null): string ); } + public function useCustomPaymentLinkUrl($storeId = null): bool + { + return $this->isSetFlag(static::PAYMENT_USE_CUSTOM_PAYMENTLINK_URL, $storeId); + } + + public function customPaymentLinkUrl($storeId = null): string + { + return (string)$this->getPath( + static::PAYMENT_CUSTOM_PAYMENTLINK_URL, + $storeId + ); + } + /** * @param string $method * @param int|null $storeId diff --git a/Controller/Checkout/PaymentLink.php b/Controller/Checkout/PaymentLink.php index 0db0b5b7144..3f3aaee1928 100644 --- a/Controller/Checkout/PaymentLink.php +++ b/Controller/Checkout/PaymentLink.php @@ -12,12 +12,9 @@ use Magento\Framework\App\RequestInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Controller\ResultInterface; -use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Message\ManagerInterface; -use Magento\Sales\Api\OrderRepositoryInterface; -use Magento\Sales\Model\Order; -use Mollie\Payment\Model\Mollie; +use Mollie\Payment\Service\Magento\PaymentLinkRedirect; class PaymentLink implements HttpGetActionInterface { @@ -25,10 +22,6 @@ class PaymentLink implements HttpGetActionInterface * @var RequestInterface */ private $request; - /** - * @var EncryptorInterface - */ - private $encryptor; /** * @var ResultFactory */ @@ -38,28 +31,20 @@ class PaymentLink implements HttpGetActionInterface */ private $messageManager; /** - * @var OrderRepositoryInterface + * @var PaymentLinkRedirect */ - private $orderRepository; - /** - * @var Mollie - */ - private $mollie; + private $paymentLinkRedirect; public function __construct( RequestInterface $request, - EncryptorInterface $encryptor, ResultFactory $resultFactory, ManagerInterface $messageManager, - OrderRepositoryInterface $orderRepository, - Mollie $mollie + PaymentLinkRedirect $paymentLinkRedirect ) { $this->request = $request; - $this->encryptor = $encryptor; $this->resultFactory = $resultFactory; $this->messageManager = $messageManager; - $this->orderRepository = $orderRepository; - $this->mollie = $mollie; + $this->paymentLinkRedirect = $paymentLinkRedirect; } public function execute() @@ -69,27 +54,19 @@ public function execute() return $this->returnStatusCode(400); } - $id = $this->encryptor->decrypt(base64_decode($orderKey)); - - if (empty($id)) { - return $this->returnStatusCode(404); - } - try { - $order = $this->orderRepository->get($id); + $result = $this->paymentLinkRedirect->execute($orderKey); } catch (NoSuchEntityException $exception) { return $this->returnStatusCode(404); } - if (in_array($order->getState(), [Order::STATE_PROCESSING, Order::STATE_COMPLETE])) { + if ($result->isAlreadyPaid()) { $this->messageManager->addSuccessMessage(__('Your order has already been paid.')); return $this->resultFactory->create(ResultFactory::TYPE_REDIRECT)->setUrl('/'); } - $url = $this->mollie->startTransaction($order); - - return $this->resultFactory->create(ResultFactory::TYPE_REDIRECT)->setUrl($url); + return $this->resultFactory->create(ResultFactory::TYPE_REDIRECT)->setUrl($result->getRedirectUrl()); } public function returnStatusCode(int $code): ResultInterface diff --git a/GraphQL/Resolver/Checkout/PaymentLinkRedirect.php b/GraphQL/Resolver/Checkout/PaymentLinkRedirect.php new file mode 100644 index 00000000000..20444e711f6 --- /dev/null +++ b/GraphQL/Resolver/Checkout/PaymentLinkRedirect.php @@ -0,0 +1,46 @@ +paymentLinkRedirect = $paymentLinkRedirect; + } + + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + $order = $args['order']; + + try { + $result = $this->paymentLinkRedirect->execute($order); + } catch (NotFoundException $exception) { + throw new GraphQlNoSuchEntityException(__('Order not found')); + } + + return [ + 'already_paid' => $result->isAlreadyPaid(), + 'redirect_url' => $result->getRedirectUrl(), + ]; + } +} diff --git a/Service/Magento/PaymentLinkRedirect.php b/Service/Magento/PaymentLinkRedirect.php new file mode 100644 index 00000000000..2204733bcc1 --- /dev/null +++ b/Service/Magento/PaymentLinkRedirect.php @@ -0,0 +1,75 @@ +encryptor = $encryptor; + $this->orderRepository = $orderRepository; + $this->mollie = $mollie; + $this->paymentLinkRedirectResultFactory = $paymentLinkRedirectResultFactory; + } + + public function execute(string $orderId): PaymentLinkRedirectResult + { + $id = $this->encryptor->decrypt(base64_decode($orderId)); + + if (empty($id)) { + throw new NotFoundException(__('Order not found')); + } + + try { + $order = $this->orderRepository->get($id); + } catch (NoSuchEntityException $exception) { + throw new NotFoundException(__('Order not found')); + } + + if (in_array($order->getState(), [Order::STATE_PROCESSING, Order::STATE_COMPLETE])) { + return $this->paymentLinkRedirectResultFactory->create([ + 'redirectUrl' => null, + 'alreadyPaid' => true, + ]); + } + + return $this->paymentLinkRedirectResultFactory->create([ + 'redirectUrl' => $this->mollie->startTransaction($order), + 'alreadyPaid' => false, + ]); + } +} diff --git a/Service/Magento/PaymentLinkRedirectResult.php b/Service/Magento/PaymentLinkRedirectResult.php new file mode 100644 index 00000000000..4c0c0c0566e --- /dev/null +++ b/Service/Magento/PaymentLinkRedirectResult.php @@ -0,0 +1,39 @@ +alreadyPaid = $alreadyPaid; + $this->redirectUrl = $redirectUrl; + } + + public function isAlreadyPaid(): bool + { + return $this->alreadyPaid; + } + + public function getRedirectUrl(): ?string + { + return $this->redirectUrl; + } +} diff --git a/Service/Magento/PaymentLinkUrl.php b/Service/Magento/PaymentLinkUrl.php new file mode 100644 index 00000000000..4f93f819d2d --- /dev/null +++ b/Service/Magento/PaymentLinkUrl.php @@ -0,0 +1,70 @@ +encryptor = $encryptor; + $this->urlBuilder = $urlBuilder; + $this->orderRepository = $orderRepository; + $this->config = $config; + } + + public function execute(int $orderId): string + { + $order = $this->orderRepository->get($orderId); + $orderId = base64_encode($this->encryptor->encrypt((string)$orderId)); + if ($this->config->useCustomPaymentLinkUrl($order->getStoreId())) { + return $this->generateCustomUrl($orderId, $order->getStoreId()); + } + + return $this->urlBuilder->getUrl('mollie/checkout/paymentlink', [ + 'order' => $orderId, + ]); + } + + private function generateCustomUrl(string $order, $storeId = null) + { + $url = $this->config->customPaymentLinkUrl($storeId); + + if (stristr($url, '{{order}}')) { + return str_replace('{{order}}', $order, $url); + } + + return $url . $order; + } +} diff --git a/Test/Fakes/Service/Magento/PaymentLinkRedirectFake.php b/Test/Fakes/Service/Magento/PaymentLinkRedirectFake.php new file mode 100644 index 00000000000..277f04a1028 --- /dev/null +++ b/Test/Fakes/Service/Magento/PaymentLinkRedirectFake.php @@ -0,0 +1,55 @@ +paymentLinkRedirectResultFactory = $paymentLinkRedirectResultFactory; + } + + public function fakeResponse(?string $redirectUrl, bool $alreadyPaid) + { + $this->result = $this->paymentLinkRedirectResultFactory->create([ + 'alreadyPaid' => $alreadyPaid, + 'redirectUrl' => $redirectUrl, + ]); + } + + public function execute(string $orderId): PaymentLinkRedirectResult + { + if ($this->result) { + return $this->result; + } + + return parent::execute($orderId); + } +} diff --git a/Test/Integration/GraphQL/Resolver/Checkout/PaymentLinkRedirectTest.php b/Test/Integration/GraphQL/Resolver/Checkout/PaymentLinkRedirectTest.php new file mode 100644 index 00000000000..e4936e6f1e3 --- /dev/null +++ b/Test/Integration/GraphQL/Resolver/Checkout/PaymentLinkRedirectTest.php @@ -0,0 +1,61 @@ +objectManager->get(PaymentLinkRedirectFake::class); + $fakeInstance->fakeResponse('https://www.example.com', false); + + $this->objectManager->addSharedInstance($fakeInstance, PaymentLinkRedirect::class); + + $this->objectManager->removeSharedInstance(\Mollie\Payment\GraphQL\Resolver\Checkout\PaymentLinkRedirect::class); + + $result = $this->graphQlQuery(' + mutation { + molliePaymentLinkRedirect(order: "999") { + already_paid + redirect_url + } + } + '); + + $this->assertSame($result['molliePaymentLinkRedirect']['redirect_url'], 'https://www.example.com'); + $this->assertSame($result['molliePaymentLinkRedirect']['already_paid'], false); + } + + public function testReturnsValidResultWhenAlreadyPaid(): void + { + $fakeInstance = $this->objectManager->get(PaymentLinkRedirectFake::class); + $fakeInstance->fakeResponse(null, true); + + $this->objectManager->addSharedInstance($fakeInstance, PaymentLinkRedirect::class); + + $result = $this->graphQlQuery(' + mutation { + molliePaymentLinkRedirect(order: "999") { + already_paid + redirect_url + } + } + '); + + $this->assertSame($result['molliePaymentLinkRedirect']['redirect_url'], null); + $this->assertSame($result['molliePaymentLinkRedirect']['already_paid'], true); + } +} diff --git a/Test/Integration/Service/Magento/PaymentLinkUrlTest.php b/Test/Integration/Service/Magento/PaymentLinkUrlTest.php new file mode 100644 index 00000000000..b4cb3d17362 --- /dev/null +++ b/Test/Integration/Service/Magento/PaymentLinkUrlTest.php @@ -0,0 +1,68 @@ +loadOrder('100000001'); + + /** @var PaymentLinkUrl $instance */ + $instance = $this->objectManager->create(PaymentLinkUrl::class); + + $result = $instance->execute((int)$order->getEntityId()); + + $this->assertStringContainsString('/mollie/checkout/paymentlink/order/', $result); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoConfigFixture default_store payment/mollie_general/use_custom_paymentlink_url 1 + * @magentoConfigFixture default_store payment/mollie_general/custom_paymentlink_url https://example.com + * @return void + */ + public function testUsesTheCustomPaymentLinkUrl(): void + { + $order = $this->loadOrder('100000001'); + + /** @var PaymentLinkUrl $instance */ + $instance = $this->objectManager->create(PaymentLinkUrl::class); + + $result = $instance->execute((int)$order->getEntityId()); + + $this->assertStringContainsString('https://example.com', $result); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoConfigFixture default_store payment/mollie_general/use_custom_paymentlink_url 1 + * @magentoConfigFixture default_store payment/mollie_general/custom_paymentlink_url https://example.com/?order={{order}} + * @return void + */ + public function testReplacesThePlaceholderWhenAvailable(): void + { + $order = $this->loadOrder('100000001'); + + /** @var PaymentLinkUrl $instance */ + $instance = $this->objectManager->create(PaymentLinkUrl::class); + + $result = $instance->execute((int)$order->getEntityId()); + + $this->assertStringContainsString('https://example.com/?order=', $result); + $this->assertStringNotContainsString('{{order}}', $result); + } +} diff --git a/etc/adminhtml/di.xml b/etc/adminhtml/di.xml index 904793d0a33..2b2fb4e0f6d 100644 --- a/etc/adminhtml/di.xml +++ b/etc/adminhtml/di.xml @@ -54,7 +54,7 @@ - + Magento\Framework\Url diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index a632aa67edb..56e02c17273 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -303,6 +303,25 @@ 1 + + + Magento\Config\Model\Config\Source\Yesno + payment/mollie_general/use_custom_paymentlink_url + + + + payment/mollie_general/custom_paymentlink_url + Note: You can use the following placeholders:
+
+ {{order}}: Mandatory. The encrypted entity ID of the order.
+ ]]>
+ + 1 + +
diff --git a/etc/schema.graphqls b/etc/schema.graphqls index a128922032a..411f19e7f83 100644 --- a/etc/schema.graphqls +++ b/etc/schema.graphqls @@ -8,6 +8,9 @@ type Mutation { domain: String @doc(description: "The domain to validate. If this is omitted, the base url of the store is used.") validationUrl: String! @doc(description: "The validation URL provided by Apple Pay.") ): MollieApplePayValidationOutput @resolver(class: "Mollie\\Payment\\GraphQL\\Resolver\\Checkout\\ApplePay\\ApplePayValidation") + molliePaymentLinkRedirect( + order: String @doc(description: "The encrypted order ID that is included in the payment link url") + ): MolliePaymentLinkRedirectOutput @resolver(class: "Mollie\\Payment\\GraphQL\\Resolver\\Checkout\\PaymentLinkRedirect") } type Order { @@ -100,6 +103,11 @@ type MollieApplePayValidationOutput { response: String! } +type MolliePaymentLinkRedirectOutput { + redirect_url: String + already_paid: Boolean! +} + input PaymentMethodInput { mollie_applepay_payment_token: String @doc(description: "The Apple Pay payment token") mollie_card_token: String @doc(description: "The card token provided by Mollie Components") diff --git a/view/adminhtml/templates/form/mollie_paymentlink.phtml b/view/adminhtml/templates/form/mollie_paymentlink.phtml index b65ea5baa92..f74f0f35d32 100644 --- a/view/adminhtml/templates/form/mollie_paymentlink.phtml +++ b/view/adminhtml/templates/form/mollie_paymentlink.phtml @@ -45,15 +45,3 @@ $code; ?>" style="display:none">

escapeHtml(__('If only one method is chosen, the selection screen is skipped and the customer is sent directly to the payment method.')); ?>

- - From e338b9cf6a0ca86735eea0e03fb6568a4384f125 Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Thu, 25 Jan 2024 11:23:22 +0100 Subject: [PATCH 2/4] Bugfix: Return the correct type for the webapi --- Api/Data/IssuerInterface.php | 2 +- Test/Integration/WebapiTest.php | 23 +++++++++++++++++++++++ etc/webapi.xml | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 Test/Integration/WebapiTest.php diff --git a/Api/Data/IssuerInterface.php b/Api/Data/IssuerInterface.php index 18d11a43190..0256f325d40 100644 --- a/Api/Data/IssuerInterface.php +++ b/Api/Data/IssuerInterface.php @@ -24,7 +24,7 @@ public function getName(): string; public function getImage(): string; /** - * @return array + * @return string[] */ public function getImages(): array; diff --git a/Test/Integration/WebapiTest.php b/Test/Integration/WebapiTest.php new file mode 100644 index 00000000000..7aa6914acdf --- /dev/null +++ b/Test/Integration/WebapiTest.php @@ -0,0 +1,23 @@ +expectNotToPerformAssertions(); + + // This is used by the /swagger endpoint. If this fails, the /swagger endpoint will fail as well. + $instance = $this->objectManager->create(Generator::class); + $instance->getListOfServices(); + } +} diff --git a/etc/webapi.xml b/etc/webapi.xml index 7566c3d399f..0c8bcee049b 100644 --- a/etc/webapi.xml +++ b/etc/webapi.xml @@ -24,7 +24,7 @@ - + From d6f36d18e0778ef3d3d7806a8cd41d6df1f6096d Mon Sep 17 00:00:00 2001 From: Michiel Gerritsen Date: Mon, 29 Jan 2024 15:49:50 +0100 Subject: [PATCH 3/4] Bugfix: Validate the order id by the payment token --- Controller/Checkout/Process.php | 26 ++- Service/Mollie/ValidateProcessRequest.php | 97 ++++++++++ .../Mollie/FakeValidateProcessRequest.php | 30 ++++ .../Controller/Checkout/ProcessTest.php | 20 +++ .../Mollie/ValidateProcessRequestTest.php | 169 ++++++++++++++++++ 5 files changed, 326 insertions(+), 16 deletions(-) create mode 100644 Service/Mollie/ValidateProcessRequest.php create mode 100644 Test/Fakes/Service/Mollie/FakeValidateProcessRequest.php create mode 100644 Test/Integration/Service/Mollie/ValidateProcessRequestTest.php diff --git a/Controller/Checkout/Process.php b/Controller/Checkout/Process.php index d3994ed055e..89e17e8fabc 100644 --- a/Controller/Checkout/Process.php +++ b/Controller/Checkout/Process.php @@ -17,6 +17,7 @@ use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context; use Magento\Checkout\Model\Session; +use Mollie\Payment\Service\Mollie\ValidateProcessRequest; use Mollie\Payment\Service\Order\RedirectOnError; /** @@ -58,6 +59,10 @@ class Process extends Action * @var RedirectOnError */ private $redirectOnError; + /** + * @var ValidateProcessRequest + */ + private $validateProcessRequest; public function __construct( Context $context, @@ -67,7 +72,8 @@ public function __construct( MollieHelper $mollieHelper, OrderRepositoryInterface $orderRepository, RedirectOnError $redirectOnError, - ManagerInterface $eventManager + ManagerInterface $eventManager, + ValidateProcessRequest $validateProcessRequest ) { $this->checkoutSession = $checkoutSession; $this->paymentHelper = $paymentHelper; @@ -76,6 +82,7 @@ public function __construct( $this->orderRepository = $orderRepository; $this->redirectOnError = $redirectOnError; $this->eventManager = $eventManager; + $this->validateProcessRequest = $validateProcessRequest; parent::__construct($context); } @@ -85,7 +92,7 @@ public function __construct( */ public function execute() { - $orderIds = $this->getOrderIds(); + $orderIds = $this->validateProcessRequest->execute(); if (!$orderIds) { $this->mollieHelper->addTolog('error', __('Invalid return, missing order id.')); $this->messageManager->addNoticeMessage(__('Invalid return from Mollie.')); @@ -94,8 +101,7 @@ public function execute() try { $result = []; - $paymentToken = $this->getRequest()->getParam('payment_token'); - foreach ($orderIds as $orderId) { + foreach ($orderIds as $orderId => $paymentToken) { $result = $this->mollieModel->processTransaction($orderId, 'success', $paymentToken); } } catch (\Exception $e) { @@ -134,18 +140,6 @@ public function execute() return $this->handleNonSuccessResult($result, $orderIds); } - /** - * @return array - */ - protected function getOrderIds(): array - { - if ($orderId = $this->getRequest()->getParam('order_id')) { - return [$orderId]; - } - - return $this->getRequest()->getParam('order_ids') ?? []; - } - protected function handleNonSuccessResult(array $result, array $orderIds): ResponseInterface { $this->checkIfLastRealOrder($orderIds); diff --git a/Service/Mollie/ValidateProcessRequest.php b/Service/Mollie/ValidateProcessRequest.php new file mode 100644 index 00000000000..8acfb8ae7cc --- /dev/null +++ b/Service/Mollie/ValidateProcessRequest.php @@ -0,0 +1,97 @@ +request = $request; + $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory; + $this->paymentTokenRepository = $paymentTokenRepository; + } + + /** + * @return array + * @throws AuthorizationException + */ + public function execute(): array + { + $orderIds = $this->getOrderIds(); + $paymentTokens = $this->getPaymentTokens(); + + if (count($orderIds) !== count($paymentTokens)) { + throw new AuthorizationException(__('Invalid payment token')); + } + + /** @var SearchCriteriaBuilder $criteria */ + $criteria = $this->searchCriteriaBuilderFactory->create(); + $criteria->addFilter('token', $paymentTokens, 'in'); + + $output = []; + $validOrderIds = []; + $models = $this->paymentTokenRepository->getList($criteria->create())->getItems(); + + /** @var PaymentTokenInterface $model */ + foreach ($models as $model) { + $validOrderIds[] = $model->getOrderId(); + $output[$model->getOrderId()] = $model->getToken(); + } + + sort($orderIds, SORT_NUMERIC); + sort($validOrderIds, SORT_NUMERIC); + + if ($orderIds !== $validOrderIds) { + throw new AuthorizationException(__('Invalid payment token')); + } + + return $output; + } + + private function getPaymentTokens(): array + { + if ($this->request->getParam('payment_token')) { + return [$this->request->getParam('payment_token')]; + } + + return $this->request->getParam('payment_tokens', []); + } + + private function getOrderIds(): array + { + if ($orderId = $this->request->getParam('order_id')) { + return [$orderId]; + } + + return $this->request->getParam('order_ids') ?? []; + } +} diff --git a/Test/Fakes/Service/Mollie/FakeValidateProcessRequest.php b/Test/Fakes/Service/Mollie/FakeValidateProcessRequest.php new file mode 100644 index 00000000000..53b5180effd --- /dev/null +++ b/Test/Fakes/Service/Mollie/FakeValidateProcessRequest.php @@ -0,0 +1,30 @@ +response = $response; + } + + public function execute(): array + { + if ($this->response !== null) { + return $this->response; + } + + return parent::execute(); // TODO: Change the autogenerated stub + } +} diff --git a/Test/Integration/Controller/Checkout/ProcessTest.php b/Test/Integration/Controller/Checkout/ProcessTest.php index 7d609109cc3..5b8af9c6254 100644 --- a/Test/Integration/Controller/Checkout/ProcessTest.php +++ b/Test/Integration/Controller/Checkout/ProcessTest.php @@ -12,6 +12,8 @@ use Magento\Sales\Api\OrderRepositoryInterface; use Magento\TestFramework\TestCase\AbstractController; use Mollie\Payment\Model\Mollie; +use Mollie\Payment\Service\Mollie\ValidateProcessRequest; +use Mollie\Payment\Test\Fakes\Service\Mollie\FakeValidateProcessRequest; class ProcessTest extends AbstractController { @@ -25,6 +27,8 @@ public function testDoesRedirectsToCartWhenNoIdProvided() public function testRedirectsToCartOnException() { + $this->fakeValidation(['123' => 'abc']); + $mollieModel = $this->createMock(Mollie::class); $mollieModel->method('processTransaction')->willThrowException(new \Exception('[TEST] Transaction failed. Please verify your billing information and payment method, and try again.')); @@ -45,6 +49,7 @@ public function testRedirectsToCartOnException() public function testUsesOrderIdParameter() { $order = $this->loadOrderById('100000001'); + $this->fakeValidation([(string)$order->getId() => 'abc']); $mollieModel = $this->createMock(Mollie::class); $mollieModel->expects($this->once())->method('processTransaction')->with($order->getId())->willReturn([]); @@ -64,6 +69,13 @@ public function testUsesOrderIdsParameter() $order3 = $this->loadOrderById('100000003'); $order4 = $this->loadOrderById('100000004'); + $this->fakeValidation([ + (string)$order1->getId() => 'abc', + (string)$order2->getId() => 'def', + (string)$order3->getId() => 'ghi', + (string)$order4->getId() => 'jkl', + ]); + $mollieModel = $this->createMock(Mollie::class); $mollieModel->expects($this->exactly(4))->method('processTransaction')->willReturn([]); @@ -93,4 +105,12 @@ private function loadOrderById($orderId) return array_shift($orderList); } + + private function fakeValidation(array $response): void + { + $validateProcessRequest = $this->_objectManager->create(FakeValidateProcessRequest::class); + $validateProcessRequest->setResponse($response); + + $this->_objectManager->addSharedInstance($validateProcessRequest, ValidateProcessRequest::class); + } } diff --git a/Test/Integration/Service/Mollie/ValidateProcessRequestTest.php b/Test/Integration/Service/Mollie/ValidateProcessRequestTest.php new file mode 100644 index 00000000000..f5fc15d265c --- /dev/null +++ b/Test/Integration/Service/Mollie/ValidateProcessRequestTest.php @@ -0,0 +1,169 @@ +getOrder(); + + $paymentTokenForOrder = $this->objectManager->get(PaymentTokenForOrder::class); + $token = $paymentTokenForOrder->execute($order); + + $request = $this->objectManager->create(RequestInterface::class); + $request->setParam('order_id', $order->getId()); + $request->setParam('payment_token', $token); + + $instance = $this->objectManager->create(ValidateProcessRequest::class, [ + 'request' => $request, + ]); + + $result = $instance->execute(); + + $this->assertEquals([$order->getId() => $token], $result); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order_list.php + * @magentoDataFixture Magento/Sales/_files/quote.php + */ + public function testValidatesWithMultiplePaymentTokens(): void + { + $orders = [ + $this->getOrder('100000002'), + $this->getOrder('100000003'), + $this->getOrder('100000004'), + ]; + + $orderIds = []; + $paymentTokens = []; + foreach ($orders as $order) { + $paymentTokenForOrder = $this->objectManager->get(PaymentTokenForOrder::class); + $orderIds[] = $order->getEntityId(); + $paymentTokens[] = $paymentTokenForOrder->execute($order); + } + + $request = $this->objectManager->create(RequestInterface::class); + $request->setParam('order_ids', $orderIds); + $request->setParam('payment_tokens', $paymentTokens); + + $instance = $this->objectManager->create(ValidateProcessRequest::class, [ + 'request' => $request, + ]); + + $result = $instance->execute(); + + $this->assertEquals([ + $orders[0]->getId() => $paymentTokens[0], + $orders[1]->getId() => $paymentTokens[1], + $orders[2]->getId() => $paymentTokens[2], + ], $result); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoDataFixture Magento/Sales/_files/quote.php + */ + public function testFailsWhenTheOrderIdIsWrong(): void + { + $order = $this->getOrder(); + + $paymentTokenForOrder = $this->objectManager->get(PaymentTokenForOrder::class); + $token = $paymentTokenForOrder->execute($order); + + $request = $this->objectManager->create(RequestInterface::class); + $request->setParam('order_ids', [999]); + $request->setParam('payment_tokens', [$token]); + + $instance = $this->objectManager->create(ValidateProcessRequest::class, [ + 'request' => $request, + ]); + + $this->expectException(AuthorizationException::class); + + $instance->execute(); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoDataFixture Magento/Sales/_files/quote.php + */ + public function testFailsWhenTheOrdersDontMatchTheTokens(): void + { + $order = $this->getOrder(); + + $paymentTokenForOrder = $this->objectManager->get(PaymentTokenForOrder::class); + $token = $paymentTokenForOrder->execute($order); + + $request = $this->objectManager->create(RequestInterface::class); + $request->setParam('order_ids', [888, $order->getId()]); + $request->setParam('payment_tokens', [$token]); + + $instance = $this->objectManager->create(ValidateProcessRequest::class, [ + 'request' => $request, + ]); + + $this->expectException(AuthorizationException::class); + + $instance->execute(); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order_list.php + * @magentoDataFixture Magento/Sales/_files/quote.php + */ + public function testFailsWhenTheTokensDontMatchTheOrders(): void + { + $orders = [ + $this->getOrder('100000002'), + $this->getOrder('100000003'), + ]; + + $paymentTokenForOrder = $this->objectManager->get(PaymentTokenForOrder::class); + $tokens = [ + $paymentTokenForOrder->execute($orders[0]), + $paymentTokenForOrder->execute($orders[1]), + ]; + + $request = $this->objectManager->create(RequestInterface::class); + $request->setParam('order_ids', [$orders[0]->getId()]); // Only 1 order, should be 2. + $request->setParam('payment_tokens', $tokens); + + $instance = $this->objectManager->create(ValidateProcessRequest::class, [ + 'request' => $request, + ]); + + $this->expectException(AuthorizationException::class); + + $instance->execute(); + } + + private function getOrder(string $orderId = '100000001'): OrderInterface + { + $cart = $this->objectManager->create(Quote::class); + $cart->load('test01', 'reserved_order_id'); + + $order = $this->loadOrder($orderId); + $order->setQuoteId($cart->getId()); + return $order; + } +} From 8e2728958470c2b0316607c9b243e9420de4bec7 Mon Sep 17 00:00:00 2001 From: Marvin Besselsen Date: Tue, 30 Jan 2024 12:41:15 +0100 Subject: [PATCH 4/4] 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 8e08fe4479b..2cf82852f5c 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "mollie/magento2", "description": "Mollie Payment Module for Magento 2", - "version": "2.33.0", + "version": "2.34.0", "keywords": [ "mollie", "payment", diff --git a/etc/config.xml b/etc/config.xml index 176c026a8d3..cee30a1031b 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -3,7 +3,7 @@ - v2.33.0 + v2.34.0 0 0 test