From 94bf57a7d1d7cd66965fbad484ca225f225e9a8e Mon Sep 17 00:00:00 2001 From: Jeantwan Teuma Date: Mon, 26 Sep 2022 06:06:43 -0400 Subject: [PATCH 01/20] Update getPaymentMethod annotation (#1726) --- Api/Data/OrderPaymentInterface.php | 2 +- Model/Order/Payment.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Api/Data/OrderPaymentInterface.php b/Api/Data/OrderPaymentInterface.php index c25579791..0ee374a18 100644 --- a/Api/Data/OrderPaymentInterface.php +++ b/Api/Data/OrderPaymentInterface.php @@ -104,7 +104,7 @@ public function setPaymentId($paymentId); /** * Gets the Paymentmethod for the payment. * - * @return int|null PaymentMethod. + * @return string|null PaymentMethod. */ public function getPaymentMethod(); diff --git a/Model/Order/Payment.php b/Model/Order/Payment.php index 0494cd819..6f4183013 100644 --- a/Model/Order/Payment.php +++ b/Model/Order/Payment.php @@ -97,7 +97,7 @@ public function setPaymentId($paymentId) } /** - * @return mixed + * @return string|null */ public function getPaymentMethod() { From fab3b4dfb47a3d1f9e47b50a1e93aece591554d7 Mon Sep 17 00:00:00 2001 From: raoulritter <59527829+raoulritter@users.noreply.github.com> Date: Mon, 26 Sep 2022 15:29:20 +0200 Subject: [PATCH 02/20] [PW-6969] Fix External Platform Integrator to Adyen. (#1727) * [PW-7119] Add configuration to magento admin to setup the live domain file for Applepay. * [PW-6969] Add Adyen as External Platform Integrator for native Magento Plugin * Revert "[PW-7119] Add configuration to magento admin to setup the live domain file for Applepay." This reverts commit ede408145907bf3736c766e59ec8b61051d37013. * Update Helper/Data.php Change from double to single quotes and add a space for cleanliness. Co-authored-by: Jeantwan Teuma Co-authored-by: Jeantwan Teuma --- Helper/Data.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Helper/Data.php b/Helper/Data.php index 2e21a5595..cb24665b3 100755 --- a/Helper/Data.php +++ b/Helper/Data.php @@ -1489,7 +1489,7 @@ public function initializeAdyenClient($storeId = null, $apiKey = null, $motoMerc $moduleVersion = $this->getModuleVersion(); $client->setMerchantApplication($this->getModuleName(), $moduleVersion); - $client->setExternalPlatform($this->productMetadata->getName(), $this->productMetadata->getVersion()); + $client->setExternalPlatform($this->productMetadata->getName(), $this->productMetadata->getVersion(), 'Adyen'); if ($isDemo) { $client->setEnvironment(\Adyen\Environment::TEST); $client->setLogger($this->adyenLogger); From 42b1fa162ca5db158c4ee8a99a252aef11449c1d Mon Sep 17 00:00:00 2001 From: Michael Paul Date: Tue, 27 Sep 2022 09:59:17 +0200 Subject: [PATCH 03/20] PW-7197: Display merchant ID instead of name (#1728) * PW-7197: Fallback to merchant ID when name is undefined * PW-7197: Aways display ID Co-authored-by: Jeantwan Teuma --- Helper/ManagementHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Helper/ManagementHelper.php b/Helper/ManagementHelper.php index 04067da8e..3495a982e 100644 --- a/Helper/ManagementHelper.php +++ b/Helper/ManagementHelper.php @@ -83,7 +83,7 @@ public function getMerchantAccountsAndClientKey(string $apiKey, bool $demoMode): }); $merchantAccounts[] = [ 'id' => $merchantAccount['id'], - 'name' => $merchantAccount['name'], + 'name' => $merchantAccount['id'], 'liveEndpointPrefix' => !empty($defaultDC) ? $defaultDC[0]['livePrefix'] : '' ]; } From b46b2ef7818d2f0bd3c1c81e9b297bab61b34828 Mon Sep 17 00:00:00 2001 From: Cenk Kucukiravul Date: Tue, 27 Sep 2022 04:30:14 -0400 Subject: [PATCH 04/20] [PW-7229] Do a clean install for E2E tests (#1725) * Do a clean install for E2E tests * Remove condition on saving test artifacts * Revert the conditional removal for keeping the artifacts Co-authored-by: cenkk Co-authored-by: Jeantwan Teuma --- .github/scripts/e2e.sh | 3 ++- .github/workflows/integration-test.yml | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/scripts/e2e.sh b/.github/scripts/e2e.sh index 99f4a8259..960a4b382 100755 --- a/.github/scripts/e2e.sh +++ b/.github/scripts/e2e.sh @@ -8,7 +8,8 @@ cd adyen-integration-tools-tests; git checkout $INTEGRATION_TESTS_BRANCH; # Setup environment -npm ci; +rm -rf package-lock.json; +npm i; npx playwright install --with-deps; # Run tests diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 92a63e0b0..ba01d3aee 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -5,11 +5,11 @@ jobs: build: strategy: matrix: - php-version: ['7.4'] - magento-version: ['2.3.7', '2.4.2'] + php-version: ["7.4"] + magento-version: ["2.3.7", "2.4.2"] include: - - php-version: '8.1' - magento-version: '2.4.5' + - php-version: "8.1" + magento-version: "2.4.5" runs-on: ubuntu-latest env: PHP_VERSION: ${{ matrix.php-version }} From 9c1835c3a695357687c88b7514fb21b6c0eb40f2 Mon Sep 17 00:00:00 2001 From: Cenk Kucukiravul Date: Tue, 27 Sep 2022 17:18:19 +0200 Subject: [PATCH 05/20] PW-7195 Add new e2e workflow for integration tests (#1730) Co-authored-by: cenkk --- .github/workflows/e2e-only.yml | 63 ++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/workflows/e2e-only.yml diff --git a/.github/workflows/e2e-only.yml b/.github/workflows/e2e-only.yml new file mode 100644 index 000000000..54446c012 --- /dev/null +++ b/.github/workflows/e2e-only.yml @@ -0,0 +1,63 @@ +name: E2E Tests + +on: + workflow_dispatch: + inputs: + testBranch: + description: "Test Branch from Integration Tools Repo" + required: true + default: "develop" + +jobs: + build: + runs-on: ubuntu-latest + env: + PHP_VERSION: "8.1" + MAGENTO_VERSION: "2.4.5" + ADMIN_USERNAME: ${{secrets.MAGENTO_ADMIN_USERNAME}} + ADMIN_PASSWORD: ${{secrets.MAGENTO_ADMIN_PASSWORD}} + steps: + - uses: actions/checkout@v2 + + - name: Install Magento + run: docker-compose -f .github/workflows/templates/docker-compose.yml run --rm web make magento + + - name: Start web server in background + run: docker-compose -f .github/workflows/templates/docker-compose.yml up -d web + env: + DONATION_ACCOUNT: ${{secrets.DONATION_ACCOUNT}} + ADYEN_MERCHANT: ${{secrets.ADYEN_MERCHANT}} + ADYEN_API_KEY: ${{secrets.ADYEN_API_KEY}} + ADYEN_CLIENT_KEY: ${{secrets.ADYEN_CLIENT_KEY}} + + - name: Setup permissions + run: docker exec magento2-container make fs + + - name: Check install + run: docker exec magento2-container make sys-check + + - name: Install plugin + run: docker exec -u www-data magento2-container make plugin + + - name: Switch to production mode + run: docker exec -u www-data magento2-container make production + + - name: Setup permissions + run: docker exec magento2-container make fs + + - name: Run E2E tests + continue-on-error: true + run: docker-compose -f .github/workflows/templates/docker-compose.yml run --rm playwright /e2e.sh + env: + INTEGRATION_TESTS_BRANCH: ${{inputs.testBranch}} + MAGENTO_ADMIN_USERNAME: ${{secrets.MAGENTO_ADMIN_USERNAME}} + MAGENTO_ADMIN_PASSWORD: ${{secrets.MAGENTO_ADMIN_PASSWORD}} + MAGENTO_BASE_URL: ${{secrets.MAGENTO_BASE_URL}} + PAYPAL_USERNAME: ${{secrets.PLAYWRIGHT_PAYPAL_USERNAME}} + PAYPAL_PASSWORD: ${{secrets.PLAYWRIGHT_PAYPAL_PASSWORD}} + + - name: Archive test result artifacts + uses: actions/upload-artifact@v3 + with: + name: html-report + path: test-report From bf2c87b3c6fab236992d43c71384bf5f3cde485c Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Wed, 28 Sep 2022 09:58:58 +0200 Subject: [PATCH 06/20] [PW-7189] - Disable cross button and ESC key on action modal (#1731) * [PW-7189] - Disable cross button and ESC key on action modal * [PW-7189] - Code formatting --- view/frontend/web/js/model/adyen-payment-modal.js | 4 ++++ .../web/js/view/payment/method-renderer/adyen-cc-method.js | 7 ++++--- .../view/payment/method-renderer/adyen-oneclick-method.js | 5 ++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/view/frontend/web/js/model/adyen-payment-modal.js b/view/frontend/web/js/model/adyen-payment-modal.js index 63c10057c..b3f83ad6c 100644 --- a/view/frontend/web/js/model/adyen-payment-modal.js +++ b/view/frontend/web/js/model/adyen-payment-modal.js @@ -21,6 +21,10 @@ define( let popupModal = $('#' + modalLabel).modal({ // disable user to hide popup clickableOverlay: false, + // disable escape key to hide popup + keyEventHandlers: { + escapeKey: function () { return; } + }, responsive: true, innerScroll: false, // empty buttons, we don't need that diff --git a/view/frontend/web/js/view/payment/method-renderer/adyen-cc-method.js b/view/frontend/web/js/view/payment/method-renderer/adyen-cc-method.js index c9e8590c9..6650fb33a 100755 --- a/view/frontend/web/js/view/payment/method-renderer/adyen-cc-method.js +++ b/view/frontend/web/js/view/payment/method-renderer/adyen-cc-method.js @@ -211,7 +211,10 @@ define( } }, showModal: function() { - return AdyenPaymentModal.showModal(adyenPaymentService, fullScreenLoader, this.messageContainer, this.orderId, this.modalLabel, this.isPlaceOrderActionAllowed) + let actionModal = AdyenPaymentModal.showModal(adyenPaymentService, fullScreenLoader, this.messageContainer, this.orderId, this.modalLabel, this.isPlaceOrderActionAllowed); + $("." + this.modalLabel + " .action-close").hide(); + + return actionModal; }, /** * This method is a workaround to close the modal in the right way and reconstruct the threeDS2Modal. @@ -309,8 +312,6 @@ define( } }, handleOnAdditionalDetails: function(result) { - $('.action-close').hide(); - var self = this; var request = result.data; diff --git a/view/frontend/web/js/view/payment/method-renderer/adyen-oneclick-method.js b/view/frontend/web/js/view/payment/method-renderer/adyen-oneclick-method.js index 2826261bc..ace544771 100755 --- a/view/frontend/web/js/view/payment/method-renderer/adyen-oneclick-method.js +++ b/view/frontend/web/js/view/payment/method-renderer/adyen-oneclick-method.js @@ -511,7 +511,10 @@ define( return true; }, showModal: function() { - return adyenPaymentModal.showModal(adyenPaymentService, fullScreenLoader, this.messageContainer, this.orderId, this.modalLabel, this.isPlaceOrderActionAllowed) + let actionModal = adyenPaymentModal.showModal(adyenPaymentService, fullScreenLoader, this.messageContainer, this.orderId, this.modalLabel, this.isPlaceOrderActionAllowed) + $("." + this.modalLabel + " .action-close").hide(); + + return actionModal; }, closeModal: function(popupModal) { adyenPaymentModal.closeModal(popupModal, this.modalLabel) From c636a3a5150111db243308f2ebd5f9981dfc7dc4 Mon Sep 17 00:00:00 2001 From: Cenk Kucukiravul Date: Wed, 28 Sep 2022 16:19:20 +0200 Subject: [PATCH 07/20] [PW-7195] Add dynamic naming for E2E workflows (#1733) * Rename e2e workflow upon request * Add dynamic naming for workflow run * Update docker image version of Playwright Co-authored-by: Cenk Kucukiravul --- .github/workflows/{e2e-only.yml => e2e.yml} | 1 + .github/workflows/templates/docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) rename .github/workflows/{e2e-only.yml => e2e.yml} (95%) diff --git a/.github/workflows/e2e-only.yml b/.github/workflows/e2e.yml similarity index 95% rename from .github/workflows/e2e-only.yml rename to .github/workflows/e2e.yml index 54446c012..48bdb76d7 100644 --- a/.github/workflows/e2e-only.yml +++ b/.github/workflows/e2e.yml @@ -1,4 +1,5 @@ name: E2E Tests +run-name: Headless E2E test run for adyen-integration-tools-tests repo ${{inputs.testBranch}} on: workflow_dispatch: diff --git a/.github/workflows/templates/docker-compose.yml b/.github/workflows/templates/docker-compose.yml index 3edd6844e..3aa56f74a 100644 --- a/.github/workflows/templates/docker-compose.yml +++ b/.github/workflows/templates/docker-compose.yml @@ -59,7 +59,7 @@ services: aliases: - magento2.test.com playwright: - image: mcr.microsoft.com/playwright:v1.20.0-focal + image: mcr.microsoft.com/playwright:v1.25.0-focal shm_size: 1gb ipc: host cap_add: From 2479621cac02d92f1ee43fdf228149d9c7146e56 Mon Sep 17 00:00:00 2001 From: Rok Popov Ledinski Date: Wed, 28 Sep 2022 17:58:42 +0200 Subject: [PATCH 08/20] [PW-7032] Add unit tests for Adyen Order Payment helper (#1732) * add test for isFullAmountFinalized - immediate capture * [PW-7208] Update adyen method call for virtual products (#1716) * Update adyen method call for virtual products * Remove checking for shipping adress before doing the payment methods call Co-authored-by: Jeantwan Teuma Co-authored-by: Peter Ojo * Version bump 8.6.0 * [PW-7147] Handle Capture Requests triggered from Customer Area (#1714) * create adyen and magento invoice from webhook if captured manually in CA * remove code smells * throw exception if adyen_invoice null and canInvoice is false * throw exception in case of CA initiated partial manual capture * use AdyenWebhookException instead of a generic one * rename method Co-authored-by: Can Demiralp * use renamed method Co-authored-by: Can Demiralp * update $adyenInvoice Co-authored-by: Michael Paul * resolve merge conflicts * get rid of code smells * remove final code smells * use $adyenInvoiceObject instead of $adyenInvoice * remove code smell Co-authored-by: Can Demiralp Co-authored-by: Michael Paul Co-authored-by: Peter Ojo * refactor the way we are building mock AdyenOrderPaymentHelper, fix testCreateAdyenOrderPayment * setup testIsFullAmountAuthorized() * add testIsFullAmountNotFinalizedManualCapture() and testIsFullAmountAuthorized() * setup testRefundAdyenOrderPayment() * remove code smells * align params - code smell * fix testRefundAdyenOrderPayment Co-authored-by: Alexandros Moraitis Co-authored-by: Jeantwan Teuma Co-authored-by: Peter Ojo Co-authored-by: peterojo Co-authored-by: Can Demiralp Co-authored-by: Michael Paul --- Helper/AdyenOrderPayment.php | 6 +- Test/Unit/Helper/AdyenOrderPaymentTest.php | 318 +++++++++++++++++---- 2 files changed, 273 insertions(+), 51 deletions(-) diff --git a/Helper/AdyenOrderPayment.php b/Helper/AdyenOrderPayment.php index 40814f81d..67b03e89d 100644 --- a/Helper/AdyenOrderPayment.php +++ b/Helper/AdyenOrderPayment.php @@ -172,7 +172,6 @@ public function isFullAmountFinalized(Order $order): bool } $orderAmountCents = $this->adyenDataHelper->formatAmount($orderAmountCurrency->getAmount(), $orderAmountCurrency->getCurrencyCode()); - return $invoiceAmountCents === $orderAmountCents; } @@ -185,7 +184,8 @@ public function isFullAmountFinalized(Order $order): bool public function isFullAmountAuthorized(Order $order): bool { $payment = $order->getPayment(); - $authorisedAdyenOrderPayments = $this->orderPaymentResourceModel->getLinkedAdyenOrderPayments($payment->getEntityId()); + $entityId = $payment->getEntityId(); + $authorisedAdyenOrderPayments = $this->orderPaymentResourceModel->getLinkedAdyenOrderPayments($entityId); return $this->compareAdyenOrderPaymentsAmount($order, $authorisedAdyenOrderPayments); } @@ -267,7 +267,7 @@ private function compareAdyenOrderPaymentsAmount(Order $order, array $adyenOrder $orderAmountCurrency = $this->adyenChargedCurrencyHelper->getOrderAmountCurrency($order); foreach ($adyenOrderPayments as $adyenOrderPayment) { - $adyenOrderPaymentsTotal += $adyenOrderPayment[OrderPaymentInterface::AMOUNT]; + $adyenOrderPaymentsTotal += $adyenOrderPayment->getAmount(); } $adyenOrderPaymentsTotalCents = $this->adyenDataHelper->formatAmount($adyenOrderPaymentsTotal, $orderAmountCurrency->getCurrencyCode()); diff --git a/Test/Unit/Helper/AdyenOrderPaymentTest.php b/Test/Unit/Helper/AdyenOrderPaymentTest.php index 18c54e421..4405182a1 100644 --- a/Test/Unit/Helper/AdyenOrderPaymentTest.php +++ b/Test/Unit/Helper/AdyenOrderPaymentTest.php @@ -16,6 +16,7 @@ use Adyen\Payment\Helper\Data; use Adyen\Payment\Helper\Invoice; use Adyen\Payment\Logger\AdyenLogger; +use Adyen\Payment\Model\AdyenAmountCurrency; use Adyen\Payment\Model\Notification; use Adyen\Payment\Model\Order\Payment as AdyenPaymentModel; use Adyen\Payment\Model\Order\PaymentFactory; @@ -26,51 +27,6 @@ class AdyenOrderPaymentTest extends AbstractAdyenTestCase { - /** - * @var AdyenOrderPayment - */ - private $adyenOrderPaymentHelper; - /** - * @var Payment|\PHPUnit\Framework\MockObject\MockObject - */ - private $mockOrderPaymentResourceModel; - /** - * @var Data|\PHPUnit\Framework\MockObject\MockObject - */ - private $mockAdyenDataHelper; - /** - * @var PaymentFactory|\PHPUnit\Framework\MockObject\MockObject - */ - private $mockAdyenOrderPaymentFactory; - - /** - * @var Invoice|\PHPUnit\Framework\MockObject\MockObject - */ - private $mockInvoiceHelper; - - public function setUp(): void - { - $mockContext = $this->createMock(Context::class); - $mockLogger = $this->createMock(AdyenLogger::class); - $this->mockAdyenDataHelper = $this->createMock(Data::class); - $mockChargedCurrency = $this->createMock(ChargedCurrency::class); - $this->mockOrderPaymentResourceModel = $this->createMock(Payment::class); - $mockAdyenOrderPaymentCollection = $this->createGeneratedMock(Payment\CollectionFactory::class); - $this->mockAdyenOrderPaymentFactory = $this->createGeneratedMock(PaymentFactory::class, ['create']); - $this->mockInvoiceHelper = $this->createMock(Invoice::class); - - $this->adyenOrderPaymentHelper = new AdyenOrderPayment( - $mockContext, - $mockLogger, - $mockAdyenOrderPaymentCollection, - $this->mockAdyenDataHelper, - $mockChargedCurrency, - $this->mockOrderPaymentResourceModel, - $this->mockAdyenOrderPaymentFactory, - $this->mockInvoiceHelper - ); - } - public function testCreateAdyenOrderPayment() { $paymentId = 1; @@ -95,9 +51,275 @@ public function testCreateAdyenOrderPayment() 'getMerchantReference' => $merchantReference ]); - $this->mockAdyenDataHelper->method('originalAmount')->willReturn($amount); - $this->mockAdyenOrderPaymentFactory->method('create')->willReturn($adyenOrderPayment); - $result = $this->adyenOrderPaymentHelper->createAdyenOrderPayment($order, $notification, true); + $mockAdyenDataHelper = $this->createGeneratedMock(Data::class, ['originalAmount']); + + $mockAdyenOrderPaymentFactory = $this->createGeneratedMock(PaymentFactory::class, ['create']); + + $adyenOrderPaymentHelper = $this->createAdyenOrderPaymentHelper( + null, + null, + $mockAdyenDataHelper, + null, + null, + $mockAdyenOrderPaymentFactory, + null + ); + + $mockAdyenDataHelper->method('originalAmount')->willReturn($amount); + $mockAdyenOrderPaymentFactory->method('create')->willReturn($adyenOrderPayment); + $result = $adyenOrderPaymentHelper->createAdyenOrderPayment($order, $notification, true); $this->assertInstanceOf(AdyenPaymentModel::class, $result); } + + public function testIsFullAmountFinalizedAutoCapture() + { + $orderAmountCurrency = new AdyenAmountCurrency( + 10.33, + 'EUR', + null, + null, + 10.33 + ); + + $mockChargedCurrency = $this->createConfiguredMock(ChargedCurrency::class, [ + 'getOrderAmountCurrency' => $orderAmountCurrency + ]); + + $adyenOrderPayment = $this->createConfiguredMock(AdyenPaymentModel::class, [ + 'getAmount' => 10.33 + ]); + + $payment = $this->createConfiguredMock(Order\Payment::class, [ + 'getEntityId' => 55 + ]); + + $order = $this->createConfiguredMock(Order::class, [ + 'getPayment' => $payment + ]); + + $mockOrderPaymentResourceModel = $this->createConfiguredMock(Payment::class, [ + 'getLinkedAdyenOrderPayments' => [$adyenOrderPayment] + ]); + + $mockAdyenDataHelper = $this->createPartialMock(Data::class, []); + + $adyenOrderPaymentHelper = $this->createAdyenOrderPaymentHelper( + null, + null, + $mockAdyenDataHelper, + $mockChargedCurrency, + $mockOrderPaymentResourceModel + ); + + $this->assertTrue($adyenOrderPaymentHelper->isFullAMountFinalized($order)); + } + + public function testIsFullAmountFinalizedManualCapture() + { + $invoice = $this->createConfiguredMock(Order\Invoice::class, [ + 'getGrandTotal' => 10.33, + 'getOrderCurrencyCode' => 'EUR' + ]); + + $mockInvoiceHelper = $this->createConfiguredMock(Invoice::class, [ + 'isFullInvoiceAmountManuallyCaptured' => true + ]); + + $mockAdyenDataHelper = $this->createPartialMock(Data::class, []); + + $payment = $this->createConfiguredMock(Order\Payment::class, [ + 'getEntityId' => 55 + ]); + + $order = $this->createConfiguredMock(Order::class, [ + 'getInvoiceCollection' => [$invoice], + 'getPayment' => $payment + ]); + + $orderAmountCurrency = new AdyenAmountCurrency( + 10.33, + 'EUR', + null, + null, + 10.33 + ); + + $mockChargedCurrency = $this->createConfiguredMock(ChargedCurrency::class, [ + 'getOrderAmountCurrency' => $orderAmountCurrency + ]); + + $adyenOrderPaymentHelper = $this->createAdyenOrderPaymentHelper( + null, + null, + $mockAdyenDataHelper, + $mockChargedCurrency, + null, + null, + $mockInvoiceHelper, + ); + + $this->assertTrue($adyenOrderPaymentHelper->isFullAmountFinalized($order)); + } + + public function testIsFullAmountNotFinalizedManualCapture() + { + $invoice = $this->createConfiguredMock(Order\Invoice::class, [ + 'getGrandTotal' => 10.53, + 'getOrderCurrencyCode' => 'EUR' + ]); + + $mockInvoiceHelper = $this->createConfiguredMock(Invoice::class, [ + 'isFullInvoiceAmountManuallyCaptured' => true + ]); + + $mockAdyenDataHelper = $this->createPartialMock(Data::class, []); + + $payment = $this->createConfiguredMock(Order\Payment::class, [ + 'getEntityId' => 55 + ]); + + $order = $this->createConfiguredMock(Order::class, [ + 'getInvoiceCollection' => [$invoice], + 'getPayment' => $payment + ]); + + $orderAmountCurrency = new AdyenAmountCurrency( + 10.33, + 'EUR', + null, + null, + 10.33 + ); + + $mockChargedCurrency = $this->createConfiguredMock(ChargedCurrency::class, [ + 'getOrderAmountCurrency' => $orderAmountCurrency + ]); + + $adyenOrderPaymentHelper = $this->createAdyenOrderPaymentHelper( + null, + null, + $mockAdyenDataHelper, + $mockChargedCurrency, + null, + null, + $mockInvoiceHelper, + ); + + $this->assertFalse($adyenOrderPaymentHelper->isFullAmountFinalized($order)); + } + + public function testIsFullAmountAuthorized() + { + $orderAmountCurrency = new AdyenAmountCurrency( + 10.33, + 'EUR', + null, + null, + 10.33 + ); + + $mockChargedCurrency = $this->createConfiguredMock(ChargedCurrency::class, [ + 'getOrderAmountCurrency' => $orderAmountCurrency + ]); + + $payment = $this->createConfiguredMock(Order\Payment::class, [ + 'getEntityId' => 55 + ]); + + $adyenOrderPayment = $this->createConfiguredMock(\Adyen\Payment\Model\Order\Payment::class, [ + 'getAmount' => 10.33 + ]); + + $order = $this->createConfiguredMock(Order::class, [ + 'getPayment' => $payment + ]); + + $mockOrderPaymentResourceModel = $this->createConfiguredMock(Payment::class, [ + 'getLinkedAdyenOrderPayments' => [$adyenOrderPayment] + ]); + + $mockAdyenDataHelper = $this->createPartialMock(Data::class, []); + + $adyenOrderPaymentHelper = $this->createAdyenOrderPaymentHelper( + null, + null, + $mockAdyenDataHelper, + $mockChargedCurrency, + $mockOrderPaymentResourceModel + ); + + $this->assertTrue($adyenOrderPaymentHelper->isFullAmountAuthorized($order)); + } + + public function testRefundAdyenOrderPayment() + { + $payment = $this->createMock(\Adyen\Payment\Model\Order\Payment::class); + + $payment->expects($this->once())->method('save'); + + $notification = $this->createMock(Notification::class); + + $mockAdyenDataHelper = $this->createGeneratedMock(Data::class, ['originalAmount']); + + $adyenOrderPaymentHelper = $this->createAdyenOrderPaymentHelper( + null, + null, + $mockAdyenDataHelper + ); + + $result = $adyenOrderPaymentHelper->refundAdyenOrderPayment($payment, $notification); + + $this->assertSame($payment, $result); + } + + protected function createAdyenOrderPaymentHelper( + $mockLogger = null, + $mockAdyenOrderPaymentCollection = null, + $mockAdyenDataHelper = null, + $mockChargedCurrency = null, + $mockOrderPaymentResourceModel = null, + $mockAdyenOrderPaymentFactory = null, + $mockInvoiceHelper = null + ): AdyenOrderPayment { + $mockContext = $this->createMock(Context::class); + + if (is_null($mockLogger)) { + $mockLogger = $this->createMock(AdyenLogger::class); + } + + if (is_null($mockAdyenOrderPaymentCollection)) { + $mockAdyenOrderPaymentCollection = $this->createGeneratedMock(Payment\CollectionFactory::class); + } + + if (is_null($mockAdyenDataHelper)) { + $mockAdyenDataHelper = $this->createMock(Data::class); + } + + if (is_null($mockChargedCurrency)) { + $mockChargedCurrency = $this->createMock(ChargedCurrency::class); + } + + if (is_null($mockOrderPaymentResourceModel)) { + $mockOrderPaymentResourceModel = $this->createMock(Payment::class); + } + + if (is_null($mockAdyenOrderPaymentFactory)) { + $mockAdyenOrderPaymentFactory = $this->createGeneratedMock(PaymentFactory::class, ['create']); + } + + if (is_null($mockInvoiceHelper)) { + $mockInvoiceHelper = $this->createMock(Invoice::class); + } + + return new AdyenOrderPayment( + $mockContext, + $mockLogger, + $mockAdyenOrderPaymentCollection, + $mockAdyenDataHelper, + $mockChargedCurrency, + $mockOrderPaymentResourceModel, + $mockAdyenOrderPaymentFactory, + $mockInvoiceHelper + ); + } } From 05cad3da6178b428c2773ce7cc880ac28e2efe5e Mon Sep 17 00:00:00 2001 From: Rok Popov Ledinski Date: Fri, 30 Sep 2022 11:29:26 +0200 Subject: [PATCH 09/20] fix comparison when handling capture webhook from CA (#1742) --- Helper/Invoice.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Helper/Invoice.php b/Helper/Invoice.php index 2df63d555..b0db9d3cd 100644 --- a/Helper/Invoice.php +++ b/Helper/Invoice.php @@ -283,7 +283,7 @@ public function handleCaptureWebhook(Order $order, Notification $notification): $isFullAmountCaptured = $this->adyenDataHelper->originalAmount( $notification->getAmountValue(), $notification->getAmountCurrency() - ) === $order->getBaseGrandTotal(); + ) === floatval($order->getBaseGrandTotal()); if (is_null($adyenInvoice) && $order->canInvoice()) { if ($isFullAmountCaptured) { From 55c723584daaadcb18095b26e6548b45835c3747 Mon Sep 17 00:00:00 2001 From: Tigren Solutions Date: Mon, 3 Oct 2022 19:12:57 +0700 Subject: [PATCH 10/20] Update ManagementHelper.php (#1738) Co-authored-by: Jeantwan Teuma --- Helper/ManagementHelper.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Helper/ManagementHelper.php b/Helper/ManagementHelper.php index 3495a982e..aabfd1351 100644 --- a/Helper/ManagementHelper.php +++ b/Helper/ManagementHelper.php @@ -20,6 +20,7 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\StoreManager; use Adyen\Payment\Logger\AdyenLogger; +use Magento\Framework\Encryption\EncryptorInterface; class ManagementHelper { @@ -35,6 +36,11 @@ class ManagementHelper * @var Config */ private $configHelper; + + /** + * @var EncryptorInterface + */ + private $encryptor; /** * Logging instance @@ -46,17 +52,20 @@ class ManagementHelper /** * ManagementHelper constructor. * @param StoreManager $storeManager + * @param EncryptorInterface $encryptor * @param Data $dataHelper * @param Config $configHelper */ public function __construct( StoreManager $storeManager, + EncryptorInterface $encryptor, Data $dataHelper, Config $configHelper, AdyenLogger $adyenLogger ) { $this->dataHelper = $dataHelper; $this->storeManager = $storeManager; + $this->encryptor = $encryptor; $this->configHelper = $configHelper; $this->adyenLogger = $adyenLogger; } @@ -169,7 +178,8 @@ public function setupWebhookCredentials( // generate hmac key and save $response = $management->merchantWebhooks->generateHmac($merchantId, $webhookId); - $hmac = $response['hmacKey']; + $hmacKey = $response['hmacKey']; + $hmac = $this->encryptor->encrypt($hmacKey); $mode = $demoMode ? 'test' : 'live'; $this->configHelper->setConfigData($hmac, 'notification_hmac_key_' . $mode, Config::XML_ADYEN_ABSTRACT_PREFIX); } From 8a76889764cd1a7b0b922e5ef6db8df41352a74c Mon Sep 17 00:00:00 2001 From: Rok Popov Ledinski Date: Mon, 3 Oct 2022 17:15:12 +0200 Subject: [PATCH 11/20] [PW-7261] Call to getAmount() failing in AdyenOrderPayment helper in develop branch --- Helper/AdyenOrderPayment.php | 2 +- Test/Unit/Helper/AdyenOrderPaymentTest.php | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Helper/AdyenOrderPayment.php b/Helper/AdyenOrderPayment.php index 67b03e89d..d2f5f20b3 100644 --- a/Helper/AdyenOrderPayment.php +++ b/Helper/AdyenOrderPayment.php @@ -267,7 +267,7 @@ private function compareAdyenOrderPaymentsAmount(Order $order, array $adyenOrder $orderAmountCurrency = $this->adyenChargedCurrencyHelper->getOrderAmountCurrency($order); foreach ($adyenOrderPayments as $adyenOrderPayment) { - $adyenOrderPaymentsTotal += $adyenOrderPayment->getAmount(); + $adyenOrderPaymentsTotal += $adyenOrderPayment[OrderPaymentInterface::AMOUNT]; } $adyenOrderPaymentsTotalCents = $this->adyenDataHelper->formatAmount($adyenOrderPaymentsTotal, $orderAmountCurrency->getCurrencyCode()); diff --git a/Test/Unit/Helper/AdyenOrderPaymentTest.php b/Test/Unit/Helper/AdyenOrderPaymentTest.php index 4405182a1..9b09a49b2 100644 --- a/Test/Unit/Helper/AdyenOrderPaymentTest.php +++ b/Test/Unit/Helper/AdyenOrderPaymentTest.php @@ -11,6 +11,7 @@ namespace Adyen\Payment\Tests\Unit\Helper; +use Adyen\Payment\Api\Data\OrderPaymentInterface; use Adyen\Payment\Helper\AdyenOrderPayment; use Adyen\Payment\Helper\ChargedCurrency; use Adyen\Payment\Helper\Data; @@ -85,9 +86,7 @@ public function testIsFullAmountFinalizedAutoCapture() 'getOrderAmountCurrency' => $orderAmountCurrency ]); - $adyenOrderPayment = $this->createConfiguredMock(AdyenPaymentModel::class, [ - 'getAmount' => 10.33 - ]); + $adyenOrderPayment = [OrderPaymentInterface::AMOUNT => 10.33]; $payment = $this->createConfiguredMock(Order\Payment::class, [ 'getEntityId' => 55 @@ -226,9 +225,7 @@ public function testIsFullAmountAuthorized() 'getEntityId' => 55 ]); - $adyenOrderPayment = $this->createConfiguredMock(\Adyen\Payment\Model\Order\Payment::class, [ - 'getAmount' => 10.33 - ]); + $adyenOrderPayment = [OrderPaymentInterface::AMOUNT => 10.33]; $order = $this->createConfiguredMock(Order::class, [ 'getPayment' => $payment From 61f70f6385c0d2674132a231f72544d712602fe0 Mon Sep 17 00:00:00 2001 From: Jeantwan Teuma Date: Wed, 5 Oct 2022 10:08:26 +0200 Subject: [PATCH 12/20] Recurring payments added functionality (#1729) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * PW-6764 - Delete TransactionAuthorization class and start using checkout to process payments using vault tokens * PW-6764 - Create new AdyenPaymentMethod model, repository and data patch * PW-6764 - Move function to build payment method recurring data to vault helper. Also create ResourceModel * PW-6764 - Add try catch block to function in Vault * PW-6764 - Save paypal token in vault table * PW-6764 - Create new exception. Add creation logic to CheckoutPaymentsDetailsHandler. Move validation in the getVaultPaymentMethodToken function * PW-6764 - Do not force exception * PW-6764 - Create function to check if recurringDetailReference is included in response * PW-6764 - Update AdyenHppObserver to add string in additionalData to ensure that token is visible, IF it is eventually created * PW-6764 - Setup functionality to display a payment method in the checkout page * PW-6764 - Create new paymentMethodVault which will be used to ensure that for hpp tokens, no initialization is required * PW-6764 - Create PayPal tokens w/type=Account and the brand in the details column * PW-6764 - Update PaymentMethodRenderer * PW-6764 - Add exception handling around createAdyenPaymentMethod function * PW-6764 - Create new vault data builder to be used for hpp methods. Also save the tokenType when it is created and then re-use it when token is to be re-used * PW-6764 - Add functionality to only CardOnFile payment method tokens, in checkout * PW-6764 - Add instantPurchase functionality to Payment Methods * PW-6764 - Add SEPA * PW-6764 - Update admin tooltip. Rename functions in Vault helper to make it clear which one is used for what * PW-6764 - Update label shown in checkout page * PW-6764 - Run upgrade scripts on newer version * PW-6764 - Create new observer which will call Adyen to disable a token, after it is disabled on magento side * PW-6764 - Add exception handling to disable call * PW-6764 - Update shopper template * PW-6764 - Add function to check if provider is a recurring one and use it to send the shopper interaction * PW-6764 - Split enableRecurring in payment methods to enableSubscription And enableCardOnFile. Use either, depending on what token type is in use * PW-6790 - Move functionality to get the creditcard types to the paymentMethods Helper. Also create new payment method class for googlepay * PW-6790 - Create new parent uiComponentProvider to be able to render the card provider even in the hpp provider (used for googlepay, applepay) * PW-6790 - Render card tokens created with googlepay in the card section, on the user page * PW-6790 - Remove useless methods from PaymentMethodInterface * PW-6790 - Add token handling in the PaymentResponseHandler and update vault to not need the object payment method * PW-6790 - Add isWallet function and use it for GooglePay * PW-6790 - Add Apple pay classes * PW-6790 - Create Abstract token formatter for instantPurchase, to show card formatting if a wallet payment (googlepay) was used * PW-6790 - Update admin area tooltip * PW-6790 - Remove unnecessary dependencies * PW-6790 - Remove RecurringPmVaultDataBuilder and use the normal vault data builder instead * PW-6790 - Remove SEPA related code that was previously unused * [PW-6858] Add multiselect config for selecting which PMs to tokenize (#1605) * [PW-6858] add config for selecting payment methods that should be tokenized * [PW-6858] remove adyenpaymentmethods patch script * [PW-6858] update property declaration syntax to support php 7.3 * Add UnscheduledCardOnFile for alternative payment methods (#1612) * [PW-6910] Add functionality to tokenize AmazonPay payments (#1621) * Add amazon pay payment method * Add the payment method from statedata * Update statedata property name as there is another statedata in the same class * Add get variant method to statedata helper * [PW-6974] - Handle tokenizations on wallet methods with the correct txVariant (#1668) * PW-6974 - Refactor payment method classes by creating a TxVariant class and an AbstractWalletPaymentMethod * PW-6974 - Move functionality from CheckoutPaymentsDetailsHandler to VaultDetailsHandler. Update Vault function to save card type and wallet type * PW-6974 - Update paymentMethod factory * PW-6974 - Remove isWalletPaymentMethod function and instead check the instance of the object * PW-6974 - Add comment to admin interface * PW-6974 - Move IS_ACTIVE flag to vault handler. Also do not allow ApplePay to be selected for now, since it cannot be tested yet * PW-6974 - Update comment * PW-6974 - Remove SEPA specific field when handling webhook * PW-6974 - Add signature to files * [PW-6785] - Coverage report for SonarCloud (#1665) * [PW-6785] - Coverage report for SonarCloud * [PW-6785] - Coverage report for SonarCloud * [PW-6785] - Coverage report for SonarCloud * [PW-6785] - Coverage report for SonarCloud * [PW-6785] - Coverage report for SonarCloud * [PW-6785] - Coverage report for SonarCloud * [PW-6785] - Coverage report for SonarCloud * [PW-6785] - Coverage report for SonarCloud * [PW-6785] - Coverage report for SonarCloud * [PW-6785] - Refactor phpunit.xml for configuration and main.yml for pipeline * [PW-6785] - Add sonar-project.properties file * [PW-6785] - Remove sonar analysis from CI * [PW-6785] - Remove unnecessary dependencies * [PW-6785] - Coverage test function * [PW-6785] - Enable CI based sonar analysis * [PW-6785] - Code formatting * [PW-6785] - Temporarily disable PHP8.1 matrix * [PW-6785] - Change allowed list for scan * [PW-6785] - Change allowed list for scan * [PW-6785] - Exclude vendor directory from sonar analysis * [PW-6785] - Remove the filter for sonar analysis and include all the project files * [PW-6785] - Revert commits 6f6ca5d and bd74dd3 * [PW-7021] - Add failure counter for pay by link attempts (#1667) * [PW-7021] - Add failure counter for pay by link attempts * Update Helper/Webhook/AuthorisationWebhookHandler.php Co-authored-by: Michael Paul * Update Helper/Webhook/AuthorisationWebhookHandler.php Co-authored-by: Michael Paul * [PW-7021] - Code formatting Co-authored-by: Michael Paul * Revert "[PW-6785] - Coverage report for SonarCloud (#1665)" (#1671) This reverts commit f053b6f93fa900197ff751d0db04242688d15ae6. * Fix issue where CC vault tokens were not being created * [PW-7100] Add CardOnFile and UnshceduledCardOnFile to Vault Card (#1693) * update recurring_card_token_type in adyen_oneclick to show all contract types if vault is selected * abstract recurring type value passed to buildCardRecurringData(), save tokenType in details field * update TokenUiComponentProvider to display token if condition is met * cleanup comment in the header * add new template an js file for vault * add interceptor that returns filtered customer session tokens * remove banner Co-authored-by: Michael Paul * remove comment * update unit tests * use different function for checking whether the value for tokenType is set Co-authored-by: Jeantwan Teuma * update tooltip for adyen_oneclick Co-authored-by: Jeantwan Teuma * update return type of afterGetCustomerSessionTokens function Co-authored-by: Michael Paul Co-authored-by: Jeantwan Teuma * [PW-7179] - Adjust vault tokenization functionality (#1709) * PW-7179 - Re-add parent AdyenUiComponentProvider * PW-7179 - Check if property exists before checking type * PW-7179 - Fix issue created in merge commit and update PaymentVaultDeleteToken * PW-7179 - Do not show ApplePay and AmazonPay for now. Also add to do annotations for them * [PW-7157]Add India live prefix for frontend (#1708) * Add India live prefix for frontend * revert adyen-methods.js Co-authored-by: Jeantwan Teuma * PW-7179 - Move tooltip to comment * PW-7129: Remove full request/response logging (#1712) * PW-7129: Remove full request/response logging Add new log processors to help with troubleshooting. * PW-7129: Use context placeholders * Adding number format to getAmountIncludingTax in test (#1715) * Adding number format to getAmountIncludingTax in test * Apply number format on getTaxAmount Co-authored-by: Alexandros Moraitis Co-authored-by: Michael Paul Co-authored-by: Ángel Campos * recurring-changes - Remove unused paymentMethodsHelper * Remove duplicate AdyenPaymentHppFacade * Update formatting due to code smells * More updates related to code smells Co-authored-by: Peter Ojo Co-authored-by: Alexandros Moraitis Co-authored-by: Can Demiralp Co-authored-by: Michael Paul Co-authored-by: Rok Popov Ledinski Co-authored-by: Ángel Campos --- Block/Customer/CardRenderer.php | 8 +- Block/Customer/PaymentMethodRenderer.php | 74 ++++++ Block/Form/Cc.php | 26 +-- Controller/Process/Result.php | 4 +- Exception/InvalidAdditionalDataException.php | 18 ++ Exception/PaymentMethodException.php | 18 ++ .../Http/Client/TransactionAuthorization.php | 65 ------ Gateway/Request/RecurringDataBuilder.php | 18 +- Gateway/Request/RecurringVaultDataBuilder.php | 20 +- .../Request/ShopperInteractionDataBuilder.php | 13 +- .../CheckoutPaymentsDetailsHandler.php | 34 +-- Gateway/Response/VaultDetailsHandler.php | 98 +++++++- Helper/Config.php | 5 + Helper/PaymentMethods.php | 66 ++++-- .../AbstractWalletPaymentMethod.php | 48 ++++ .../PaymentMethods/AmazonPayPaymentMethod.php | 55 +++++ .../PaymentMethods/ApplePayPaymentMethod.php | 54 +++++ .../PaymentMethods/GooglePayPaymentMethod.php | 53 +++++ Helper/PaymentMethods/PayPalPaymentMethod.php | 53 +++++ .../PaymentMethods/PaymentMethodFactory.php | 51 +++++ .../PaymentMethods/PaymentMethodInterface.php | 30 +++ Helper/PaymentMethods/SepaPaymentMethod.php | 58 +++++ Helper/PaymentMethods/TxVariant.php | 58 +++++ Helper/PaymentResponseHandler.php | 69 +++++- Helper/Recurring.php | 9 +- Helper/Requests.php | 34 +-- Helper/StateData.php | 11 + Helper/Vault.php | 215 +++++++++++++++++- .../RecurringContractWebhookHandler.php | 2 - Model/Billing/Agreement.php | 40 ---- Model/Comment/ApiKeyEnding.php | 10 + Model/Comment/TokenizedPaymentMethods.php | 28 +++ .../Config/Source/TokenizedPaymentMethods.php | 39 ++++ .../AbstractAdyenTokenFormatter.php | 94 ++++++++ .../CreditCard/TokenFormatter.php | 78 +------ .../PaymentMethods/AvailabilityChecker.php | 41 ++++ .../PaymentMethods/TokenFormatter.php | 47 ++++ Model/Method/PaymentMethodVault.php | 81 +++++++ Model/Ui/AdyenCcConfigProvider.php | 120 ++++------ Model/Ui/AdyenHppConfigProvider.php | 2 + Model/Ui/AdyenOneclickConfigProvider.php | 32 +-- Model/Ui/AdyenUiComponentProvider.php | 60 +++++ Model/Ui/PaymentMethodUiComponentProvider.php | 85 +++++++ Model/Ui/TokenUiComponentProvider.php | 44 +--- Observer/AdyenHppDataAssignObserver.php | 39 +--- Observer/VaultDeleteTokenObserver.php | 108 +++++++++ Plugin/CustomerFilterVaultTokens.php | 48 ++++ Plugin/PaymentVaultDeleteToken.php | 30 ++- Test/Unit/Helper/RequestsTest.php | 6 +- etc/adminhtml/system/adyen_hpp.xml | 15 +- etc/adminhtml/system/adyen_oneclick.xml | 12 +- etc/config.xml | 10 + etc/di.xml | 76 ++++++- etc/events.xml | 3 + etc/frontend/di.xml | 1 + .../layout/vault_cards_listaction.xml | 6 +- .../customer_account/payment_method.phtml | 37 +++ .../method-renderer/adyen-vault-method.js | 66 ++++++ .../method-renderer/payment_method_vault.js | 66 ++++++ .../payment/payment-method-vault-form.html | 45 ++++ .../payment/payment-method-vault.html | 39 ++++ 61 files changed, 2157 insertions(+), 518 deletions(-) create mode 100644 Block/Customer/PaymentMethodRenderer.php create mode 100644 Exception/InvalidAdditionalDataException.php create mode 100644 Exception/PaymentMethodException.php delete mode 100644 Gateway/Http/Client/TransactionAuthorization.php create mode 100644 Helper/PaymentMethods/AbstractWalletPaymentMethod.php create mode 100644 Helper/PaymentMethods/AmazonPayPaymentMethod.php create mode 100644 Helper/PaymentMethods/ApplePayPaymentMethod.php create mode 100644 Helper/PaymentMethods/GooglePayPaymentMethod.php create mode 100644 Helper/PaymentMethods/PayPalPaymentMethod.php create mode 100644 Helper/PaymentMethods/PaymentMethodFactory.php create mode 100644 Helper/PaymentMethods/PaymentMethodInterface.php create mode 100644 Helper/PaymentMethods/SepaPaymentMethod.php create mode 100644 Helper/PaymentMethods/TxVariant.php create mode 100644 Model/Comment/TokenizedPaymentMethods.php create mode 100644 Model/Config/Source/TokenizedPaymentMethods.php create mode 100644 Model/InstantPurchase/AbstractAdyenTokenFormatter.php create mode 100644 Model/InstantPurchase/PaymentMethods/AvailabilityChecker.php create mode 100644 Model/InstantPurchase/PaymentMethods/TokenFormatter.php create mode 100644 Model/Method/PaymentMethodVault.php create mode 100644 Model/Ui/AdyenUiComponentProvider.php create mode 100644 Model/Ui/PaymentMethodUiComponentProvider.php create mode 100644 Observer/VaultDeleteTokenObserver.php create mode 100644 Plugin/CustomerFilterVaultTokens.php create mode 100644 view/frontend/templates/customer_account/payment_method.phtml create mode 100644 view/frontend/web/js/view/payment/method-renderer/adyen-vault-method.js create mode 100644 view/frontend/web/js/view/payment/method-renderer/payment_method_vault.js create mode 100644 view/frontend/web/template/payment/payment-method-vault-form.html create mode 100644 view/frontend/web/template/payment/payment-method-vault.html diff --git a/Block/Customer/CardRenderer.php b/Block/Customer/CardRenderer.php index b6236e95c..0ba4560e4 100644 --- a/Block/Customer/CardRenderer.php +++ b/Block/Customer/CardRenderer.php @@ -37,14 +37,16 @@ public function __construct( } /** - * Can render specified token + * Returns true if methodCode = adyen_cc OR (methodCode = adyen_hpp AND maskedCC exists in details. For googlepay) * * @param PaymentTokenInterface $token * @return boolean */ - public function canRender(PaymentTokenInterface $token) + public function canRender(PaymentTokenInterface $token): bool { - return $token->getPaymentMethodCode() === AdyenCcConfigProvider::CODE; + $details = json_decode($token->getTokenDetails() ?: '{}', true); + return $token->getPaymentMethodCode() === AdyenCcConfigProvider::CODE || + ($token->getPaymentMethodCode() === AdyenHppConfigProvider::CODE && array_key_exists('maskedCC', $details)); } /** diff --git a/Block/Customer/PaymentMethodRenderer.php b/Block/Customer/PaymentMethodRenderer.php new file mode 100644 index 000000000..0ba7d92ca --- /dev/null +++ b/Block/Customer/PaymentMethodRenderer.php @@ -0,0 +1,74 @@ + + */ + +namespace Adyen\Payment\Block\Customer; + +use Adyen\Payment\Exception\PaymentMethodException; +use Adyen\Payment\Helper\Data; +use Adyen\Payment\Helper\PaymentMethods\PaymentMethodFactory; +use Adyen\Payment\Helper\PaymentMethods\PaymentMethodInterface; +use Adyen\Payment\Model\Ui\AdyenHppConfigProvider; +use Magento\Framework\View\Element\Template\Context; +use Magento\Vault\Api\Data\PaymentTokenInterface; +use Magento\Vault\Block\AbstractTokenRenderer; + +class PaymentMethodRenderer extends AbstractTokenRenderer +{ + /** @var Data */ + private $dataHelper; + + /** @var PaymentMethodFactory */ + private $paymentMethodFactory; + + + public function __construct( + Context $context, + Data $dataHelper, + PaymentMethodFactory $paymentMethodFactory, + array $data = [] + ) { + parent::__construct($context, $data); + $this->dataHelper = $dataHelper; + $this->paymentMethodFactory = $paymentMethodFactory; + } + + public function getText(): string + { + try { + $paymentMethod = $this->paymentMethodFactory::createAdyenPaymentMethod($this->getTokenDetails()['type']); + $text = $paymentMethod->getPaymentMethodName(); + } catch (PaymentMethodException $exception) { + $text = ''; + } + + return $text; + } + + public function getIconUrl(): string + { + return $this->dataHelper->getVariantIcon($this->getTokenDetails()['type'])['url']; + } + + public function getIconHeight(): int + { + return $this->dataHelper->getVariantIcon($this->getTokenDetails()['type'])['height']; + } + + public function getIconWidth(): int + { + return $this->dataHelper->getVariantIcon($this->getTokenDetails()['type'])['width']; + } + + public function canRender(PaymentTokenInterface $token): bool + { + return $token->getPaymentMethodCode() === AdyenHppConfigProvider::CODE; + } +} diff --git a/Block/Form/Cc.php b/Block/Form/Cc.php index 64a1c2578..7ae0746a4 100755 --- a/Block/Form/Cc.php +++ b/Block/Form/Cc.php @@ -15,6 +15,7 @@ use Adyen\Payment\Helper\Config; use Adyen\Payment\Helper\Data; use Adyen\Payment\Helper\Installments; +use Adyen\Payment\Helper\PaymentMethods; use Adyen\Payment\Helper\Recurring; use Adyen\Payment\Helper\Vault; use Adyen\Payment\Logger\AdyenLogger; @@ -80,6 +81,9 @@ class Cc extends \Magento\Payment\Block\Form\Cc */ private $vaultHelper; + /** @var PaymentMethods */ + private $paymentMethodsHelper; + /** * Cc constructor. * @@ -108,6 +112,7 @@ public function __construct( Config $configHelper, Session $customerSession, Vault $vaultHelper, + PaymentMethods $paymentMethodsHelper, array $data = [] ) { parent::__construct($context, $paymentConfig); @@ -121,6 +126,7 @@ public function __construct( $this->configHelper = $configHelper; $this->customerSession = $customerSession; $this->vaultHelper = $vaultHelper; + $this->paymentMethodsHelper = $paymentMethodsHelper; } /** @@ -163,26 +169,14 @@ public function getLocale() } /** - * Retrieve available credit card type codes by alt code + * Retrieve available credit card type codes by alt code. Function is required so that it can be called + * from block form * * @return array */ - public function getCcAvailableTypesByAlt() + public function getCcAvailableTypesByAlt(): array { - $types = []; - $ccTypes = $this->adyenHelper->getAdyenCcTypes(); - - $availableTypes = $this->adyenHelper->getAdyenCcConfigData('cctypes'); - if ($availableTypes) { - $availableTypes = explode(',', $availableTypes); - foreach (array_keys($ccTypes) as $code) { - if (in_array($code, $availableTypes)) { - $types[$ccTypes[$code]['code_alt']] = $code; - } - } - } - - return $types; + return $this->paymentMethodsHelper->getCcAvailableTypesByAlt(); } /** diff --git a/Controller/Process/Result.php b/Controller/Process/Result.php index 0e5da8d45..e6a6c86f0 100755 --- a/Controller/Process/Result.php +++ b/Controller/Process/Result.php @@ -268,10 +268,10 @@ protected function validateResponse($response) if (!empty($response['additionalData']['recurring.recurringDetailReference']) && $this->payment->getMethodInstance()->getCode() !== \Adyen\Payment\Model\Ui\AdyenOneclickConfigProvider::CODE) { if ($this->vaultHelper->isCardVaultEnabled()) { - $this->vaultHelper->saveRecurringDetails($this->payment, $response['additionalData']); + $this->vaultHelper->saveRecurringCardDetails($this->payment, $response['additionalData']); } else { $order = $this->payment->getOrder(); - $this->recurringHelper->createAdyenBillingAgreement($order, $response['additionalData'], $this->payment->getAdditionalInformation()); + $this->recurringHelper->createAdyenBillingAgreement($order, $response['additionalData']); } $this->orderResourceModel->save($order); } diff --git a/Exception/InvalidAdditionalDataException.php b/Exception/InvalidAdditionalDataException.php new file mode 100644 index 000000000..0f2fbe6f9 --- /dev/null +++ b/Exception/InvalidAdditionalDataException.php @@ -0,0 +1,18 @@ + + * @copyright (c) 2022 Adyen B.V. + * @license https://opensource.org/licenses/MIT MIT license + * This file is open source and available under the MIT license. + * See the LICENSE file for more info. + */ + +namespace Adyen\Payment\Exception; + +class InvalidAdditionalDataException extends AbstractAdyenException +{ + +} diff --git a/Exception/PaymentMethodException.php b/Exception/PaymentMethodException.php new file mode 100644 index 000000000..dfc809690 --- /dev/null +++ b/Exception/PaymentMethodException.php @@ -0,0 +1,18 @@ + + * @copyright (c) 2022 Adyen B.V. + * @license https://opensource.org/licenses/MIT MIT license + * This file is open source and available under the MIT license. + * See the LICENSE file for more info. + */ + +namespace Adyen\Payment\Exception; + +class PaymentMethodException extends AbstractAdyenException +{ + +} diff --git a/Gateway/Http/Client/TransactionAuthorization.php b/Gateway/Http/Client/TransactionAuthorization.php deleted file mode 100644 index 4be92a382..000000000 --- a/Gateway/Http/Client/TransactionAuthorization.php +++ /dev/null @@ -1,65 +0,0 @@ - - */ - -namespace Adyen\Payment\Gateway\Http\Client; - -use Magento\Payment\Gateway\Http\ClientInterface; -use Adyen\Payment\Model\ApplicationInfo; - -class TransactionAuthorization implements ClientInterface -{ - - /** - * @var \Adyen\Client - */ - protected $client; - - /** - * @var ApplicationInfo - */ - private $applicationInfo; - - /** - * TransactionAuthorization constructor. - * @param \Adyen\Payment\Helper\Data $adyenHelper - * @param ApplicationInfo $applicationInfo - * @throws \Adyen\AdyenException - */ - public function __construct( - \Adyen\Payment\Helper\Data $adyenHelper, - \Adyen\Payment\Model\ApplicationInfo $applicationInfo - ) { - $this->applicationInfo = $applicationInfo; - $this->client = $adyenHelper->initializeAdyenClient(); - } - - /** - * @param \Magento\Payment\Gateway\Http\TransferInterface $transferObject - * @return array|mixed - * @throws \Adyen\AdyenException - */ - public function placeRequest(\Magento\Payment\Gateway\Http\TransferInterface $transferObject) - { - $request = $transferObject->getBody(); - $requestOptions = []; - - // call lib - $service = new \Adyen\Service\Payment($this->client); - - try { - $response = $service->authorise($request, $requestOptions); - } catch (\Adyen\AdyenException $e) { - $response['error'] = $e->getMessage(); - } - - return $response; - } -} diff --git a/Gateway/Request/RecurringDataBuilder.php b/Gateway/Request/RecurringDataBuilder.php index 05647ed88..0b3096bae 100644 --- a/Gateway/Request/RecurringDataBuilder.php +++ b/Gateway/Request/RecurringDataBuilder.php @@ -13,6 +13,8 @@ use Adyen\Payment\Helper\PaymentMethods; use Adyen\Payment\Helper\Requests; +use Adyen\Payment\Helper\Vault; +use Adyen\Payment\Helper\StateData; use Adyen\Payment\Logger\AdyenLogger; use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Request\BuilderInterface; @@ -25,12 +27,22 @@ class RecurringDataBuilder implements BuilderInterface /** @var AdyenLogger */ private $adyenLogger; + /** @var Vault */ + private $vaultHelper; + + /** @var StateData */ + private $stateData; + public function __construct( Requests $adyenRequestsHelper, - AdyenLogger $adyenLogger + AdyenLogger $adyenLogger, + Vault $vaultHelper, + StateData $stateData ) { $this->adyenRequestsHelper = $adyenRequestsHelper; $this->adyenLogger = $adyenLogger; + $this->vaultHelper = $vaultHelper; + $this->stateData = $stateData; } /** @@ -46,11 +58,11 @@ public function build(array $buildSubject): array $order = $payment->getOrder(); $storeId = $order->getStoreId(); $method = $payment->getMethod(); - + $brand = $this->stateData->getPaymentMethodVariant($order->getQuoteId()); if ($method === PaymentMethods::ADYEN_CC) { $body = $this->adyenRequestsHelper->buildCardRecurringData($storeId, $payment); } elseif ($method === PaymentMethods::ADYEN_HPP) { - $body = $this->adyenRequestsHelper->buildAlternativePaymentRecurringData($storeId, $payment); + $body = $this->vaultHelper->buildPaymentMethodRecurringData($storeId, $brand); } elseif ($method === PaymentMethods::ADYEN_ONE_CLICK) { $body = $this->adyenRequestsHelper->buildAdyenTokenizedPaymentRecurringData($storeId, $payment); } else { diff --git a/Gateway/Request/RecurringVaultDataBuilder.php b/Gateway/Request/RecurringVaultDataBuilder.php index 72a4830dc..ccb2216f6 100644 --- a/Gateway/Request/RecurringVaultDataBuilder.php +++ b/Gateway/Request/RecurringVaultDataBuilder.php @@ -3,7 +3,7 @@ * * Adyen Payment module (https://www.adyen.com/) * - * Copyright (c) 2019 Adyen BV (https://www.adyen.com/) + * Copyright (c) 2022 Adyen BV (https://www.adyen.com/) * See LICENSE.txt for license details. * * Author: Adyen @@ -11,6 +11,9 @@ namespace Adyen\Payment\Gateway\Request; +use Adyen\Payment\Helper\Vault; +use Magento\Payment\Gateway\Data\PaymentDataObject; +use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Request\BuilderInterface; class RecurringVaultDataBuilder implements BuilderInterface @@ -22,15 +25,20 @@ class RecurringVaultDataBuilder implements BuilderInterface public function build(array $buildSubject) { $requestBody = []; - $recurring = ['contract' => \Adyen\Payment\Model\RecurringType::RECURRING]; - $requestBody['recurring'] = $recurring; - /** @var \Magento\Payment\Gateway\Data\PaymentDataObject $paymentDataObject */ - $paymentDataObject = \Magento\Payment\Gateway\Helper\SubjectReader::readPayment($buildSubject); + /** @var PaymentDataObject $paymentDataObject */ + $paymentDataObject = SubjectReader::readPayment($buildSubject); $payment = $paymentDataObject->getPayment(); $extensionAttributes = $payment->getExtensionAttributes(); $paymentToken = $extensionAttributes->getVaultPaymentToken(); + $details = json_decode($paymentToken->getTokenDetails() ?: '{}', true); - $requestBody['selectedRecurringDetailReference'] = $paymentToken->getGatewayToken(); + // Add paymentMethod object in array, since currently checkout component is not loaded for vault payment methods + $requestBody['paymentMethod'] = ['storedPaymentMethodId' => $paymentToken->getGatewayToken()]; + + // For now this will only be used by tokens created trough adyen_hpp payment methods + if (array_key_exists(Vault::TOKEN_TYPE, $details)) { + $requestBody['recurringProcessingModel'] = $details[Vault::TOKEN_TYPE]; + } $request['body'] = $requestBody; diff --git a/Gateway/Request/ShopperInteractionDataBuilder.php b/Gateway/Request/ShopperInteractionDataBuilder.php index 35993bdbc..c8f6def76 100644 --- a/Gateway/Request/ShopperInteractionDataBuilder.php +++ b/Gateway/Request/ShopperInteractionDataBuilder.php @@ -15,6 +15,7 @@ use Adyen\Payment\Model\Ui\AdyenCcConfigProvider; use Adyen\Payment\Model\Ui\AdyenOneclickConfigProvider; use Adyen\Payment\Model\Ui\AdyenPayByLinkConfigProvider; +use Magento\Framework\App\State; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Model\Context; use Magento\Payment\Gateway\Helper\SubjectReader; @@ -27,18 +28,12 @@ class ShopperInteractionDataBuilder implements BuilderInterface const SHOPPER_INTERACTION_ECOMMERCE = 'Ecommerce'; /** - * @var \Magento\Framework\App\State + * @var State */ private $appState; - /** - * RecurringDataBuilder constructor. - * - * @param Context $context - */ - public function __construct( - Context $context - ) { + public function __construct(Context $context) + { $this->appState = $context->getAppState(); } diff --git a/Gateway/Response/CheckoutPaymentsDetailsHandler.php b/Gateway/Response/CheckoutPaymentsDetailsHandler.php index d91be5fdc..eb493bcfe 100644 --- a/Gateway/Response/CheckoutPaymentsDetailsHandler.php +++ b/Gateway/Response/CheckoutPaymentsDetailsHandler.php @@ -3,7 +3,7 @@ * * Adyen Payment module (https://www.adyen.com/) * - * Copyright (c) 2015 Adyen BV (https://www.adyen.com/) + * Copyright (c) 2022 Adyen BV (https://www.adyen.com/) * See LICENSE.txt for license details. * * Author: Adyen @@ -12,41 +12,31 @@ namespace Adyen\Payment\Gateway\Response; use Adyen\Payment\Helper\Data; -use Adyen\Payment\Helper\Recurring; -use Adyen\Payment\Helper\Vault; +use Adyen\Payment\Model\Ui\AdyenBoletoConfigProvider; +use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Sales\Model\Order\Payment; class CheckoutPaymentsDetailsHandler implements HandlerInterface { /** @var Data */ protected $adyenHelper; - /** @var Recurring */ - private $recurringHelper; - - /** @var Vault */ - private $vaultHelper; - public function __construct( - Data $adyenHelper, - Recurring $recurringHelper, - Vault $vaultHelper + Data $adyenHelper ) { $this->adyenHelper = $adyenHelper; - $this->recurringHelper = $recurringHelper; - $this->vaultHelper = $vaultHelper; } /** * This is being used for all checkout methods (adyen hpp payment method) * - * @param array $handlingSubject - * @param array $response */ public function handle(array $handlingSubject, array $response) { - $paymentDataObject = \Magento\Payment\Gateway\Helper\SubjectReader::readPayment($handlingSubject); + $paymentDataObject = SubjectReader::readPayment($handlingSubject); + /** @var Payment $payment */ $payment = $paymentDataObject->getPayment(); // set transaction not to processing by default wait for notification @@ -54,7 +44,7 @@ public function handle(array $handlingSubject, array $response) // Email sending is set at CheckoutDataBuilder for Boleto // Otherwise, we don't want to send a confirmation email - if ($payment->getMethod() != \Adyen\Payment\Model\Ui\AdyenBoletoConfigProvider::CODE) { + if ($payment->getMethod() != AdyenBoletoConfigProvider::CODE) { $payment->getOrder()->setCanSendNewEmailFlag(false); } @@ -67,14 +57,6 @@ public function handle(array $handlingSubject, array $response) $payment->setTransactionId($response['pspReference']); } - if (!empty($response['additionalData']['recurring.recurringDetailReference']) && - !$this->vaultHelper->isCardVaultEnabled() && - $payment->getMethodInstance()->getCode() !== \Adyen\Payment\Model\Ui\AdyenOneclickConfigProvider::CODE - ) { - $order = $payment->getOrder(); - $this->recurringHelper->createAdyenBillingAgreement($order, $response['additionalData'], $payment->getAdditionalInformation()); - } - // do not close transaction so you can do a cancel() and void $payment->setIsTransactionClosed(false); $payment->setShouldCloseParentTransaction(false); diff --git a/Gateway/Response/VaultDetailsHandler.php b/Gateway/Response/VaultDetailsHandler.php index bafed3321..1babf0274 100644 --- a/Gateway/Response/VaultDetailsHandler.php +++ b/Gateway/Response/VaultDetailsHandler.php @@ -11,30 +11,57 @@ namespace Adyen\Payment\Gateway\Response; +use Adyen\Payment\Exception\PaymentMethodException; +use Adyen\Payment\Helper\Config; +use Adyen\Payment\Helper\PaymentMethods\AbstractWalletPaymentMethod; +use Adyen\Payment\Helper\PaymentMethods\PaymentMethodFactory; +use Adyen\Payment\Helper\Recurring; use Adyen\Payment\Helper\Vault; +use Adyen\Payment\Logger\AdyenLogger; +use Adyen\Payment\Model\Ui\AdyenCcConfigProvider; +use Adyen\Payment\Model\Ui\AdyenHppConfigProvider; +use Adyen\Payment\Model\Ui\AdyenOneclickConfigProvider; +use Magento\Framework\Exception\LocalizedException; use Magento\Payment\Gateway\Data\PaymentDataObject; use Magento\Payment\Gateway\Helper\SubjectReader; use Magento\Payment\Gateway\Response\HandlerInterface; +use Magento\Sales\Model\Order\Payment; +use Magento\Vault\Model\Ui\VaultConfigProvider; class VaultDetailsHandler implements HandlerInterface { - /** - * @var Vault - */ + /** @var Vault */ private $vaultHelper; - /** - * VaultDetailsHandler constructor. - * - * @param Vault $vaultHelper - */ - public function __construct(Vault $vaultHelper) - { + /** @var PaymentMethodFactory */ + private $paymentMethodFactory; + + /** @var Config */ + private $configHelper; + + /** @var AdyenLogger */ + private $adyenLogger; + + /** @var Recurring */ + private $recurringHelper; + + public function __construct( + Vault $vaultHelper, + PaymentMethodFactory $paymentMethodFactory, + Config $configHelper, + AdyenLogger $adyenLogger, + Recurring $recurringHelper + ) { $this->vaultHelper = $vaultHelper; + $this->paymentMethodFactory = $paymentMethodFactory; + $this->configHelper = $configHelper; + $this->adyenLogger = $adyenLogger; + $this->recurringHelper = $recurringHelper; } /** * @inheritdoc + * @throws PaymentMethodException|LocalizedException */ public function handle(array $handlingSubject, array $response) { @@ -43,9 +70,56 @@ public function handle(array $handlingSubject, array $response) } /** @var PaymentDataObject $orderPayment */ $orderPayment = SubjectReader::readPayment($handlingSubject); + /** @var Payment $payment */ + $payment = $orderPayment->getPayment(); + $paymentMethodInstance = $payment->getMethodInstance(); + + if ($this->vaultHelper->hasRecurringDetailReference($response) && + $paymentMethodInstance->getCode() !== AdyenOneclickConfigProvider::CODE + ) { + $storeId = $paymentMethodInstance->getStore(); + $paymentInstanceCode = $paymentMethodInstance->getCode(); + $storePaymentMethods = $this->configHelper->isStoreAlternativePaymentMethodEnabled($storeId); + $cardVaultEnabled = $this->vaultHelper->isCardVaultEnabled($storeId); + $adyenTokensEnabled = $this->recurringHelper->areAdyenTokensEnabled($storeId); - if ($this->vaultHelper->isCardVaultEnabled()) { - $this->vaultHelper->saveRecurringDetails($orderPayment->getPayment(), $response['additionalData']); + // If payment method is HPP and hpp config enabled + // Else if payment method is card and vault is enabled + // Else if payment method is card and vault is disabled and adyen tokens are enabled + if ($storePaymentMethods && $paymentInstanceCode === AdyenHppConfigProvider::CODE) { + $paymentMethod = $response['additionalData']['paymentMethod']; + try { + $payment->setAdditionalInformation(VaultConfigProvider::IS_ACTIVE_CODE, true); + $adyenPaymentMethod = $this->paymentMethodFactory::createAdyenPaymentMethod($paymentMethod); + if ($adyenPaymentMethod instanceof AbstractWalletPaymentMethod) { + $this->vaultHelper->saveRecurringCardDetails( + $payment, + $response['additionalData'], + $adyenPaymentMethod + ); + } else { + $this->vaultHelper->saveRecurringPaymentMethodDetails($payment, $response['additionalData']); + } + } catch (PaymentMethodException $e) { + $this->adyenLogger->error(sprintf( + 'Unable to create payment method with tx variant %s in details handler', + $paymentMethod + )); + } + } elseif ($cardVaultEnabled && $paymentInstanceCode === AdyenCcConfigProvider::CODE) { + $this->vaultHelper->saveRecurringCardDetails($payment, $response['additionalData']); + } elseif ( + !$cardVaultEnabled && + $adyenTokensEnabled && + $paymentInstanceCode === AdyenCcConfigProvider::CODE + ) { + $order = $payment->getOrder(); + $this->recurringHelper->createAdyenBillingAgreement( + $order, + $response['additionalData'], + $payment->getAdditionalInformation() + ); + } } } } diff --git a/Helper/Config.php b/Helper/Config.php index 392a96bc0..913618847 100644 --- a/Helper/Config.php +++ b/Helper/Config.php @@ -452,6 +452,11 @@ public function getCardRecurringType($storeId): ?string return $this->getConfigData('card_type', self::XML_ADYEN_ONECLICK, $storeId); } + public function getTokenizedPaymentMethods($storeId) + { + return $this->getConfigData('tokenized_payment_methods', self::XML_ADYEN_HPP, $storeId); + } + public function debugLogsEnabled($storeId): bool { return $this->getConfigData('debug', self::XML_ADYEN_ABSTRACT_PREFIX, $storeId, true); diff --git a/Helper/PaymentMethods.php b/Helper/PaymentMethods.php index 6d1769bf6..dcf8aa775 100644 --- a/Helper/PaymentMethods.php +++ b/Helper/PaymentMethods.php @@ -12,6 +12,9 @@ namespace Adyen\Payment\Helper; use Adyen\AdyenException; +use Adyen\Payment\Model\Ui\AdyenCcConfigProvider; +use Adyen\Payment\Model\Ui\AdyenHppConfigProvider; +use Adyen\Payment\Model\Ui\AdyenOneclickConfigProvider; use Adyen\Payment\Logger\AdyenLogger; use Adyen\Payment\Model\Notification; use Adyen\Util\ManualCapture; @@ -555,25 +558,62 @@ public function checkPaymentMethod($payment, string $method): bool } /** - * Check if the passed payment method supports recurring functionality. + * Check if the passed payment method provider is a recurring one or not * - * Currently only SEPA is allowed on our Magento plugin. - * Possible future payment methods: + * @param string $provider + * @return bool + */ + public function isRecurringProvider(string $provider): bool + { + return in_array($provider, [ + AdyenCcConfigProvider::CC_VAULT_CODE, + AdyenHppConfigProvider::HPP_VAULT_CODE, + AdyenOneclickConfigProvider::CODE + ]); + } + + /** + * Retrieve available credit card types * - * 'ach','amazonpay','applepay','directdebit_GB','bcmc','dana','dankort','eps','gcash','giropay','googlepay','paywithgoogle', - * 'gopay_wallet','ideal','kakaopay','klarna','klarna_account','klarna_b2b','klarna_paynow','momo_wallet','paymaya_wallet', - * 'paypal','trustly','twint','uatp','billdesk_upi','payu_IN_upi','vipps','yandex_money','zip' + * @return array + */ + public function getCcAvailableTypes(): array + { + $types = []; + $ccTypes = $this->adyenHelper->getAdyenCcTypes(); + $availableTypes = $this->adyenHelper->getAdyenCcConfigData('cctypes'); + if ($availableTypes) { + $availableTypes = explode(',', $availableTypes); + foreach (array_keys($ccTypes) as $code) { + if (in_array($code, $availableTypes)) { + $types[$code] = $ccTypes[$code]['name']; + } + } + } + + return $types; + } + + /** + * Retrieve available credit card type codes by alt code * - * @param string $paymentMethod - * @return bool + * @return array */ - public function paymentMethodSupportsRecurring(string $paymentMethod): bool + public function getCcAvailableTypesByAlt(): array { - $paymentMethodRecurring = [ - 'sepadirectdebit', - ]; + $types = []; + $ccTypes = $this->adyenHelper->getAdyenCcTypes(); + $availableTypes = $this->adyenHelper->getAdyenCcConfigData('cctypes'); + if ($availableTypes) { + $availableTypes = explode(',', $availableTypes); + foreach (array_keys($ccTypes) as $code) { + if (in_array($code, $availableTypes)) { + $types[$ccTypes[$code]['code_alt']] = $code; + } + } + } - return in_array($paymentMethod, $paymentMethodRecurring); + return $types; } /** diff --git a/Helper/PaymentMethods/AbstractWalletPaymentMethod.php b/Helper/PaymentMethods/AbstractWalletPaymentMethod.php new file mode 100644 index 000000000..654d1c471 --- /dev/null +++ b/Helper/PaymentMethods/AbstractWalletPaymentMethod.php @@ -0,0 +1,48 @@ + + */ + +namespace Adyen\Payment\Helper\PaymentMethods; + +abstract class AbstractWalletPaymentMethod implements PaymentMethodInterface +{ + /** @var string */ + private $cardScheme; + + abstract public function getTxVariant(): string; + + abstract public function getPaymentMethodName(): string; + + abstract public function supportsCardOnFile(): bool; + + abstract public function supportsSubscription(): bool; + + abstract public function supportsManualCapture(): bool; + + abstract public function supportsAutoCapture(): bool; + + abstract public function supportsUnscheduledCardOnFile(): bool; + + public function __construct(?string $cardScheme) + { + $this->cardScheme = $cardScheme; + } + + public function getCardScheme(): string + { + return $this->cardScheme; + } + + public function setCardScheme(string $cardScheme): void + { + $this->cardScheme = $cardScheme; + } +} diff --git a/Helper/PaymentMethods/AmazonPayPaymentMethod.php b/Helper/PaymentMethods/AmazonPayPaymentMethod.php new file mode 100644 index 000000000..c469828c0 --- /dev/null +++ b/Helper/PaymentMethods/AmazonPayPaymentMethod.php @@ -0,0 +1,55 @@ + + */ + +namespace Adyen\Payment\Helper\PaymentMethods; + +/** TODO: This PM can be enabled for recurring purposes once tested */ +class AmazonPayPaymentMethod extends AbstractWalletPaymentMethod +{ + const TX_VARIANT = 'amazonpay'; + const NAME = 'Amazon Pay'; + + public function getTxVariant(): string + { + return self::TX_VARIANT; + } + + public function getPaymentMethodName(): string + { + return self::NAME; + } + + public function supportsManualCapture(): bool + { + return true; + } + + public function supportsAutoCapture(): bool + { + return true; + } + + public function supportsCardOnFile(): bool + { + return false; + } + + public function supportsSubscription(): bool + { + return false; + } + + public function supportsUnscheduledCardOnFile(): bool + { + return false; + } +} diff --git a/Helper/PaymentMethods/ApplePayPaymentMethod.php b/Helper/PaymentMethods/ApplePayPaymentMethod.php new file mode 100644 index 000000000..f02363f7a --- /dev/null +++ b/Helper/PaymentMethods/ApplePayPaymentMethod.php @@ -0,0 +1,54 @@ + + */ +namespace Adyen\Payment\Helper\PaymentMethods; + +/** TODO: This PM can be enabled for recurring purposes once tested */ +class ApplePayPaymentMethod extends AbstractWalletPaymentMethod +{ + const TX_VARIANT = 'applepay'; + const NAME = 'Apple Pay'; + + public function getTxVariant(): string + { + return self::TX_VARIANT; + } + + public function getPaymentMethodName(): string + { + return self::NAME; + } + + public function supportsManualCapture(): bool + { + return true; + } + + public function supportsAutoCapture(): bool + { + return true; + } + + public function supportsCardOnFile(): bool + { + return false; + } + + public function supportsSubscription(): bool + { + return false; + } + + public function supportsUnscheduledCardOnFile(): bool + { + return false; + } +} diff --git a/Helper/PaymentMethods/GooglePayPaymentMethod.php b/Helper/PaymentMethods/GooglePayPaymentMethod.php new file mode 100644 index 000000000..193562569 --- /dev/null +++ b/Helper/PaymentMethods/GooglePayPaymentMethod.php @@ -0,0 +1,53 @@ + + */ +namespace Adyen\Payment\Helper\PaymentMethods; + +class GooglePayPaymentMethod extends AbstractWalletPaymentMethod +{ + const TX_VARIANT = 'googlepay'; + const NAME = 'Google Pay'; + + public function getTxVariant(): string + { + return self::TX_VARIANT; + } + + public function getPaymentMethodName(): string + { + return self::NAME; + } + + public function supportsManualCapture(): bool + { + return true; + } + + public function supportsAutoCapture(): bool + { + return true; + } + + public function supportsCardOnFile(): bool + { + return true; + } + + public function supportsSubscription(): bool + { + return true; + } + + public function supportsUnscheduledCardOnFile(): bool + { + return true; + } +} diff --git a/Helper/PaymentMethods/PayPalPaymentMethod.php b/Helper/PaymentMethods/PayPalPaymentMethod.php new file mode 100644 index 000000000..e576c2c64 --- /dev/null +++ b/Helper/PaymentMethods/PayPalPaymentMethod.php @@ -0,0 +1,53 @@ + + */ +namespace Adyen\Payment\Helper\PaymentMethods; + +class PayPalPaymentMethod implements PaymentMethodInterface +{ + const TX_VARIANT = 'paypal'; + const NAME = 'PayPal'; + + public function getTxVariant(): string + { + return self::TX_VARIANT; + } + + public function getPaymentMethodName(): string + { + return self::NAME; + } + + public function supportsManualCapture(): bool + { + return true; + } + + public function supportsAutoCapture(): bool + { + return true; + } + + public function supportsCardOnFile(): bool + { + return false; + } + + public function supportsSubscription(): bool + { + return true; + } + + public function supportsUnscheduledCardOnFile(): bool + { + return true; + } +} diff --git a/Helper/PaymentMethods/PaymentMethodFactory.php b/Helper/PaymentMethods/PaymentMethodFactory.php new file mode 100644 index 000000000..32497defb --- /dev/null +++ b/Helper/PaymentMethods/PaymentMethodFactory.php @@ -0,0 +1,51 @@ + + * @copyright (c) 2022 Adyen B.V. + * @license https://opensource.org/licenses/MIT MIT license + * This file is open source and available under the MIT license. + * See the LICENSE file for more info. + */ + +namespace Adyen\Payment\Helper\PaymentMethods; + +use Adyen\Payment\Exception\PaymentMethodException; +use Adyen\Payment\Logger\AdyenLogger; + +class PaymentMethodFactory +{ + private static $adyenLogger; + + public function __construct(AdyenLogger $adyenLogger) + { + self::$adyenLogger = $adyenLogger; + } + + /** + * @throws PaymentMethodException + */ + public static function createAdyenPaymentMethod(string $txVariant): PaymentMethodInterface + { + $txVariantObject = new TxVariant($txVariant); + + switch ($txVariantObject->getPaymentMethod()) { + case PayPalPaymentMethod::TX_VARIANT: + return new PayPalPaymentMethod(); + case SepaPaymentMethod::TX_VARIANT: + return new SepaPaymentMethod(); + case ApplePayPaymentMethod::TX_VARIANT: + return new ApplePayPaymentMethod($txVariantObject->getCard()); + case AmazonPayPaymentMethod::TX_VARIANT: + return new AmazonPayPaymentMethod($txVariantObject->getCard()); + case GooglePayPaymentMethod::TX_VARIANT: + return new GooglePayPaymentMethod($txVariantObject->getCard()); + default: + $message = sprintf('Unknown txVariant: %s', $txVariant); + self::$adyenLogger->debug($message); + throw new PaymentMethodException(__($message)); + } + } +} diff --git a/Helper/PaymentMethods/PaymentMethodInterface.php b/Helper/PaymentMethods/PaymentMethodInterface.php new file mode 100644 index 000000000..1c1c53eea --- /dev/null +++ b/Helper/PaymentMethods/PaymentMethodInterface.php @@ -0,0 +1,30 @@ + + * @copyright (c) 2022 Adyen B.V. + * @license https://opensource.org/licenses/MIT MIT license + * This file is open source and available under the MIT license. + * See the LICENSE file for more info. + */ + +namespace Adyen\Payment\Helper\PaymentMethods; + +interface PaymentMethodInterface +{ + public function getTxVariant(): string; + + public function getPaymentMethodName(): string; + + public function supportsCardOnFile(): bool; + + public function supportsSubscription(): bool; + + public function supportsManualCapture(): bool; + + public function supportsAutoCapture(): bool; + + public function supportsUnscheduledCardOnFile(): bool; +} diff --git a/Helper/PaymentMethods/SepaPaymentMethod.php b/Helper/PaymentMethods/SepaPaymentMethod.php new file mode 100644 index 000000000..9587c95a2 --- /dev/null +++ b/Helper/PaymentMethods/SepaPaymentMethod.php @@ -0,0 +1,58 @@ + + */ +namespace Adyen\Payment\Helper\PaymentMethods; + +class SepaPaymentMethod implements PaymentMethodInterface +{ + const TX_VARIANT = 'sepadirectdebit'; + const NAME = 'SEPA Direct Debit'; + + public function getTxVariant(): string + { + return self::TX_VARIANT; + } + + public function getPaymentMethodName(): string + { + return self::NAME; + } + + public function supportsRecurring(): bool + { + return true; + } + + public function supportsManualCapture(): bool + { + return true; + } + + public function supportsAutoCapture(): bool + { + return true; + } + + public function supportsCardOnFile(): bool + { + return false; + } + + public function supportsSubscription(): bool + { + return true; + } + + public function supportsUnscheduledCardOnFile(): bool + { + return true; + } +} diff --git a/Helper/PaymentMethods/TxVariant.php b/Helper/PaymentMethods/TxVariant.php new file mode 100644 index 000000000..3dedb7f2f --- /dev/null +++ b/Helper/PaymentMethods/TxVariant.php @@ -0,0 +1,58 @@ + + */ + +namespace Adyen\Payment\Helper\PaymentMethods; + +class TxVariant +{ + /** @var string */ + private $card; + + /** @var string */ + private $paymentMethod; + + public function __construct(string $txVariant) + { + $splitVariant = explode('_', $txVariant, 2); + if (count($splitVariant) > 1) { + $this->card = $splitVariant[0]; + $this->paymentMethod = $splitVariant[1]; + } else { + $this->paymentMethod = $splitVariant[0]; + } + } + + public function getCard(): ?string + { + return $this->card; + } + + public function setCard(?string $card): void + { + $this->card = $card; + } + + public function getPaymentMethod(): string + { + return $this->paymentMethod; + } + + public function setPaymentMethod(string $paymentMethod): void + { + $this->paymentMethod = $paymentMethod; + } + + public function isWalletVariant(): bool + { + return isset($this->card); + } +} diff --git a/Helper/PaymentResponseHandler.php b/Helper/PaymentResponseHandler.php index 2a2933693..d735e8ffa 100644 --- a/Helper/PaymentResponseHandler.php +++ b/Helper/PaymentResponseHandler.php @@ -11,10 +11,18 @@ namespace Adyen\Payment\Helper; +use Adyen\Payment\Exception\PaymentMethodException; +use Adyen\Payment\Helper\PaymentMethods\AbstractWalletPaymentMethod; +use Adyen\Payment\Helper\PaymentMethods\PaymentMethodFactory; use Adyen\Payment\Logger\AdyenLogger; +use Adyen\Payment\Model\Ui\AdyenHppConfigProvider; +use Adyen\Payment\Model\Ui\AdyenOneclickConfigProvider; +use Adyen\Payment\Observer\AdyenHppDataAssignObserver; use Exception; use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\Data\OrderPaymentInterface; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\ResourceModel\Order; class PaymentResponseHandler { @@ -45,7 +53,7 @@ class PaymentResponseHandler private $vaultHelper; /** - * @var \Magento\Sales\Model\ResourceModel\Order + * @var Order */ private $orderResourceModel; @@ -63,6 +71,16 @@ class PaymentResponseHandler */ private $quoteHelper; + /** + * @var Config + */ + private $configHelper; + + /** + * @var PaymentMethodFactory + */ + private $paymentMethodFactory; + /** * PaymentResponseHandler constructor. * @@ -74,10 +92,12 @@ public function __construct( AdyenLogger $adyenLogger, Data $adyenHelper, Vault $vaultHelper, - \Magento\Sales\Model\ResourceModel\Order $orderResourceModel, + Order $orderResourceModel, Data $dataHelper, Recurring $recurringHelper, - Quote $quoteHelper + Quote $quoteHelper, + Config $configHelper, + PaymentMethodFactory $paymentMethodFactory ) { $this->adyenLogger = $adyenLogger; $this->adyenHelper = $adyenHelper; @@ -86,6 +106,8 @@ public function __construct( $this->dataHelper = $dataHelper; $this->recurringHelper = $recurringHelper; $this->quoteHelper = $quoteHelper; + $this->configHelper = $configHelper; + $this->paymentMethodFactory = $paymentMethodFactory; } public function formatPaymentResponse($resultCode, $action = null, $additionalData = null) @@ -129,9 +151,10 @@ public function formatPaymentResponse($resultCode, $action = null, $additionalDa /** * @param $paymentsResponse - * @param OrderPaymentInterface $payment + * @param Payment $payment * @param OrderInterface|null $order * @return bool + * @throws \Magento\Framework\Exception\LocalizedException */ public function handlePaymentResponse($paymentsResponse, $payment, $order = null) { @@ -193,14 +216,42 @@ public function handlePaymentResponse($paymentsResponse, $payment, $order = null // set transaction $payment->setTransactionId($paymentsResponse['pspReference']); } + $paymentMethodInstance = $payment->getMethodInstance(); + + if ($this->vaultHelper->hasRecurringDetailReference($paymentsResponse) && + $paymentMethodInstance->getCode() !== AdyenOneclickConfigProvider::CODE) { + $storeId = $paymentMethodInstance->getStore(); + $paymentInstanceCode = $paymentMethodInstance->getCode(); + $storePaymentMethods = $this->configHelper->isStoreAlternativePaymentMethodEnabled($storeId); - if (!empty($paymentsResponse['additionalData']['recurring.recurringDetailReference']) && - $payment->getMethodInstance()->getCode() !== \Adyen\Payment\Model\Ui\AdyenOneclickConfigProvider::CODE) { - if ($this->vaultHelper->isCardVaultEnabled()) { - $this->vaultHelper->saveRecurringDetails($payment, $paymentsResponse['additionalData']); + if ($storePaymentMethods && $paymentInstanceCode === AdyenHppConfigProvider::CODE) { + $brand = $payment->getAdditionalInformation(AdyenHppDataAssignObserver::BRAND_CODE); + try { + $adyenPaymentMethod = $this->paymentMethodFactory::createAdyenPaymentMethod($brand); + if ($adyenPaymentMethod instanceof AbstractWalletPaymentMethod) { + $this->vaultHelper->saveRecurringCardDetails( + $payment, + $paymentsResponse['additionalData'] + ); + } else { + $this->vaultHelper->saveRecurringPaymentMethodDetails( + $payment, + $paymentsResponse['additionalData'] + ); + } + } catch (PaymentMethodException $e) { + $this->adyenLogger->error(sprintf( + 'Unable to create payment method with tx variant %s in details handler', + $brand + )); + } } else { $order = $payment->getOrder(); - $this->recurringHelper->createAdyenBillingAgreement($order, $paymentsResponse['additionalData']); + $this->recurringHelper->createAdyenBillingAgreement( + $order, + $paymentsResponse['additionalData'], + $payment->getAdditionalInformation() + ); } } diff --git a/Helper/Recurring.php b/Helper/Recurring.php index f99b6bde1..b3e475f29 100644 --- a/Helper/Recurring.php +++ b/Helper/Recurring.php @@ -23,6 +23,7 @@ class Recurring const CARD_ON_FILE = 'CardOnFile'; const SUBSCRIPTION = 'Subscription'; + const UNSCHEDULED_CARD_ON_FILE = 'UnscheduledCardOnFile'; /** @var AdyenLogger */ private $adyenLogger; @@ -66,7 +67,8 @@ public static function getRecurringTypes(): array { return [ self::CARD_ON_FILE, - self::SUBSCRIPTION + self::SUBSCRIPTION, + self::UNSCHEDULED_CARD_ON_FILE ]; } @@ -84,9 +86,8 @@ public static function getRecurringMethods(): array /** * @param $order * @param $additionalData - * @param array $savedPaymentData */ - public function createAdyenBillingAgreement($order, $additionalData, array $savedPaymentData = []) + public function createAdyenBillingAgreement($order, $additionalData) { if (!empty($additionalData['recurring.recurringDetailReference'])) { try { @@ -123,8 +124,6 @@ public function createAdyenBillingAgreement($order, $additionalData, array $save if ($payment->getMethod() === PaymentMethods::ADYEN_CC) { $billingAgreement->setCcBillingAgreement($additionalData, $storeOneClick, $order->getStoreId()); - } elseif ($payment->getAdditionalInformation(AdyenHppDataAssignObserver::BRAND_CODE) === Data::SEPA) { - $billingAgreement->setSepaBillingAgreement($additionalData, $order->getStoreId(), $savedPaymentData); } $billingAgreementErrors = $billingAgreement->getErrors(); diff --git a/Helper/Requests.php b/Helper/Requests.php index 60571fc9c..14ffd604b 100644 --- a/Helper/Requests.php +++ b/Helper/Requests.php @@ -19,7 +19,9 @@ class Requests extends AbstractHelper { - CONST MERCHANT_ACCOUNT = 'merchantAccount'; + const MERCHANT_ACCOUNT = 'merchantAccount'; + const SHOPPER_REFERENCE = 'shopperReference'; + const RECURRING_DETAIL_REFERENCE = 'recurringDetailReference'; /** * @var Data @@ -369,7 +371,7 @@ public function buildCardRecurringData(int $storeId, $payment): array if ($storePaymentMethod) { if ($this->vaultHelper->isCardVaultEnabled()) { - $request['recurringProcessingModel'] = 'Subscription'; + $request['recurringProcessingModel'] = $this->adyenConfig->getCardRecurringType($storeId); } else { $recurringType = $this->adyenConfig->getCardRecurringType($storeId); $request['recurringProcessingModel'] = $recurringType; @@ -379,34 +381,6 @@ public function buildCardRecurringData(int $storeId, $payment): array return $request; } - /** - * Build the recurring data when payment is done trough an alternative payment method - * - * @param int $storeId - * @param $payment - * @return array - */ - public function buildAlternativePaymentRecurringData(int $storeId, $payment): array - { - $request = []; - - $brand = $payment->getAdditionalInformation(AdyenHppDataAssignObserver::BRAND_CODE); - if (!$this->adyenConfig->isStoreAlternativePaymentMethodEnabled() || - !$this->paymentMethodsHelper->paymentMethodSupportsRecurring($brand)) { - - return $request; - } - - - $recurringModel = $this->adyenConfig->getAlternativePaymentMethodTokenType($storeId); - if (isset($recurringModel)) { - $request['storePaymentMethod'] = true; - $request['recurringProcessingModel'] = $recurringModel; - } - - return $request; - } - /** * Build the recurring data to be sent in case of an Adyen Tokenized payment. * Model will be fetched according to the type (card/other pm) of the original payment diff --git a/Helper/StateData.php b/Helper/StateData.php index d10c3b4a8..0818512a6 100644 --- a/Helper/StateData.php +++ b/Helper/StateData.php @@ -87,4 +87,15 @@ public function getStateData(int $quoteId): array { return $this->stateData[$quoteId] ?? []; } + + /** + * Returns the payment method type from state data + * @param int $quoteId + * @return string + */ + public function getPaymentMethodVariant(int $quoteId): string + { + $stateDataByQuoteId = $this->stateData[$quoteId]; + return $stateDataByQuoteId['paymentMethod']['type']; + } } diff --git a/Helper/Vault.php b/Helper/Vault.php index 46f19a9e9..bd9012b28 100644 --- a/Helper/Vault.php +++ b/Helper/Vault.php @@ -12,11 +12,19 @@ namespace Adyen\Payment\Helper; +use Adyen\Payment\Exception\InvalidAdditionalDataException; +use Adyen\Payment\Exception\PaymentMethodException; +use Adyen\Payment\Helper\PaymentMethods\AbstractWalletPaymentMethod; +use Adyen\Payment\Helper\PaymentMethods\PaymentMethodFactory; +use Adyen\Payment\Helper\PaymentMethods\PaymentMethodInterface; use Adyen\Payment\Logger\AdyenLogger; +use Adyen\Payment\Model\Ui\AdyenCcConfigProvider; +use Adyen\Payment\Model\Ui\AdyenHppConfigProvider; use DateInterval; use DateTime; use DateTimeZone; use Exception; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Payment\Model\InfoInterface; use Magento\Sales\Api\Data\OrderPaymentExtensionInterface; use Magento\Vault\Api\Data\PaymentTokenFactoryInterface; @@ -30,6 +38,7 @@ class Vault const CARD_SUMMARY = 'cardSummary'; const EXPIRY_DATE = 'expiryDate'; const PAYMENT_METHOD = 'paymentMethod'; + const TOKEN_TYPE = 'tokenType'; const ADDITIONAL_DATA_ERRORS = [ self::RECURRING_DETAIL_REFERENCE => 'Missing Token in Result please enable in ' . 'Settings -> API URLs and Response menu in the Adyen Customer Area Recurring details setting', @@ -71,13 +80,21 @@ class Vault */ private $config; + /** @var PaymentMethods */ + private $paymentMethodsHelper; + + /** @var PaymentMethodFactory */ + private $paymentMethodFactory; + public function __construct( Data $adyenHelper, AdyenLogger $adyenLogger, PaymentTokenManagement $paymentTokenManagement, PaymentTokenFactoryInterface $paymentTokenFactory, PaymentTokenRepositoryInterface $paymentTokenRepository, - Config $config + Config $config, + PaymentMethods $paymentMethodsHelper, + PaymentMethodFactory $paymentMethodFactory ) { $this->adyenHelper = $adyenHelper; $this->adyenLogger = $adyenLogger; @@ -85,6 +102,8 @@ public function __construct( $this->paymentTokenFactory = $paymentTokenFactory; $this->paymentTokenRepository = $paymentTokenRepository; $this->config = $config; + $this->paymentMethodsHelper = $paymentMethodsHelper; + $this->paymentMethodFactory = $paymentMethodFactory; } /** @@ -100,11 +119,24 @@ public function isCardVaultEnabled($storeId = null): bool } /** - * @param $payment - * @param array $additionalData + * @param array $response + * @return bool */ - public function saveRecurringDetails($payment, array $additionalData) + public function hasRecurringDetailReference(array $response): bool { + if (array_key_exists('additionalData', $response) && + array_key_exists(self::RECURRING_DETAIL_REFERENCE, $response['additionalData'])) { + return true; + } + + return false; + } + + public function saveRecurringCardDetails( + $payment, + array $additionalData, + AbstractWalletPaymentMethod $paymentMethod = null + ) { if (!$this->isCardVaultEnabled($payment->getOrder()->getStoreId()) && !$this->adyenHelper->isHppVaultEnabled($payment->getOrder()->getStoreId())) { return; @@ -115,7 +147,7 @@ public function saveRecurringDetails($payment, array $additionalData) } try { - $paymentToken = $this->getVaultPaymentToken($payment, $additionalData); + $paymentToken = $this->getVaultPaymentToken($payment, $additionalData, $paymentMethod); } catch (Exception $exception) { $this->adyenLogger->error(json_encode($exception)); return; @@ -135,12 +167,125 @@ public function saveRecurringDetails($payment, array $additionalData) } /** + * Save recurring details related to the payment method. + * * @param $payment * @param array $additionalData * @return PaymentTokenInterface|null - * @throws Exception */ - private function getVaultPaymentToken($payment, array $additionalData): PaymentTokenInterface + public function saveRecurringPaymentMethodDetails($payment, array $additionalData): ?PaymentTokenInterface + { + try { + $paymentToken = $this->createVaultAccountToken($payment, $additionalData); + $extensionAttributes = $this->getExtensionAttributes($payment); + $extensionAttributes->setVaultPaymentToken($paymentToken); + } catch (PaymentMethodException $e) { + $this->adyenLogger->error(sprintf( + 'Unable to create token for order %s', + $payment->getOrder()->getEntityId() + )); + + return null; + } catch (Exception $exception) { + $this->adyenLogger->error($exception->getMessage()); + + return null; + } + + return $paymentToken; + } + + /** + * Build the recurring data when payment is done trough a payment method (not card) + * + * @param int $storeId + * @param $payment + * @return array + */ + public function buildPaymentMethodRecurringData(int $storeId, $brand): array + { + $request = []; + if (!$this->config->isStoreAlternativePaymentMethodEnabled()) { + return $request; + } + try { + $adyenPaymentMethod = $this->paymentMethodFactory::createAdyenPaymentMethod($brand); + $allowRecurring = $this->allowRecurringOnPaymentMethod($adyenPaymentMethod, $storeId); + } catch (PaymentMethodException $exception) { + $this->adyenLogger->error(sprintf('Unable to create payment method with tx variant %s', $brand)); + return $request; + } catch (NoSuchEntityException $exception) { + $this->adyenLogger->error(sprintf('Unable to find payment method with tx variant %s', $brand)); + return $request; + } + + if (!$allowRecurring) { + return $request; + } + + $recurringModel = $this->config->getAlternativePaymentMethodTokenType($storeId); + if (isset($recurringModel)) { + $request['storePaymentMethod'] = true; + $request['recurringProcessingModel'] = $recurringModel; + } + return $request; + } + + /** + * Check if recurring should be allowed for payment method by checking: + * What type of recurring is currently enabled AND if the payment method supports that specific type recurring + * AND if the admin has enabled recurring for this payment method + * + * @param PaymentMethodInterface $adyenPaymentMethod + * @param int|null $storeId + * @return bool + * @throws NoSuchEntityException + */ + public function allowRecurringOnPaymentMethod(PaymentMethodInterface $adyenPaymentMethod, ?int $storeId): bool + { + $currentRecurringTokenSetting = $this->config->getAlternativePaymentMethodTokenType($storeId); + if ($currentRecurringTokenSetting === Recurring::CARD_ON_FILE) { + $methodSupportsRecurring = $adyenPaymentMethod->supportsCardOnFile(); + } elseif ($currentRecurringTokenSetting === Recurring::UNSCHEDULED_CARD_ON_FILE) { + $methodSupportsRecurring = $adyenPaymentMethod->supportsUnscheduledCardOnFile(); + } else { + $methodSupportsRecurring = $adyenPaymentMethod->supportsSubscription(); + } + + $tokenizedPaymentMethods = array_map( + 'trim', + explode(',', $this->config->getTokenizedPaymentMethods($storeId)) + ); + $shouldTokenize = in_array($adyenPaymentMethod->getTxVariant(), $tokenizedPaymentMethods); + + return $methodSupportsRecurring && $shouldTokenize; + } + + /** + * Return the Adyen token type (CardOnFile/Subscription) + * If it does not exist (token was created in an older version) return null + * + * @param PaymentTokenInterface $paymentToken + * @return string|null + */ + public function getAdyenTokenType(PaymentTokenInterface $paymentToken): ?string + { + $details = json_decode($paymentToken->getTokenDetails() ?: '{}', true); + if (array_key_exists(self::TOKEN_TYPE, $details)) { + return $details[self::TOKEN_TYPE]; + } + + return null; + } + + /** + * Create an entry in the vault table w/type=Account (for pms such as PayPal) + * If the token has already been created, do nothing + * Before doing this, validate the additionalData sent by Adyen, based on the params required by the payment method + * + * @throws InvalidAdditionalDataException + */ + private function createVaultAccountToken($payment, array $additionalData): PaymentTokenInterface { // Check if paymentToken exists already $paymentToken = $this->paymentTokenManagement->getByGatewayToken( @@ -149,6 +294,41 @@ private function getVaultPaymentToken($payment, array $additionalData): PaymentT $payment->getOrder()->getCustomerId() ); + // In case the payment token does not exist, create it based on the additionalData + if (is_null($paymentToken)) { + $storeId = $payment->getOrder()->getStoreId(); + $recurringModel = $this->config->getAlternativePaymentMethodTokenType($storeId); + $paymentToken = $this->paymentTokenFactory->create(PaymentTokenFactoryInterface::TOKEN_TYPE_ACCOUNT); + $paymentToken->setGatewayToken($additionalData[self::RECURRING_DETAIL_REFERENCE]); + $expiryDate = new DateTime(); + $expiryDate->add(new DateInterval('P1Y')); + $paymentToken->setExpiresAt($expiryDate); + $details = [ + 'type' => $payment->getCcType(), + self::TOKEN_TYPE => $recurringModel + ]; + + $paymentToken->setTokenDetails(json_encode($details, JSON_FORCE_OBJECT)); + } + + return $paymentToken; + } + + /** + * @throws Exception + */ + private function getVaultPaymentToken( + $payment, + array $additionalData, + AbstractWalletPaymentMethod $paymentMethod = null + ): PaymentTokenInterface { + // Check if paymentToken exists already + $paymentToken = $this->paymentTokenManagement->getByGatewayToken( + $additionalData[self::RECURRING_DETAIL_REFERENCE], + $payment->getMethodInstance()->getCode(), + $payment->getOrder()->getCustomerId() + ); + $paymentTokenSaveRequired = false; // In case the payment token does not exist, create it based on the additionalData @@ -163,7 +343,14 @@ private function getVaultPaymentToken($payment, array $additionalData): PaymentT $paymentToken->setExpiresAt($this->getExpirationDate($additionalData[self::EXPIRY_DATE])); - $details = ['type' => $additionalData[self::PAYMENT_METHOD]]; + if (isset($paymentMethod)) { + $details = [ + 'type' => $paymentMethod->getCardScheme(), + 'walletType' => $paymentMethod->getTxVariant() + ]; + } else { + $details = ['type' => $additionalData[self::PAYMENT_METHOD]]; + } if (!empty($additionalData[self::CARD_SUMMARY])) { $details['maskedCC'] = $additionalData[self::CARD_SUMMARY]; @@ -173,6 +360,18 @@ private function getVaultPaymentToken($payment, array $additionalData): PaymentT $details['expirationDate'] = $additionalData[self::EXPIRY_DATE]; } + // Set token type (alternative payment methods) for card tokens created using googlepay, applepay. + // This will be done for all card tokens once all vault changes are implemented + if ($payment->getMethodInstance()->getCode() === AdyenHppConfigProvider::CODE) { + $storeId = $payment->getOrder()->getStoreId(); + $recurringModel = $this->config->getAlternativePaymentMethodTokenType($storeId); + $details[self::TOKEN_TYPE] = $recurringModel; + } elseif ($payment->getMethodInstance()->getCode() === AdyenCcConfigProvider::CODE) { + $storeId = $payment->getOrder()->getStoreId(); + $recurringModel = $this->config->getCardRecurringType($storeId); + $details[self::TOKEN_TYPE] = $recurringModel; + } + $paymentToken->setTokenDetails(json_encode($details)); // If the token is updated, it needs to be saved to keep the changes diff --git a/Helper/Webhook/RecurringContractWebhookHandler.php b/Helper/Webhook/RecurringContractWebhookHandler.php index 1244913a5..830e6abf8 100644 --- a/Helper/Webhook/RecurringContractWebhookHandler.php +++ b/Helper/Webhook/RecurringContractWebhookHandler.php @@ -312,8 +312,6 @@ private function handlePaymentMethodContract(MagentoOrder $order, Notification $ $details = [ 'type' => $notification->getPaymentMethod(), - 'maskedCC' => $payment->getAdditionalInformation()['ibanNumber'], - 'expirationDate' => 'N/A' ]; $paymentTokenAlternativePaymentMethod->setCustomerId($customerId) diff --git a/Model/Billing/Agreement.php b/Model/Billing/Agreement.php index c695b0ed2..414492d9e 100644 --- a/Model/Billing/Agreement.php +++ b/Model/Billing/Agreement.php @@ -256,46 +256,6 @@ public function setCcBillingAgreement($contractDetail, $storeOneClick, $storeId) return $this; } - /** - * Set SEPA billing agreement This should be changed to utilise the factory method for different payment methods - * in the future - * - * @param array $additionalData - * @param $storeId - * @param array $savedPaymentData - * @return $this - */ - public function setSepaBillingAgreement(array $additionalData, $storeId, array $savedPaymentData): Agreement - { - $this - ->setMethodCode(PaymentMethods::ADYEN_ONE_CLICK) - ->setReferenceId($additionalData['recurring.recurringDetailReference']); - - $variant = $additionalData['paymentMethod']; - - $label = __( - '%1, %2', - $savedPaymentData['ownerName'], - $savedPaymentData['iban'] - ); - - $this->setAgreementLabel($label); - $recurringType = $this->configHelper->getAlternativePaymentMethodTokenType($storeId); - - $agreementData = [ - 'bank' => [ - 'ownerName' => $savedPaymentData['ownerName'], - 'iban' => $savedPaymentData['iban'], - ], - 'variant' => $variant, - 'contractTypes' => [$recurringType] - ]; - - $this->setAgreementData($agreementData); - - return $this; - } - /** * @param Payment $payment * @param $recurringDetailReference diff --git a/Model/Comment/ApiKeyEnding.php b/Model/Comment/ApiKeyEnding.php index 4476112b0..b797a95e2 100644 --- a/Model/Comment/ApiKeyEnding.php +++ b/Model/Comment/ApiKeyEnding.php @@ -1,4 +1,14 @@ + * @copyright (c) 2022 Adyen B.V. + * @license https://opensource.org/licenses/MIT MIT license + * This file is open source and available under the MIT license. + * See the LICENSE file for more info. + */ namespace Adyen\Payment\Model\Comment; diff --git a/Model/Comment/TokenizedPaymentMethods.php b/Model/Comment/TokenizedPaymentMethods.php new file mode 100644 index 000000000..7450b74f8 --- /dev/null +++ b/Model/Comment/TokenizedPaymentMethods.php @@ -0,0 +1,28 @@ + + * @copyright (c) 2022 Adyen B.V. + * @license https://opensource.org/licenses/MIT MIT license + * This file is open source and available under the MIT license. + * See the LICENSE file for more info. + */ + +namespace Adyen\Payment\Model\Comment; + +use Magento\Config\Model\Config\CommentInterface; + +class TokenizedPaymentMethods implements CommentInterface +{ + /** + * + * @param string $elementValue The value of the field with this commented + */ + public function getCommentText($elementValue): string + { + return 'Selected payment methods will automatically be tokenized on every transaction. + At the moment, CardOnFile tokens can only be created using Wallet payment methods (Google Pay).'; + } +} diff --git a/Model/Config/Source/TokenizedPaymentMethods.php b/Model/Config/Source/TokenizedPaymentMethods.php new file mode 100644 index 000000000..0af6ee7ca --- /dev/null +++ b/Model/Config/Source/TokenizedPaymentMethods.php @@ -0,0 +1,39 @@ + PaymentMethods\ApplePayPaymentMethod::TX_VARIANT, + 'label' => PaymentMethods\ApplePayPaymentMethod::NAME + ], + [ + 'value' => PaymentMethods\AmazonPayPaymentMethod::TX_VARIANT, + 'label' => PaymentMethods\AmazonPayPaymentMethod::NAME + ],*/ + [ + 'value' => PaymentMethods\GooglePayPaymentMethod::TX_VARIANT, + 'label' => PaymentMethods\GooglePayPaymentMethod::NAME + ], + [ + 'value' => PaymentMethods\PayPalPaymentMethod::TX_VARIANT, + 'label' => PaymentMethods\PayPalPaymentMethod::NAME + ], + [ + 'value' => PaymentMethods\SepaPaymentMethod::TX_VARIANT, + 'label' => PaymentMethods\SepaPaymentMethod::NAME + ], + ]; + } +} diff --git a/Model/InstantPurchase/AbstractAdyenTokenFormatter.php b/Model/InstantPurchase/AbstractAdyenTokenFormatter.php new file mode 100644 index 000000000..a9c4b0504 --- /dev/null +++ b/Model/InstantPurchase/AbstractAdyenTokenFormatter.php @@ -0,0 +1,94 @@ + + */ + +namespace Adyen\Payment\Model\InstantPurchase; + +use Magento\Vault\Api\Data\PaymentTokenFactoryInterface; +use Magento\Vault\Api\Data\PaymentTokenInterface; + +abstract class AbstractAdyenTokenFormatter +{ + /** + * Most used credit card types + * + * @var array + */ + public static $baseCardTypes = [ + 'AE' => 'American Express', + 'VI' => 'Visa', + 'MC' => 'MasterCard', + 'DI' => 'Discover', + 'JBC' => 'JBC', + 'CUP' => 'China Union Pay', + 'MI' => 'Maestro', + ]; + + /** + * Most used HPP types + * + * @var array + */ + public static $baseHppTypes = [ + 'sepadirectdebit' => 'SEPA Direct Debit' + ]; + + abstract public function formatPaymentToken(PaymentTokenInterface $paymentToken): string; + + public function formatCardPaymentToken(PaymentTokenInterface $paymentToken): string + { + $details = json_decode($paymentToken->getTokenDetails() ?: '{}', true); + + // Credit card type vaults + if (PaymentTokenFactoryInterface::TOKEN_TYPE_CREDIT_CARD === $paymentToken->getType()) { + if (!isset($details['type'], $details['maskedCC'], $details['expirationDate'])) { + throw new \InvalidArgumentException('Invalid Adyen credit card token details.'); + } + + if (isset(self::$baseCardTypes[$details['type']])) { + $ccType = self::$baseCardTypes[$details['type']]; + } else { + $ccType = $details['type']; + } + + return sprintf( + '%s: %s, %s: %s (%s: %s)', + __('Credit Card'), + $ccType, + __('ending'), + $details['maskedCC'], + __('expires'), + $details['expirationDate'] + ); + } else { + // Account type vaults + if (!isset($details['type'], $details['maskedCC'], $details['expirationDate'])) { + throw new \InvalidArgumentException('Invalid Adyen local payment method token details.'); + } + + if (isset(self::$baseHppTypes[$details['type']])) { + $hppType = self::$baseHppTypes[$details['type']]; + } else { + $hppType = $details['type']; + } + + return sprintf( + '%s: %s, %s: %s (%s: %s)', + __('Account'), + $hppType, + __('number'), + $details['maskedCC'], + __('expires'), + $details['expirationDate'] + ); + } + } +} diff --git a/Model/InstantPurchase/CreditCard/TokenFormatter.php b/Model/InstantPurchase/CreditCard/TokenFormatter.php index dfd91ba6e..5f11276db 100644 --- a/Model/InstantPurchase/CreditCard/TokenFormatter.php +++ b/Model/InstantPurchase/CreditCard/TokenFormatter.php @@ -3,7 +3,7 @@ * * Adyen Payment Module * - * Copyright (c) 2019 Adyen B.V. + * Copyright (c) 2022 Adyen B.V. * This file is open source and available under the MIT license. * See the LICENSE file for more info. * @@ -12,88 +12,18 @@ namespace Adyen\Payment\Model\InstantPurchase\CreditCard; +use Adyen\Payment\Model\InstantPurchase\AbstractAdyenTokenFormatter; use Magento\InstantPurchase\PaymentMethodIntegration\PaymentTokenFormatterInterface; -use Magento\Vault\Api\Data\PaymentTokenFactoryInterface; use Magento\Vault\Api\Data\PaymentTokenInterface; /** * Adyen stored credit card formatter. */ -class TokenFormatter implements PaymentTokenFormatterInterface +class TokenFormatter extends AbstractAdyenTokenFormatter implements PaymentTokenFormatterInterface { - /** - * Most used credit card types - * - * @var array - */ - public static $baseCardTypes = [ - 'AE' => 'American Express', - 'VI' => 'Visa', - 'MC' => 'MasterCard', - 'DI' => 'Discover', - 'JBC' => 'JBC', - 'CUP' => 'China Union Pay', - 'MI' => 'Maestro', - ]; - /** - * Most used HPP types - * - * @var array - */ - public static $baseHppTypes = [ - 'sepadirectdebit' => 'SEPA Direct Debit' - ]; - - /** - * @inheritdoc - */ public function formatPaymentToken(PaymentTokenInterface $paymentToken): string { - $details = json_decode($paymentToken->getTokenDetails() ?: '{}', true); - - // Credit card type vaults - if (PaymentTokenFactoryInterface::TOKEN_TYPE_CREDIT_CARD === $paymentToken->getType()) { - if (!isset($details['type'], $details['maskedCC'], $details['expirationDate'])) { - throw new \InvalidArgumentException('Invalid Adyen credit card token details.'); - } - - if (isset(self::$baseCardTypes[$details['type']])) { - $ccType = self::$baseCardTypes[$details['type']]; - } else { - $ccType = $details['type']; - } - - return sprintf( - '%s: %s, %s: %s (%s: %s)', - __('Credit Card'), - $ccType, - __('ending'), - $details['maskedCC'], - __('expires'), - $details['expirationDate'] - ); - } else { - // Account type vaults - if (!isset($details['type'], $details['maskedCC'], $details['expirationDate'])) { - throw new \InvalidArgumentException('Invalid Adyen local payment method token details.'); - } - - if (isset(self::$baseHppTypes[$details['type']])) { - $hppType = self::$baseHppTypes[$details['type']]; - } else { - $hppType = $details['type']; - } - - return sprintf( - '%s: %s, %s: %s (%s: %s)', - __('Account'), - $hppType, - __('number'), - $details['maskedCC'], - __('expires'), - $details['expirationDate'] - ); - } + return $this->formatCardPaymentToken($paymentToken); } } diff --git a/Model/InstantPurchase/PaymentMethods/AvailabilityChecker.php b/Model/InstantPurchase/PaymentMethods/AvailabilityChecker.php new file mode 100644 index 000000000..df9ddb663 --- /dev/null +++ b/Model/InstantPurchase/PaymentMethods/AvailabilityChecker.php @@ -0,0 +1,41 @@ + + */ + +namespace Adyen\Payment\Model\InstantPurchase\PaymentMethods; + +use Adyen\Payment\Helper\Config; +use Adyen\Payment\Helper\Recurring; +use Magento\InstantPurchase\PaymentMethodIntegration\AvailabilityCheckerInterface; + +class AvailabilityChecker implements AvailabilityCheckerInterface +{ + /** @var Config */ + private $configHelper; + + public function __construct(Config $configHelper) + { + $this->configHelper = $configHelper; + } + + /** + * Check if store alternative payment methods is set to true AND the type is set to CardOnFile + * + * @inheritdoc + */ + public function isAvailable(): bool + { + $storeVaultPmEnabled = $this->configHelper->isStoreAlternativePaymentMethodEnabled(); + $vaultPmTokenType = $this->configHelper->getAlternativePaymentMethodTokenType() === Recurring::CARD_ON_FILE; + + return $storeVaultPmEnabled && $vaultPmTokenType; + } +} diff --git a/Model/InstantPurchase/PaymentMethods/TokenFormatter.php b/Model/InstantPurchase/PaymentMethods/TokenFormatter.php new file mode 100644 index 000000000..de6dc58f3 --- /dev/null +++ b/Model/InstantPurchase/PaymentMethods/TokenFormatter.php @@ -0,0 +1,47 @@ + + */ + +namespace Adyen\Payment\Model\InstantPurchase\PaymentMethods; + +use Adyen\Payment\Exception\PaymentMethodException; +use Adyen\Payment\Helper\PaymentMethods\PaymentMethodFactory; +use Adyen\Payment\Model\InstantPurchase\AbstractAdyenTokenFormatter; +use Magento\InstantPurchase\PaymentMethodIntegration\PaymentTokenFormatterInterface; +use Magento\Vault\Api\Data\PaymentTokenInterface; + +/** + * Adyen stored payment method formatter. + */ +class TokenFormatter extends AbstractAdyenTokenFormatter implements PaymentTokenFormatterInterface +{ + /** @var PaymentMethodFactory */ + private $paymentMethodFactory; + + public function __construct(PaymentMethodFactory $paymentMethodFactory) + { + $this->paymentMethodFactory = $paymentMethodFactory; + } + + public function formatPaymentToken(PaymentTokenInterface $paymentToken): string + { + $details = json_decode($paymentToken->getTokenDetails() ?: '{}', true); + // If payment method cannot be created based on the type, this implies that a card token was created using + // a wallet method (googlepay/applepay etc.). Hence, return the card component for this token. + try { + $adyenPaymentMethod = $this->paymentMethodFactory::createAdyenPaymentMethod($details['type']); + } catch (PaymentMethodException $e) { + return $this->formatCardPaymentToken($paymentToken); + } + + return $adyenPaymentMethod->getPaymentMethodName(); + } +} diff --git a/Model/Method/PaymentMethodVault.php b/Model/Method/PaymentMethodVault.php new file mode 100644 index 000000000..f7240815c --- /dev/null +++ b/Model/Method/PaymentMethodVault.php @@ -0,0 +1,81 @@ + + */ + +namespace Adyen\Payment\Model\Method; + +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Payment\Gateway\Command\CommandManagerPoolInterface; +use Magento\Payment\Gateway\Config\ValueHandlerPoolInterface; +use Magento\Payment\Gateway\ConfigFactoryInterface; +use Magento\Payment\Gateway\ConfigInterface; +use Magento\Payment\Model\Method; +use Magento\Payment\Model\MethodInterface; +use Magento\Sales\Api\Data\OrderPaymentExtensionInterfaceFactory; +use Magento\Vault\Api\PaymentTokenManagementInterface; +use Magento\Vault\Model\Method\Vault; + +class PaymentMethodVault extends Vault +{ + /** + * Constructor + * + * @param ConfigInterface $config + * @param ConfigFactoryInterface $configFactory + * @param ObjectManagerInterface $objectManager + * @param MethodInterface $vaultProvider + * @param ManagerInterface $eventManager + * @param ValueHandlerPoolInterface $valueHandlerPool + * @param CommandManagerPoolInterface $commandManagerPool + * @param PaymentTokenManagementInterface $tokenManagement + * @param OrderPaymentExtensionInterfaceFactory $paymentExtensionFactory + * @param string $code + * @param Json|null $jsonSerializer + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + ConfigInterface $config, + ConfigFactoryInterface $configFactory, + ObjectManagerInterface $objectManager, + MethodInterface $vaultProvider, + ManagerInterface $eventManager, + ValueHandlerPoolInterface $valueHandlerPool, + CommandManagerPoolInterface $commandManagerPool, + PaymentTokenManagementInterface $tokenManagement, + OrderPaymentExtensionInterfaceFactory $paymentExtensionFactory, + $code, + Json $jsonSerializer = null + ) { + parent::__construct( + $config, + $configFactory, + $objectManager, + $vaultProvider, + $eventManager, + $valueHandlerPool, + $commandManagerPool, + $tokenManagement, + $paymentExtensionFactory, + $code, + $jsonSerializer + ); + } + + /** + * @return false + */ + public function isInitializeNeeded(): bool + { + return false; + } +} diff --git a/Model/Ui/AdyenCcConfigProvider.php b/Model/Ui/AdyenCcConfigProvider.php index 525e93162..44809ff49 100755 --- a/Model/Ui/AdyenCcConfigProvider.php +++ b/Model/Ui/AdyenCcConfigProvider.php @@ -12,8 +12,16 @@ namespace Adyen\Payment\Model\Ui; use Adyen\Payment\Helper\Config; +use Adyen\Payment\Helper\Data; +use Adyen\Payment\Helper\PaymentMethods; use Adyen\Payment\Helper\Recurring; use Magento\Checkout\Model\ConfigProviderInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Asset\Source; +use Magento\Payment\Model\CcConfig; +use Magento\Store\Model\StoreManagerInterface; class AdyenCcConfigProvider implements ConfigProviderInterface { @@ -21,12 +29,7 @@ class AdyenCcConfigProvider implements ConfigProviderInterface const CC_VAULT_CODE = 'adyen_cc_vault'; /** - * @var PaymentHelper - */ - protected $_paymentHelper; - - /** - * @var \Adyen\Payment\Helper\Data + * @var Data */ protected $_adyenHelper; @@ -38,57 +41,59 @@ class AdyenCcConfigProvider implements ConfigProviderInterface /** * Request object * - * @var \Magento\Framework\App\RequestInterface + * @var RequestInterface */ protected $_request; /** - * @var \Magento\Framework\UrlInterface + * @var UrlInterface */ protected $_urlBuilder; /** - * @var \Magento\Payment\Model\CcConfig + * @var CcConfig */ private $ccConfig; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ private $storeManager; /** - * @var \Magento\Framework\Serialize\SerializerInterface + * @var SerializerInterface */ private $serializer; /** @var Config $configHelper */ private $configHelper; + /** @var PaymentMethods */ + private $paymentMethodsHelper; + /** * AdyenCcConfigProvider constructor. * - * @param \Magento\Payment\Helper\Data $paymentHelper - * @param \Adyen\Payment\Helper\Data $adyenHelper - * @param \Magento\Framework\App\RequestInterface $request - * @param \Magento\Framework\UrlInterface $urlBuilder - * @param \Magento\Framework\View\Asset\Source $assetSource - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Payment\Model\CcConfig $ccConfig - * @param \Magento\Framework\Serialize\SerializerInterface $serializer + * @param Data $adyenHelper + * @param RequestInterface $request + * @param UrlInterface $urlBuilder + * @param Source $assetSource + * @param StoreManagerInterface $storeManager + * @param CcConfig $ccConfig + * @param SerializerInterface $serializer + * @param Config $configHelper */ public function __construct( - \Magento\Payment\Helper\Data $paymentHelper, - \Adyen\Payment\Helper\Data $adyenHelper, - \Magento\Framework\App\RequestInterface $request, - \Magento\Framework\UrlInterface $urlBuilder, - \Magento\Framework\View\Asset\Source $assetSource, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Payment\Model\CcConfig $ccConfig, - \Magento\Framework\Serialize\SerializerInterface $serializer, - Config $configHelper + Data $adyenHelper, + RequestInterface $request, + UrlInterface $urlBuilder, + Source $assetSource, + StoreManagerInterface $storeManager, + CcConfig $ccConfig, + SerializerInterface $serializer, + Config $configHelper, + PaymentMethods $paymentMethodsHelper ) { - $this->_paymentHelper = $paymentHelper; $this->_adyenHelper = $adyenHelper; $this->_request = $request; $this->_urlBuilder = $urlBuilder; @@ -97,6 +102,7 @@ public function __construct( $this->storeManager = $storeManager; $this->serializer = $serializer; $this->configHelper = $configHelper; + $this->paymentMethodsHelper = $paymentMethodsHelper; } /** @@ -125,11 +131,15 @@ public function getConfig() [ 'payment' => [ 'ccform' => [ - 'availableTypes' => [$methodCode => $this->getCcAvailableTypes()], - 'availableTypesByAlt' => [$methodCode => $this->getCcAvailableTypesByAlt()], + 'availableTypes' => [ + $methodCode => $this->paymentMethodsHelper->getCcAvailableTypes() + ], + 'availableTypesByAlt' => [ + $methodCode => $this->paymentMethodsHelper->getCcAvailableTypesByAlt() + ], 'months' => [$methodCode => $this->getCcMonths()], 'years' => [$methodCode => $this->getCcYears()], - 'hasVerification' => [$methodCode => $this->hasVerification($methodCode)], + 'hasVerification' => [$methodCode => $this->hasVerification()], 'cvvImageUrl' => [$methodCode => $this->getCvvImageUrl()] ] ] @@ -162,50 +172,6 @@ public function getConfig() return $config; } - /** - * Retrieve available credit card types - * - * @return array - */ - protected function getCcAvailableTypes() - { - $types = []; - $ccTypes = $this->_adyenHelper->getAdyenCcTypes(); - $availableTypes = $this->_adyenHelper->getAdyenCcConfigData('cctypes'); - if ($availableTypes) { - $availableTypes = explode(',', $availableTypes); - foreach (array_keys($ccTypes) as $code) { - if (in_array($code, $availableTypes)) { - $types[$code] = $ccTypes[$code]['name']; - } - } - } - - return $types; - } - - /** - * Retrieve available credit card type codes by alt code - * - * @return array - */ - protected function getCcAvailableTypesByAlt() - { - $types = []; - $ccTypes = $this->_adyenHelper->getAdyenCcTypes(); - $availableTypes = $this->_adyenHelper->getAdyenCcConfigData('cctypes'); - if ($availableTypes) { - $availableTypes = explode(',', $availableTypes); - foreach (array_keys($ccTypes) as $code) { - if (in_array($code, $availableTypes)) { - $types[$ccTypes[$code]['code_alt']] = $code; - } - } - } - - return $types; - } - /** * Get icons for available payment methods * @@ -275,7 +241,7 @@ protected function getCvvImageUrl() /** * Retrieve request object * - * @return \Magento\Framework\App\RequestInterface + * @return RequestInterface */ protected function _getRequest() { diff --git a/Model/Ui/AdyenHppConfigProvider.php b/Model/Ui/AdyenHppConfigProvider.php index bf1736ff7..49200895c 100755 --- a/Model/Ui/AdyenHppConfigProvider.php +++ b/Model/Ui/AdyenHppConfigProvider.php @@ -18,6 +18,7 @@ class AdyenHppConfigProvider implements ConfigProviderInterface { const CODE = 'adyen_hpp'; + const HPP_VAULT_CODE = 'adyen_hpp_vault'; /** * @var PaymentHelper @@ -103,6 +104,7 @@ public function getConfig() $config = [ 'payment' => [ self::CODE => [ + 'vaultCode' => self::HPP_VAULT_CODE, 'isActive' => true, 'successPage' => $this->urlBuilder->getUrl( 'checkout/onepage/success', diff --git a/Model/Ui/AdyenOneclickConfigProvider.php b/Model/Ui/AdyenOneclickConfigProvider.php index 78d3afe4d..9e4c997b2 100644 --- a/Model/Ui/AdyenOneclickConfigProvider.php +++ b/Model/Ui/AdyenOneclickConfigProvider.php @@ -13,6 +13,7 @@ use Adyen\Payment\Helper\ChargedCurrency; use Adyen\Payment\Helper\Data; +use Adyen\Payment\Helper\PaymentMethods; use Adyen\Payment\Helper\Recurring; use Adyen\Payment\Helper\Vault; use Magento\Checkout\Model\ConfigProviderInterface; @@ -77,6 +78,9 @@ class AdyenOneclickConfigProvider implements ConfigProviderInterface */ private $vaultHelper; + /** @var PaymentMethods */ + private $paymentMethodsHelper; + /** * AdyenOneclickConfigProvider constructor. * @@ -99,7 +103,8 @@ public function __construct( UrlInterface $urlBuilder, CcConfig $ccConfig, ChargedCurrency $chargedCurrency, - Vault $vaultHelper + Vault $vaultHelper, + PaymentMethods $paymentMethodsHelper ) { $this->_adyenHelper = $adyenHelper; $this->_request = $request; @@ -110,6 +115,7 @@ public function __construct( $this->ccConfig = $ccConfig; $this->chargedCurrency = $chargedCurrency; $this->vaultHelper = $vaultHelper; + $this->paymentMethodsHelper = $paymentMethodsHelper; } /** @@ -146,7 +152,7 @@ public function getConfig() [ 'payment' => [ 'ccform' => [ - 'availableTypes' => [$methodCode => $this->getCcAvailableTypes()], + 'availableTypes' => [$methodCode => $this->paymentMethodsHelper->getCcAvailableTypes()], 'months' => [$methodCode => $this->getCcMonths()], 'years' => [$methodCode => $this->getCcYears()], 'hasVerification' => [$methodCode => $this->hasVerification($methodCode)], @@ -203,28 +209,6 @@ protected function _getQuote() return $this->_session->getQuote(); } - /** - * Retrieve availables credit card types - * - * @return array - */ - protected function getCcAvailableTypes() - { - $types = []; - $ccTypes = $this->_adyenHelper->getAdyenCcTypes(); - $availableTypes = $this->_adyenHelper->getAdyenCcConfigData('cctypes'); - if ($availableTypes) { - $availableTypes = explode(',', $availableTypes); - foreach (array_keys($ccTypes) as $code) { - if (in_array($code, $availableTypes)) { - $types[$code] = $ccTypes[$code]['name']; - } - } - } - - return $types; - } - /** * Retrieve credit card expire months * diff --git a/Model/Ui/AdyenUiComponentProvider.php b/Model/Ui/AdyenUiComponentProvider.php new file mode 100644 index 000000000..cd7d9af34 --- /dev/null +++ b/Model/Ui/AdyenUiComponentProvider.php @@ -0,0 +1,60 @@ + + */ + +namespace Adyen\Payment\Model\Ui; + +use Adyen\Payment\Helper\Data; +use Magento\Vault\Api\Data\PaymentTokenInterface; +use Magento\Vault\Model\Ui\TokenUiComponentInterface; +use Magento\Vault\Model\Ui\TokenUiComponentProviderInterface; +use Magento\Vault\Model\Ui\TokenUiComponentInterfaceFactory; + +class AdyenUiComponentProvider +{ + + protected $componentFactory; + protected $dataHelper; + + /** + * @param TokenUiComponentInterfaceFactory $componentFactory + * @param Data $dataHelper + */ + public function __construct( + TokenUiComponentInterfaceFactory $componentFactory, + Data $dataHelper + ) { + $this->componentFactory = $componentFactory; + $this->dataHelper = $dataHelper; + } + + /** + * Get UI component for token + * + * @param PaymentTokenInterface $paymentToken + * @return TokenUiComponentInterface + */ + public function getCardComponentForToken(PaymentTokenInterface $paymentToken): TokenUiComponentInterface + { + $details = json_decode($paymentToken->getTokenDetails() ?: '{}', true); + $details['icon'] = $this->dataHelper->getVariantIcon($details['type']); + + return $this->componentFactory->create( + [ + 'config' => [ + 'code' => AdyenCcConfigProvider::CC_VAULT_CODE, + TokenUiComponentProviderInterface::COMPONENT_DETAILS => $details, + TokenUiComponentProviderInterface::COMPONENT_PUBLIC_HASH => $paymentToken->getPublicHash() + ], + 'name' => 'Adyen_Payment/js/view/payment/method-renderer/vault' + ] + ); + } +} diff --git a/Model/Ui/PaymentMethodUiComponentProvider.php b/Model/Ui/PaymentMethodUiComponentProvider.php new file mode 100644 index 000000000..0d4fdab10 --- /dev/null +++ b/Model/Ui/PaymentMethodUiComponentProvider.php @@ -0,0 +1,85 @@ + + */ + +namespace Adyen\Payment\Model\Ui; + +use Adyen\Payment\Exception\PaymentMethodException; +use Adyen\Payment\Helper\Data; +use Adyen\Payment\Helper\PaymentMethods\PaymentMethodFactory; +use Adyen\Payment\Helper\Recurring; +use Adyen\Payment\Helper\Vault; +use Exception; +use Magento\Vault\Api\Data\PaymentTokenInterface; +use Magento\Vault\Model\Ui\TokenUiComponentInterface; +use Magento\Vault\Model\Ui\TokenUiComponentProviderInterface; +use Magento\Vault\Model\Ui\TokenUiComponentInterfaceFactory; + +class PaymentMethodUiComponentProvider extends AdyenUiComponentProvider implements TokenUiComponentProviderInterface +{ + + private $vaultHelper; + private $paymentMethodFactory; + + /** + * @param TokenUiComponentInterfaceFactory $componentFactory + * @param Data $dataHelper + * @param Vault $vaultHelper + * @param PaymentMethodFactory $paymentMethodFactory + */ + public function __construct( + TokenUiComponentInterfaceFactory $componentFactory, + Data $dataHelper, + Vault $vaultHelper, + PaymentMethodFactory $paymentMethodFactory + ) { + parent::__construct($componentFactory, $dataHelper); + $this->vaultHelper = $vaultHelper; + $this->paymentMethodFactory = $paymentMethodFactory; + } + + /** + * Get UI component for token + * + * @param PaymentTokenInterface $paymentToken + * @return TokenUiComponentInterface + * @throws PaymentMethodException + * @throws Exception + */ + public function getComponentForToken(PaymentTokenInterface $paymentToken): TokenUiComponentInterface + { + $tokenType = $this->vaultHelper->getAdyenTokenType($paymentToken); + $details = json_decode($paymentToken->getTokenDetails() ?: '{}', true); + // If payment method cannot be created based on the type, this implies that a card token was created using + // a wallet method (googlepay/applepay etc.). Hence, return the card component for this token. + try { + $adyenPaymentMethod = $this->paymentMethodFactory::createAdyenPaymentMethod($details['type']); + } catch (PaymentMethodException $exception) { + return $this->getCardComponentForToken($paymentToken); + } + + $details['icon'] = $this->dataHelper->getVariantIcon($details['type']); + $createdAt = new \DateTime($paymentToken->getCreatedAt()); + $details['created'] = $createdAt->format('Y-m-d'); + $details['displayToken'] = $tokenType === Recurring::CARD_ON_FILE; + $details['label'] = $adyenPaymentMethod->getPaymentMethodName(); + + return $this->componentFactory->create( + [ + 'config' => [ + 'code' => AdyenHppConfigProvider::HPP_VAULT_CODE, + TokenUiComponentProviderInterface::COMPONENT_DETAILS => $details, + TokenUiComponentProviderInterface::COMPONENT_PUBLIC_HASH => $paymentToken->getPublicHash() + ], + 'name' => 'Adyen_Payment/js/view/payment/method-renderer/payment_method_vault' + ] + ); + } +} diff --git a/Model/Ui/TokenUiComponentProvider.php b/Model/Ui/TokenUiComponentProvider.php index 5b9c9a7fc..da1884217 100644 --- a/Model/Ui/TokenUiComponentProvider.php +++ b/Model/Ui/TokenUiComponentProvider.php @@ -12,57 +12,23 @@ namespace Adyen\Payment\Model\Ui; use Adyen\Payment\Helper\Data; +use Adyen\Payment\Helper\Vault; +use Adyen\Payment\Helper\Recurring; use Magento\Vault\Api\Data\PaymentTokenInterface; use Magento\Vault\Model\Ui\TokenUiComponentInterface; use Magento\Vault\Model\Ui\TokenUiComponentProviderInterface; use Magento\Vault\Model\Ui\TokenUiComponentInterfaceFactory; -use Magento\Framework\UrlInterface; -class TokenUiComponentProvider implements TokenUiComponentProviderInterface +class TokenUiComponentProvider extends AdyenUiComponentProvider implements TokenUiComponentProviderInterface { - /** - * @var TokenUiComponentInterfaceFactory - */ - private $componentFactory; - - /** - * @var Data - */ - private $adyenHelper; - - /** - * @param TokenUiComponentInterfaceFactory $componentFactory - * @param UrlInterface $urlBuilder - */ - public function __construct( - TokenUiComponentInterfaceFactory $componentFactory, - Data $adyenHelper - ) { - $this->componentFactory = $componentFactory; - $this->adyenHelper = $adyenHelper; - } - /** * Get UI component for token * * @param PaymentTokenInterface $paymentToken * @return TokenUiComponentInterface */ - public function getComponentForToken(PaymentTokenInterface $paymentToken) + public function getComponentForToken(PaymentTokenInterface $paymentToken): TokenUiComponentInterface { - $details = json_decode($paymentToken->getTokenDetails() ?: '{}', true); - $details['icon'] = $this->adyenHelper->getVariantIcon($details['type']); - - $component = $this->componentFactory->create( - [ - 'config' => [ - 'code' => AdyenCcConfigProvider::CC_VAULT_CODE, - TokenUiComponentProviderInterface::COMPONENT_DETAILS => $details, - TokenUiComponentProviderInterface::COMPONENT_PUBLIC_HASH => $paymentToken->getPublicHash() - ], - 'name' => 'Adyen_Payment/js/view/payment/method-renderer/vault' - ] - ); - return $component; + return $this->getCardComponentForToken($paymentToken); } } diff --git a/Observer/AdyenHppDataAssignObserver.php b/Observer/AdyenHppDataAssignObserver.php index 316f1a77f..332b7c44e 100644 --- a/Observer/AdyenHppDataAssignObserver.php +++ b/Observer/AdyenHppDataAssignObserver.php @@ -11,7 +11,6 @@ namespace Adyen\Payment\Observer; -use Adyen\Payment\Helper\Data; use Adyen\Payment\Helper\StateData; use Adyen\Payment\Model\ResourceModel\StateData\Collection; use Adyen\Service\Validator\CheckoutStateDataValidator; @@ -19,6 +18,7 @@ use Magento\Framework\Event\Observer; use Magento\Payment\Observer\AbstractDataAssignObserver; use Magento\Quote\Api\Data\PaymentInterface; +use Magento\Store\Model\StoreManagerInterface; class AdyenHppDataAssignObserver extends AbstractDataAssignObserver { @@ -55,21 +55,27 @@ class AdyenHppDataAssignObserver extends AbstractDataAssignObserver */ private $stateData; + /** @var StoreManagerInterface */ + private $storeManager; + /** * AdyenHppDataAssignObserver constructor. * * @param CheckoutStateDataValidator $checkoutStateDataValidator * @param Collection $stateDataCollection * @param StateData $stateData + * @param StoreManagerInterface $storeManager */ public function __construct( CheckoutStateDataValidator $checkoutStateDataValidator, Collection $stateDataCollection, - StateData $stateData + StateData $stateData, + StoreManagerInterface $storeManager ) { $this->checkoutStateDataValidator = $checkoutStateDataValidator; $this->stateDataCollection = $stateDataCollection; $this->stateData = $stateData; + $this->storeManager = $storeManager; } /** @@ -108,10 +114,6 @@ public function execute(Observer $observer) $this->stateData->setStateData($stateData, $paymentInfo->getData('quote_id')); } - if (array_key_exists(self::BRAND_CODE, $additionalData) && $additionalData[self::BRAND_CODE] === Data::SEPA) { - $additionalDataToSave = $this->getSepaAdditionalDataToSave($stateData); - } - unset($additionalData[self::STATE_DATA]); @@ -120,29 +122,10 @@ public function execute(Observer $observer) $paymentInfo->setAdditionalInformation($key, $data); } - // set ccType + // Set ccType. If payment method is tokenizable, update additional information if (!empty($additionalData[self::BRAND_CODE])) { - $paymentInfo->setCcType($additionalData[self::BRAND_CODE]); + $paymentMethod = $additionalData[self::BRAND_CODE]; + $paymentInfo->setCcType($paymentMethod); } } - - /** - * Get the additional data to save. This data will be required if the payment is to be tokenized - * - * @param array $stateData - * @return array - */ - private function getSepaAdditionalDataToSave(array $stateData): array - { - $additionalData = []; - if (array_key_exists('iban', $stateData['paymentMethod'])) { - $additionalData['iban'] = $stateData['paymentMethod']['iban']; - } - - if (array_key_exists('ownerName', $stateData['paymentMethod'])) { - $additionalData['ownerName'] = $stateData['paymentMethod']['ownerName']; - } - - return $additionalData; - } } diff --git a/Observer/VaultDeleteTokenObserver.php b/Observer/VaultDeleteTokenObserver.php new file mode 100644 index 000000000..f4a007e95 --- /dev/null +++ b/Observer/VaultDeleteTokenObserver.php @@ -0,0 +1,108 @@ + + */ + +namespace Adyen\Payment\Observer; + +use Adyen\AdyenException; +use Adyen\Payment\Helper\Data; +use Adyen\Payment\Helper\Requests; +use Adyen\Payment\Logger\AdyenLogger; +use Adyen\Service\Recurring; +use Magento\Customer\Model\Session; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Vault\Api\Data\PaymentTokenInterface; +use Magento\Vault\Api\PaymentTokenManagementInterface; + +class VaultDeleteTokenObserver implements ObserverInterface +{ + /** @var PaymentTokenManagementInterface */ + private $paymentTokenManagement; + + /** @var Session */ + private $customerSession; + + /** @var Data */ + private $dataHelper; + + /** @var Requests */ + private $requestsHelper; + + /** @var AdyenLogger */ + private $adyenLogger; + + public function __construct( + PaymentTokenManagementInterface $paymentTokenManagement, + Session $customerSession, + Data $dataHelper, + Requests $requestsHelper, + AdyenLogger $adyenLogger + ) { + $this->paymentTokenManagement = $paymentTokenManagement; + $this->customerSession = $customerSession; + $this->dataHelper = $dataHelper; + $this->requestsHelper = $requestsHelper; + $this->adyenLogger = $adyenLogger; + } + + public function execute(Observer $observer) + { + $customerId = $this->customerSession->getCustomerId(); + $paymentToken = $this->getPaymentToken($observer->getData('request'), $customerId); + + $request = [ + Requests::MERCHANT_ACCOUNT => $this->dataHelper->getAdyenMerchantAccount( + $paymentToken->getPaymentMethodCode() + ), + Requests::SHOPPER_REFERENCE => $this->requestsHelper->getShopperReference($customerId, null), + Requests::RECURRING_DETAIL_REFERENCE => $paymentToken->getGatewayToken() + ]; + + try { + $client = $this->dataHelper->initializeAdyenClient(); + $recurringService = new Recurring($client); + $recurringService->disable($request); + } catch (AdyenException $e) { + $this->adyenLogger->error(sprintf( + 'Error while attempting to disable token with id %s: %s', + $paymentToken->getEntityId(), + $e->getMessage()) + ); + } catch (NoSuchEntityException $e) { + $this->adyenLogger->error(sprintf( + 'No such entity while attempting to disable token with id %s: %s', + $paymentToken->getEntityId(), + $e->getMessage()) + ); + } + } + + /** + * @param Http $request + * @param string $customerId + * @return PaymentTokenInterface|null + */ + private function getPaymentToken(Http $request, string $customerId): ?PaymentTokenInterface + { + $publicHash = $request->getPostValue(PaymentTokenInterface::PUBLIC_HASH); + + if ($publicHash === null) { + return null; + } + + return $this->paymentTokenManagement->getByPublicHash( + $publicHash, + $customerId + ); + } +} diff --git a/Plugin/CustomerFilterVaultTokens.php b/Plugin/CustomerFilterVaultTokens.php new file mode 100644 index 000000000..20d2001a3 --- /dev/null +++ b/Plugin/CustomerFilterVaultTokens.php @@ -0,0 +1,48 @@ + + */ + +namespace Adyen\Payment\Plugin; + +use Adyen\Payment\Helper\Vault; +use Magento\Vault\Model\CustomerTokenManagement; +use Adyen\Payment\Helper\Recurring; + +class CustomerFilterVaultTokens +{ + /** + * Returns filtered list of payment tokens for current customer session + * Hide token if it is specifically set to SUBSCRIPTION or UNSCHEDULED_CARD_ON_FILE + * + * @param CustomerTokenManagement $customerTokenManagement + * @param array $customerSessionTokens + * @return array + */ + public function afterGetCustomerSessionTokens( + CustomerTokenManagement $customerTokenManagement, + array $customerSessionTokens + ): array { + foreach ($customerSessionTokens as $key => $token) { + if (strpos($token->getPaymentMethodCode(), 'adyen_') === 0) { + $tokenDetails = json_decode($token->getTokenDetails()); + if (property_exists($tokenDetails, Vault::TOKEN_TYPE) && + in_array($tokenDetails->tokenType, [ + Recurring::SUBSCRIPTION, + Recurring::UNSCHEDULED_CARD_ON_FILE] + ) + ) { + unset($customerSessionTokens[$key]); + } + } + } + + return $customerSessionTokens; + } +} diff --git a/Plugin/PaymentVaultDeleteToken.php b/Plugin/PaymentVaultDeleteToken.php index 47a5923b8..57dd51939 100644 --- a/Plugin/PaymentVaultDeleteToken.php +++ b/Plugin/PaymentVaultDeleteToken.php @@ -3,7 +3,7 @@ * * Adyen Payment module (https://www.adyen.com/) * - * Copyright (c) 2019 Adyen BV (https://www.adyen.com/) + * Copyright (c) 2022 Adyen BV (https://www.adyen.com/) * See LICENSE.txt for license details. * * Author: Adyen @@ -11,50 +11,62 @@ namespace Adyen\Payment\Plugin; +use Adyen\Payment\Model\Api\PaymentRequest; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\StoreManagerInterface; use Magento\Vault\Api\Data\PaymentTokenInterface; +use Magento\Vault\Api\PaymentTokenRepositoryInterface; class PaymentVaultDeleteToken { /** - * @var \Adyen\Payment\Model\Api\PaymentRequest + * @var PaymentRequest */ protected $paymentRequest; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; /** * PaymentVaultDeleteToken constructor. * - * @param \Adyen\Payment\Model\Api\PaymentRequest $paymentRequest + * @param PaymentRequest $paymentRequest + * @param StoreManagerInterface $storeManager */ public function __construct( - \Adyen\Payment\Model\Api\PaymentRequest $paymentRequest, - \Magento\Store\Model\StoreManagerInterface $storeManager + PaymentRequest $paymentRequest, + StoreManagerInterface $storeManager ) { $this->paymentRequest = $paymentRequest; $this->storeManager = $storeManager; } + /** + * @throws NoSuchEntityException + * @throws LocalizedException + */ public function beforeDelete( - \Magento\Vault\Api\PaymentTokenRepositoryInterface $subject, + PaymentTokenRepositoryInterface $subject, PaymentTokenInterface $paymentToken ) { $paymentMethodCode = $paymentToken->getPaymentMethodCode(); + $storeId = $this->storeManager->getStore()->getStoreId(); if (is_null($paymentMethodCode) || strpos($paymentMethodCode, 'adyen_') !== 0) { return [$paymentToken]; } + try { $this->paymentRequest->disableRecurringContract( $paymentToken->getGatewayToken(), $paymentToken->getCustomerId(), - $this->storeManager->getStore()->getStoreId() + $storeId ); } catch (\Exception $e) { - throw new \Magento\Framework\Exception\LocalizedException(__('Failed to disable this contract')); + throw new LocalizedException(__('Failed to disable this contract')); } } } diff --git a/Test/Unit/Helper/RequestsTest.php b/Test/Unit/Helper/RequestsTest.php index c443febd9..4d69eee61 100644 --- a/Test/Unit/Helper/RequestsTest.php +++ b/Test/Unit/Helper/RequestsTest.php @@ -38,7 +38,7 @@ public function testBuildCardRecurringStorePaymentMethodFalse() public function testBuildCardRecurringStorePaymentMethodTrueVault() { - $this->setMockObjects(['storePaymentMethod' => true], true, ''); + $this->setMockObjects(['storePaymentMethod' => true], true, Recurring::SUBSCRIPTION); $request = $this->sut->buildCardRecurringData(1, $this->paymentMock); $this->assertTrue($request['storePaymentMethod']); @@ -63,7 +63,7 @@ public function testBuildCardRecurringStorePaymentMethodTrueAdyenSubscription() $this->assertEquals(Recurring::SUBSCRIPTION, $request['recurringProcessingModel']); } - private function setMockObjects(array $stateDataArray, bool $vaultEnabled, string $adyenTokenType): void + private function setMockObjects(array $stateDataArray, bool $vaultEnabled, string $tokenType): void { $stateDataMock = $this->createConfiguredMock(StateData::class, [ 'getStateData' => $stateDataArray @@ -75,7 +75,7 @@ private function setMockObjects(array $stateDataArray, bool $vaultEnabled, strin $configHelperMock = $this->createConfiguredMock(Config::class, [ - 'getCardRecurringType' => $adyenTokenType + 'getCardRecurringType' => $tokenType ]); $this->sut = new Requests( diff --git a/etc/adminhtml/system/adyen_hpp.xml b/etc/adminhtml/system/adyen_hpp.xml index 21767b61f..ad0c1b711 100755 --- a/etc/adminhtml/system/adyen_hpp.xml +++ b/etc/adminhtml/system/adyen_hpp.xml @@ -33,13 +33,24 @@ Magento\Config\Model\Config\Source\Yesno - Alternative payment methods will only be shown during checkout if Magento Vault is not being used. Currently only SEPA tokenization is supported. + Alternative payment methods will only be shown during checkout if Magento Vault is enabled. If this is option enabled, all Apple pay, Google pay, SEPA and PayPal payment methods will be tokenized. payment/adyen_hpp_vault/active 1 - + + + payment/adyen_hpp/tokenized_payment_methods + Adyen\Payment\Model\Config\Source\TokenizedPaymentMethods + Selected payment methods will be tokenized. Please make sure to enable these payment methods in your Adyen Customer Area. + + + 1 + 1 + + + Adyen\Payment\Model\Config\Source\Recurring\RecurringType payment/adyen_hpp/token_type diff --git a/etc/adminhtml/system/adyen_oneclick.xml b/etc/adminhtml/system/adyen_oneclick.xml index 0bb7a080e..4e203897e 100755 --- a/etc/adminhtml/system/adyen_oneclick.xml +++ b/etc/adminhtml/system/adyen_oneclick.xml @@ -26,9 +26,9 @@ - - Choose between using Magento Vault (one-click CVC-less payments) or the more secure Adyen Tokenization (one-click CVC required payments) functionality - + + Magento Vault (one-click CVC-less payments) or secure Adyen Tokenization (one-click CVC required payments) + Adyen\Payment\Model\Config\Source\Recurring\RecurringMode payment/adyen_oneclick/card_mode @@ -38,16 +38,12 @@ - Choose between CardOnFile (one-click payments) or Subscription (fixed schedule payments) + Choose between CardOnFile (one-click), UnscheduledCardOnFile (non-fixed schedule MIT payments) or Subscription (fixed schedule MIT payments) Adyen\Payment\Model\Config\Source\Recurring\RecurringType payment/adyen_oneclick/card_type More information]]> - - 1 - Adyen Tokenization - diff --git a/etc/config.xml b/etc/config.xml index 095e6bbad..5e44e6c1f 100755 --- a/etc/config.xml +++ b/etc/config.xml @@ -104,8 +104,18 @@ 1 1 1 + 1 + 1 adyen + + AdyenPaymentHppVaultFacade + Stored Payment Methods (Adyen) + + Adyen\Payment\Model\InstantPurchase\PaymentMethods\AvailabilityChecker + Adyen\Payment\Model\InstantPurchase\PaymentMethods\TokenFormatter + + 0 AdyenPaymentPosCloudFacade diff --git a/etc/di.xml b/etc/di.xml index 3af5ff21a..3dc57b297 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -52,6 +52,15 @@ + + + Adyen\Payment\Model\Ui\AdyenHppConfigProvider::HPP_VAULT_CODE + AdyenPaymentHppVaultConfig + AdyenPaymentHppVaultPaymentValueHandlerPool + AdyenPaymentHppFacade + + + Adyen\Payment\Model\Ui\AdyenOneclickConfigProvider::CODE @@ -116,6 +125,18 @@ + + + AdyenPaymentHppVaultConfig + + + + + + AdyenPaymentHppVaultPaymentValueHandler + + + @@ -242,6 +263,12 @@ + + + Adyen\Payment\Model\Ui\AdyenHppConfigProvider::HPP_VAULT_CODE + + + Adyen\Payment\Model\Ui\AdyenOneclickConfigProvider::CODE @@ -301,10 +328,16 @@ AdyenPaymentCcCommandPool + + + AdyenPaymentHppCommandPool + + AdyenPaymentCcCommandManager + AdyenPaymentHppCommandManager @@ -325,10 +358,10 @@ AdyenPaymentCcVaultAuthorizeRequest Adyen\Payment\Gateway\Http\TransferFactory - Adyen\Payment\Gateway\Http\Client\TransactionAuthorization + Adyen\Payment\Gateway\Http\Client\TransactionPayment CheckoutResponseValidator - AdyenPaymentCcVaultResponseHandlerComposite + AdyenPaymentVaultResponseHandlerComposite @@ -348,10 +381,22 @@ AdyenPaymentCancelCommand AdyenPaymentRefundCommand AdyenPaymentCancelCommand + AdyenPaymentHppVaultAuthorizeCommand + AdyenPaymentHppVaultAuthorizeCommand + + + AdyenPaymentHppVaultAuthorizeRequest + Adyen\Payment\Gateway\Http\TransferFactory + Adyen\Payment\Gateway\Http\Client\TransactionPayment + CheckoutResponseValidator + AdyenPaymentVaultResponseHandlerComposite + + + @@ -605,7 +650,29 @@ - + + + + Adyen\Payment\Gateway\Request\MerchantAccountDataBuilder + + Adyen\Payment\Gateway\Request\CustomerDataBuilder + Adyen\Payment\Gateway\Request\CustomerIpDataBuilder + Adyen\Payment\Gateway\Request\AddressDataBuilder + Adyen\Payment\Gateway\Request\PaymentDataBuilder + Adyen\Payment\Gateway\Request\RiskDataBuilder + Adyen\Payment\Gateway\Request\AdditionalDataLevel23DataBuilder + Adyen\Payment\Gateway\Request\ReturnUrlDataBuilder + Adyen\Payment\Gateway\Request\ChannelDataBuilder + Adyen\Payment\Gateway\Request\OriginDataBuilder + Adyen\Payment\Gateway\Request\BrowserInfoDataBuilder + Adyen\Payment\Gateway\Request\RecurringVaultDataBuilder + Adyen\Payment\Gateway\Request\ShopperInteractionDataBuilder + + + + + @@ -1092,6 +1159,9 @@ + + + diff --git a/etc/events.xml b/etc/events.xml index 9251cc98d..fb86338cd 100644 --- a/etc/events.xml +++ b/etc/events.xml @@ -44,4 +44,7 @@ + + + diff --git a/etc/frontend/di.xml b/etc/frontend/di.xml index 519923edf..cd105d18c 100755 --- a/etc/frontend/di.xml +++ b/etc/frontend/di.xml @@ -28,6 +28,7 @@ Adyen\Payment\Model\Ui\TokenUiComponentProvider + Adyen\Payment\Model\Ui\PaymentMethodUiComponentProvider diff --git a/view/frontend/layout/vault_cards_listaction.xml b/view/frontend/layout/vault_cards_listaction.xml index ff85c8a35..d31d58319 100644 --- a/view/frontend/layout/vault_cards_listaction.xml +++ b/view/frontend/layout/vault_cards_listaction.xml @@ -15,8 +15,10 @@ - + + + + diff --git a/view/frontend/templates/customer_account/payment_method.phtml b/view/frontend/templates/customer_account/payment_method.phtml new file mode 100644 index 000000000..c83c17d44 --- /dev/null +++ b/view/frontend/templates/customer_account/payment_method.phtml @@ -0,0 +1,37 @@ + + + + + + +
+ getBlockHtml('formkey') ?> + + +
+ + diff --git a/view/frontend/web/js/view/payment/method-renderer/adyen-vault-method.js b/view/frontend/web/js/view/payment/method-renderer/adyen-vault-method.js new file mode 100644 index 000000000..7353aff60 --- /dev/null +++ b/view/frontend/web/js/view/payment/method-renderer/adyen-vault-method.js @@ -0,0 +1,66 @@ +/** + * + * Adyen Payment module (https://www.adyen.com/) + * + * Copyright (c) 2022 Adyen NV (https://www.adyen.com/) + * See LICENSE.txt for license details. + * + * Author: Adyen + */ +/*browser:true*/ +/*global define*/ +define([ + 'jquery', + 'Magento_Vault/js/view/payment/method-renderer/vault' +], function ($, VaultComponent) { + 'use strict'; + + return VaultComponent.extend({ + defaults: { + template: 'Adyen_Payment/payment/payment-method-vault' + }, + /** + * Check if token should be displayed + * @returns {boolean} + */ + displayToken: function() { + return this.details.displayToken; + }, + /** + * Get tx_variant + * @returns {String} + */ + getPaymentMethodType: function () { + return this.details.type; + }, + + /** + * Get payment method name + * @returns {String} + */ + getLabel: function () { + return this.details.label; + }, + + /** + * Get expiration date + * @returns {String} + */ + getCreatedDate: function () { + return this.details.created; + }, + /** + * @returns {String} + */ + getToken: function () { + return this.publicHash; + }, + /** + * @param {String} type + * @returns {Boolean} + */ + getIcons: function (type) { + return this.details.icon; + } + }); +}); diff --git a/view/frontend/web/js/view/payment/method-renderer/payment_method_vault.js b/view/frontend/web/js/view/payment/method-renderer/payment_method_vault.js new file mode 100644 index 000000000..e784fb427 --- /dev/null +++ b/view/frontend/web/js/view/payment/method-renderer/payment_method_vault.js @@ -0,0 +1,66 @@ +/** + * + * Adyen Payment module (https://www.adyen.com/) + * + * Copyright (c) 2019 Adyen BV (https://www.adyen.com/) + * See LICENSE.txt for license details. + * + * Author: Adyen + */ +/*browser:true*/ +/*global define*/ +define([ + 'jquery', + 'Magento_Vault/js/view/payment/method-renderer/vault' +], function ($, VaultComponent) { + 'use strict'; + + return VaultComponent.extend({ + defaults: { + template: 'Adyen_Payment/payment/payment-method-vault-form' + }, + /** + * Check if token should be displayed + * @returns {boolean} + */ + displayToken: function() { + return this.details.displayToken; + }, + /** + * Get tx_variant + * @returns {String} + */ + getPaymentMethodType: function () { + return this.details.type; + }, + + /** + * Get payment method name + * @returns {String} + */ + getLabel: function () { + return this.details.label; + }, + + /** + * Get expiration date + * @returns {String} + */ + getCreatedDate: function () { + return this.details.created; + }, + /** + * @returns {String} + */ + getToken: function () { + return this.publicHash; + }, + /** + * @param {String} type + * @returns {Boolean} + */ + getIcons: function (type) { + return this.details.icon; + } + }); +}); diff --git a/view/frontend/web/template/payment/payment-method-vault-form.html b/view/frontend/web/template/payment/payment-method-vault-form.html new file mode 100644 index 000000000..f14f9f03d --- /dev/null +++ b/view/frontend/web/template/payment/payment-method-vault-form.html @@ -0,0 +1,45 @@ + +
+
+ + +
+ +
+
+
+ +
+
+
+
diff --git a/view/frontend/web/template/payment/payment-method-vault.html b/view/frontend/web/template/payment/payment-method-vault.html new file mode 100644 index 000000000..6dda8fa5e --- /dev/null +++ b/view/frontend/web/template/payment/payment-method-vault.html @@ -0,0 +1,39 @@ +
+
+ + +
+ +
+
+
+ +
+
+
+
From 67b11c448594bfaec75318a8c81952371106f336 Mon Sep 17 00:00:00 2001 From: Rok Popov Ledinski Date: Wed, 5 Oct 2022 16:13:07 +0200 Subject: [PATCH 13/20] [PW-7268] Change the way we are comparing payment amounts in handleCaptureWebhook() (#1751) * use formatAmount() on $order and change comparison for $isFullAmountCaptured * remove code smell --- Helper/Invoice.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Helper/Invoice.php b/Helper/Invoice.php index b0db9d3cd..7870f78b3 100644 --- a/Helper/Invoice.php +++ b/Helper/Invoice.php @@ -280,10 +280,12 @@ public function handleCaptureWebhook(Order $order, Notification $notification): { $invoiceFactory = $this->adyenInvoiceFactory->create(); $adyenInvoice = $this->adyenInvoiceResourceModel->getAdyenInvoiceByCaptureWebhook($order, $notification); - $isFullAmountCaptured = $this->adyenDataHelper->originalAmount( - $notification->getAmountValue(), - $notification->getAmountCurrency() - ) === floatval($order->getBaseGrandTotal()); + $formattedAdyenOrderAmount = $this->adyenDataHelper->formatAmount( + $order->getBaseGrandTotal(), + $order->getOrderCurrencyCode() + ); + $notificationAmount = $notification->getAmountValue(); + $isFullAmountCaptured = $formattedAdyenOrderAmount == $notificationAmount; if (is_null($adyenInvoice) && $order->canInvoice()) { if ($isFullAmountCaptured) { From 13f94dde9a5d5cd1329d7d38826eaee525d76c38 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Wed, 5 Oct 2022 17:16:37 +0200 Subject: [PATCH 14/20] [PW-7214] - Implement combo card funding source selection for POS payments in the frontend (#1749) * [PW-7214] - Implement combo card funding source selection for POS payments in the frontend * [PW-7214] - Code formatting * [PW-7234] - CS code formatting Co-authored-by: Jeantwan Teuma --- .../Http/Client/TransactionPosCloudSync.php | 51 ++++++++++++------ Gateway/Request/PosCloudBuilder.php | 6 ++- Helper/PaymentMethods.php | 3 ++ Model/Ui/AdyenPosCloudConfigProvider.php | 12 +++++ Observer/AdyenPosCloudDataAssignObserver.php | 4 +- view/frontend/web/css/styles.css | 6 +++ .../method-renderer/adyen-pos-cloud-method.js | 53 +++++++++++++++---- .../web/template/payment/pos-cloud-form.html | 19 +++++++ 8 files changed, 123 insertions(+), 31 deletions(-) diff --git a/Gateway/Http/Client/TransactionPosCloudSync.php b/Gateway/Http/Client/TransactionPosCloudSync.php index 4854da9b2..a7e56c5a2 100644 --- a/Gateway/Http/Client/TransactionPosCloudSync.php +++ b/Gateway/Http/Client/TransactionPosCloudSync.php @@ -15,6 +15,7 @@ use Adyen\AdyenException; use Adyen\Payment\Helper\ChargedCurrency; use Adyen\Payment\Helper\Data; +use Adyen\Payment\Helper\PaymentMethods; use Adyen\Payment\Helper\PointOfSale; use Adyen\Payment\Logger\AdyenLogger; use Adyen\Payment\Model\Ui\AdyenPosCloudConfigProvider; @@ -108,7 +109,11 @@ public function placeRequest(TransferInterface $transferObject): array $terminalId = $request['terminalID']; if (array_key_exists('chainCalls', $request)) { - $quote = $this->initiatePosPayment($terminalId, $request['numberOfInstallments']); + $quote = $this->initiatePosPayment( + $terminalId, + $request['fundingSource'], + $request['numberOfInstallments'] + ); $quoteInfoInstance = $quote->getPayment()->getMethodInstance()->getInfoInstance(); $timeDiff = (int)$statusDate - (int)$quoteInfoInstance->getAdditionalInformation('initiateDate'); $serviceId = $quoteInfoInstance->getAdditionalInformation('serviceID'); @@ -117,7 +122,6 @@ public function placeRequest(TransferInterface $transferObject): array $serviceId = $request['serviceID']; } - $totalTimeout = $this->adyenHelper->getAdyenPosCloudConfigData('total_timeout', $this->storeId); if ($timeDiff > $totalTimeout) { throw new LocalizedException(__("POS connection timed out.")); @@ -180,15 +184,18 @@ public function placeRequest(TransferInterface $transferObject): array * Initiate a POS payment by sending a /sync call to Adyen * * @param string $terminalId + * @param string $fundingSource * @param string|null $numberOfInstallments * @return CartInterface * @throws AdyenException * @throws LocalizedException * @throws NoSuchEntityException */ - public function initiatePosPayment(string $terminalId, ?string $numberOfInstallments): CartInterface - { - + public function initiatePosPayment( + string $terminalId, + string $fundingSource, + ?string $numberOfInstallments + ): CartInterface { // Validate JSON that has just been parsed if it was in a valid format if (json_last_error() !== JSON_ERROR_NONE) { throw new LocalizedException( @@ -248,22 +255,32 @@ public function initiatePosPayment(string $terminalId, ?string $numberOfInstallm ] ]; - if (isset($numberOfInstallments)) { - $request['SaleToPOIRequest']['PaymentRequest']['PaymentData'] = [ - "PaymentType" => "Instalment", - "Instalment" => [ - "InstalmentType" => "EqualInstalments", - "SequenceNumber" => 1, - "Period" => 1, - "PeriodUnit" => "Monthly", - "TotalNbOfPayments" => intval($numberOfInstallments) - ] - ]; + if ($fundingSource === PaymentMethods::FUNDING_SOURCE_CREDIT) { + if (isset($numberOfInstallments)) { + $request['SaleToPOIRequest']['PaymentRequest']['PaymentData'] = [ + "PaymentType" => "Instalment", + "Instalment" => [ + "InstalmentType" => "EqualInstalments", + "SequenceNumber" => 1, + "Period" => 1, + "PeriodUnit" => "Monthly", + "TotalNbOfPayments" => intval($numberOfInstallments) + ] + ]; + } else { + $request['SaleToPOIRequest']['PaymentData'] = [ + 'PaymentType' => $transactionType, + ]; + } $request['SaleToPOIRequest']['PaymentRequest']['PaymentTransaction']['TransactionConditions'] = [ "DebitPreferredFlag" => false ]; - } else { + } elseif ($fundingSource === PaymentMethods::FUNDING_SOURCE_DEBIT) { + $request['SaleToPOIRequest']['PaymentRequest']['PaymentTransaction']['TransactionConditions'] = [ + "DebitPreferredFlag" => true + ]; + $request['SaleToPOIRequest']['PaymentData'] = [ 'PaymentType' => $transactionType, ]; diff --git a/Gateway/Request/PosCloudBuilder.php b/Gateway/Request/PosCloudBuilder.php index fb05e443a..c8f3f3838 100644 --- a/Gateway/Request/PosCloudBuilder.php +++ b/Gateway/Request/PosCloudBuilder.php @@ -35,14 +35,16 @@ public function build(array $buildSubject) $body = [ 'terminalID' => $payment->getAdditionalInformation('terminal_id'), 'numberOfInstallments' => $payment->getAdditionalInformation('number_of_installments'), - 'chainCalls' => $payment->getAdditionalInformation('chain_calls') + 'chainCalls' => $payment->getAdditionalInformation('chain_calls'), + 'fundingSource' => $payment->getAdditionalInformation('funding_source') ]; } else { $body = [ "response" => $payment->getAdditionalInformation("terminalResponse"), "serviceID" => $payment->getAdditionalInformation("serviceID"), "initiateDate" => $payment->getAdditionalInformation("initiateDate"), - "terminalID" => $payment->getAdditionalInformation("terminal_id") + "terminalID" => $payment->getAdditionalInformation("terminal_id"), + 'fundingSource' => $payment->getAdditionalInformation('funding_source') ]; } diff --git a/Helper/PaymentMethods.php b/Helper/PaymentMethods.php index dcf8aa775..4fd3db8bc 100644 --- a/Helper/PaymentMethods.php +++ b/Helper/PaymentMethods.php @@ -53,6 +53,9 @@ class PaymentMethods extends AbstractHelper "scheme" => "card" ]; + const FUNDING_SOURCE_DEBIT = 'debit'; + const FUNDING_SOURCE_CREDIT = 'credit'; + /** * @var CartRepositoryInterface */ diff --git a/Model/Ui/AdyenPosCloudConfigProvider.php b/Model/Ui/AdyenPosCloudConfigProvider.php index 4f0981a1a..47498c8cf 100644 --- a/Model/Ui/AdyenPosCloudConfigProvider.php +++ b/Model/Ui/AdyenPosCloudConfigProvider.php @@ -90,6 +90,7 @@ public function getConfig() if ($this->adyenHelper->getAdyenPosCloudConfigDataFlag("active")) { $config['payment']['adyenPos']['connectedTerminals'] = $this->getConnectedTerminals(); + $config['payment']['adyenPos']['fundingSourceOptions'] = $this->getFundingSourceOptions(); } // has installments by default false @@ -133,4 +134,15 @@ protected function getConnectedTerminals() return []; } + + /** + * @return string[] + */ + protected function getFundingSourceOptions(): array + { + return [ + 'credit' => 'Credit Card', + 'debit' => 'Debit Card' + ]; + } } diff --git a/Observer/AdyenPosCloudDataAssignObserver.php b/Observer/AdyenPosCloudDataAssignObserver.php index 6005d38dc..f86fea0b5 100644 --- a/Observer/AdyenPosCloudDataAssignObserver.php +++ b/Observer/AdyenPosCloudDataAssignObserver.php @@ -20,6 +20,7 @@ class AdyenPosCloudDataAssignObserver extends AbstractDataAssignObserver const TERMINAL_ID = 'terminal_id'; const NUMBER_OF_INSTALLMENTS = 'number_of_installments'; const CHAIN_CALLS = 'chain_calls'; + const FUNDING_SOURCE = 'funding_source'; /** * @var array @@ -27,7 +28,8 @@ class AdyenPosCloudDataAssignObserver extends AbstractDataAssignObserver protected $additionalInformationList = [ self::TERMINAL_ID, self::NUMBER_OF_INSTALLMENTS, - self::CHAIN_CALLS + self::CHAIN_CALLS, + self::FUNDING_SOURCE ]; /** diff --git a/view/frontend/web/css/styles.css b/view/frontend/web/css/styles.css index 84c5489b7..fb329faaa 100644 --- a/view/frontend/web/css/styles.css +++ b/view/frontend/web/css/styles.css @@ -15,3 +15,9 @@ .payment-method+#hpp_actionModalWrapper { border-bottom: 1px solid #ccc; } + +#adyen_pos_cloud_connected_terminals_div, +#adyen_pos_cloud_funding_source_div, +#adyen_pos_cloud_installments_div { + margin-bottom: 15px; +} diff --git a/view/frontend/web/js/view/payment/method-renderer/adyen-pos-cloud-method.js b/view/frontend/web/js/view/payment/method-renderer/adyen-pos-cloud-method.js index 18e4a4122..f90091ac4 100644 --- a/view/frontend/web/js/view/payment/method-renderer/adyen-pos-cloud-method.js +++ b/view/frontend/web/js/view/payment/method-renderer/adyen-pos-cloud-method.js @@ -36,10 +36,12 @@ define( defaults: { template: 'Adyen_Payment/payment/pos-cloud-form' }, + fundingSource: ko.observable('credit'), initObservable: function () { this._super() .observe([ 'terminalId', + 'fundingSource', 'installments', 'installment' ]); @@ -48,15 +50,15 @@ define( }, initialize: function () { this._super(); - var self = this; + let self = this; // installments - var allInstallments = self.getAllInstallments(); - var grandTotal = quote.totals().grand_total; - var precision = quote.getPriceFormat().precision; - var currencyCode = quote.totals().quote_currency_code; + let allInstallments = self.getAllInstallments(); + let grandTotal = quote.totals().grand_total; + let precision = quote.getPriceFormat().precision; + let currencyCode = quote.totals().quote_currency_code; - var numberOfInstallments = installmentsHelper.getInstallmentsWithPrices(allInstallments, grandTotal, precision, currencyCode); + let numberOfInstallments = installmentsHelper.getInstallmentsWithPrices(allInstallments, grandTotal, precision, currencyCode); if (numberOfInstallments) { self.installments(numberOfInstallments); @@ -71,7 +73,7 @@ define( } }, placeOrderPos: function () { - var self = this; + let self = this; return $.when( placeOrderAction(self.getData(), new Messages()) ).fail( @@ -92,10 +94,10 @@ define( ) }, getConnectedTerminals: function () { - var connectedTerminals = []; + let connectedTerminals = []; const connectedTerminalsList = window.checkoutConfig.payment.adyenPos.connectedTerminals; - for (var i = 0; i < connectedTerminalsList.length; i++) { + for (let i = 0; i < connectedTerminalsList.length; i++) { connectedTerminals.push( { key: connectedTerminalsList[i], @@ -106,6 +108,34 @@ define( return connectedTerminals; }, + isFundingSourceAvailable: function () { + if (quote.billingAddress() === null) { + return false; + } + let countryId = quote.billingAddress().countryId; + let currencyCode = quote.totals().quote_currency_code; + let allowedCurrenciesByCountry = { + 'BR': 'BRL', + 'MX': 'MXN', + }; + return allowedCurrenciesByCountry[countryId] && + currencyCode === allowedCurrenciesByCountry[countryId]; + }, + getFundingSourceOptions: function () { + let fundingSource = []; + const fundingSourceOptions = window.checkoutConfig.payment.adyenPos.fundingSourceOptions; + + for (let i = 0; i < Object.values(fundingSourceOptions).length; i++) { + fundingSource.push( + { + value: Object.keys(fundingSourceOptions)[i], + key: Object.values(fundingSourceOptions)[i] + } + ); + } + + return fundingSource; + }, /** * Get data for place order * @returns {{method: *}} @@ -116,12 +146,13 @@ define( additional_data: { 'terminal_id': this.terminalId(), 'number_of_installments': this.installment(), - 'chain_calls': true + 'chain_calls': true, + 'funding_source': this.fundingSource() } }; }, hasInstallments: function () { - return window.checkoutConfig.payment.adyenPos.hasInstallments; + return window.checkoutConfig.payment.adyenPos.hasInstallments && this.fundingSource() === 'credit'; }, getAllInstallments: function () { return window.checkoutConfig.payment.adyenPos.installments; diff --git a/view/frontend/web/template/payment/pos-cloud-form.html b/view/frontend/web/template/payment/pos-cloud-form.html index d4f66b3a0..29bc691d4 100644 --- a/view/frontend/web/template/payment/pos-cloud-form.html +++ b/view/frontend/web/template/payment/pos-cloud-form.html @@ -55,6 +55,25 @@ + +
+ +
+ +
+
+ +
From 0c9358c38ff683a0a1c7d4227c4faa71be41ed45 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Thu, 6 Oct 2022 15:36:05 +0200 Subject: [PATCH 15/20] [PW-7275] - Add the hmac key field to the manual configuration mode (#1755) --- .../system/adyen_required_settings.xml | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/etc/adminhtml/system/adyen_required_settings.xml b/etc/adminhtml/system/adyen_required_settings.xml index c901e7b8e..8c389edb9 100755 --- a/etc/adminhtml/system/adyen_required_settings.xml +++ b/etc/adminhtml/system/adyen_required_settings.xml @@ -149,7 +149,39 @@ Note that if you change your password here, you will also need to update it in the Customer Area - + + + 1 + manual + + Magento\Config\Model\Config\Backend\Encrypted + payment/adyen_abstract/notification_hmac_key_test + + Adyen documentation. + ]]> + + + + + + 0 + manual + + Magento\Config\Model\Config\Backend\Encrypted + payment/adyen_abstract/notification_hmac_key_live + + Adyen documentation. + ]]> + + + @@ -160,7 +192,7 @@ - @@ -171,7 +203,7 @@ Adyen\Payment\Model\Config\Source\ChargedCurrency payment/adyen_abstract/charged_currency - @@ -180,7 +212,7 @@ Magento\Config\Model\Config\Source\Enabledisable payment/adyen_abstract/debug - + auto From 0859ad48e02593536e9ed56ee1c3efc329379348 Mon Sep 17 00:00:00 2001 From: raoulritter <59527829+raoulritter@users.noreply.github.com> Date: Thu, 6 Oct 2022 16:35:04 +0200 Subject: [PATCH 16/20] PW-7278 Fix HPP Naming for Ratepay (#1757) * [PW-7278] Fix Ratepay Name in Admin HTML --- .github/docker-compose.e2e.yml | 2 +- etc/adminhtml/system/adyen_hpp.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/docker-compose.e2e.yml b/.github/docker-compose.e2e.yml index fb35ff8ba..6088617ef 100644 --- a/.github/docker-compose.e2e.yml +++ b/.github/docker-compose.e2e.yml @@ -17,4 +17,4 @@ services: - PAYPAL_PASSWORD volumes: - ../../scripts/e2e.sh:/e2e.sh - - ../../../test-report:/tmp/test-report \ No newline at end of file + - ../../../test-report:/tmp/test-report diff --git a/etc/adminhtml/system/adyen_hpp.xml b/etc/adminhtml/system/adyen_hpp.xml index ad0c1b711..9a3a08800 100755 --- a/etc/adminhtml/system/adyen_hpp.xml +++ b/etc/adminhtml/system/adyen_hpp.xml @@ -61,7 +61,7 @@ - + Magento\Config\Block\System\Config\Form\Fieldset From 8b899ac015394a441f9148d555df3c5a9081b555 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Thu, 6 Oct 2022 16:48:01 +0200 Subject: [PATCH 17/20] [PW-7270] - Allow auto configuration on test environment without webhook & hmac key (#1756) --- Helper/ManagementHelper.php | 59 +++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/Helper/ManagementHelper.php b/Helper/ManagementHelper.php index aabfd1351..2da7eee9a 100644 --- a/Helper/ManagementHelper.php +++ b/Helper/ManagementHelper.php @@ -18,6 +18,7 @@ use Adyen\AdyenException; use Adyen\Service\Management; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Message\ManagerInterface; use Magento\Store\Model\StoreManager; use Adyen\Payment\Logger\AdyenLogger; use Magento\Framework\Encryption\EncryptorInterface; @@ -49,25 +50,34 @@ class ManagementHelper */ private $adyenLogger; + /** + * @var ManagerInterface + */ + protected $messageManager; + /** * ManagementHelper constructor. * @param StoreManager $storeManager * @param EncryptorInterface $encryptor * @param Data $dataHelper * @param Config $configHelper + * @param AdyenLogger $adyenLogger + * @param ManagerInterface $messageManager */ public function __construct( StoreManager $storeManager, EncryptorInterface $encryptor, Data $dataHelper, Config $configHelper, - AdyenLogger $adyenLogger + AdyenLogger $adyenLogger, + ManagerInterface $messageManager ) { $this->dataHelper = $dataHelper; $this->storeManager = $storeManager; $this->encryptor = $encryptor; $this->configHelper = $configHelper; $this->adyenLogger = $adyenLogger; + $this->messageManager = $messageManager; } /** @@ -165,23 +175,34 @@ public function setupWebhookCredentials( ]; $webhookId = $this->configHelper->getWebhookId($storeId); $savedMerchantAccount = $this->configHelper->getMerchantAccount($storeId); - // reuse saved webhookId if merchant account is the same. - if (!empty($webhookId) && $merchantId === $savedMerchantAccount) { - $management->merchantWebhooks->update($merchantId, $webhookId, $params); - } else { - $params['type'] = 'standard'; - $response = $management->merchantWebhooks->create($merchantId, $params); - // save webhook_id to configuration - $webhookId = $response['id']; - $this->configHelper->setConfigData($webhookId, 'webhook_id', Config::XML_ADYEN_ABSTRACT_PREFIX); - } - // generate hmac key and save - $response = $management->merchantWebhooks->generateHmac($merchantId, $webhookId); - $hmacKey = $response['hmacKey']; - $hmac = $this->encryptor->encrypt($hmacKey); - $mode = $demoMode ? 'test' : 'live'; - $this->configHelper->setConfigData($hmac, 'notification_hmac_key_' . $mode, Config::XML_ADYEN_ABSTRACT_PREFIX); + try { + // reuse saved webhookId if merchant account is the same. + if (!empty($webhookId) && $merchantId === $savedMerchantAccount) { + $management->merchantWebhooks->update($merchantId, $webhookId, $params); + } else { + $params['type'] = 'standard'; + $response = $management->merchantWebhooks->create($merchantId, $params); + // save webhook_id to configuration + $webhookId = $response['id']; + $this->configHelper->setConfigData($webhookId, 'webhook_id', Config::XML_ADYEN_ABSTRACT_PREFIX); + } + + // generate hmac key and save + $response = $management->merchantWebhooks->generateHmac($merchantId, $webhookId); + $hmacKey = $response['hmacKey']; + $hmac = $this->encryptor->encrypt($hmacKey); + $mode = $demoMode ? 'test' : 'live'; + $this->configHelper->setConfigData($hmac, 'notification_hmac_key_' . $mode, Config::XML_ADYEN_ABSTRACT_PREFIX); + } catch (\Exception $exception) { + $this->adyenLogger->error($exception->getMessage()); + + if (!$demoMode) { + throw $exception; + } + + $this->messageManager->addErrorMessage(__("Credentials saved but webhook and HMAC key couldn't be generated! Please check the error logs.")); + } } /** @@ -236,13 +257,13 @@ public function webhookTest(string $merchantId) $client = $this->dataHelper->initializeAdyenClient(); $management = new Management($client); $response = $management->merchantWebhooks->test($merchantId, $webhookId, $params); - $this->adyenLogger->addInfo( + $this->adyenLogger->info( sprintf( 'response from webhook test %s', json_encode($response)) ); return $response; } catch (AdyenException $exception) { - $this->adyenLogger->addError($exception->getMessage()); + $this->adyenLogger->error($exception->getMessage()); return $exception->getMessage(); } } From 6c711f6dbb9abe0a6aad77bf934fbcf3ad2467ac Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Thu, 6 Oct 2022 16:58:05 +0200 Subject: [PATCH 18/20] Version bump --- composer.json | 2 +- etc/module.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 1396b3b8e..44ff4ffef 100755 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "adyen/module-payment", "description": "Official Magento2 Plugin to connect to Payment Service Provider Adyen.", "type": "magento2-module", - "version": "8.6.0", + "version": "8.7.0", "license": "MIT", "repositories": [ { diff --git a/etc/module.xml b/etc/module.xml index 647a954fa..b823e747d 100755 --- a/etc/module.xml +++ b/etc/module.xml @@ -12,7 +12,7 @@ --> - + From 1daaab0251f6b0f7906f11b7fb3f07d4de044df6 Mon Sep 17 00:00:00 2001 From: Can Demiralp Date: Thu, 6 Oct 2022 17:03:25 +0200 Subject: [PATCH 19/20] [PW-7234] - Update regular expression for address parsing (#1753) * [PW-7234] - Update regular expression * [PW-7234] - Update regex condition * [PW-7234] - Update regex * [PW-7234] - Update regex * [PW-7234] - Code formatting * [PW-7234] - Code formatting * [PW-7234] - Update regex Co-authored-by: Alexandros Moraitis --- Helper/Address.php | 4 ++-- Test/Unit/Helper/AddressTest.php | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Helper/Address.php b/Helper/Address.php index a0a1f5a93..2b15160f4 100644 --- a/Helper/Address.php +++ b/Helper/Address.php @@ -33,8 +33,8 @@ public function __construct(AdyenLogger $logger) } // Regex to extract the house number from the street line if needed (e.g. 'Street address 1 A' => '1 A') - const STREET_FIRST_REGEX = "/(?[[:alnum:].'\- ]+)\s+(?\d{1,10}((\s)?\w{1,3})?)$/u"; - const NUMBER_FIRST_REGEX = "/^(?\d{1,10}((\s)?\w{1,3})?)\s+(?[[:alnum:].'\- ]+)/u"; + const STREET_FIRST_REGEX = "/(?[[:alnum:].'\-\s]+[\p{L}.']+)\s+(?[\d\s\-\/,.]+\w{1,3})$/u"; + const NUMBER_FIRST_REGEX = "/^(?[\d\s\-\/,.]+\w{1,3})\s+(?[[:alnum:].'\-\s]+[\p{L}.']+)/u"; /** * @param AddressAdapterInterface $address diff --git a/Test/Unit/Helper/AddressTest.php b/Test/Unit/Helper/AddressTest.php index 30381b804..0ea3c839f 100755 --- a/Test/Unit/Helper/AddressTest.php +++ b/Test/Unit/Helper/AddressTest.php @@ -22,10 +22,23 @@ class AddressTest extends AbstractAdyenTestCase const HOUSE_NUMBER = '123'; const HOUSE_NUMBER_LETTER = '456B'; const HOUSE_NUMBER_SPACE_LETTER = '789 C'; - const HOUSE_NUMBERS = [self::HOUSE_NUMBER, self::HOUSE_NUMBER_LETTER, self::HOUSE_NUMBER_SPACE_LETTER]; + const HOUSE_NUMBER_SEPARATOR_LETTER = '103, 45/47 BG'; + const HOUSE_NUMBER_RANGE = '45-53'; + const HOUSE_NUMBERS = [ + self::HOUSE_NUMBER, + self::HOUSE_NUMBER_LETTER, + self::HOUSE_NUMBER_SPACE_LETTER, + self::HOUSE_NUMBER_SEPARATOR_LETTER, + self::HOUSE_NUMBER_RANGE + ]; const STREET_NAME_SPECIAL_CHARS = "Wróblewskiego"; const STREET_NAME = "John-Paul's Ave."; - const STREET_NAMES = [self::STREET_NAME_SPECIAL_CHARS,self::STREET_NAME]; + const STREET_NAME_WITH_NUMBER = "Simon 2e Carmiggeltstraat"; + const STREET_NAMES = [ + self::STREET_NAME_SPECIAL_CHARS, + self::STREET_NAME, + self::STREET_NAME_WITH_NUMBER + ]; /** @@ -182,7 +195,7 @@ public static function addressConfigProvider(): array 'name' => $street_name, 'house_number' => $house_number ] - ], + ] ]); } } From c133886c01d6d414d5df9000bcab2223b832c311 Mon Sep 17 00:00:00 2001 From: Jeantwan Teuma Date: Fri, 7 Oct 2022 10:07:46 +0200 Subject: [PATCH 20/20] PW-7294 - Get brand only under the HPP scenario (#1758) --- Gateway/Request/RecurringDataBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gateway/Request/RecurringDataBuilder.php b/Gateway/Request/RecurringDataBuilder.php index 0b3096bae..2ef1ce7cc 100644 --- a/Gateway/Request/RecurringDataBuilder.php +++ b/Gateway/Request/RecurringDataBuilder.php @@ -58,10 +58,10 @@ public function build(array $buildSubject): array $order = $payment->getOrder(); $storeId = $order->getStoreId(); $method = $payment->getMethod(); - $brand = $this->stateData->getPaymentMethodVariant($order->getQuoteId()); if ($method === PaymentMethods::ADYEN_CC) { $body = $this->adyenRequestsHelper->buildCardRecurringData($storeId, $payment); } elseif ($method === PaymentMethods::ADYEN_HPP) { + $brand = $this->stateData->getPaymentMethodVariant($order->getQuoteId()); $body = $this->vaultHelper->buildPaymentMethodRecurringData($storeId, $brand); } elseif ($method === PaymentMethods::ADYEN_ONE_CLICK) { $body = $this->adyenRequestsHelper->buildAdyenTokenizedPaymentRecurringData($storeId, $payment);