diff --git a/CHANGELOG.md b/CHANGELOG.md index aede09e..246f994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,206 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## v1.2.0-alpha +## v1.2.1 Added -- Mollie custom application initialization +- New custom field for transaction: `sctm_transaction_refund_for_mollie_payment` which would store the Mollie Payment ID that need to be refunded + +Fixes + +[Create Refund](./docs/CreateRefund.md) +- Handling the Refund Creation for the case that the Payment has more than one Success Charge transaction + - Changing the way to determine the Create Refund action: + - Before + ```Typescript + // processor/src/utils/paymentAction.utils.ts + + if (groups.successCharge.length === 1 && groups.initialRefund.length) { + return ConnectorActions.CreateRefund; + } + ``` + + - After + ```Typescript + // processor/src/utils/paymentAction.utils.ts + + if (groups.successCharge.length >= 1 && groups.initialRefund.length) { + return ConnectorActions.CreateRefund; + } + ``` + + - We are supporting to create the refund for the payment which has more than one Success Charge transactions + - By default, we will create the Refund for the latest Success Charge transaction. For example: + ```Typescript + // CommerceTools Payment + { + id: 'payment-id', + transactions: [ + { + type: 'Charge', + state: 'Success', + interactionId: 'tr_123456' // Mollie Payment ID + }, + { + type: 'Charge', + state: 'Success', + interactionId: 'tr_999999' // Mollie Payment ID + }, + { + type: 'Refund', + state: 'Initial', // Creating a Refund for the Mollie Payment tr_999999 + }, + ] + } + ``` + + - However, you can also specify the Mollie Payment ID (which stored in the `interactionId` of the Success Charge transaction) that you want to create a refund for by adding the Mollie Payment ID to the custom field `sctm_transaction_refund_for_mollie_payment` of the Initial Refund transaction. For example: + + ```Typescript + // CommerceTools Payment + { + id: 'payment-id', + transactions: [ + { + type: 'Charge', + state: 'Success', + interactionId: 'tr_123456' // Mollie Payment ID + }, + { + type: 'Charge', + state: 'Success', + interactionId: 'tr_999999' // Mollie Payment ID + }, + { + type: 'Refund', + state: 'Initial', + custom: { + type: { + ... + }, + fields: { + sctm_transaction_refund_for_mollie_payment: 'tr_123456' // Creating a Refund for the Mollie Payment tr_123456 + } + } + }, + ] + } + ``` + +[Cancel Refund](./docs/CancelPaymentRefund.md) +- Following the changes for creating refund, we also updated the handler for Refund Cancellation to match with the above changes + - Changing the way to determine the Cancel Refund action: + - Before + ```Typescript + // processor/src/utils/paymentAction.utils.ts + + if ( + groups.successCharge.length === 1 && + groups.pendingRefund.length === 1 && + groups.initialCancelAuthorization.length === 1 + ) { + return ConnectorActions.CancelRefund; + } + ``` + + - After + ```Typescript + // processor/src/utils/paymentAction.utils.ts + + if ( + groups.successCharge.length >= 1 && + groups.pendingRefund.length >= 1 && + groups.initialCancelAuthorization.length === 1 + ) { + return ConnectorActions.CancelRefund; + } + ``` + + - To support the old versions, we will create the cancellation for the latest Pending Refund transaction (which is a pending refund for the latest Success Charge transaction in that payment). For example: + ```Typescript + // CommerceTools Payment + { + id: 'payment-id', + transactions: [ + { + type: 'Charge', + state: 'Success', + interactionId: 'tr_123456' // Mollie Payment ID + }, + { + type: 'Charge', + state: 'Success', + interactionId: 'tr_999999' // Mollie Payment ID + }, + { + id: 'refund-transaction-1', + type: 'Refund', + state: 'Pending', + interactionId: 're_123456', // Mollie Refund ID + }, + { + id: 'refund-transaction-2', + type: 'Refund', + state: 'Pending', + interactionId: 're_999999', // Mollie Refund ID + }, + { + type: 'CancelAuthorization', + state: 'Initial' + // interactionId is not set + } + ] + } + + // In this case, this will be considered as a Cancellation request for the Pending Refund with id: refund-transaction-2 + ``` + __*Note:* The above solution is just for supporting the old versions and will be remove in the near future (in next versions). From this version, please follow the below solution.__ + + - However, to do it in a correct way, from this version, you should specify the Mollie Refund ID (which stored in the `interactionId` of the Pending Refund transaction) that you want to cancel by putting it in the `interactionId` of the Initial CancelAuthorization. For example: + ```Typescript + // CommerceTools Payment + { + id: 'payment-id', + transactions: [ + { + type: 'Charge', + state: 'Success', + interactionId: 'tr_123456' // Mollie Payment ID + }, + { + type: 'Charge', + state: 'Success', + interactionId: 'tr_999999' // Mollie Payment ID + }, + { + id: 'refund-transaction-1', + type: 'Refund', + state: 'Pending', + interactionId: 're_123456', // Mollie Refund ID + }, + { + id: 'refund-transaction-2', + type: 'Refund', + state: 'Pending', + interactionId: 're_999999', // Mollie Refund ID + }, + { + type: 'CancelAuthorization', + state: 'Initial', + interactionId: 're_123456' // Mollie Refund ID that you want to cancel + } + ] + } + + // In this case, this will be considered as a Cancellation request for the Pending Refund with id: refund-transaction-1 + ``` + +## v1.2.0 + +Added + +- Mollie custom application Updated @@ -59,6 +254,13 @@ Added - DockerImage for self hosting on AWS - Installation endpoint for required configurations +## v1.0.4 + +Added + +- Add configuration to enable authorization mode +- OAuth middleware for securing connector endpoint + ## v1.0.3 Added diff --git a/application/.env b/application/.env index f876afd..68bdf32 100644 --- a/application/.env +++ b/application/.env @@ -1,2 +1,2 @@ ENABLE_NEW_JSX_TRANSFORM="true" -FAST_REFRESH="true" \ No newline at end of file +FAST_REFRESH="true" diff --git a/application/.env.dev b/application/.env.dev index 62f04ed..a1bbb22 100644 --- a/application/.env.dev +++ b/application/.env.dev @@ -2,7 +2,7 @@ ENABLE_NEW_JSX_TRANSFORM="true" FAST_REFRESH="true" ENTRY_POINT_URI_PATH="mollie" -PROJECT_KEY="shopm-adv-dev" +PROJECT_KEY="your-project-key" CLOUD_IDENTIFIER="gcp-eu" CUSTOM_APPLICATION_ID="app-id" -APPLICATION_URL="http://localhost:3001" \ No newline at end of file +APPLICATION_URL="http://localhost:3001" diff --git a/application/.gitignore b/application/.gitignore index 25c7165..9250924 100644 --- a/application/.gitignore +++ b/application/.gitignore @@ -6,4 +6,4 @@ dist # Cypress cypress/.env cypress/screenshots/** -cypress/videos/** \ No newline at end of file +cypress/videos/** diff --git a/application/cypress/.env.example b/application/cypress/.env.example index 1e18dc4..748dedb 100644 --- a/application/cypress/.env.example +++ b/application/cypress/.env.example @@ -3,4 +3,4 @@ CYPRESS_LOGIN_PASSWORD= CYPRESS_PROJECT_KEY= CYPRESS_PACKAGE_NAME="application" CYPRESS_BASE_URL="https://mc.europe-west1.gcp.commercetools.com/" -CYPRESS_LOCALE="en-GB" \ No newline at end of file +CYPRESS_LOCALE="en-GB" diff --git a/application/cypress/fixtures/fetch-project.json b/application/cypress/fixtures/fetch-project.json index c0401be..9807ab7 100644 --- a/application/cypress/fixtures/fetch-project.json +++ b/application/cypress/fixtures/fetch-project.json @@ -1,400 +1,419 @@ { "data": { "project": { - "key": "shopm-adv-windev", + "key": "shopm-adv-dev", "version": 16, - "name": "shopm-adv-windev", - "countries": ["GB", "DE", "US", "IT", "AT", "PL"], - "currencies": ["EUR", "GBP", "USD", "PLN"], - "languages": ["en-GB", "de-DE", "en-US", "de-AT", "it-IT", "pl-PL"], + "name": "shopm-adv-dev", + "countries": [ + "GB", + "DE", + "US", + "IT", + "AT", + "PL" + ], + "currencies": [ + "EUR", + "GBP", + "USD", + "PLN" + ], + "languages": [ + "en-GB", + "de-DE", + "en-US", + "de-AT", + "it-IT", + "pl-PL" + ], "initialized": true, "expiry": { - "isActive": false, - "daysLeft": 29, - "__typename": "ProjectExpiry" + "isActive": false, + "daysLeft": 29, + "__typename": "ProjectExpiry" }, "suspension": { - "isActive": false, - "reason": null, - "__typename": "ProjectSuspension" + "isActive": false, + "reason": null, + "__typename": "ProjectSuspension" }, "isProductionProject": false, "allAppliedPermissions": [ - { - "name": "canViewMollie", - "value": true, - "__typename": "AppliedPermission" - }, - { - "name": "canManageMollie", - "value": true, - "__typename": "AppliedPermission" - } - ], - "allAppliedActionRights": [ - { - "group": "products", - "name": "canAddPrices", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "products", - "name": "canAddProducts", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "products", - "name": "canDeletePrices", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "products", - "name": "canDeleteProducts", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "products", - "name": "canEditPrices", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "products", - "name": "canPublishProducts", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "products", - "name": "canUnpublishProducts", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "products", - "name": "canEditAttributes:all", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "orders", - "name": "canAddOrders", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "orders", - "name": "canAddDiscountCodes", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "orders", - "name": "canCreateReturns", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "quotes", - "name": "canSendQuote", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "quotes", - "name": "canCreateDraftQuote", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "standalonePrices", - "name": "canAddPrices", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "standalonePrices", - "name": "canDeletePrices", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "standalonePrices", - "name": "canEditPrices", - "value": false, - "__typename": "AppliedActionRight" - } - ], - "allAppliedDataFences": [], - "allPermissionsForAllApplications": { - "allAppliedPermissions": [ { - "name": "canViewMollie", - "value": true, - "__typename": "AppliedPermission" + "name": "canViewMollie", + "value": true, + "__typename": "AppliedPermission" }, { - "name": "canManageMollie", - "value": true, - "__typename": "AppliedPermission" + "name": "canManageMollie", + "value": true, + "__typename": "AppliedPermission" } - ], - "allAppliedActionRights": [ - { - "group": "products", - "name": "canAddPrices", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "products", - "name": "canAddProducts", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "products", - "name": "canDeletePrices", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "products", - "name": "canDeleteProducts", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "products", - "name": "canEditPrices", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "products", - "name": "canPublishProducts", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "products", - "name": "canUnpublishProducts", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "products", - "name": "canEditAttributes:all", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "orders", - "name": "canAddOrders", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "orders", - "name": "canAddDiscountCodes", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "orders", - "name": "canCreateReturns", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "quotes", - "name": "canSendQuote", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "quotes", - "name": "canCreateDraftQuote", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "standalonePrices", - "name": "canAddPrices", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "standalonePrices", - "name": "canDeletePrices", - "value": false, - "__typename": "AppliedActionRight" - }, - { - "group": "standalonePrices", - "name": "canEditPrices", - "value": false, - "__typename": "AppliedActionRight" - } - ], - "allAppliedMenuVisibilities": [ - { - "name": "hideDashboard", - "value": false, - "__typename": "AppliedMenuVisibilities" - }, - { - "name": "hideProductsList", - "value": false, - "__typename": "AppliedMenuVisibilities" - }, - { - "name": "hideModifiedProducts", - "value": false, - "__typename": "AppliedMenuVisibilities" - }, - { - "name": "hideAddProduct", - "value": false, - "__typename": "AppliedMenuVisibilities" - }, - { - "name": "hideProductSelectionsList", - "value": false, - "__typename": "AppliedMenuVisibilities" - }, - { - "name": "hideAddProductSelection", - "value": false, - "__typename": "AppliedMenuVisibilities" - }, - { - "name": "hideDirectAccess", - "value": false, - "__typename": "AppliedMenuVisibilities" - }, - { - "name": "hideCategoriesList", - "value": false, - "__typename": "AppliedMenuVisibilities" - }, - { - "name": "hideCategoriesSearch", - "value": false, - "__typename": "AppliedMenuVisibilities" - }, - { - "name": "hideAddCategory", - "value": false, - "__typename": "AppliedMenuVisibilities" - }, - { - "name": "hideCustomersList", - "value": false, - "__typename": "AppliedMenuVisibilities" - }, - { - "name": "hideAddCustomer", - "value": false, - "__typename": "AppliedMenuVisibilities" - }, - { - "name": "hideCustomerGroupsList", - "value": false, - "__typename": "AppliedMenuVisibilities" - }, + ], + "allAppliedActionRights": [ { - "name": "hideAddCustomerGroup", - "value": false, - "__typename": "AppliedMenuVisibilities" + "group": "products", + "name": "canAddPrices", + "value": false, + "__typename": "AppliedActionRight" }, { - "name": "hideBusinessUnitsList", - "value": false, - "__typename": "AppliedMenuVisibilities" + "group": "products", + "name": "canAddProducts", + "value": false, + "__typename": "AppliedActionRight" }, { - "name": "hideAddBusinessUnit", - "value": false, - "__typename": "AppliedMenuVisibilities" + "group": "products", + "name": "canDeletePrices", + "value": false, + "__typename": "AppliedActionRight" }, { - "name": "hideOrdersList", - "value": false, - "__typename": "AppliedMenuVisibilities" + "group": "products", + "name": "canDeleteProducts", + "value": false, + "__typename": "AppliedActionRight" }, { - "name": "hideAddOrder", - "value": false, - "__typename": "AppliedMenuVisibilities" + "group": "products", + "name": "canEditPrices", + "value": false, + "__typename": "AppliedActionRight" }, { - "name": "hideQuotes", - "value": false, - "__typename": "AppliedMenuVisibilities" + "group": "products", + "name": "canPublishProducts", + "value": false, + "__typename": "AppliedActionRight" }, { - "name": "hideProductDiscountsList", - "value": false, - "__typename": "AppliedMenuVisibilities" + "group": "products", + "name": "canUnpublishProducts", + "value": false, + "__typename": "AppliedActionRight" }, { - "name": "hideCartDiscountsList", - "value": false, - "__typename": "AppliedMenuVisibilities" + "group": "products", + "name": "canEditAttributes:all", + "value": false, + "__typename": "AppliedActionRight" }, { - "name": "hideDiscountCodesList", - "value": false, - "__typename": "AppliedMenuVisibilities" + "group": "orders", + "name": "canAddOrders", + "value": false, + "__typename": "AppliedActionRight" }, { - "name": "hideAddDiscounts", - "value": false, - "__typename": "AppliedMenuVisibilities" + "group": "orders", + "name": "canAddDiscountCodes", + "value": false, + "__typename": "AppliedActionRight" }, { - "name": "hideGenerateDiscountCodes", - "value": false, - "__typename": "AppliedMenuVisibilities" + "group": "orders", + "name": "canCreateReturns", + "value": false, + "__typename": "AppliedActionRight" }, { - "name": "hideProjectSettings", - "value": false, - "__typename": "AppliedMenuVisibilities" + "group": "quotes", + "name": "canSendQuote", + "value": false, + "__typename": "AppliedActionRight" }, { - "name": "hideProductTypes", - "value": false, - "__typename": "AppliedMenuVisibilities" + "group": "quotes", + "name": "canCreateDraftQuote", + "value": false, + "__typename": "AppliedActionRight" }, { - "name": "hideDeveloperSettings", - "value": false, - "__typename": "AppliedMenuVisibilities" + "group": "standalonePrices", + "name": "canAddPrices", + "value": false, + "__typename": "AppliedActionRight" }, { - "name": "hideStandalonePriceList", - "value": false, - "__typename": "AppliedMenuVisibilities" + "group": "standalonePrices", + "name": "canDeletePrices", + "value": false, + "__typename": "AppliedActionRight" }, { - "name": "hideAddStandalonePrice", - "value": false, - "__typename": "AppliedMenuVisibilities" + "group": "standalonePrices", + "name": "canEditPrices", + "value": false, + "__typename": "AppliedActionRight" } - ], - "allAppliedDataFences": [], - "__typename": "AllPermissionsForAllApplications" + ], + "allAppliedDataFences": [], + "allPermissionsForAllApplications": { + "allAppliedPermissions": [ + { + "name": "canViewMollie", + "value": true, + "__typename": "AppliedPermission" + }, + { + "name": "canManageMollie", + "value": true, + "__typename": "AppliedPermission" + } + ], + "allAppliedActionRights": [ + { + "group": "products", + "name": "canAddPrices", + "value": false, + "__typename": "AppliedActionRight" + }, + { + "group": "products", + "name": "canAddProducts", + "value": false, + "__typename": "AppliedActionRight" + }, + { + "group": "products", + "name": "canDeletePrices", + "value": false, + "__typename": "AppliedActionRight" + }, + { + "group": "products", + "name": "canDeleteProducts", + "value": false, + "__typename": "AppliedActionRight" + }, + { + "group": "products", + "name": "canEditPrices", + "value": false, + "__typename": "AppliedActionRight" + }, + { + "group": "products", + "name": "canPublishProducts", + "value": false, + "__typename": "AppliedActionRight" + }, + { + "group": "products", + "name": "canUnpublishProducts", + "value": false, + "__typename": "AppliedActionRight" + }, + { + "group": "products", + "name": "canEditAttributes:all", + "value": false, + "__typename": "AppliedActionRight" + }, + { + "group": "orders", + "name": "canAddOrders", + "value": false, + "__typename": "AppliedActionRight" + }, + { + "group": "orders", + "name": "canAddDiscountCodes", + "value": false, + "__typename": "AppliedActionRight" + }, + { + "group": "orders", + "name": "canCreateReturns", + "value": false, + "__typename": "AppliedActionRight" + }, + { + "group": "quotes", + "name": "canSendQuote", + "value": false, + "__typename": "AppliedActionRight" + }, + { + "group": "quotes", + "name": "canCreateDraftQuote", + "value": false, + "__typename": "AppliedActionRight" + }, + { + "group": "standalonePrices", + "name": "canAddPrices", + "value": false, + "__typename": "AppliedActionRight" + }, + { + "group": "standalonePrices", + "name": "canDeletePrices", + "value": false, + "__typename": "AppliedActionRight" + }, + { + "group": "standalonePrices", + "name": "canEditPrices", + "value": false, + "__typename": "AppliedActionRight" + } + ], + "allAppliedMenuVisibilities": [ + { + "name": "hideDashboard", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideProductsList", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideModifiedProducts", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideAddProduct", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideProductSelectionsList", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideAddProductSelection", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideDirectAccess", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideCategoriesList", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideCategoriesSearch", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideAddCategory", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideCustomersList", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideAddCustomer", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideCustomerGroupsList", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideAddCustomerGroup", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideBusinessUnitsList", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideAddBusinessUnit", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideOrdersList", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideAddOrder", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideQuotes", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideProductDiscountsList", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideCartDiscountsList", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideDiscountCodesList", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideAddDiscounts", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideGenerateDiscountCodes", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideProjectSettings", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideProductTypes", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideDeveloperSettings", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideStandalonePriceList", + "value": false, + "__typename": "AppliedMenuVisibilities" + }, + { + "name": "hideAddStandalonePrice", + "value": false, + "__typename": "AppliedMenuVisibilities" + } + ], + "allAppliedDataFences": [], + "__typename": "AllPermissionsForAllApplications" }, "owner": { - "id": "9adf9042-7177-4ea0-a928-a8e194c63009", - "name": "Shopmacher", - "__typename": "Organization" + "id": "9adf9042-7177-4ea0-a928-a8e194c63009", + "name": "Shopmacher", + "__typename": "Organization" }, "sampleDataImportDataset": "B2CLIFESTYLE", "isUserAdminOfCurrentProject": true, diff --git a/application/schemas/ctp.json b/application/schemas/ctp.json index cd42d8c..cfeeff1 100644 --- a/application/schemas/ctp.json +++ b/application/schemas/ctp.json @@ -110765,4 +110765,4 @@ } ] } -} \ No newline at end of file +} diff --git a/application/setup.js b/application/setup.js index 76e656b..9ae8a4b 100644 --- a/application/setup.js +++ b/application/setup.js @@ -5,7 +5,7 @@ jest.mock('./src/constants', () => { View: 'ViewMollie', Manage: 'TestMollie', }, - PROJECT_KEY: 'shopm-adv-dev', + PROJECT_KEY: 'your-project-key', CLOUD_IDENTIFIER: 'gcp-eu', CUSTOM_APPLICATION_ID: '', APPLICATION_URL: 'http://localhost:3001', @@ -14,9 +14,9 @@ jest.mock('./src/constants', () => { EXTENSION_URL_PATH: '/processor', APPLICATION_URL_PATH: '/application/methods', USER_AGENT: { - name: 'ShopmacherMollieCommercetoolsConnector/1.2.0-alpha', + name: 'ShopmacherMollieCommercetoolsConnector/1.2.0', version: '1.2.0', - libraryName: 'ShopmacherMollieCommercetoolsConnector/1.2.0-alpha', + libraryName: 'ShopmacherMollieCommercetoolsConnector/1.2.0', contactEmail: 'info@mollie.com', }, }; diff --git a/application/src/components/welcome/web-developer.svg b/application/src/components/welcome/web-developer.svg index 508e796..a1b681c 100644 --- a/application/src/components/welcome/web-developer.svg +++ b/application/src/components/welcome/web-developer.svg @@ -1 +1 @@ -web_development \ No newline at end of file +web_development diff --git a/connect.yaml b/connect.yaml index 2d99ef6..d8b5790 100644 --- a/connect.yaml +++ b/connect.yaml @@ -11,8 +11,12 @@ deployAs: description: Commercetools Composable Commerce API region required: true default: "europe-west1.gcp" + - key: CTP_AUTH_URL + description: Commercetools Auth URL + default: https://auth.europe-west1.gcp.commercetools.com + required: true - key: MOLLIE_CARD_COMPONENT - description: Enable Mollie card component (0 or 1) + description: Enable Mollie cart component (0 or 1) required: false default: "0" - key: CONNECTOR_MODE @@ -52,26 +56,3 @@ deployAs: - key: CTP_SCOPE description: Commercetools Composable Commerce client scope required: true - - key: CTP_SESSION_AUDIENCE - description: Commercetools Composable Commerce client session audience. The value should only contain the origin URL (protocol, hostname, port). - required: true - - key: CTP_SESSION_ISSUER - description: Commercetools Composable Commerce client session issuer. The cloud identifier that maps to the MC API URL of the related cloud region or the MC API URL. - required: true - - - name: application - applicationType: merchant-center-custom-application - scripts: - postDeploy: yarn install && yarn build - preUndeploy: yarn install && yarn build - configuration: - standardConfiguration: - - key: CUSTOM_APPLICATION_ID - description: The Custom Application ID - required: true - - key: CLOUD_IDENTIFIER - description: The cloud identifier - default: 'gcp-eu' - - key: ENTRY_POINT_URI_PATH - description: The Application entry point URI path - required: true \ No newline at end of file diff --git a/docs/Authorization.md b/docs/Authorization.md index 2a4c330..7ccecd6 100644 --- a/docs/Authorization.md +++ b/docs/Authorization.md @@ -17,7 +17,7 @@ CREAT/UPDATE Extension "url": "https://efd6-115-74-115-119.ngrok-free.app/processor", "authorization": { "type": "AuthorizationHeader", - "headerValue": "_token_" + "headerValue": "***tAjsIR2!srt" } } ... diff --git a/docs/CancelPaymentRefund.md b/docs/CancelPaymentRefund.md index e5d9f42..13a6738 100644 --- a/docs/CancelPaymentRefund.md +++ b/docs/CancelPaymentRefund.md @@ -3,6 +3,7 @@ * [Parameters map](#parameters-map) * [Representation: CT Payment](#representation-ct-payment) * [Creating CommerceTools actions from Mollie's response](#creating-commercetools-actions-from-mollies-response) + * [Update per version](#update-per-version) ## Overview This functionality is used to cancel the pending refund which means it is created but not complete yet. @@ -155,3 +156,8 @@ When order is successfully cancelled on Mollie, we update commercetools payment | `changeTransactionState` | `transactionId: , state: 'Failure'` | | `changeTransactionState` | `transactionId: , state: 'Success'` | | `setTransactionCustomType` | `transactionId: , type.key:sctm_payment_cancel_reason, fields: {reasonText: "cancellation reason", statusText: "cancelled from shop side"}` | + +## Update per version + +The function was updated at: +- [v1.2.1](../CHANGELOG.md#v121) \ No newline at end of file diff --git a/docs/CreateRefund.md b/docs/CreateRefund.md index 58067ed..6f7fbc8 100644 --- a/docs/CreateRefund.md +++ b/docs/CreateRefund.md @@ -4,6 +4,7 @@ * [Parameters map](#parameters-map) * [Representation: CommerceTools Payment](#representation-ct-payment) * [Creating commercetools actions from Mollie's response](#creating-commercetools-actions-from-mollies-response) + * [Update per version](#update-per-version) ## Overview @@ -24,8 +25,6 @@ A transaction with type "Refund" and state "Initial" triggers a refund. In commercetools, we have a Payment which has one Transaction. This maps to an order in mollie. The commercetools Payment's key is the mollie orderId, and the commercetools Transaction maps to the payment in mollie. -In commercetools, we have a Payment which has one Transaction. This maps to an order in mollie. The commercetools Payment's key is the mollie orderId, and the commercetools Transaction maps to the payment in mollie. - ``` { id: "c0887a2d-bfbf-4f77-8f3d-fc33fb4c0920", @@ -96,4 +95,9 @@ transactions: [ ] ``` -When the refund is completed, this transaction's state will be updated by the notifications module to "Success" or "Failure". \ No newline at end of file +When the refund is completed, this transaction's state will be updated by the notifications module to "Success" or "Failure". + +## Update per version + +The function was updated at: +- [v1.2.1](../CHANGELOG.md#v121) \ No newline at end of file diff --git a/docs/GetPaymentMethods.md b/docs/GetPaymentMethods.md index 337e986..2db071c 100644 --- a/docs/GetPaymentMethods.md +++ b/docs/GetPaymentMethods.md @@ -69,21 +69,17 @@ _Body:_ ```json { - "actions": [ - { - "actions": [ - { - "action": "setCustomField", - "name": "sctm_mollie_profile_id", - "value": "pfl_SPkYGi***" - }, - { - "action": "setCustomField", - "name": "sctm_payment_methods_response", - "value": "{\"count\":7,\"methods\":[{\"id\":\"przelewy24\",\"name\":{\"en-GB\":\"Przelewy24\",\"de-DE\":\"Przelewy24\",\"pl-PL\":\"Przelewy24\"},\"description\":{\"en-GB\":\"\",\"de-DE\":\"Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of \",\"pl-PL\":\"\"},\"image\":\"https://www.mollie.com/external/icons/payment-methods/przelewy24.svg\",\"order\":4},{\"id\":\"banktransfer\",\"name\":{\"en-GB\":\"Bank transfer\",\"de-DE\":\"Bank transfer\",\"pl-PL\":\"Bank transfer\"},\"description\":{\"en-GB\":\"\",\"de-DE\":\"\",\"pl-PL\":\"\"},\"image\":\"https://www.mollie.com/external/icons/payment-methods/banktransfer.svg\",\"order\":3},{\"id\":\"applepay\",\"name\":{\"en-GB\":\"Apple Pay\",\"de-DE\":\"Apple Pay\",\"pl-PL\":\"Apple Pay\"},\"description\":{\"en-GB\":\"Apple Pay description\",\"de-DE\":\"Apple Pay description\",\"pl-PL\":\"\"},\"image\":\"https://www.mollie.com/external/icons/payment-methods/applepay.svg\",\"order\":2},{\"id\":\"paypal\",\"name\":{\"en-GB\":\"PayPal\",\"de-DE\":\"PayPal\",\"pl-PL\":\"PayPal\"},\"description\":{\"en-GB\":\"\",\"de-DE\":\"\",\"pl-PL\":\"\"},\"image\":\"https://www.mollie.com/external/icons/payment-methods/paypal.svg\",\"order\":0},{\"id\":\"ideal\",\"name\":{\"en-GB\":\"iDEAL\",\"de-DE\":\"iDEAL\",\"pl-PL\":\"iDEAL\"},\"description\":{\"en-GB\":\"\",\"de-DE\":\"\",\"pl-PL\":\"\"},\"image\":\"https://www.mollie.com/external/icons/payment-methods/ideal.svg\",\"order\":0},{\"id\":\"bancontact\",\"name\":{\"en-GB\":\"Bancontact\",\"de-DE\":\"Bancontact\",\"pl-PL\":\"Bancontact\"},\"description\":{\"en-GB\":\"\",\"de-DE\":\"\",\"pl-PL\":\"\"},\"image\":\"https://www.mollie.com/external/icons/payment-methods/bancontact.svg\",\"order\":0},{\"id\":\"kbc\",\"name\":{\"en-GB\":\"KBC/CBC Payment Button\",\"de-DE\":\"KBC/CBC Payment Button\",\"pl-PL\":\"KBC/CBC Payment Button\"},\"description\":{\"en-GB\":\"\",\"de-DE\":\"\",\"pl-PL\":\"\"},\"image\":\"https://www.mollie.com/external/icons/payment-methods/kbc.svg\",\"order\":0}]}" - } - ] - } - ] + "actions": [ + { + "action": "setCustomField", + "name": "sctm_payment_methods_response", + "value": "{\"count\":11,\"methods\":[{\"resource\":\"method\",\"id\":\"applepay\",\"description\":\"Apple Pay\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"10000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/applepay.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/applepay%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/applepay.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/applepay\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"creditcard\",\"description\":\"Karte\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"10000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/creditcard.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/creditcard%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/creditcard.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/creditcard\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"paypal\",\"description\":\"PayPal\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":null,\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/paypal.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/paypal%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/paypal.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/paypal\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"banktransfer\",\"description\":\"Überweisung\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"1000000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/banktransfer.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/banktransfer%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/banktransfer.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/banktransfer\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"ideal\",\"description\":\"iDEAL\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"50000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/ideal.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/ideal%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/ideal.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/ideal\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"bancontact\",\"description\":\"Bancontact\",\"minimumAmount\":{\"value\":\"0.02\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"50000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/bancontact.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/bancontact%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/bancontact.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/bancontact\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"eps\",\"description\":\"eps\",\"minimumAmount\":{\"value\":\"1.00\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"50000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/eps.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/eps%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/eps.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/eps\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"przelewy24\",\"description\":\"Przelewy24\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"12815.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/przelewy24.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/przelewy24%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/przelewy24.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/przelewy24\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"kbc\",\"description\":\"KBC/CBC Zahlungsbutton\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"50000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/kbc.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/kbc%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/kbc.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/kbc\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"belfius\",\"description\":\"Belfius Pay Button\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"50000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/belfius.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/belfius%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/belfius.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/belfius\",\"type\":\"application/hal+json\"}}},{\"resource\":\"method\",\"id\":\"bancomatpay\",\"description\":\"Bancomat Pay\",\"minimumAmount\":{\"value\":\"0.01\",\"currency\":\"EUR\"},\"maximumAmount\":{\"value\":\"50000.00\",\"currency\":\"EUR\"},\"image\":{\"size1x\":\"https://www.mollie.com/external/icons/payment-methods/bancomatpay.png\",\"size2x\":\"https://www.mollie.com/external/icons/payment-methods/bancomatpay%402x.png\",\"svg\":\"https://www.mollie.com/external/icons/payment-methods/bancomatpay.svg\"},\"status\":\"activated\",\"_links\":{\"self\":{\"href\":\"https://api.mollie.com/v2/methods/bancomatpay\",\"type\":\"application/hal+json\"}}}]}" + }, + { + "action": "setCustomField", + "name": "sctm_mollie_profile_id", + "value": "pfl_SPkYGiEQjf" + } + ] } ``` diff --git a/docs/contribution/ContributionGuidelines.md b/docs/contribution/ContributionGuidelines.md new file mode 100644 index 0000000..414ccaf --- /dev/null +++ b/docs/contribution/ContributionGuidelines.md @@ -0,0 +1,35 @@ +# Contribution Guide + +* [Prerequisites](#prerequisites) +* [Development](#development) + +## Prerequisites + +To merge your changes, your commits must be [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). + +Minimum requirements are: +- **Node.js** version 18 +- **Npm** npm v10.7.0 +- **Bash** shell +- **Signed** git commits + +You can install all dependencies using `npm` with following command: + +``` +npm install +``` + +## Development +While developing project you can use some predefined commands for running tests, running linter or generating coverage. + +- Execute `npm run test` to run all tests. +- Execute `npm run lint` to show lint errors in the code. + +## Release procedure +At the moment there is no automated release scripts, which means we do releases manually. We use [gitflow workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) for releases. Please **squash commits when finishing the feature branch**, this way we can keep relatively clean history on develop and main. + +Contributing by following these steps: + +1. Create and check out a new branch off main and make your necessary change here. The branch name should have a specific prefix depends on the purpose, e.g: `bugfix/`, `feat/` +2. Bump [version](https://semver.org/) on npm package (in /processor). Commit changes on the above branch. +3. Create PR and wait for the approval from the development team. \ No newline at end of file diff --git a/docs/contribution/IssueTemplate.md b/docs/contribution/IssueTemplate.md new file mode 100644 index 0000000..3673bf9 --- /dev/null +++ b/docs/contribution/IssueTemplate.md @@ -0,0 +1,26 @@ +# Issue Template + +**Deployment** +_My deployment is on:_ + +- [ ] Docker +- [ ] AWS Lambda +- [ ] GCP +- [ ] Azure +- [ ] Other _Please specify:_ + +**Expected behaviour** + +_Please explain what should happen._ + +**Actual behaviour** + +_Please explain what is happening instead. Provide a log message if relevant._ + +**Attempted fixes** + +_Please explain what you've done so far to try and fix the problem._ + +**Please provide more information about how this can be reproduced. Please include jsfiddle links, code snippets or any other necessary information** + +_Include extra info here._ \ No newline at end of file diff --git a/processor/.env.example b/processor/.env.example index 7c2688e..576da7b 100644 --- a/processor/.env.example +++ b/processor/.env.example @@ -10,16 +10,6 @@ AUTHENTICATION_MODE= CTP_AUTH_URL=https://auth..commercetools.com CTP_API_URL=https://api..commercetools.com -## CT Session Middleware options -### The public-facing URL used to connect to the server / serverless function. -### The value should only contain the origin URL (protocol, hostname, port), -### the request path is inferred from the incoming request. -CTP_SESSION_AUDIENCE=https://mc..commercetools.com -### The cloud identifier (see `CLOUD_IDENTIFIERS`) that maps to the MC API URL -### of the related cloud region or the MC API URL -### e.g. gcp-eu -CTP_SESSION_ISSUER=gcp-eu - ## Mollie PSP credentials DEBUG= ## Either 1 for enable or 0 for disable CONNECTOR_MODE= ## Either test or live diff --git a/processor/.env.jest b/processor/.env.jest index 7674d6d..4a873a6 100644 --- a/processor/.env.jest +++ b/processor/.env.jest @@ -5,9 +5,11 @@ CTP_CLIENT_SECRET=12345678901234567890123456789012 CTP_PROJECT_KEY=TEST CTP_SCOPE=TEST CTP_REGION=europe-west1.gcp +CTP_AUTH_URL=https://auth.europe-west1.gcp.commercetools.com AUTHENTICATION_MODE=0 CTP_SESSION_AUDIENCE=https://mc.europe-west1.gcp.commercetools.com CTP_SESSION_ISSUER=gcp-eu + ## MOLLIE vars MOLLIE_PROFILE_ID=pfl_12345 DEBUG=0 diff --git a/processor/jest.config.cjs b/processor/jest.config.cjs index c6143f6..108d1c2 100644 --- a/processor/jest.config.cjs +++ b/processor/jest.config.cjs @@ -7,5 +7,5 @@ module.exports = { setupFiles: ['/src/jest.setup.ts'], setupFilesAfterEnv: ['/src/jest.setupAfterEnv.ts'], modulePathIgnorePatterns: ['/src/jest.setup.ts'], - reporters: ['default', 'jest-junit'], + reporters: ['default', 'jest-junit'] }; diff --git a/processor/package-lock.json b/processor/package-lock.json index c498963..fdb27c8 100644 --- a/processor/package-lock.json +++ b/processor/package-lock.json @@ -1,12 +1,12 @@ { "name": "shopmacher-mollie-processor", - "version": "1.2.0-alpha25.11.24.1241", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "shopmacher-mollie-processor", - "version": "1.2.0-alpha25.11.24.1241", + "version": "1.2.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/processor/package.json b/processor/package.json index 8986aad..b4bd3a6 100644 --- a/processor/package.json +++ b/processor/package.json @@ -1,7 +1,7 @@ { "name": "shopmacher-mollie-processor", "description": "Integration between commercetools and mollie payment service provider", - "version": "1.2.0-alpha25.11.24.1241", + "version": "1.2.1", "main": "index.js", "private": true, "scripts": { diff --git a/processor/src/commercetools/auth.commercetools.ts b/processor/src/commercetools/auth.commercetools.ts new file mode 100644 index 0000000..473ea13 --- /dev/null +++ b/processor/src/commercetools/auth.commercetools.ts @@ -0,0 +1,22 @@ +import { readConfiguration } from '../utils/config.utils'; +import fetch from 'node-fetch'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const getAccessToken = async (): Promise => { + const config = readConfiguration(); + + const credentials = btoa(config.commerceTools.clientId + ':' + config.commerceTools.clientSecret); + + const headers = { + Authorization: `Basic ${credentials}`, + 'Content-Type': 'application/json', + }; + + const response = await fetch(`${config.commerceTools.authUrl}/oauth/token?grant_type=client_credentials`, { + headers: headers, + method: 'POST', + redirect: 'follow', + }); + + return await response.json(); +}; diff --git a/processor/src/commercetools/customFields.commercetools.ts b/processor/src/commercetools/customFields.commercetools.ts index 7bd9e4e..0d1c507 100644 --- a/processor/src/commercetools/customFields.commercetools.ts +++ b/processor/src/commercetools/customFields.commercetools.ts @@ -333,7 +333,7 @@ export async function createTransactionSurchargeCustomType(): Promise { .types() .post({ body: { - key: CustomFields.createPayment.interfaceInteraction.key, + key: CustomFields.transactionSurchargeCost, name: { en: 'SCTM - Transaction surcharge amount', de: 'SCTM - Betrag des Transaktionszuschlags', @@ -379,3 +379,83 @@ export async function createTransactionSurchargeCustomType(): Promise { return; } } + +export async function createTransactionRefundForMolliePaymentCustomType(): Promise { + const apiRoot = createApiRoot(); + const customFields: FieldDefinition[] = [ + { + name: CustomFields.transactionRefundForMolliePayment, + label: { + en: 'Identify the Mollie payment which is being refunded', + de: 'Identifizieren Sie die Mollie-Zahlung, die zurückerstattet wird', + }, + required: false, + type: { + name: 'String', + }, + inputHint: 'MultiLine', + }, + ]; + + const { + body: { results: types }, + } = await apiRoot + .types() + .get({ + queryArgs: { + where: `key = "${CustomFields.transactionRefundForMolliePayment}"`, + }, + }) + .execute(); + + if (types.length <= 0) { + await apiRoot + .types() + .post({ + body: { + key: CustomFields.transactionRefundForMolliePayment, + name: { + en: 'Identify the Mollie payment which is being refunded', + de: 'Identifizieren Sie die Mollie-Zahlung, die zurückerstattet wird', + }, + resourceTypeIds: ['transaction'], + fieldDefinitions: customFields, + }, + }) + .execute(); + + return; + } + + const type = types[0]; + const definitions = type.fieldDefinitions; + + if (definitions.length > 0) { + const actions: TypeUpdateAction[] = []; + definitions.forEach((definition) => { + actions.push({ + action: 'removeFieldDefinition', + fieldName: definition.name, + }); + }); + customFields.forEach((field) => { + actions.push({ + action: 'addFieldDefinition', + fieldDefinition: field, + }); + }); + + await apiRoot + .types() + .withKey({ key: CustomFields.transactionRefundForMolliePayment }) + .post({ + body: { + version: type.version, + actions, + }, + }) + .execute(); + + return; + } +} diff --git a/processor/src/commercetools/extensions.commercetools.ts b/processor/src/commercetools/extensions.commercetools.ts index 6331d47..62f5a46 100644 --- a/processor/src/commercetools/extensions.commercetools.ts +++ b/processor/src/commercetools/extensions.commercetools.ts @@ -3,7 +3,7 @@ import { Extension } from '@commercetools/platform-sdk'; export const PAYMENT_EXTENSION_KEY = 'sctm-payment-create-update-extension'; -export async function createPaymentExtension(applicationUrl: string): Promise { +export async function createPaymentExtension(applicationUrl: string, accessToken: string): Promise { const apiRoot = createApiRoot(); const extension = await getPaymentExtension(); @@ -22,7 +22,7 @@ export async function createPaymentExtension(applicationUrl: string): Promise => { - await createPaymentExtension(extensionUrl); + const response = await getAccessToken(); + await createPaymentExtension(extensionUrl, response?.access_token as string); await createCustomPaymentType(); await createCustomPaymentInterfaceInteractionType(); await createCustomPaymentTransactionCancelReasonType(); await createTransactionSurchargeCustomType(); + await createTransactionRefundForMolliePaymentCustomType(); }; export const removeExtension = async (): Promise => { diff --git a/processor/src/service/payment.service.ts b/processor/src/service/payment.service.ts index c0f5cd7..39f7951 100644 --- a/processor/src/service/payment.service.ts +++ b/processor/src/service/payment.service.ts @@ -71,6 +71,7 @@ import { convertCentToEUR, parseStringToJsonObject, roundSurchargeAmountToCent, + sortTransactionsByLatestCreationTime, } from '../utils/app.utils'; import ApplePaySession from '@mollie/api-client/dist/types/src/data/applePaySession/ApplePaySession'; import { getMethodConfigObjects, getSingleMethodConfigObject } from '../commercetools/customObjects.commercetools'; @@ -422,10 +423,6 @@ export const handleCreatePayment = async (ctPayment: Payment): Promise => { - const successChargeTransaction = ctPayment.transactions.find( - (transaction) => transaction.type === CTTransactionType.Charge && transaction.state === CTTransactionState.Success, - ); + let successChargeTransaction; + const updateActions = [] as UpdateAction[]; const initialRefundTransaction = ctPayment.transactions.find( (transaction) => transaction.type === CTTransactionType.Refund && transaction.state === CTTransactionState.Initial, ); + if (initialRefundTransaction?.custom?.fields[CustomFields.transactionRefundForMolliePayment]) { + logger.debug('SCTM - handleCreateRefund - creating a refund with specific payment id'); + + successChargeTransaction = ctPayment.transactions.find( + (transaction) => + transaction.type === CTTransactionType.Charge && + transaction.state === CTTransactionState.Success && + transaction.interactionId === + initialRefundTransaction?.custom?.fields[CustomFields.transactionRefundForMolliePayment], + ); + } else { + logger.debug('SCTM - handleCreateRefund - creating a refund for the latest success charge transaction'); + + const latestTransactions = sortTransactionsByLatestCreationTime(ctPayment.transactions); + + successChargeTransaction = latestTransactions.find( + (transaction) => + transaction.type === CTTransactionType.Charge && transaction.state === CTTransactionState.Success, + ); + + updateActions.push( + setTransactionCustomType(initialRefundTransaction?.id as string, CustomFields.transactionRefundForMolliePayment, { + [CustomFields.transactionRefundForMolliePayment]: successChargeTransaction?.interactionId, + }), + ); + } + + if (!successChargeTransaction) { + throw new CustomError(400, 'SCTM - handleCreateRefund - Cannot find valid success charge transaction'); + } + const paymentCreateRefundParams: CreateParameters = { paymentId: successChargeTransaction?.interactionId as string, amount: makeMollieAmount(initialRefundTransaction?.amount as CentPrecisionMoney), @@ -545,12 +572,14 @@ export const handleCreateRefund = async (ctPayment: Payment): Promise => { - const successChargeTransaction = ctPayment.transactions.find( - (transaction) => transaction.type === CTTransactionType.Charge && transaction.state === CTTransactionState.Success, - ); - - const pendingRefundTransaction = ctPayment.transactions.find( - (transaction) => transaction.type === CTTransactionType.Refund && transaction.state === CTTransactionState.Pending, - ); + let pendingRefundTransaction: any; + let successChargeTransaction: any; const initialCancelAuthorization = ctPayment.transactions.find( (transaction) => transaction.type === CTTransactionType.CancelAuthorization && transaction.state === CTTransactionState.Initial, ); + if (initialCancelAuthorization?.interactionId) { + pendingRefundTransaction = ctPayment.transactions.find( + (transaction) => + transaction.type === CTTransactionType.Refund && + transaction.state === CTTransactionState.Pending && + transaction?.interactionId === initialCancelAuthorization.interactionId, + ) as Transaction; + + if (pendingRefundTransaction) { + successChargeTransaction = ctPayment.transactions.find( + (transaction) => + transaction.type === CTTransactionType.Charge && + transaction.state === CTTransactionState.Success && + transaction.interactionId === + pendingRefundTransaction?.custom?.fields[CustomFields.transactionRefundForMolliePayment], + ) as Transaction; + } + + if (!successChargeTransaction) { + throw new CustomError( + 400, + 'SCTM - handlePaymentCancelRefund - Cannot find the valid Success Charge transaction.', + ); + } + } + + /** + * @deprecated v1.2 - Will be remove in the next version + */ + if (!pendingRefundTransaction || !successChargeTransaction) { + const latestTransactions = sortTransactionsByLatestCreationTime(ctPayment.transactions); + + pendingRefundTransaction = latestTransactions.find( + (transaction) => + transaction.type === CTTransactionType.Refund && transaction.state === CTTransactionState.Pending, + ); + + successChargeTransaction = latestTransactions.find( + (transaction) => + transaction.type === CTTransactionType.Charge && transaction.state === CTTransactionState.Success, + ); + } + /** + * end deprecated + */ + const paymentGetRefundParams: CancelParameters = { paymentId: successChargeTransaction?.interactionId as string, }; diff --git a/processor/src/types/index.types.ts b/processor/src/types/index.types.ts index e86712b..7808e76 100644 --- a/processor/src/types/index.types.ts +++ b/processor/src/types/index.types.ts @@ -21,6 +21,7 @@ export type ConnectorEnvVars = { projectKey: string; scope: string; region: string; + authUrl: string; authMode: string; sessionAudience: string; sessionIssuer: string; diff --git a/processor/src/utils/app.utils.ts b/processor/src/utils/app.utils.ts index 5c30a59..fbfe82e 100644 --- a/processor/src/utils/app.utils.ts +++ b/processor/src/utils/app.utils.ts @@ -1,5 +1,5 @@ import { SurchargeCost } from './../types/commercetools.types'; -import { Payment } from '@commercetools/platform-sdk'; +import { Payment, Transaction } from '@commercetools/platform-sdk'; import CustomError from '../errors/custom.error'; import { logger } from './logger.utils'; /** @@ -101,3 +101,22 @@ export const calculateTotalSurchargeAmount = (ctPayment: Payment, surcharges?: S export const roundSurchargeAmountToCent = (surchargeAmountInEur: number, fractionDigits: number): number => { return Math.round(surchargeAmountInEur * Math.pow(10, fractionDigits)); }; + +export const sortTransactionsByLatestCreationTime = (transactions: Transaction[]): Transaction[] => { + const clonedTransactions = Object.assign([], transactions); + + return clonedTransactions.sort((a: Transaction, b: Transaction) => { + const timeA = a.timestamp as string; + const timeB = b.timestamp as string; + + if (timeA < timeB) { + return 1; + } + + if (timeA > timeB) { + return -1; + } + + return 0; + }); +}; diff --git a/processor/src/utils/config.utils.ts b/processor/src/utils/config.utils.ts index 65d0e76..58e6f6e 100644 --- a/processor/src/utils/config.utils.ts +++ b/processor/src/utils/config.utils.ts @@ -16,6 +16,7 @@ export const readConfiguration = () => { projectKey: process.env.CTP_PROJECT_KEY as string, scope: process.env.CTP_SCOPE as string, region: process.env.CTP_REGION as string, + authUrl: process.env.CTP_AUTH_URL as string, authMode: process.env.AUTHENTICATION_MODE as string, sessionAudience: (process.env.CTP_SESSION_AUDIENCE as string) || 'https://mc.europe-west1.gcp.commercetools.com', sessionIssuer: (process.env.CTP_SESSION_ISSUER as string) || 'gcp-eu', diff --git a/processor/src/utils/constant.utils.ts b/processor/src/utils/constant.utils.ts index 3e39cee..d060fdf 100644 --- a/processor/src/utils/constant.utils.ts +++ b/processor/src/utils/constant.utils.ts @@ -39,6 +39,7 @@ export const CustomFields = { }, }, transactionSurchargeCost: 'sctm_transaction_surcharge_cost', + transactionRefundForMolliePayment: 'sctm_transaction_refund_for_mollie_payment', }; export enum ConnectorActions { diff --git a/processor/src/utils/paymentAction.utils.ts b/processor/src/utils/paymentAction.utils.ts index cf8cc3e..03bbadc 100644 --- a/processor/src/utils/paymentAction.utils.ts +++ b/processor/src/utils/paymentAction.utils.ts @@ -68,13 +68,13 @@ const determineAction = (groups: ReturnType): Deter return ConnectorActions.CancelPayment; } - if (groups.successCharge.length === 1 && groups.initialRefund.length) { + if (groups.successCharge.length >= 1 && groups.initialRefund.length) { return ConnectorActions.CreateRefund; } if ( - groups.successCharge.length === 1 && - groups.pendingRefund.length === 1 && + groups.successCharge.length >= 1 && + groups.pendingRefund.length >= 1 && groups.initialCancelAuthorization.length === 1 ) { return ConnectorActions.CancelRefund; diff --git a/processor/tests/commercetools/action.commercetools.spec.ts b/processor/tests/commercetools/action.commercetools.spec.ts index faa6d57..3035ef1 100644 --- a/processor/tests/commercetools/action.commercetools.spec.ts +++ b/processor/tests/commercetools/action.commercetools.spec.ts @@ -1,4 +1,4 @@ -import { ConnectorActions, MOLLIE_SURCHARGE_CUSTOM_LINE_ITEM } from '../../src/utils/constant.utils'; +import { ConnectorActions, CustomFields, MOLLIE_SURCHARGE_CUSTOM_LINE_ITEM } from '../../src/utils/constant.utils'; import { describe, test, expect, jest } from '@jest/globals'; import { addCustomLineItem, @@ -184,15 +184,17 @@ describe('Test actions.utils.ts', () => { }); }); - test('should return a action for adding transaction custom fields', () => { - const name = 'customFieldName'; - const value = 'customFieldValue'; - const transactionId = 'transactionId'; + test('should be able to return the correct setTransactionCustomField action', () => { + const name = CustomFields.transactionSurchargeCost; + const surchargeInCentAmount = { + surchargeInCentAmount: 12345, + }; + const transactionId = 'test'; - expect(setTransactionCustomField(name, value, transactionId)).toStrictEqual({ + expect(setTransactionCustomField(name, JSON.stringify(surchargeInCentAmount), transactionId)).toStrictEqual({ action: 'setTransactionCustomField', name, - value, + value: JSON.stringify(surchargeInCentAmount), transactionId, }); }); diff --git a/processor/tests/commercetools/auth.commercetools.spec.ts b/processor/tests/commercetools/auth.commercetools.spec.ts new file mode 100644 index 0000000..abaf9a3 --- /dev/null +++ b/processor/tests/commercetools/auth.commercetools.spec.ts @@ -0,0 +1,42 @@ +import { getAccessToken } from './../../src/commercetools/auth.commercetools'; +import { afterEach, describe, expect, jest, it } from '@jest/globals'; +import fetch from 'node-fetch'; + +// @ts-expect-error: Mock fetch globally +fetch = jest.fn() as jest.Mock; + +describe('test getAccessToken', () => { + afterEach(() => { + jest.clearAllMocks(); // Clear all mocks after each test + }); + + it('should call fetch with the correct parameters', async () => { + const expectedHeaders = { + Authorization: `Basic ${btoa(`${process.env.CTP_CLIENT_ID}:${process.env.CTP_CLIENT_SECRET}`)}`, + 'Content-Type': 'application/json', + }; + + const expectedUrl = `${process.env.CTP_AUTH_URL}/oauth/token?grant_type=client_credentials`; + + (fetch as unknown as jest.Mock).mockImplementation(async () => + Promise.resolve({ + json: () => Promise.resolve({ data: [] }), + headers: new Headers(), + ok: true, + redirected: false, + status: 201, + statusText: 'OK', + url: '', + }), + ); + + await getAccessToken(); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(expectedUrl, { + method: 'POST', + headers: expectedHeaders, + redirect: 'follow', + }); + }); +}); diff --git a/processor/tests/commercetools/extension.commercetools.spec.ts b/processor/tests/commercetools/extension.commercetools.spec.ts index 09ee528..b851b95 100644 --- a/processor/tests/commercetools/extension.commercetools.spec.ts +++ b/processor/tests/commercetools/extension.commercetools.spec.ts @@ -170,7 +170,9 @@ describe('Test extension.commercetools', () => { }), ); - await createPaymentExtension(mockUrl); + const accessToken = 'token123'; + + await createPaymentExtension(mockUrl, accessToken); expect(getExtensions).toHaveBeenCalledTimes(1); expect(withKey).toHaveBeenCalledTimes(1); @@ -190,7 +192,7 @@ describe('Test extension.commercetools', () => { url: mockUrl, authentication: { type: 'AuthorizationHeader', - headerValue: 'Bearer _token_', + headerValue: `Bearer ${accessToken}`, }, }, triggers: [ @@ -213,7 +215,9 @@ describe('Test extension.commercetools', () => { }), ); - await createPaymentExtension(mockUrl); + const accessToken = 'token123'; + + await createPaymentExtension(mockUrl, accessToken); expect(getExtensions).toHaveBeenCalledTimes(1); expect(createMock).toHaveBeenCalledTimes(1); @@ -225,7 +229,7 @@ describe('Test extension.commercetools', () => { url: mockUrl, authentication: { type: 'AuthorizationHeader', - headerValue: 'Bearer _token_', + headerValue: `Bearer ${accessToken}`, }, }, triggers: [ diff --git a/processor/tests/routes/processor.route.spec.ts b/processor/tests/routes/processor.route.spec.ts index d01c1d5..2e69811 100644 --- a/processor/tests/routes/processor.route.spec.ts +++ b/processor/tests/routes/processor.route.spec.ts @@ -8,6 +8,7 @@ import { createCustomPaymentInterfaceInteractionType, createCustomPaymentTransactionCancelReasonType, createTransactionSurchargeCustomType, + createTransactionRefundForMolliePaymentCustomType, } from '../../src/commercetools/customFields.commercetools'; jest.mock('../../src/commercetools/extensions.commercetools', () => ({ @@ -20,6 +21,7 @@ jest.mock('../../src/commercetools/customFields.commercetools', () => ({ createCustomPaymentInterfaceInteractionType: jest.fn(), createCustomPaymentTransactionCancelReasonType: jest.fn(), createTransactionSurchargeCustomType: jest.fn(), + createTransactionRefundForMolliePaymentCustomType: jest.fn(), })); describe('Test src/route/processor.route.ts', () => { @@ -112,6 +114,7 @@ describe('Test src/route/processor.route.ts', () => { (createCustomPaymentInterfaceInteractionType as jest.Mock).mockReturnValueOnce(Promise.resolve()); (createCustomPaymentTransactionCancelReasonType as jest.Mock).mockReturnValueOnce(Promise.resolve()); (createTransactionSurchargeCustomType as jest.Mock).mockReturnValueOnce(Promise.resolve()); + (createTransactionRefundForMolliePaymentCustomType as jest.Mock).mockReturnValueOnce(Promise.resolve()); req = { hostname: 'test.com', diff --git a/processor/tests/service/payment.service.spec.ts b/processor/tests/service/payment.service.spec.ts index 11d56f6..5bdfa86 100644 --- a/processor/tests/service/payment.service.spec.ts +++ b/processor/tests/service/payment.service.spec.ts @@ -1814,7 +1814,7 @@ describe('Test handleCreatePayment', () => { ], interfaceInteractions: [], paymentMethodInfo: { - method: 'googlepay', + method: 'ideal', }, custom: { type: { @@ -1904,7 +1904,7 @@ describe('Test handleCreatePayment', () => { }); (createMollieCreatePaymentParams as jest.Mock).mockReturnValueOnce({ - method: 'googlepay', + method: 'ideal', }); (changeTransactionState as jest.Mock).mockReturnValueOnce({ @@ -1961,7 +1961,7 @@ describe('Test handleCreatePayment', () => { sctm_id: '5c8b0375-305a-4f19-ae8e-07806b101999', sctm_action_type: 'createPayment', sctm_created_at: '2024-03-20T09:13:37+00:00', - sctm_request: '{"transactionId":"5c8b0375-305a-4f19-ae8e-07806b101999","paymentMethod":"googlepay"}', + sctm_request: '{"transactionId":"5c8b0375-305a-4f19-ae8e-07806b101999","paymentMethod":"ideal"}', sctm_response: '{"molliePaymentId":"tr_7UhSN1zuXS","checkoutUrl":"https://www.mollie.com/checkout/select-method/7UhSN1zuXS","transactionId":"5c8b0375-305a-4f19-ae8e-07806b101999"}', }, @@ -2001,7 +2001,7 @@ describe('Test handleCreatePayment', () => { }); describe('Test handleCreateRefund', () => { - it('should return status code and array of actions', async () => { + it('should return status code and array of actions (1 success charge transaction)', async () => { const CTPayment: Payment = { id: '5c8b0375-305a-4f19-ae8e-07806b101999', version: 1, @@ -2065,6 +2065,227 @@ describe('Test handleCreateRefund', () => { const result = await handleCreateRefund(CTPayment); + expect(createPaymentRefund).toBeCalledTimes(1); + expect(createPaymentRefund).toBeCalledWith(paymentCreateRefundParams); + expect(result.statusCode).toBe(201); + expect(result.actions).toStrictEqual([ + { + action: 'setTransactionCustomType', + type: { + key: CustomFieldName.transactionRefundForMolliePayment, + }, + transactionId: 'test_refund', + fields: { + [CustomFieldName.transactionRefundForMolliePayment]: 'tr_123123', + }, + }, + { + action: 'changeTransactionInteractionId', + transactionId: 'test_refund', + interactionId: 'fake_refund_id', + }, + { + action: 'changeTransactionState', + transactionId: 'test_refund', + state: 'Pending', + }, + ]); + }); + + it('should return status code and array of actions (more than 1 success charge transaction, with Mollie payment that need to be refunded is not specified)', async () => { + const targetedMolliePaymentId = 'tr_123456'; + + const CTPayment: Payment = { + id: '5c8b0375-305a-4f19-ae8e-07806b101999', + version: 1, + createdAt: '2024-07-04T14:07:35.625Z', + lastModifiedAt: '2024-07-04T14:07:35.625Z', + amountPlanned: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + paymentStatus: {}, + transactions: [ + { + id: uuid, + timestamp: '2024-06-24T08:28:43.474Z', + type: 'Charge', + interactionId: 'tr_123123', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Success', + }, + { + id: 'test-123', + timestamp: '2024-06-24T08:30:43.474Z', + type: 'Charge', + interactionId: targetedMolliePaymentId, + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Success', + }, + { + id: 'test_refund', + type: 'Refund', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Initial', + }, + ], + interfaceInteractions: [], + paymentMethodInfo: { + method: 'creditcard', + }, + }; + + (changeTransactionState as jest.Mock).mockReturnValueOnce({ + action: 'changeTransactionState', + state: 'Pending', + transactionId: 'test_refund', + }); + + (createPaymentRefund as jest.Mock).mockReturnValue({ + id: 'fake_refund_id', + }); + + const paymentCreateRefundParams: CreateParameters = { + paymentId: targetedMolliePaymentId, + amount: { + value: '10.00', + currency: 'EUR', + }, + }; + + const result = await handleCreateRefund(CTPayment); + + expect(createPaymentRefund).toBeCalledTimes(1); + expect(createPaymentRefund).toBeCalledWith(paymentCreateRefundParams); + expect(result.statusCode).toBe(201); + expect(result.actions).toStrictEqual([ + { + action: 'setTransactionCustomType', + type: { + key: CustomFieldName.transactionRefundForMolliePayment, + }, + transactionId: 'test_refund', + fields: { + [CustomFieldName.transactionRefundForMolliePayment]: targetedMolliePaymentId, + }, + }, + { + action: 'changeTransactionInteractionId', + transactionId: 'test_refund', + interactionId: 'fake_refund_id', + }, + { + action: 'changeTransactionState', + transactionId: 'test_refund', + state: 'Pending', + }, + ]); + }); + + it('should return status code and array of actions (more than 1 success charge transaction, with Mollie payment that need to be refunded is specified)', async () => { + const targetedMolliePaymentId = 'tr_123123'; + + const CTPayment: Payment = { + id: '5c8b0375-305a-4f19-ae8e-07806b101999', + version: 1, + createdAt: '2024-07-04T14:07:35.625Z', + lastModifiedAt: '2024-07-04T14:07:35.625Z', + amountPlanned: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + paymentStatus: {}, + transactions: [ + { + id: uuid, + type: 'Charge', + interactionId: targetedMolliePaymentId, + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Success', + }, + { + id: 'test-123', + type: 'Charge', + interactionId: 'tr_123456', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Success', + }, + { + id: 'test_refund', + type: 'Refund', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Initial', + custom: { + type: { + typeId: 'type', + id: 'custom-type-id', + }, + fields: { + [CustomFieldName.transactionRefundForMolliePayment]: targetedMolliePaymentId, + }, + }, + }, + ], + interfaceInteractions: [], + paymentMethodInfo: { + method: 'creditcard', + }, + }; + + (changeTransactionState as jest.Mock).mockReturnValueOnce({ + action: 'changeTransactionState', + state: 'Pending', + transactionId: 'test_refund', + }); + + (createPaymentRefund as jest.Mock).mockReturnValue({ + id: 'fake_refund_id', + }); + + const paymentCreateRefundParams: CreateParameters = { + paymentId: targetedMolliePaymentId, + amount: { + value: '10.00', + currency: 'EUR', + }, + }; + + const result = await handleCreateRefund(CTPayment); + expect(createPaymentRefund).toBeCalledTimes(1); expect(createPaymentRefund).toBeCalledWith(paymentCreateRefundParams); expect(result.statusCode).toBe(201); @@ -2237,7 +2458,6 @@ describe('Test handlePaymentCancelRefund', () => { { id: '5c8b0375-305a-4f19-ae8e-07806b102000', type: 'CancelAuthorization', - interactionId: 're_4qqhO89gsT', amount: { type: 'centPrecision', currencyCode: 'EUR', @@ -2312,7 +2532,7 @@ describe('Test handlePaymentCancelRefund', () => { } }); - it('should return status code and array of actions', async () => { + it('should return status code and array of actions (interactionId is not defined in the Initial CancelAuthorization transaction)', async () => { const mollieRefund: Refund = { resource: 'refund', id: CTPayment.transactions[1].interactionId, @@ -2358,6 +2578,297 @@ describe('Test handlePaymentCancelRefund', () => { paymentId: CTPayment.transactions[0].interactionId, }); }); + + it('should return status code and array of actions (interactionId is defined in the Initial CancelAuthorization transaction)', async () => { + const CTPaymentMocked: Payment = { + id: '5c8b0375-305a-4f19-ae8e-07806b101999', + version: 1, + createdAt: '2024-07-04T14:07:35.625Z', + lastModifiedAt: '2024-07-04T14:07:35.625Z', + amountPlanned: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + paymentStatus: {}, + transactions: [ + { + id: '5c8b0375-305a-4f19-ae8e-07806b101992', + type: 'Charge', + interactionId: 'tr_test', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Success', + }, + { + id: '5c8b0375-305a-4f19-ae8e-07806b101999', + type: 'Charge', + interactionId: 'tr_123123', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Success', + }, + { + id: '5c8b0375-305a-4f19-ae8e-07806b102011', + type: 'Refund', + interactionId: 're_TEST', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Pending', + }, + { + id: '5c8b0375-305a-4f19-ae8e-07806b102000', + type: 'Refund', + interactionId: 're_4qqhO89gsT', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Pending', + custom: { + type: { + typeId: 'type', + id: 'custom-type', + }, + fields: { + [CustomFieldName.transactionRefundForMolliePayment]: 'tr_123123', + }, + }, + }, + { + id: '5c8b0375-305a-4f19-ae8e-07806b102000', + type: 'CancelAuthorization', + interactionId: 're_4qqhO89gsT', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Initial', + custom: { + type: { + typeId: 'type', + id: 'sctm_payment_cancel_reason', + }, + fields: { + reasonText: 'dummy reason', + }, + }, + }, + ], + interfaceInteractions: [], + paymentMethodInfo: { + method: 'creditcard', + }, + }; + + const mollieRefund: Refund = { + resource: 'refund', + id: CTPaymentMocked.transactions[3].interactionId, + description: 'Order', + amount: { + currency: 'EUR', + value: '5.95', + }, + status: 'pending', + metadata: '{"bookkeeping_id":12345}', + paymentId: 'tr_7UhSN1zuXS', + createdAt: '2023-03-14T17:09:02.0Z', + _links: { + self: { + href: '...', + type: 'application/hal+json', + }, + payment: { + href: 'https://api.mollie.com/v2/payments/tr_7UhSN1zuXS', + type: 'application/hal+json', + }, + documentation: { + href: '...', + type: 'text/html', + }, + }, + } as Refund; + + (getPaymentRefund as jest.Mock).mockReturnValueOnce(mollieRefund); + + (cancelPaymentRefund as jest.Mock).mockReturnValueOnce(true); + + (getPaymentCancelActions as jest.Mock).mockReturnValueOnce([]); + + await handlePaymentCancelRefund(CTPaymentMocked); + + expect(getPaymentRefund).toBeCalledTimes(1); + expect(getPaymentRefund).toBeCalledWith(CTPaymentMocked.transactions[3].interactionId, { + paymentId: CTPaymentMocked.transactions[1].interactionId, + }); + expect(cancelPaymentRefund).toBeCalledTimes(1); + expect(cancelPaymentRefund).toBeCalledWith(CTPaymentMocked.transactions[3].interactionId, { + paymentId: CTPaymentMocked.transactions[1].interactionId, + }); + }); + + it('should throw error if valid Success Charge transaction was not found (interactionId is defined in the Initial CancelAuthorization transaction)', async () => { + const CTPaymentMocked: Payment = { + id: '5c8b0375-305a-4f19-ae8e-07806b101999', + version: 1, + createdAt: '2024-07-04T14:07:35.625Z', + lastModifiedAt: '2024-07-04T14:07:35.625Z', + amountPlanned: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + paymentStatus: {}, + transactions: [ + { + id: '5c8b0375-305a-4f19-ae8e-07806b101992', + type: 'Charge', + interactionId: 'tr_test123123', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Success', + }, + { + id: '5c8b0375-305a-4f19-ae8e-07806b101999', + type: 'Charge', + interactionId: 'tr_dummy', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Success', + }, + { + id: '5c8b0375-305a-4f19-ae8e-07806b102011', + type: 'Refund', + interactionId: 're_TEST', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Pending', + }, + { + id: '5c8b0375-305a-4f19-ae8e-07806b102000', + type: 'Refund', + interactionId: 're_4qqhO89gsT', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Pending', + custom: { + type: { + typeId: 'type', + id: 'custom-type', + }, + fields: { + [CustomFieldName.transactionRefundForMolliePayment]: 'tr_123123', + }, + }, + }, + { + id: '5c8b0375-305a-4f19-ae8e-07806b102000', + type: 'CancelAuthorization', + interactionId: 're_4qqhO89gsT', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Initial', + custom: { + type: { + typeId: 'type', + id: 'sctm_payment_cancel_reason', + }, + fields: { + reasonText: 'dummy reason', + }, + }, + }, + ], + interfaceInteractions: [], + paymentMethodInfo: { + method: 'creditcard', + }, + }; + + const mollieRefund: Refund = { + resource: 'refund', + id: CTPaymentMocked.transactions[3].interactionId, + description: 'Order', + amount: { + currency: 'EUR', + value: '5.95', + }, + status: 'pending', + metadata: '{"bookkeeping_id":12345}', + paymentId: 'tr_7UhSN1zuXS', + createdAt: '2023-03-14T17:09:02.0Z', + _links: { + self: { + href: '...', + type: 'application/hal+json', + }, + payment: { + href: 'https://api.mollie.com/v2/payments/tr_7UhSN1zuXS', + type: 'application/hal+json', + }, + documentation: { + href: '...', + type: 'text/html', + }, + }, + } as Refund; + + (getPaymentRefund as jest.Mock).mockReturnValueOnce(mollieRefund); + + (cancelPaymentRefund as jest.Mock).mockReturnValueOnce(true); + + (getPaymentCancelActions as jest.Mock).mockReturnValueOnce([]); + + try { + await handlePaymentCancelRefund(CTPaymentMocked); + } catch (error: any) { + expect(getPaymentRefund).toBeCalledTimes(0); + expect(cancelPaymentRefund).toBeCalledTimes(0); + + expect(error).toBeInstanceOf(CustomError); + expect((error as CustomError).message).toBe( + 'SCTM - handlePaymentCancelRefund - Cannot find the valid Success Charge transaction.', + ); + } + }); }); describe('Test handlePaymentWebhook', () => { diff --git a/processor/tests/utils/app.utils.spec.ts b/processor/tests/utils/app.utils.spec.ts index 60eda64..f76db6c 100644 --- a/processor/tests/utils/app.utils.spec.ts +++ b/processor/tests/utils/app.utils.spec.ts @@ -6,11 +6,12 @@ import { parseStringToJsonObject, removeEmptyProperties, roundSurchargeAmountToCent, + sortTransactionsByLatestCreationTime, validateEmail, } from '../../src/utils/app.utils'; import { logger } from '../../src/utils/logger.utils'; import CustomError from '../../src/errors/custom.error'; -import { Payment } from '@commercetools/platform-sdk'; +import { Payment, Transaction } from '@commercetools/platform-sdk'; import { SurchargeCost } from '../../src/types/commercetools.types'; describe('Test createDateNowString', () => { @@ -145,3 +146,63 @@ describe('Test roundSurchargeAmountToCent', () => { expect(roundSurchargeAmountToCent(surchargeAmountInEur, fractionDigits)).toBe(30100); }); }); + +describe('Test sortTransactionsByLatestCreationTime', () => { + it('should return the correct order', () => { + const data = [ + { + id: '39c1eae1-e9b4-45f0-ac18-7d83ec429cc8', + timestamp: '2024-06-24T08:28:43.474Z', + type: 'Authorization', + amount: { + type: 'centPrecision', + currencyCode: 'GBP', + centAmount: 61879, + fractionDigits: 2, + }, + interactionId: '12789fae-d6d6-4b66-9739-3a420dbda2a8', + state: 'Failure', + }, + { + id: '39c1eae1-e9b4-45f0-ac18-7d83ec429cde', + timestamp: '2024-06-24T08:29:43.474Z', + type: 'Authorization', + amount: { + type: 'centPrecision', + currencyCode: 'GBP', + centAmount: 61879, + fractionDigits: 2, + }, + interactionId: '12789fae-d6d6-4b66-9739-3a420dbda2a8', + state: 'Failure', + }, + { + id: '39c1eae1-e9b4-45f0-ac18-7d83ec429cd9', + timestamp: '2024-06-24T08:30:43.474Z', + type: 'Authorization', + amount: { + type: 'centPrecision', + currencyCode: 'GBP', + centAmount: 61879, + fractionDigits: 2, + }, + interactionId: '12789fae-d6d6-4b66-9739-3a420dbda2a8', + state: 'Failure', + }, + { + id: '39c1eae1-e9b4-45f0-ac18-7d83ec429111', + type: 'Authorization', + amount: { + type: 'centPrecision', + currencyCode: 'GBP', + centAmount: 61879, + fractionDigits: 2, + }, + interactionId: '12789fae-d6d6-4b66-9739-3a420dbda2a8', + state: 'Failure', + }, + ] as Transaction[]; + + expect(sortTransactionsByLatestCreationTime(data)).toStrictEqual([data[2], data[1], data[0], data[3]]); + }); +}); diff --git a/processor/tests/utils/config.utils.spec.ts b/processor/tests/utils/config.utils.spec.ts index b867d89..36918cb 100644 --- a/processor/tests/utils/config.utils.spec.ts +++ b/processor/tests/utils/config.utils.spec.ts @@ -12,6 +12,7 @@ describe('Test src/utils/config.utils.ts', () => { projectKey: process.env.CTP_PROJECT_KEY, scope: process.env.CTP_SCOPE, region: process.env.CTP_REGION, + authUrl: process.env.CTP_AUTH_URL, authMode: process.env.AUTHENTICATION_MODE, sessionAudience: process.env.CTP_SESSION_AUDIENCE, sessionIssuer: process.env.CTP_SESSION_ISSUER, diff --git a/processor/tests/validators/helpers.validators.spec.ts b/processor/tests/validators/helpers.validators.spec.ts index 509e604..5bcdc64 100644 --- a/processor/tests/validators/helpers.validators.spec.ts +++ b/processor/tests/validators/helpers.validators.spec.ts @@ -255,6 +255,7 @@ describe('Test helpers.validators.ts', () => { projectKey: process.env.CTP_PROJECT_KEY as string, scope: process.env.CTP_SCOPE as string, region: process.env.CTP_REGION as string, + authUrl: process.env.CTP_AUTH_URL as string, authMode: process.env.AUTHENTICATION_MODE as string, sessionAudience: process.env.CTP_SESSION_AUDIENCE as string, sessionIssuer: process.env.CTP_SESSION_ISSUER as string, @@ -282,6 +283,7 @@ describe('Test helpers.validators.ts', () => { projectKey: process.env.CTP_PROJECT_KEY as string, scope: process.env.CTP_SCOPE as string, region: process.env.CTP_REGION as string, + authUrl: process.env.CTP_AUTH_URL as string, authMode: process.env.AUTHENTICATION_MODE as string, sessionAudience: process.env.CTP_SESSION_AUDIENCE as string, sessionIssuer: process.env.CTP_SESSION_ISSUER as string, @@ -349,6 +351,7 @@ describe('test getValidateMessages', () => { projectKey: process.env.CTP_PROJECT_KEY as string, scope: process.env.CTP_SCOPE as string, region: process.env.CTP_REGION as string, + authUrl: process.env.CTP_AUTH_URL as string, authMode: process.env.AUTHENTICATION_MODE as string, sessionAudience: process.env.CTP_SESSION_AUDIENCE as string, sessionIssuer: process.env.CTP_SESSION_ISSUER as string, @@ -384,6 +387,7 @@ describe('test getValidateMessages', () => { projectKey: process.env.CTP_PROJECT_KEY as string, scope: process.env.CTP_SCOPE as string, region: process.env.CTP_REGION as string, + authUrl: process.env.CTP_AUTH_URL as string, authMode: process.env.AUTHENTICATION_MODE as string, sessionAudience: process.env.CTP_SESSION_AUDIENCE as string, sessionIssuer: process.env.CTP_SESSION_ISSUER as string, @@ -419,6 +423,7 @@ describe('test getValidateMessages', () => { projectKey: process.env.CTP_PROJECT_KEY as string, scope: process.env.CTP_SCOPE as string, region: process.env.CTP_REGION as string, + authUrl: process.env.CTP_AUTH_URL as string, authMode: process.env.AUTHENTICATION_MODE as string, sessionAudience: process.env.CTP_SESSION_AUDIENCE as string, sessionIssuer: process.env.CTP_SESSION_ISSUER as string,