diff --git a/docs/adr/0012-remove-obsoleted-api-call.md b/docs/adr/0012-remove-obsoleted-api-call.md
index 3c7052a57..f1b68411e 100644
--- a/docs/adr/0012-remove-obsoleted-api-call.md
+++ b/docs/adr/0012-remove-obsoleted-api-call.md
@@ -4,7 +4,7 @@ Date: 2023-02-24
## Status
-[Accepted](https://github.com/commercetools/commercetools-adyen-integration/pull/1050)
+[Deprecated](https://github.com/commercetools/commercetools-adyen-integration/pull/1050)
## Context
diff --git a/docs/adr/0013-add-support-for-advanced-checkout-flow.md b/docs/adr/0013-add-support-for-advanced-checkout-flow.md
new file mode 100644
index 000000000..1c2d7bf92
--- /dev/null
+++ b/docs/adr/0013-add-support-for-advanced-checkout-flow.md
@@ -0,0 +1,25 @@
+# 13. Add support fro advanced checkout flow
+
+Date: 2023-09-15
+
+## Status
+
+[Accepted]
+
+## Context
+
+In Adyen web component version 5, new endpoint `/sessions` is introduced. It allows merchant to create payment session
+before shopper selects payment methods and drastically simplify the checkout flow.
+
+For details, please refer to [Adyen Documentation](https://docs.adyen.com/online-payments/web-components).
+
+On the other hand, API calls for web component v4 can be used for advanced checkout flow, for details,
+please refer to [Adyen Documentation](https://docs.adyen.com/online-payments/build-your-integration/additional-use-cases/advanced-flow-integration/?platform=Web&integration=Components&version=5.39.1).
+
+## Decision
+
+- We add all required API payment calls to be able to support advanced checkout flow.
+
+## Consequences
+- The commercetools extension can be utilized for advanced checkout flow implementation.
+- The migration to the Checkout web components V5 should be easier since advanced flow support keeps backward compatibility.
diff --git a/extension/resources/web-components-payment-type.json b/extension/resources/web-components-payment-type.json
index e98f22bd7..af80bbbca 100644
--- a/extension/resources/web-components-payment-type.json
+++ b/extension/resources/web-components-payment-type.json
@@ -49,6 +49,50 @@
"inputHint": "MultiLine",
"required": false
},
+ {
+ "name": "makePaymentRequest",
+ "label": {
+ "en": "makePaymentRequest"
+ },
+ "type": {
+ "name": "String"
+ },
+ "inputHint": "MultiLine",
+ "required": false
+ },
+ {
+ "name": "makePaymentResponse",
+ "label": {
+ "en": "makePaymentResponse"
+ },
+ "type": {
+ "name": "String"
+ },
+ "inputHint": "MultiLine",
+ "required": false
+ },
+ {
+ "name": "submitAdditionalPaymentDetailsRequest",
+ "label": {
+ "en": "submitAdditionalPaymentDetailsRequest"
+ },
+ "type": {
+ "name": "String"
+ },
+ "inputHint": "MultiLine",
+ "required": false
+ },
+ {
+ "name": "submitAdditionalPaymentDetailsResponse",
+ "label": {
+ "en": "submitAdditionalPaymentDetailsResponse"
+ },
+ "type": {
+ "name": "String"
+ },
+ "inputHint": "MultiLine",
+ "required": false
+ },
{
"name": "languageCode",
"label": {
diff --git a/extension/src/config/constants.js b/extension/src/config/constants.js
index 00d166ed8..3cdd106c5 100644
--- a/extension/src/config/constants.js
+++ b/extension/src/config/constants.js
@@ -7,6 +7,12 @@ export default {
CTP_INTERACTION_TYPE_CANCEL_PAYMENT: 'cancelPayment',
CTP_INTERACTION_TYPE_GET_PAYMENT_METHODS: 'getPaymentMethods',
CTP_CUSTOM_FIELD_GET_PAYMENT_METHODS_RESPONSE: 'getPaymentMethodsResponse',
+ CTP_INTERACTION_TYPE_MAKE_PAYMENT: 'makePayment',
+ CTP_CUSTOM_FIELD_MAKE_PAYMENT_RESPONSE: 'makePaymentResponse',
+ CTP_INTERACTION_TYPE_SUBMIT_ADDITIONAL_PAYMENT_DETAILS:
+ 'submitAdditionalPaymentDetails',
+ CTP_CUSTOM_FIELD_SUBMIT_ADDITIONAL_PAYMENT_DETAILS_RESPONSE:
+ 'submitAdditionalPaymentDetailsResponse',
CTP_INTERACTION_TYPE_MANUAL_CAPTURE: 'manualCapture',
CTP_INTERACTION_TYPE_REFUND: 'refund',
diff --git a/extension/src/paymentHandler/line-items-utils.js b/extension/src/paymentHandler/line-items-utils.js
new file mode 100644
index 000000000..e756b0dae
--- /dev/null
+++ b/extension/src/paymentHandler/line-items-utils.js
@@ -0,0 +1,110 @@
+import _ from 'lodash'
+import ctpClientBuilder from '../ctp.js'
+import config from '../config/config.js'
+
+const ADYEN_PERCENTAGE_MINOR_UNIT = 10000
+const KLARNA_DEFAULT_LINE_ITEM_NAME = 'item'
+const KLARNA_DEFAULT_SHIPPING_METHOD_DESCRIPTION = 'shipping'
+
+async function fetchMatchingCart(paymentObject, ctpProjectKey) {
+ const ctpConfig = config.getCtpConfig(ctpProjectKey)
+ const ctpClient = await ctpClientBuilder.get(ctpConfig)
+ const { body } = await ctpClient.fetch(
+ ctpClient.builder.carts
+ .where(`paymentInfo(payments(id="${paymentObject.id}"))`)
+ .expand('shippingInfo.shippingMethod')
+ )
+ return body.results[0]
+}
+
+function createLineItems(payment, cart) {
+ const lineItems = []
+ const locales = _getLocales(cart, payment)
+
+ cart.lineItems.forEach((item) => {
+ if (item.taxRate)
+ lineItems.push(_createAdyenLineItemFromLineItem(item, locales))
+ })
+
+ cart.customLineItems.forEach((item) => {
+ if (item.taxRate)
+ lineItems.push(_createAdyenLineItemFromCustomLineItem(item, locales))
+ })
+
+ const { shippingInfo } = cart
+ if (shippingInfo && shippingInfo.taxRate)
+ lineItems.push(_createShippingInfoAdyenLineItem(shippingInfo, locales))
+
+ return lineItems
+}
+
+function _getLocales(cart, payment) {
+ const locales = []
+ let paymentLanguage = payment.custom && payment.custom.fields['languageCode']
+ if (!paymentLanguage) paymentLanguage = cart.locale
+ if (paymentLanguage) locales.push(paymentLanguage)
+ return locales
+}
+
+function _createAdyenLineItemFromLineItem(ctpLineItem, locales) {
+ return {
+ id: ctpLineItem.variant.sku,
+ quantity: ctpLineItem.quantity,
+ description: _localizeOrFallback(
+ ctpLineItem.name,
+ locales,
+ KLARNA_DEFAULT_LINE_ITEM_NAME
+ ),
+ amountIncludingTax: ctpLineItem.price.value.centAmount,
+ taxPercentage: ctpLineItem.taxRate.amount * ADYEN_PERCENTAGE_MINOR_UNIT,
+ }
+}
+
+function _createAdyenLineItemFromCustomLineItem(ctpLineItem, locales) {
+ return {
+ id: ctpLineItem.id,
+ quantity: ctpLineItem.quantity,
+ description: _localizeOrFallback(
+ ctpLineItem.name,
+ locales,
+ KLARNA_DEFAULT_LINE_ITEM_NAME
+ ),
+ amountIncludingTax: ctpLineItem.money.centAmount,
+ taxPercentage: ctpLineItem.taxRate.amount * ADYEN_PERCENTAGE_MINOR_UNIT,
+ }
+}
+
+function _createShippingInfoAdyenLineItem(shippingInfo, locales) {
+ return {
+ id: `${shippingInfo.shippingMethodName}`,
+ quantity: 1, // always one shipment item so far
+ description:
+ _getShippingMethodDescription(shippingInfo, locales) ||
+ KLARNA_DEFAULT_SHIPPING_METHOD_DESCRIPTION,
+ amountIncludingTax: shippingInfo.price.centAmount,
+ taxPercentage: shippingInfo.taxRate.amount * ADYEN_PERCENTAGE_MINOR_UNIT,
+ }
+}
+
+function _getShippingMethodDescription(shippingInfo, locales) {
+ const shippingMethod = shippingInfo.shippingMethod?.obj
+ if (shippingMethod) {
+ return _localizeOrFallback(
+ shippingMethod.localizedDescription,
+ locales,
+ shippingMethod.description
+ )
+ }
+ return shippingInfo.shippingMethodName
+}
+
+function _localizeOrFallback(localizedString, locales, fallback) {
+ let result
+ if (_.size(localizedString) > 0) {
+ const locale = locales?.find((l) => localizedString[l])
+ result = localizedString[locale] || Object.values(localizedString)[0]
+ } else result = fallback
+ return result
+}
+
+export default { fetchMatchingCart, createLineItems }
diff --git a/extension/src/paymentHandler/make-lineitems-payment.handler.js b/extension/src/paymentHandler/make-lineitems-payment.handler.js
new file mode 100644
index 000000000..2aad9cc7a
--- /dev/null
+++ b/extension/src/paymentHandler/make-lineitems-payment.handler.js
@@ -0,0 +1,28 @@
+import makePaymentHandler from './make-payment.handler.js'
+import lineItemsUtils from './line-items-utils.js'
+
+async function execute(paymentObject) {
+ const makePaymentRequestObj = JSON.parse(
+ paymentObject.custom.fields.makePaymentRequest
+ )
+ const commercetoolsProjectKey =
+ paymentObject.custom.fields.commercetoolsProjectKey
+ if (!makePaymentRequestObj.lineItems) {
+ const ctpCart = await lineItemsUtils.fetchMatchingCart(
+ paymentObject,
+ commercetoolsProjectKey
+ )
+ if (ctpCart) {
+ makePaymentRequestObj.lineItems = lineItemsUtils.createLineItems(
+ paymentObject,
+ ctpCart
+ )
+ paymentObject.custom.fields.makePaymentRequest = JSON.stringify(
+ makePaymentRequestObj
+ )
+ }
+ }
+
+ return makePaymentHandler.execute(paymentObject)
+}
+export default { execute }
diff --git a/extension/src/paymentHandler/make-payment.handler.js b/extension/src/paymentHandler/make-payment.handler.js
new file mode 100644
index 000000000..6752028c4
--- /dev/null
+++ b/extension/src/paymentHandler/make-payment.handler.js
@@ -0,0 +1,64 @@
+import {
+ createAddInterfaceInteractionAction,
+ createSetCustomFieldAction,
+ createSetMethodInfoMethodAction,
+ createSetMethodInfoNameAction,
+ createAddTransactionActionByResponse,
+ getPaymentKeyUpdateAction,
+} from './payment-utils.js'
+import c from '../config/constants.js'
+import { makePayment } from '../service/web-component-service.js'
+
+async function execute(paymentObject) {
+ const makePaymentRequestObj = JSON.parse(
+ paymentObject.custom.fields.makePaymentRequest
+ )
+ const adyenMerchantAccount = paymentObject.custom.fields.adyenMerchantAccount
+ const commercetoolsProjectKey =
+ paymentObject.custom.fields.commercetoolsProjectKey
+ const { request, response } = await makePayment(
+ adyenMerchantAccount,
+ commercetoolsProjectKey,
+ makePaymentRequestObj
+ )
+ const actions = [
+ createAddInterfaceInteractionAction({
+ request,
+ response,
+ type: c.CTP_INTERACTION_TYPE_MAKE_PAYMENT,
+ }),
+ createSetCustomFieldAction(
+ c.CTP_CUSTOM_FIELD_MAKE_PAYMENT_RESPONSE,
+ response
+ ),
+ ]
+
+ const requestBodyJson = JSON.parse(request.body)
+ const paymentMethod = requestBodyJson?.paymentMethod?.type
+ if (paymentMethod) {
+ actions.push(createSetMethodInfoMethodAction(paymentMethod))
+ const action = createSetMethodInfoNameAction(paymentMethod)
+ if (action) actions.push(action)
+ }
+
+ const updatePaymentAction = getPaymentKeyUpdateAction(
+ paymentObject.key,
+ request,
+ response
+ )
+ if (updatePaymentAction) actions.push(updatePaymentAction)
+
+ const addTransactionAction = createAddTransactionActionByResponse(
+ paymentObject.amountPlanned.centAmount,
+ paymentObject.amountPlanned.currencyCode,
+ response
+ )
+
+ if (addTransactionAction) actions.push(addTransactionAction)
+
+ return {
+ actions,
+ }
+}
+
+export default { execute }
diff --git a/extension/src/paymentHandler/payment-handler.js b/extension/src/paymentHandler/payment-handler.js
index 76943bcb5..64e3fdc9a 100644
--- a/extension/src/paymentHandler/payment-handler.js
+++ b/extension/src/paymentHandler/payment-handler.js
@@ -1,5 +1,7 @@
import { withPayment } from '../validator/validator-builder.js'
-
+import makePaymentHandler from './make-payment.handler.js'
+import makeLineitemsPaymentHandler from './make-lineitems-payment.handler.js'
+import submitPaymentDetailsHandler from './submit-payment-details.handler.js'
import manualCaptureHandler from './manual-capture.handler.js'
import cancelHandler from './cancel-payment.handler.js'
import refundHandler from './refund-payment.handler.js'
@@ -75,6 +77,20 @@ function _getPaymentHandlers(paymentObject) {
handlers.push(getPaymentMethodsHandler)
}
+ if (customFields.makePaymentRequest && !customFields.makePaymentResponse) {
+ const makePaymentRequestObj = JSON.parse(customFields.makePaymentRequest)
+ if (_requiresLineItems(makePaymentRequestObj))
+ handlers.push(makeLineitemsPaymentHandler)
+ else handlers.push(makePaymentHandler)
+ }
+
+ if (
+ customFields.makePaymentResponse &&
+ customFields.submitAdditionalPaymentDetailsRequest &&
+ !customFields.submitAdditionalPaymentDetailsResponse
+ )
+ handlers.push(submitPaymentDetailsHandler)
+
if (
customFields.createSessionRequest &&
!customFields.createSessionResponse
@@ -153,10 +169,9 @@ function _validatePaymentRequest(paymentObject, authToken) {
return null
}
-function _requiresLineItems(createSessionRequestObj) {
- const addCommercetoolsLineItemsFlag = _getAddCommercetoolsLineItemsFlag(
- createSessionRequestObj
- )
+function _requiresLineItems(requestObj) {
+ const addCommercetoolsLineItemsFlag =
+ _getAddCommercetoolsLineItemsFlag(requestObj)
if (
addCommercetoolsLineItemsFlag === true ||
addCommercetoolsLineItemsFlag === false
@@ -171,19 +186,18 @@ function _requiresLineItems(createSessionRequestObj) {
return false
}
-function _getAddCommercetoolsLineItemsFlag(createSessionRequestObj) {
+function _getAddCommercetoolsLineItemsFlag(requestObj) {
// The function is tend to be used to check values on the field: true, false, undefined,
// or the value set but not to true/false
// in case of the undefined or other than true/false, the function returns undefined:
// it means the other fallbacks have to be checked to decide adding line items
let addCommercetoolsLineItems
- if ('addCommercetoolsLineItems' in createSessionRequestObj) {
+ if ('addCommercetoolsLineItems' in requestObj) {
if (
- createSessionRequestObj.addCommercetoolsLineItems === true ||
- createSessionRequestObj.addCommercetoolsLineItems === false
+ requestObj.addCommercetoolsLineItems === true ||
+ requestObj.addCommercetoolsLineItems === false
) {
- addCommercetoolsLineItems =
- createSessionRequestObj.addCommercetoolsLineItems
+ addCommercetoolsLineItems = requestObj.addCommercetoolsLineItems
}
}
return addCommercetoolsLineItems
diff --git a/extension/src/paymentHandler/sessions-line-items-request.handler.js b/extension/src/paymentHandler/sessions-line-items-request.handler.js
index 5cacc4151..ca2e9ee9e 100644
--- a/extension/src/paymentHandler/sessions-line-items-request.handler.js
+++ b/extension/src/paymentHandler/sessions-line-items-request.handler.js
@@ -1,11 +1,5 @@
-import _ from 'lodash'
-import ctpClientBuilder from '../ctp.js'
import sessionRequestHandler from './sessions-request.handler.js'
-import config from '../config/config.js'
-
-const ADYEN_PERCENTAGE_MINOR_UNIT = 10000
-const KLARNA_DEFAULT_LINE_ITEM_NAME = 'item'
-const KLARNA_DEFAULT_SHIPPING_METHOD_DESCRIPTION = 'shipping'
+import lineItemsUtils from './line-items-utils.js'
async function execute(paymentObject) {
const createSessionRequestObj = JSON.parse(
@@ -14,12 +8,12 @@ async function execute(paymentObject) {
const commercetoolsProjectKey =
paymentObject.custom.fields.commercetoolsProjectKey
if (!createSessionRequestObj.lineItems) {
- const ctpCart = await _fetchMatchingCart(
+ const ctpCart = await lineItemsUtils.fetchMatchingCart(
paymentObject,
commercetoolsProjectKey
)
if (ctpCart) {
- createSessionRequestObj.lineItems = createLineItems(
+ createSessionRequestObj.lineItems = lineItemsUtils.createLineItems(
paymentObject,
ctpCart
)
@@ -32,105 +26,4 @@ async function execute(paymentObject) {
return sessionRequestHandler.execute(paymentObject)
}
-async function _fetchMatchingCart(paymentObject, ctpProjectKey) {
- const ctpConfig = config.getCtpConfig(ctpProjectKey)
- const ctpClient = await ctpClientBuilder.get(ctpConfig)
- const { body } = await ctpClient.fetch(
- ctpClient.builder.carts
- .where(`paymentInfo(payments(id="${paymentObject.id}"))`)
- .expand('shippingInfo.shippingMethod')
- )
- return body.results[0]
-}
-
-function createLineItems(payment, cart) {
- const lineItems = []
- const locales = _getLocales(cart, payment)
-
- cart.lineItems.forEach((item) => {
- if (item.taxRate)
- lineItems.push(_createAdyenLineItemFromLineItem(item, locales))
- })
-
- cart.customLineItems.forEach((item) => {
- if (item.taxRate)
- lineItems.push(_createAdyenLineItemFromCustomLineItem(item, locales))
- })
-
- const { shippingInfo } = cart
- if (shippingInfo && shippingInfo.taxRate)
- lineItems.push(_createShippingInfoAdyenLineItem(shippingInfo, locales))
-
- return lineItems
-}
-
-function _getLocales(cart, payment) {
- const locales = []
- let paymentLanguage = payment.custom && payment.custom.fields['languageCode']
- if (!paymentLanguage) paymentLanguage = cart.locale
- if (paymentLanguage) locales.push(paymentLanguage)
- return locales
-}
-
-function _createAdyenLineItemFromLineItem(ctpLineItem, locales) {
- return {
- id: ctpLineItem.variant.sku,
- quantity: ctpLineItem.quantity,
- description: _localizeOrFallback(
- ctpLineItem.name,
- locales,
- KLARNA_DEFAULT_LINE_ITEM_NAME
- ),
- amountIncludingTax: ctpLineItem.price.value.centAmount,
- taxPercentage: ctpLineItem.taxRate.amount * ADYEN_PERCENTAGE_MINOR_UNIT,
- }
-}
-
-function _createAdyenLineItemFromCustomLineItem(ctpLineItem, locales) {
- return {
- id: ctpLineItem.id,
- quantity: ctpLineItem.quantity,
- description: _localizeOrFallback(
- ctpLineItem.name,
- locales,
- KLARNA_DEFAULT_LINE_ITEM_NAME
- ),
- amountIncludingTax: ctpLineItem.money.centAmount,
- taxPercentage: ctpLineItem.taxRate.amount * ADYEN_PERCENTAGE_MINOR_UNIT,
- }
-}
-
-function _createShippingInfoAdyenLineItem(shippingInfo, locales) {
- return {
- id: `${shippingInfo.shippingMethodName}`,
- quantity: 1, // always one shipment item so far
- description:
- _getShippingMethodDescription(shippingInfo, locales) ||
- KLARNA_DEFAULT_SHIPPING_METHOD_DESCRIPTION,
- amountIncludingTax: shippingInfo.price.centAmount,
- taxPercentage: shippingInfo.taxRate.amount * ADYEN_PERCENTAGE_MINOR_UNIT,
- }
-}
-
-function _getShippingMethodDescription(shippingInfo, locales) {
- const shippingMethod = shippingInfo.shippingMethod?.obj
- if (shippingMethod) {
- return _localizeOrFallback(
- shippingMethod.localizedDescription,
- locales,
- shippingMethod.description
- )
- }
- return shippingInfo.shippingMethodName
-}
-
-function _localizeOrFallback(localizedString, locales, fallback) {
- let result
- if (_.size(localizedString) > 0) {
- const locale = locales?.find((l) => localizedString[l])
- result = localizedString[locale] || Object.values(localizedString)[0]
- } else result = fallback
- return result
-}
-
export default { execute }
diff --git a/extension/src/paymentHandler/submit-payment-details.handler.js b/extension/src/paymentHandler/submit-payment-details.handler.js
new file mode 100644
index 000000000..3882f4225
--- /dev/null
+++ b/extension/src/paymentHandler/submit-payment-details.handler.js
@@ -0,0 +1,102 @@
+import _ from 'lodash'
+import { submitAdditionalPaymentDetails } from '../service/web-component-service.js'
+import {
+ createAddInterfaceInteractionAction,
+ createSetCustomFieldAction,
+ createAddTransactionActionByResponse,
+ getPaymentKeyUpdateAction,
+} from './payment-utils.js'
+import c from '../config/constants.js'
+
+const { CTP_INTERACTION_TYPE_SUBMIT_ADDITIONAL_PAYMENT_DETAILS } = c
+
+async function execute(paymentObject) {
+ const actions = []
+ const submitAdditionalDetailsRequestObj = JSON.parse(
+ paymentObject.custom.fields.submitAdditionalPaymentDetailsRequest
+ )
+ const adyenMerchantAccount = paymentObject.custom.fields.adyenMerchantAccount
+ const commercetoolsProjectKey =
+ paymentObject.custom.fields.commercetoolsProjectKey
+ if (!submitAdditionalDetailsRequestObj.paymentData) {
+ const makePaymentResponseObj = JSON.parse(
+ paymentObject.custom.fields.makePaymentResponse
+ )
+ submitAdditionalDetailsRequestObj.paymentData =
+ makePaymentResponseObj.paymentData
+ }
+ if (_isNewRequest(submitAdditionalDetailsRequestObj, paymentObject)) {
+ const { request, response } = await submitAdditionalPaymentDetails(
+ adyenMerchantAccount,
+ commercetoolsProjectKey,
+ submitAdditionalDetailsRequestObj
+ )
+ actions.push(
+ createAddInterfaceInteractionAction({
+ request,
+ response,
+ type: c.CTP_INTERACTION_TYPE_SUBMIT_ADDITIONAL_PAYMENT_DETAILS,
+ }),
+ createSetCustomFieldAction(
+ c.CTP_CUSTOM_FIELD_SUBMIT_ADDITIONAL_PAYMENT_DETAILS_RESPONSE,
+ response
+ )
+ )
+
+ if (
+ !_hasTransactionWithPspReference(response.pspReference, paymentObject)
+ ) {
+ const addTransactionAction = createAddTransactionActionByResponse(
+ paymentObject.amountPlanned.centAmount,
+ paymentObject.amountPlanned.currencyCode,
+ response
+ )
+
+ if (addTransactionAction) actions.push(addTransactionAction)
+ }
+ const updatePaymentAction = getPaymentKeyUpdateAction(
+ paymentObject.key,
+ request,
+ response
+ )
+ if (updatePaymentAction) actions.push(updatePaymentAction)
+ }
+ return {
+ actions,
+ }
+}
+
+function _isNewRequest(
+ submitAdditionalPaymentDetailsRequestObj,
+ paymentObject
+) {
+ const interfaceInteraction = paymentObject.interfaceInteractions.find(
+ (interaction) =>
+ interaction.fields.type ===
+ CTP_INTERACTION_TYPE_SUBMIT_ADDITIONAL_PAYMENT_DETAILS
+ )
+ if (!interfaceInteraction)
+ // request is new if there are no requests yet
+ return true
+ const oldSubmitDetailsRequest = interfaceInteraction.fields.request
+ if (oldSubmitDetailsRequest) {
+ const oldSubmitDetailsRequestObj = JSON.parse(oldSubmitDetailsRequest)
+ const newSubmitDetailsRequestObj = _.cloneDeep(
+ submitAdditionalPaymentDetailsRequestObj
+ )
+ delete newSubmitDetailsRequestObj.merchantAccount
+ if (!_.isEqual(oldSubmitDetailsRequestObj, newSubmitDetailsRequestObj))
+ // request is new if new and old requests are different
+ return true
+ }
+
+ return false
+}
+
+function _hasTransactionWithPspReference(pspReference, paymentObject) {
+ return paymentObject.transactions.some(
+ (transaction) => transaction.interactionId === pspReference
+ )
+}
+
+export default { execute }
diff --git a/extension/src/service/web-component-service.js b/extension/src/service/web-component-service.js
index 0e4cc91c5..81710be6b 100644
--- a/extension/src/service/web-component-service.js
+++ b/extension/src/service/web-component-service.js
@@ -14,6 +14,41 @@ async function getPaymentMethods(merchantAccount, getPaymentMethodsRequestObj) {
)
}
+async function makePayment(
+ merchantAccount,
+ commercetoolsProjectKey,
+ makePaymentRequestObj
+) {
+ const adyenCredentials = config.getAdyenConfig(merchantAccount)
+ extendRequestObjWithMetadata(makePaymentRequestObj, commercetoolsProjectKey)
+ await extendRequestObjWithApplicationInfo(makePaymentRequestObj)
+ removeAddCommercetoolsLineItemsField(makePaymentRequestObj)
+ return callAdyen(
+ `${adyenCredentials.apiBaseUrl}/payments`,
+ merchantAccount,
+ adyenCredentials.apiKey,
+ makePaymentRequestObj
+ )
+}
+
+function submitAdditionalPaymentDetails(
+ merchantAccount,
+ commercetoolsProjectKey,
+ submitAdditionalPaymentDetailsRequestObj
+) {
+ const adyenCredentials = config.getAdyenConfig(merchantAccount)
+ extendRequestObjWithMetadata(
+ submitAdditionalPaymentDetailsRequestObj,
+ commercetoolsProjectKey
+ )
+ return callAdyen(
+ `${adyenCredentials.apiBaseUrl}/payments/details`,
+ merchantAccount,
+ adyenCredentials.apiKey,
+ submitAdditionalPaymentDetailsRequestObj
+ )
+}
+
function removeAddCommercetoolsLineItemsField(createSessionRequestObj) {
// Otherwise adyen might return a 400 response with the following message:
// Structure of PaymentRequest contains the following unknown fields: [addCommercetoolsLineItems]
@@ -249,6 +284,8 @@ function buildRequest(adyenMerchantAccount, adyenApiKey, requestObj, headers) {
export {
getPaymentMethods,
+ makePayment,
+ submitAdditionalPaymentDetails,
manualCapture,
refund,
cancelPayment,
diff --git a/extension/src/validator/error-messages.js b/extension/src/validator/error-messages.js
index 6fe18c381..15ebc58b7 100644
--- a/extension/src/validator/error-messages.js
+++ b/extension/src/validator/error-messages.js
@@ -1,11 +1,22 @@
export default {
CREATE_SESSION_REQUEST_INVALID_JSON:
'createSession does not contain valid JSON.',
- AMOUNT_PLANNED_NOT_SAME:
+ GET_PAYMENT_METHODS_REQUEST_INVALID_JSON:
+ 'getPaymentMethodsRequest does not contain valid JSON.',
+ MAKE_PAYMENT_REQUEST_INVALID_JSON:
+ 'makePaymentRequest does not contain valid JSON.',
+ SUBMIT_ADDITIONAL_PAYMENT_DETAILS_REQUEST_INVALID_JSON:
+ 'submitAdditionalPaymentDetailsRequest does not contain valid JSON.',
+ MAKE_PAYMENT_AMOUNT_PLANNED_NOT_SAME:
+ 'amountPlanned field must be the same as the amount in ' +
+ 'makePaymentRequest in the interface interactions or makePaymentRequest in the custom field',
+ CREATE_SESSION_AMOUNT_PLANNED_NOT_SAME:
'amountPlanned field must be the same as the amount in ' +
'createSessionRequest in the interface interactions or createSessionRequest in the custom field',
CREATE_SESSION_REQUEST_MISSING_REFERENCE:
'Required "reference" field is missing in createSessionReqeust.',
+ MAKE_PAYMENT_REQUEST_MISSING_REFERENCE:
+ 'Required "reference" field is missing in makePaymentRequest.',
MISSING_REQUIRED_FIELDS_CTP_PROJECT_KEY:
'Required field "commercetoolsProjectKey" is missing or empty.',
MISSING_REQUIRED_FIELDS_ADYEN_MERCHANT_ACCOUNT:
diff --git a/extension/src/validator/validator-builder.js b/extension/src/validator/validator-builder.js
index 6c0b72e94..89a670fa1 100644
--- a/extension/src/validator/validator-builder.js
+++ b/extension/src/validator/validator-builder.js
@@ -40,6 +40,20 @@ function withPayment(paymentObject) {
errors.createSessionRequest =
errorMessages.CREATE_SESSION_REQUEST_INVALID_JSON
+ if (!isValidJSON(paymentObject.custom.fields.getPaymentMethodsRequest))
+ errors.getPaymentMethodsRequest =
+ errorMessages.GET_PAYMENT_METHODS_REQUEST_INVALID_JSON
+ if (!isValidJSON(paymentObject.custom.fields.makePaymentRequest))
+ errors.makePaymentRequest =
+ errorMessages.MAKE_PAYMENT_REQUEST_INVALID_JSON
+ if (
+ !isValidJSON(
+ paymentObject.custom.fields.submitAdditionalPaymentDetailsRequest
+ )
+ )
+ errors.submitAdditionalPaymentDetailsRequest =
+ errorMessages.SUBMIT_ADDITIONAL_PAYMENT_DETAILS_REQUEST_INVALID_JSON
+
if (!isValidJSON(paymentObject.custom.fields.getCarbonOffsetCostsRequest))
errors.getCarbonOffsetCostsRequest =
errorMessages.GET_CARBON_OFFSET_COSTS_REQUEST_INVALID_JSON
@@ -49,7 +63,13 @@ function withPayment(paymentObject) {
return this
},
validateReference() {
- if (!paymentObject.custom || errors.createSessionRequest) return this
+ if (
+ !paymentObject.custom ||
+ errors.createSessionRequest ||
+ errors.makePaymentRequest
+ )
+ return this
+
if (
paymentObject.custom.fields.createSessionRequest &&
!paymentObject.custom.fields.createSessionResponse
@@ -61,17 +81,37 @@ function withPayment(paymentObject) {
errors.missingReference =
errorMessages.CREATE_SESSION_REQUEST_MISSING_REFERENCE
}
+
+ if (
+ paymentObject.custom.fields.makePaymentRequest &&
+ !paymentObject.custom.fields.makePaymentResponse
+ ) {
+ const makePaymentRequestObj = JSON.parse(
+ paymentObject.custom.fields.makePaymentRequest
+ )
+ if (!makePaymentRequestObj.reference)
+ errors.missingReference =
+ errorMessages.MAKE_PAYMENT_REQUEST_MISSING_REFERENCE
+ }
+
return this
},
validateAmountPlanned() {
- let amount
+ let createSessionAmount
+ let makePaymentAmount
const createSessionRequestInterfaceInteraction =
getLatestInterfaceInteraction(
paymentObject.interfaceInteractions,
c.CTP_INTERACTION_TYPE_CREATE_SESSION
)
+ const makePaymentRequestInterfaceInteraction =
+ getLatestInterfaceInteraction(
+ paymentObject.interfaceInteractions,
+ c.CTP_INTERACTION_TYPE_MAKE_PAYMENT
+ )
+
if (createSessionRequestInterfaceInteraction)
- amount = JSON.parse(
+ createSessionAmount = JSON.parse(
createSessionRequestInterfaceInteraction.fields.request
).amount
else {
@@ -80,19 +120,48 @@ function withPayment(paymentObject) {
paymentObject.custom.fields &&
paymentObject.custom.fields.createSessionRequest
if (createSessionRequestString)
- amount = JSON.parse(createSessionRequestString).amount
+ createSessionAmount = JSON.parse(createSessionRequestString).amount
+ }
+
+ if (makePaymentRequestInterfaceInteraction)
+ makePaymentAmount = JSON.parse(
+ makePaymentRequestInterfaceInteraction.fields.request
+ ).amount
+ else {
+ const makePaymentRequestString =
+ paymentObject.custom &&
+ paymentObject.custom.fields &&
+ paymentObject.custom.fields.makePaymentRequest
+ if (makePaymentRequestString)
+ makePaymentAmount = JSON.parse(makePaymentRequestString).amount
}
- if (amount) {
- const amountInCreateSessionRequest = Number(amount.value)
+
+ if (createSessionAmount) {
+ const amountInCreateSessionRequest = Number(createSessionAmount.value)
const amountPlannedValue = paymentObject.amountPlanned.centAmount
- const currencyInCreateSessionRequest = amount.currency
+ const currencyInCreateSessionRequest = createSessionAmount.currency
const currencyInAmountPlanned = paymentObject.amountPlanned.currencyCode
if (
amountInCreateSessionRequest !== amountPlannedValue ||
currencyInCreateSessionRequest !== currencyInAmountPlanned
)
- errors.amountPlanned = errorMessages.AMOUNT_PLANNED_NOT_SAME
+ errors.amountPlanned =
+ errorMessages.CREATE_SESSION_AMOUNT_PLANNED_NOT_SAME
}
+
+ if (makePaymentAmount) {
+ const amountInMakePaymentRequest = Number(makePaymentAmount.value)
+ const amountPlannedValue = paymentObject.amountPlanned.centAmount
+ const currencyInMakePaymentRequest = makePaymentAmount.currency
+ const currencyInAmountPlanned = paymentObject.amountPlanned.currencyCode
+ if (
+ amountInMakePaymentRequest !== amountPlannedValue ||
+ currencyInMakePaymentRequest !== currencyInAmountPlanned
+ )
+ errors.amountPlanned =
+ errorMessages.MAKE_PAYMENT_AMOUNT_PLANNED_NOT_SAME
+ }
+
return this
},
validatePaymentPspReference() {
diff --git a/extension/test/e2e/credit-card-3ds-redirect.spec.js b/extension/test/e2e/credit-card-3ds-redirect.spec.js
index 906ca28d3..df7154e13 100644
--- a/extension/test/e2e/credit-card-3ds-redirect.spec.js
+++ b/extension/test/e2e/credit-card-3ds-redirect.spec.js
@@ -48,7 +48,8 @@ function setRoute() {
}
// Flow description: https://docs.adyen.com/checkout/3d-secure/redirect-3ds2-3ds1/web-component
-describe('::creditCardPayment3dsRedirect::', () => {
+// Skipped because Adyen test cards do not return redirect response anymore
+describe.skip('::creditCardPayment3dsRedirect::', () => {
let browser
let ctpClient
diff --git a/extension/test/e2e/credit-card-advanced-flow.spec.js b/extension/test/e2e/credit-card-advanced-flow.spec.js
new file mode 100644
index 000000000..02b18d1db
--- /dev/null
+++ b/extension/test/e2e/credit-card-advanced-flow.spec.js
@@ -0,0 +1,197 @@
+import ctpClientBuilder from '../../src/ctp.js'
+import { routes } from '../../src/routes.js'
+import config from '../../src/config/config.js'
+import httpUtils from '../../src/utils.js'
+import {
+ assertPayment,
+ createPayment,
+ initPuppeteerBrowser,
+ serveFile,
+} from './e2e-test-utils.js'
+import MakePaymentFormPage from './pageObjects/CreditCardMakePaymentFormPage.js'
+import RedirectPaymentFormPage from './pageObjects/RedirectPaymentFormPageAdvancedFlow.js'
+import CreditCardNativePage from './pageObjects/CreditCard3dsNativePage.js'
+
+const logger = httpUtils.getLogger()
+
+// Flow description: https://docs.adyen.com/checkout/3d-secure/native-3ds2/web-component
+describe('::creditCardAdvancedFlow::', () => {
+ let browser
+ let ctpClient
+ const adyenMerchantAccount = config.getAllAdyenMerchantAccounts()[0]
+ const ctpProjectKey = config.getAllCtpProjectKeys()[0]
+
+ beforeEach(async () => {
+ routes['/make-payment-form'] = async (request, response) => {
+ serveFile(
+ './test/e2e/fixtures/3ds-v2-make-payment-form.html',
+ request,
+ response
+ )
+ }
+ routes['/redirect-payment-form'] = async (request, response) => {
+ serveFile(
+ './test/e2e/fixtures/redirect-payment-form-advanced-flow.html',
+ request,
+ response
+ )
+ }
+ routes['/return-url'] = async (request, response) => {
+ const body = await httpUtils.collectRequestData(request)
+ return httpUtils.sendResponse({
+ response,
+ headers: {
+ 'Content-Type': 'text/html',
+ },
+ data:
+ '
' +
+ `${body[0].toString()}
`,
+ })
+ }
+
+ const ctpConfig = config.getCtpConfig(ctpProjectKey)
+ ctpClient = await ctpClientBuilder.get(ctpConfig)
+ browser = await initPuppeteerBrowser()
+ })
+
+ afterEach(async () => {
+ await browser.close()
+ })
+
+ it(
+ `when credit card issuer is Visa and credit card number is 4917 6100 0000 0000, ` +
+ 'then it should successfully finish the payment with advanced 3DS native authentication flow',
+ async () => {
+ // See more: https://docs.adyen.com/development-resources/test-cards/test-card-numbers
+ const creditCardNumber = '4917 6100 0000 0000'
+ const creditCardDate = '03/30'
+ const creditCardCvc = '737'
+
+ const baseUrl = config.getModuleConfig().apiExtensionBaseUrl
+ const clientKey = config.getAdyenConfig(adyenMerchantAccount).clientKey
+ let paymentAfterAuthentication
+ try {
+ const browserTab = await browser.newPage()
+ const paymentAfterMakePayment = await makePayment({
+ browserTab,
+ baseUrl,
+ creditCardNumber,
+ creditCardDate,
+ creditCardCvc,
+ clientKey,
+ })
+ logger.debug(
+ 'creditCardAdvancedFlow::paymentAfterMakePayment:',
+ JSON.stringify(paymentAfterMakePayment)
+ )
+ paymentAfterAuthentication = await performChallengeFlow({
+ payment: paymentAfterMakePayment,
+ browserTab,
+ baseUrl,
+ clientKey,
+ })
+ logger.debug(
+ 'creditCardAdvancedFlow::paymentAfterAuthentication:',
+ JSON.stringify(paymentAfterAuthentication)
+ )
+ } catch (err) {
+ logger.error('creditCardAdvancedFlow::errors', JSON.stringify(err))
+ }
+ assertPayment(paymentAfterAuthentication)
+ }
+ )
+
+ async function makePayment({
+ browserTab,
+ baseUrl,
+ creditCardNumber,
+ creditCardDate,
+ creditCardCvc,
+ clientKey,
+ }) {
+ const makePaymentFormPage = new MakePaymentFormPage(browserTab, baseUrl)
+ await makePaymentFormPage.goToThisPage()
+ const makePaymentRequest = await makePaymentFormPage.getMakePaymentRequest({
+ creditCardNumber,
+ creditCardDate,
+ creditCardCvc,
+ clientKey,
+ })
+ let payment = null
+ const startTime = new Date().getTime()
+ try {
+ payment = await createPayment(
+ ctpClient,
+ adyenMerchantAccount,
+ ctpProjectKey,
+ makePaymentRequest
+ )
+ } finally {
+ const endTime = new Date().getTime()
+ logger.debug('creditCardAdvancedFlow::makePayment:', endTime - startTime)
+ }
+ return payment
+ }
+
+ async function performChallengeFlow({
+ payment,
+ browserTab,
+ baseUrl,
+ clientKey,
+ }) {
+ // Submit additional details 1
+ const { makePaymentResponse: makePaymentResponseString } =
+ payment.custom.fields
+ const makePaymentResponse = await JSON.parse(makePaymentResponseString)
+ const redirectPaymentFormPage = new RedirectPaymentFormPage(
+ browserTab,
+ baseUrl
+ )
+ await redirectPaymentFormPage.goToThisPage()
+ await redirectPaymentFormPage.redirectToAdyenPaymentPage(
+ makePaymentResponse,
+ clientKey
+ )
+
+ await browserTab.waitForTimeout(5_000)
+
+ // Submit additional details
+ const creditCardNativePage = new CreditCardNativePage(browserTab, baseUrl)
+ const additionalPaymentDetailsString =
+ await creditCardNativePage.finish3dsNativePayment()
+
+ logger.debug(
+ 'additionalPaymentDetailsString',
+ additionalPaymentDetailsString
+ )
+ let result = null
+ const startTime = new Date().getTime()
+ try {
+ result = await ctpClient.update(
+ ctpClient.builder.payments,
+ payment.id,
+ payment.version,
+ [
+ {
+ action: 'setCustomField',
+ name: 'submitAdditionalPaymentDetailsRequest',
+ value: additionalPaymentDetailsString,
+ },
+ ]
+ )
+ } catch (err) {
+ logger.error(
+ 'creditCardAdvancedFlow::performChallengeFlow::errors:',
+ JSON.stringify(err)
+ )
+ throw err
+ } finally {
+ const endTime = new Date().getTime()
+ logger.debug(
+ 'creditCardAdvancedFlow::performChallengeFlow:',
+ endTime - startTime
+ )
+ }
+ return result.body
+ }
+})
diff --git a/extension/test/e2e/credit-card-amount-update.spec.js b/extension/test/e2e/credit-card-amount-update.spec.js
index 51a7aed12..24a52c108 100644
--- a/extension/test/e2e/credit-card-amount-update.spec.js
+++ b/extension/test/e2e/credit-card-amount-update.spec.js
@@ -96,10 +96,25 @@ describe('::creditCardPayment::amount-update::', () => {
)
// Step #3 - Update Amount
- const { statusCode, updatedPayment } = await updateAmount(
- notificationInteraction,
- paymentAfterCreateSession
+ const { statusCode, updatedPayment } = await waitUntil(
+ async () => {
+ try {
+ return await updateAmount(
+ notificationInteraction,
+ paymentAfterCreateSession
+ )
+ } catch (err) {
+ logger.error(
+ 'credit-card-amount-update::errors:',
+ JSON.stringify(err)
+ )
+ return Promise.resolve()
+ }
+ },
+ 10,
+ 1_000
)
+
amountUpdatesResponse = JSON.parse(
updatedPayment.custom.fields.amountUpdatesResponse
)
diff --git a/extension/test/e2e/credit-card-cancel-payment.spec.js b/extension/test/e2e/credit-card-cancel-payment.spec.js
index 4d02960fa..bfbe711d8 100644
--- a/extension/test/e2e/credit-card-cancel-payment.spec.js
+++ b/extension/test/e2e/credit-card-cancel-payment.spec.js
@@ -97,25 +97,38 @@ describe('::creditCardPayment::cancel-payment::', () => {
)
// Step #3 - Cancel payment
-
- const { body: paymentAfterReceivingNotification } =
- await ctpClient.fetchById(
- ctpClient.builder.payments,
- paymentAfterCreateSession.id
- )
-
- const { statusCode, body: cancelledPayment } = await ctpClient.update(
- ctpClient.builder.payments,
- paymentAfterReceivingNotification.id,
- paymentAfterReceivingNotification.version,
- [
- createAddTransactionAction({
- type: 'CancelAuthorization',
- state: 'Initial',
- currency: 'EUR',
- amount: 500,
- }),
- ]
+ const { statusCode, body: cancelledPayment } = await waitUntil(
+ async () => {
+ try {
+ const { body: paymentAfterReceivingNotification } =
+ await ctpClient.fetchById(
+ ctpClient.builder.payments,
+ paymentAfterCreateSession.id
+ )
+
+ return await ctpClient.update(
+ ctpClient.builder.payments,
+ paymentAfterReceivingNotification.id,
+ paymentAfterReceivingNotification.version,
+ [
+ createAddTransactionAction({
+ type: 'CancelAuthorization',
+ state: 'Initial',
+ currency: 'EUR',
+ amount: 500,
+ }),
+ ]
+ )
+ } catch (err) {
+ logger.error(
+ 'credit-card-cancel-payment::errors:',
+ JSON.stringify(err)
+ )
+ return Promise.resolve()
+ }
+ },
+ 10,
+ 1_000
)
cancelledPaymentStatusCode = statusCode
@@ -147,7 +160,7 @@ describe('::creditCardPayment::cancel-payment::', () => {
expect(cancelledPaymentStatusCode).to.be.equal(200)
- expect(paymentAfterReceivingNotification.transactions).to.have.lengthOf(2)
+ expect(paymentAfterReceivingNotification.transactions).to.have.length.gte(2)
const transaction = paymentAfterReceivingNotification.transactions[1]
expect(transaction.type).to.equal('CancelAuthorization')
expect(transaction.state).to.equal('Success')
diff --git a/extension/test/e2e/e2e-test-utils.js b/extension/test/e2e/e2e-test-utils.js
index 9c6147a33..a29fd602a 100644
--- a/extension/test/e2e/e2e-test-utils.js
+++ b/extension/test/e2e/e2e-test-utils.js
@@ -108,6 +108,82 @@ async function createPaymentSession(
return payment
}
+function assertPayment(
+ payment,
+ finalAdyenPaymentInteractionName = 'submitAdditionalPaymentDetails'
+) {
+ const {
+ [`${finalAdyenPaymentInteractionName}Response`]:
+ finalAdyenPaymentResponseString,
+ } = payment.custom.fields
+ const finalAdyenPaymentResponse = JSON.parse(finalAdyenPaymentResponseString)
+ expect(finalAdyenPaymentResponse.resultCode).to.equal(
+ 'Authorised',
+ `resultCode is not Authorised: ${finalAdyenPaymentResponseString}`
+ )
+ expect(finalAdyenPaymentResponse.pspReference).to.match(
+ /[A-Z0-9]+/,
+ `pspReference does not match '/[A-Z0-9]+/': ${finalAdyenPaymentResponseString}`
+ )
+
+ const finalAdyenPaymentInteraction = getLatestInterfaceInteraction(
+ payment.interfaceInteractions,
+ finalAdyenPaymentInteractionName
+ )
+ expect(finalAdyenPaymentInteraction.fields.response).to.equal(
+ finalAdyenPaymentResponseString
+ )
+
+ expect(payment.transactions).to.have.lengthOf(1)
+ const transaction = payment.transactions[0]
+ expect(transaction.state).to.equal('Success')
+ expect(transaction.type).to.equal('Authorization')
+ expect(transaction.interactionId).to.equal(
+ finalAdyenPaymentResponse.pspReference
+ )
+ expect(transaction.amount.centAmount).to.equal(
+ payment.amountPlanned.centAmount
+ )
+ expect(transaction.amount.currencyCode).to.equal(
+ payment.amountPlanned.currencyCode
+ )
+}
+
+async function createPayment(
+ ctpClient,
+ adyenMerchantAccount,
+ commercetoolsProjectKey,
+ makePaymentRequest,
+ currency = 'EUR'
+) {
+ const paymentDraft = {
+ amountPlanned: {
+ currencyCode: currency,
+ centAmount: 1000,
+ },
+ paymentMethodInfo: {
+ paymentInterface: c.CTP_ADYEN_INTEGRATION,
+ },
+ custom: {
+ type: {
+ typeId: 'type',
+ key: c.CTP_PAYMENT_CUSTOM_TYPE_KEY,
+ },
+ fields: {
+ adyenMerchantAccount,
+ commercetoolsProjectKey,
+ makePaymentRequest,
+ },
+ },
+ }
+
+ const { body: payment } = await ctpClient.create(
+ ctpClient.builder.payments,
+ paymentDraft
+ )
+ return payment
+}
+
function serveFile(pathName, req, res) {
const resolvedBase = path.resolve(pathName)
const fileLoc = path.join(resolvedBase)
@@ -147,6 +223,8 @@ export {
assertCreatePaymentSession,
getCreateSessionRequest,
createPaymentSession,
+ assertPayment,
+ createPayment,
initPuppeteerBrowser,
serveFile,
getRequestParams,
diff --git a/extension/test/e2e/fixtures/3ds-v2-make-payment-form.html b/extension/test/e2e/fixtures/3ds-v2-make-payment-form.html
new file mode 100644
index 000000000..7ad1c5045
--- /dev/null
+++ b/extension/test/e2e/fixtures/3ds-v2-make-payment-form.html
@@ -0,0 +1,136 @@
+
+
+
+
+ Make Payment
+
+
+
+
+ Use following credit card to test
+
Card number: 5454 5454 5454 5454
+
Expiry date: 03/30
+
CVC: 737
+
+ More cards
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/extension/test/e2e/fixtures/redirect-payment-form-advanced-flow.html b/extension/test/e2e/fixtures/redirect-payment-form-advanced-flow.html
new file mode 100644
index 000000000..c783e1e65
--- /dev/null
+++ b/extension/test/e2e/fixtures/redirect-payment-form-advanced-flow.html
@@ -0,0 +1,72 @@
+
+
+
+ Submit additional payment details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/extension/test/e2e/klarna-capture-and-refund.spec.js b/extension/test/e2e/klarna-capture-and-refund.spec.js
index de85472da..f04fc1e43 100644
--- a/extension/test/e2e/klarna-capture-and-refund.spec.js
+++ b/extension/test/e2e/klarna-capture-and-refund.spec.js
@@ -184,16 +184,29 @@ describe('::klarnaPayment::', () => {
)
// #4 - Refund the payment
- const { body: paymentAfterReceivingCaptureNotification } =
- await ctpClient.fetchById(
- ctpClient.builder.payments,
- paymentAfterCapture.id
- )
-
- const { statusCode, paymentAfterRefund } =
- await refundPaymentTransactions(
- paymentAfterReceivingCaptureNotification
- )
+ const { statusCode, paymentAfterRefund } = await waitUntil(
+ async () => {
+ try {
+ const { body: paymentAfterReceivingCaptureNotification } =
+ await ctpClient.fetchById(
+ ctpClient.builder.payments,
+ paymentAfterCapture.id
+ )
+
+ return await refundPaymentTransactions(
+ paymentAfterReceivingCaptureNotification
+ )
+ } catch (err) {
+ logger.error(
+ 'credit-card-cancel-payment::errors:',
+ JSON.stringify(err)
+ )
+ return Promise.resolve()
+ }
+ },
+ 10,
+ 1_000
+ )
const refundPaymentStatusCode = statusCode
expect(refundPaymentStatusCode).to.be.equal(200)
@@ -203,7 +216,8 @@ describe('::klarnaPayment::', () => {
ctpClient,
paymentAfterCreateSession.id,
`refund`
- )
+ ),
+ 30
)
paymentAfterReceivingRefundNotification = await ctpClient.fetchById(
diff --git a/extension/test/e2e/pageObjects/CreditCard3dsNativePage.js b/extension/test/e2e/pageObjects/CreditCard3dsNativePage.js
new file mode 100644
index 000000000..05a99d6d3
--- /dev/null
+++ b/extension/test/e2e/pageObjects/CreditCard3dsNativePage.js
@@ -0,0 +1,26 @@
+import { executeInAdyenIframe } from '../e2e-test-utils.js'
+
+export default class CreditCard3dsNativePage {
+ constructor(page, baseUrl) {
+ this.page = page
+ this.baseUrl = baseUrl
+ }
+
+ async finish3dsNativePayment() {
+ await executeInAdyenIframe(this.page, '[name=answer]', (el) =>
+ el.type('password')
+ )
+ await executeInAdyenIframe(this.page, 'button[type=submit]', (el, frame) =>
+ frame.$eval('#buttonSubmit', async (button) => {
+ await button.click()
+ })
+ )
+
+ await this.page.waitForTimeout(1_000)
+
+ const additionalPaymentDetailsInput2 = await this.page.$(
+ '#adyen-additional-payment-details'
+ )
+ return this.page.evaluate((el) => el.value, additionalPaymentDetailsInput2)
+ }
+}
diff --git a/extension/test/e2e/pageObjects/CreditCardMakePaymentFormPage.js b/extension/test/e2e/pageObjects/CreditCardMakePaymentFormPage.js
new file mode 100644
index 000000000..fe47b9f6f
--- /dev/null
+++ b/extension/test/e2e/pageObjects/CreditCardMakePaymentFormPage.js
@@ -0,0 +1,33 @@
+import { executeInAdyenIframe } from '../e2e-test-utils.js'
+import MakePaymentFormPage from './MakePaymentFormPage.js'
+
+export default class CreditCardMakePaymentFormPage extends MakePaymentFormPage {
+ async getMakePaymentRequest({
+ creditCardNumber,
+ creditCardDate,
+ creditCardCvc,
+ clientKey,
+ }) {
+ await this.generateAdyenMakePaymentForm(clientKey)
+
+ await this.page.waitForTimeout(2_000) // wait for web component rendering
+
+ await executeInAdyenIframe(
+ this.page,
+ '[data-fieldtype=encryptedCardNumber]',
+ (el) => el.type(creditCardNumber)
+ )
+ await executeInAdyenIframe(
+ this.page,
+ 'input[data-fieldtype^=encryptedExpiry]',
+ (el) => el.type(creditCardDate)
+ )
+ await executeInAdyenIframe(
+ this.page,
+ 'input[data-fieldtype^=encryptedSecurity]',
+ (el) => el.type(creditCardCvc)
+ )
+
+ return this.getMakePaymentRequestTextAreaValue()
+ }
+}
diff --git a/extension/test/e2e/pageObjects/KlarnaAuthenticationPage.js b/extension/test/e2e/pageObjects/KlarnaAuthenticationPage.js
index dfeba148d..6dfb341d0 100644
--- a/extension/test/e2e/pageObjects/KlarnaAuthenticationPage.js
+++ b/extension/test/e2e/pageObjects/KlarnaAuthenticationPage.js
@@ -53,15 +53,13 @@ export default class KlarnaAuthenticationPage {
await klarnaIframe.click('#directdebit\\.0-ui button[role="option"]')
await klarnaIframe.click('[data-testid="pick-plan"]')
- const finalSubmitButton = await klarnaIframe.$(
- '[data-testid="summary"] [data-testid="confirm-and-pay"]'
- )
- if (finalSubmitButton) {
- // Sleep before final click because klarna uses some internal state to disable button directly with its style.
- // We just need to wait for effect to finish.
- await this.page.waitForTimeout(1_000)
- await finalSubmitButton.click()
- } else {
+ // Sleep before final searching for iban field because klarna uses some effects for rendering.
+ // We just need to wait for effect to finish.
+ await this.page.waitForTimeout(1_000)
+
+ const ibanField = await klarnaIframe.$('#iban')
+
+ if (ibanField) {
await klarnaIframe.waitForSelector('[data-testid="confirm-and-pay"]', {
visible: true,
})
@@ -84,14 +82,20 @@ export default class KlarnaAuthenticationPage {
const confirmButton = dialogButtons[dialogButtons.length - 1]
await confirmButton.click()
- // Sleep before final click because klarna uses some internal state to disable button directly with its style.
- // We just need to wait for effect to finish.
- await this.page.waitForTimeout(1_000)
await klarnaIframe.waitForSelector(
'[data-testid="summary"] [data-testid="confirm-and-pay"]',
{ visible: true }
)
await klarnaIframe.click('[data-testid="confirm-and-pay"]')
+ } else {
+ const finalSubmitButton = await klarnaIframe.$(
+ '[data-testid="summary"] [data-testid="confirm-and-pay"]'
+ )
+
+ // Sleep before final click because klarna uses some internal state to disable button directly with its style.
+ // We just need to wait for effect to finish.
+ await this.page.waitForTimeout(1_000)
+ await finalSubmitButton.click()
}
await this.page.waitForSelector('#redirect-response')
diff --git a/extension/test/e2e/pageObjects/MakePaymentFormPage.js b/extension/test/e2e/pageObjects/MakePaymentFormPage.js
new file mode 100644
index 000000000..c2515fd20
--- /dev/null
+++ b/extension/test/e2e/pageObjects/MakePaymentFormPage.js
@@ -0,0 +1,31 @@
+export default class MakePaymentFormPage {
+ constructor(page, baseUrl) {
+ this.page = page
+ this.baseUrl = baseUrl
+ }
+
+ async goToThisPage() {
+ await this.page.goto(`${this.baseUrl}/make-payment-form`)
+ }
+
+ async generateAdyenMakePaymentForm(clientKey) {
+ await this.page.waitForSelector('#adyen-client-key')
+
+ // Put Adyen API Key into HTML for e2e test
+
+ await this.page.type('#adyen-client-key', clientKey)
+ await this.page.$eval('#adyen-client-key', (e) => e.blur())
+ }
+
+ async getMakePaymentRequestTextAreaValue() {
+ await this.page.waitForSelector('.adyen-checkout__button--pay')
+ await this.page.click('.adyen-checkout__button--pay')
+
+ const makePaymentRequestTextArea = await this.page.$(
+ '#adyen-make-payment-request'
+ )
+ return (
+ await makePaymentRequestTextArea.getProperty('innerHTML')
+ ).jsonValue()
+ }
+}
diff --git a/extension/test/e2e/pageObjects/RedirectPaymentFormPageAdvancedFlow.js b/extension/test/e2e/pageObjects/RedirectPaymentFormPageAdvancedFlow.js
new file mode 100644
index 000000000..4258c502c
--- /dev/null
+++ b/extension/test/e2e/pageObjects/RedirectPaymentFormPageAdvancedFlow.js
@@ -0,0 +1,30 @@
+import { pasteValue } from '../e2e-test-utils.js'
+import httpUtils from '../../../src/utils.js'
+
+const logger = httpUtils.getLogger()
+
+export default class RedirectPaymentFormPageAdvancedFlow {
+ constructor(page, baseUrl) {
+ this.page = page
+ this.baseUrl = baseUrl
+ }
+
+ async goToThisPage() {
+ await this.page.goto(`${this.baseUrl}/redirect-payment-form`)
+ }
+
+ async redirectToAdyenPaymentPage(paymentDetailsResponse, clientKey) {
+ logger.debug(
+ 'redirectToAdyenPaymentPage::paymentDetailsResponse::',
+ paymentDetailsResponse
+ )
+ await pasteValue(
+ this.page,
+ '#adyen-make-payment-response-action-field',
+ JSON.stringify(paymentDetailsResponse.action)
+ )
+
+ await pasteValue(this.page, '#adyen-client-key', clientKey)
+ return this.page.click('#redirect-payment-button')
+ }
+}
diff --git a/extension/test/integration/make-payment.handler.spec.js b/extension/test/integration/make-payment.handler.spec.js
new file mode 100644
index 000000000..1ad14592d
--- /dev/null
+++ b/extension/test/integration/make-payment.handler.spec.js
@@ -0,0 +1,195 @@
+import { expect } from 'chai'
+import ctpClientBuilder from '../../src/ctp.js'
+import config from '../../src/config/config.js'
+import constants from '../../src/config/constants.js'
+import { initPaymentWithCart } from './integration-test-set-up.js'
+
+describe('::make-payment with multiple adyen accounts use case::', () => {
+ const [commercetoolsProjectKey] = config.getAllCtpProjectKeys()
+ const [adyenMerchantAccount1, adyenMerchantAccount2] =
+ config.getAllAdyenMerchantAccounts()
+
+ let ctpClient
+
+ beforeEach(async () => {
+ const ctpConfig = config.getCtpConfig(commercetoolsProjectKey)
+ ctpClient = await ctpClientBuilder.get(ctpConfig)
+ })
+
+ it(
+ 'given a single commercetools project and payments ' +
+ 'when makePayment custom field is set and response from Adyen is Authorised, ' +
+ 'then it should connect the 2 different adyen mechant accounts and ' +
+ 'should set key, "makePaymentResponse" custom field, interface interactions ' +
+ 'and a successfully authorized transaction',
+ async () => {
+ await Promise.all([
+ makePayment({
+ reference: `makePayment1-${new Date().getTime()}`,
+ adyenMerchantAccount: adyenMerchantAccount1,
+ metadata: {
+ orderNumber: `externalOrderSystem-12345`,
+ receiptNumber: `externalOrderSystem-receipt123`,
+ },
+ }),
+ makePayment({
+ reference: `makePayment2-${new Date().getTime()}`,
+ adyenMerchantAccount: adyenMerchantAccount2 || adyenMerchantAccount1,
+ }),
+ ])
+ }
+ )
+
+ it(
+ 'given a payment with cart ' +
+ 'when makePayment custom field and the addCommercetoolsLineItems set to true ' +
+ 'then should calculate and lineItems to the makePaymentRequest',
+ async () => {
+ const payment = await initPaymentWithCart({
+ ctpClient,
+ adyenMerchantAccount: adyenMerchantAccount1,
+ commercetoolsProjectKey,
+ })
+
+ const makePaymentRequestDraft = {
+ amount: {
+ currency: 'EUR',
+ value: 1000,
+ },
+ reference: `makePayment3-${new Date().getTime()}`,
+ paymentMethod: {
+ type: 'scheme',
+ encryptedCardNumber: 'test_4111111111111111',
+ encryptedExpiryMonth: 'test_03',
+ encryptedExpiryYear: 'test_2030',
+ encryptedSecurityCode: 'test_737',
+ },
+ metadata: {
+ orderNumber: `externalOrderSystem-12345`,
+ receiptNumber: `externalOrderSystem-receipt123`,
+ },
+ returnUrl: 'https://your-company.com/',
+ addCommercetoolsLineItems: true,
+ }
+
+ const { statusCode, body: updatedPayment } = await ctpClient.update(
+ ctpClient.builder.payments,
+ payment.id,
+ payment.version,
+ [
+ {
+ action: 'setCustomField',
+ name: 'makePaymentRequest',
+ value: JSON.stringify(makePaymentRequestDraft),
+ },
+ ]
+ )
+
+ const makePaymentResponse =
+ updatedPayment?.custom?.fields?.makePaymentResponse
+ const pspReference = JSON.parse(makePaymentResponse).pspReference
+ expect(statusCode).to.equal(200)
+ expect(updatedPayment.key).to.equal(pspReference)
+ expect(updatedPayment.paymentMethodInfo.method).to.equal('scheme')
+ expect(updatedPayment.paymentMethodInfo.name).to.eql({
+ en: 'Credit Card',
+ })
+
+ const interfaceInteraction = updatedPayment.interfaceInteractions.find(
+ (interaction) =>
+ interaction.fields.type ===
+ constants.CTP_INTERACTION_TYPE_MAKE_PAYMENT
+ )
+ const makePaymentRequest = JSON.parse(interfaceInteraction.fields.request)
+ const makePaymentRequestBody = JSON.parse(makePaymentRequest.body)
+ expect(makePaymentRequestBody.lineItems).to.have.lengthOf(3)
+ }
+ )
+
+ async function makePayment({ reference, adyenMerchantAccount, metadata }) {
+ const makePaymentRequestDraft = {
+ amount: {
+ currency: 'EUR',
+ value: 1000,
+ },
+ reference,
+ paymentMethod: {
+ type: 'scheme',
+ encryptedCardNumber: 'test_4111111111111111',
+ encryptedExpiryMonth: 'test_03',
+ encryptedExpiryYear: 'test_2030',
+ encryptedSecurityCode: 'test_737',
+ },
+ metadata,
+ returnUrl: 'https://your-company.com/',
+ }
+ const paymentDraft = {
+ amountPlanned: {
+ currencyCode: 'EUR',
+ centAmount: 1000,
+ },
+ paymentMethodInfo: {
+ paymentInterface: constants.CTP_ADYEN_INTEGRATION,
+ },
+ custom: {
+ type: {
+ typeId: 'type',
+ key: constants.CTP_PAYMENT_CUSTOM_TYPE_KEY,
+ },
+ fields: {
+ makePaymentRequest: JSON.stringify(makePaymentRequestDraft),
+ adyenMerchantAccount,
+ commercetoolsProjectKey,
+ },
+ },
+ }
+
+ const { statusCode, body: payment } = await ctpClient.create(
+ ctpClient.builder.payments,
+ paymentDraft
+ )
+
+ expect(statusCode).to.equal(201)
+ const { makePaymentResponse } = payment.custom.fields
+ const pspReference = JSON.parse(makePaymentResponse).pspReference
+ expect(payment.key).to.equal(pspReference)
+ expect(payment.paymentMethodInfo.method).to.equal('scheme')
+ expect(payment.paymentMethodInfo.name).to.eql({ en: 'Credit Card' })
+
+ const interfaceInteraction = payment.interfaceInteractions.find(
+ (interaction) =>
+ interaction.fields.type === constants.CTP_INTERACTION_TYPE_MAKE_PAYMENT
+ )
+ const makePaymentRequest = JSON.parse(interfaceInteraction.fields.request)
+ const makePaymentRequestBody = JSON.parse(makePaymentRequest.body)
+ if (metadata) {
+ expect(makePaymentRequestBody.metadata).to.deep.equal({
+ ctProjectKey: commercetoolsProjectKey,
+ ...metadata,
+ })
+ } else {
+ expect(makePaymentRequestBody.metadata).to.deep.equal({
+ ctProjectKey: commercetoolsProjectKey,
+ })
+ }
+
+ expect(makePaymentRequestBody.merchantAccount).to.be.equal(
+ adyenMerchantAccount
+ )
+
+ expect(makePaymentResponse).to.be.deep.equal(
+ interfaceInteraction.fields.response
+ )
+ expect(JSON.parse(makePaymentResponse).resultCode).to.be.equal('Authorised')
+
+ expect(payment.transactions).to.have.lengthOf(1)
+ const transaction = payment.transactions[0]
+ expect(transaction.state).to.be.equal('Success')
+ expect(transaction.type).to.be.equal('Authorization')
+ expect(transaction.amount.currencyCode).to.be.equal('EUR')
+ expect(transaction.amount.centAmount).to.be.equal(
+ paymentDraft.amountPlanned.centAmount
+ )
+ expect(transaction.interactionId).to.be.a('string')
+ }
+})
diff --git a/extension/test/unit/fixtures/adyen-make-payment-3ds-redirect-response.js b/extension/test/unit/fixtures/adyen-make-payment-3ds-redirect-response.js
new file mode 100644
index 000000000..9b121abb4
--- /dev/null
+++ b/extension/test/unit/fixtures/adyen-make-payment-3ds-redirect-response.js
@@ -0,0 +1,39 @@
+/* eslint-disable max-len */
+export default JSON.stringify({
+ resultCode: 'RedirectShopper',
+ action: {
+ paymentData:
+ 'Ab02b4c0!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',
+ paymentMethodType: 'scheme',
+ url: 'https://checkoutshopper-test.adyen.com/checkoutshopper/threeDS2.shtml',
+ data: {
+ MD: 'M2RzMi5mMzY1YjU3OWQ0Y2MxYjI2OGUwNmZjNGIwMjc0YWUxMmQ3ZWYyODExOWJiOTNkYzI5ZWQ0YzAzNTQ4ZDI1YTcw',
+ PaReq:
+ 'BQABAgADGwHF3TM-essx-XpkKlrYSDfF5VBnxoUZtAWP8IGlpg267nU0SQFZR2VCfLeIvjFhqewzEmVACTpgu1AVPNW9xwJOAeS-nWf-pAGGAx12xEIFu5F1P1I3YSsmkkXrmkNC_JhLjgHLTD-_9dcMp9tA_iBvZo5I0e881-XtYrhfbTtJ79AJSkq00R7YNykqaadhmLMK5-vEN8PP0rWFhIIGMDj-EYlfomg1A2BFjnxvQwWe_SeBp18AQckunoQ0VUK_fC-J_c7O7dqdU-Qu6XWcfx1obX4DWLcALbtPSH5bPmb1XnlP4a8f7pWfXSG7_TuzRdxT28-DPWCBhZzuIbof4UnKLHpcXoeNOZHyYixkAis_uJFZRniQQngVY2RWqKzB2eUIdoQSLMG6FEFh4lFSjttYjrgd5YmkQYKe9wTk_jG1RmJn6ex8bjlOCdSVG1Ny45qxX7M3Grc88LPOTzdeEtdobT0pS0FPy6NWVPn82VQxdt9nMg0CF2PYh8HyjVAZ4r0SGnVXaI5e9ke-V94JWaIasRZMZLCw959UHh1YB4ruWCOsPyREevKx5dksjeJvWD_lj1wko6dm2OhFWJHR4MDUh3BtVlYRppuQjNhXDN-d_sQy0z8z3febRjmb067KmpFoU8AGPc7k2iC9fI6P9P0mcXtYB-PJKut3I1RlJhCvrVN05G2y1ErOf7CIVGiHAEp7ImtleSI6IkFGMEFBQTEwM0NBNTM3RUFFRDg3QzI0REQ1MzkwOUI4MEE3OEE5MjNFMzgyM0Q2OERBQ0M5NEI5RkY4MzA1REMifT75ZJ6ZJtDHQb4ha-VCt7hIKTqIRQ_pjoUEwdR4-vWEezkspSYj2gJBCyqJvB6jckZ395Rcq3T2MaQLoEkvS8DvHDLgpV6endPF6cA-_XhzBqsK_2t02WIDVEEtkZzgfqoFImy0M5MBNlJ3RIG0vS7lOAbWiJkOd6R3dZXV6I-Q6FzELCtrhckBd-CqnUcdh3wxwlqlICR4kfSkXKY745gvNDVtuAvFRlJqQcLVFGL3ATCMLSXcBpAE6Ul7fTvEsUC0pKr9Z7RuslnXhlfgq92HzxAYGqH9-Y6rhBUNfdY617hI-OjF7b2sZvmgcDNZ58xdU6IE7MHhM2SeWblyiNg7YoyDHgsIiSjGzBCIs7I6k8Qc_nf1ek8LovWE7M21znUMKIgwiVKLIy7wdH2eqh7t8xHdX_-xXArRqI6o2d2Z96eTUdCio2WM2c_ZBPF1LjIuBQABAQCUjaP2nAs6CWYt1F9GiLGoGOruyKTPezT4hgq-eSuOPkowBfEdSD9hkKeUC7vYb636tlkdb1s5SGYC1DLwpXRrlpSJXRiiIHlMSfHlDHNFoslExa5wpTXqa4_bvIcAmh0mREgF3BhnHNCVWf0vEi1TsGk6fWpfSisOROLeR8KhKb7KcXlTbTNy9Sj21x4igWnXe5exHpBFvkhz-WnfnNwyAHvo9qNi7Ic4XuvlNWxN0b_wsT0l64c-8u5bIVaCL0zpgm8UtEVVvAvgQIzg10u4AzpuV2uXbD1eq8sUCSDZ5UeOIFmMYowsd7PUt6KMaUEZTTgn7oJOy0XrhKi3CBreEBLrWuzrv595LXcGqEU6ksMAAFP7lqIX-n5IfUUtLla0jKwP6YA9mZCdMuZKwX_4kjw9STgHSnb17diTxc8yoZJwYhP0NoCX0RY3uYHulylkRJj1tvBOu01TZOPuRCuW8AkR8a2fCQ6PpYyV7_vJ2HEjFNaUmdDROcfZe0lRO9yjd9dBmNJwWCRGZYB6GCM84GKA7lVyecBe-xVlwg28bHhu5r7FvQVI-oDZVUzuuP6Pkx6R1VBTFyVr4lJwGxq2Ww6itZ-Vc0qEGHC_YuAZh7HgawPfbN_26chY8NRcB1pYm_vAn1J7Z6XGoxC2RMjQn_b_kLkvnUN81G6HxKuJBIrrqLldxTwDrofL4eqpgcGZl9SQ9xPtnqNRYh4l04ifx0iN57DKC9-a3vhi5kn2mWAS8O-tMEHE-HQc67xKRzpMkLiyduhwrnTph7fgXrbyNvFjb3MrfUE8cQmq0DJUXEYJK9TwYCpr9_JNzVLVbA5gu53xeE05VMWQ8Ib4Wt6K2wvvZx0toUmMBN7QOixkvsj10Q29A_Kjabqx-dHEEzBLJT-Om2S3T-hKF4BCad3qX-NFepNJa1IvOKUDDD3bZd5QFfCTbLIFtidbAuUZsAPN7tYmUxTTWOlR5e5aeUS7Z51QQ6W3TuE8F_f9OcHECHW4eUuLj4tmyAEcXTZOknKdQvM_1Hz98Ef22fEA_AoX_NrFPsIzphDS5C9O6RmdhGsr2mj_ocQOZaK_ojKStMhFzs193SM35RjIh5uVhLbaDNoQF07xOB-g4pKBnsPrnE8Auo4Mtk01DAatO41_Ecp6d5dIjmdHd4OYVXALCQ5uOPohEjvLwgxwk4UKsQyqrn-t-1nOsVjsx_hUQe-lyoQmIZ3NamRbb0ccQgjhyXJna0z9Wq1fK_CkjwhBN7mqW-8QxfSKMslnUKS6gzQgjLykDW2vkHetjRMDCinWD5o4b6uqEw3tePycBzV4o2PRr-FxNsg7aVk8xRIhs_leYkNBNp6OCSQVxqe4u6UrJqCn1ipjQbVj4lmnSxc3WPH27DSnvxQenmDFpv_O_6S6-qKWfcWF4TV3UB1Q9OGLb3Ch30umnNbb7nw_E3ac6mjFFbWN2rjyjIJOEAyBUDeCplY-TEQp8KEFPoLfJ9AYTpW6H7NNW9-_kFjVaW9toeIunV-TlZzc85lFvVc9HouWiwL_xLV7IMhPlgqUNOBqbkcQx13DsFtm4BtuffOc0SvD5c3fu3qpCpcQQY1jvZD-PUHzJiZRZSobbFKgSr84Irexa_BLW5RV1AVZT7utwLdMc9v9-yxv7mFUIumnHBUdIQNZZiO3nnaqxRPYCTgXFlq42Ry-BfHzXVl_RLm16AkCcVqoDLAJ78Gqk62XtMgXA1zb8dWLpYcpgXCWkVI5j0lAlq7z0doKP8wTqHvVlV-7_7fDtjn76nA_bhpihCThMgnuPiy_Nrg0iKb6ese515qwBAPL9QSrqBThFOY1jEPnJjr5RVhBPgxyjYlMG5kgB3QQ_mquep7gcJELps39R69k9HsZZJG2dDJoiF7OhYVi3cipFIBOGHjeSdTSOm9FeXYNasYZgndwVs-BKZ9DFYSdEsH_eQG7mTuNcOMUvm6XIakh27xS2zGX4vg_or1KHd9EnfCLarxSHiP1AuOQY7jUizyXyknZvs5eN-hIQPkv-CeRTjjcT75aiEiXlWvaVLFej_Y4VT2IDkfRmwkd_MWQ7l6LFWXFgXsvsKhFZqnu_ak7LYout4cV69MM7Eiq6BZYkAb9XLlP50TcfizTwnb7K5yJVl8cRYCMgNykiQaECpOUSgtC--4B9kGozgPn0vxbZb5F4qRM40V4PEVlO_LN2U7C6-5WBmRHudf7sAx2hHuBKtZJJB9FUwgFTeDqSp5bYJQQmCaf5lxYhP3qMy4X5mYpIGu86BprCKU',
+ },
+ method: 'POST',
+ type: 'redirect',
+ },
+ details: [
+ {
+ key: 'MD',
+ type: 'text',
+ },
+ {
+ key: 'PaRes',
+ type: 'text',
+ },
+ ],
+ paymentData:
+ 'Ab02b4c0!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',
+ redirect: {
+ data: {
+ PaReq:
+ 'BQABAgADGwHF3TM-essx-XpkKlrYSDfF5VBnxoUZtAWP8IGlpg267nU0SQFZR2VCfLeIvjFhqewzEmVACTpgu1AVPNW9xwJOAeS-nWf-pAGGAx12xEIFu5F1P1I3YSsmkkXrmkNC_JhLjgHLTD-_9dcMp9tA_iBvZo5I0e881-XtYrhfbTtJ79AJSkq00R7YNykqaadhmLMK5-vEN8PP0rWFhIIGMDj-EYlfomg1A2BFjnxvQwWe_SeBp18AQckunoQ0VUK_fC-J_c7O7dqdU-Qu6XWcfx1obX4DWLcALbtPSH5bPmb1XnlP4a8f7pWfXSG7_TuzRdxT28-DPWCBhZzuIbof4UnKLHpcXoeNOZHyYixkAis_uJFZRniQQngVY2RWqKzB2eUIdoQSLMG6FEFh4lFSjttYjrgd5YmkQYKe9wTk_jG1RmJn6ex8bjlOCdSVG1Ny45qxX7M3Grc88LPOTzdeEtdobT0pS0FPy6NWVPn82VQxdt9nMg0CF2PYh8HyjVAZ4r0SGnVXaI5e9ke-V94JWaIasRZMZLCw959UHh1YB4ruWCOsPyREevKx5dksjeJvWD_lj1wko6dm2OhFWJHR4MDUh3BtVlYRppuQjNhXDN-d_sQy0z8z3febRjmb067KmpFoU8AGPc7k2iC9fI6P9P0mcXtYB-PJKut3I1RlJhCvrVN05G2y1ErOf7CIVGiHAEp7ImtleSI6IkFGMEFBQTEwM0NBNTM3RUFFRDg3QzI0REQ1MzkwOUI4MEE3OEE5MjNFMzgyM0Q2OERBQ0M5NEI5RkY4MzA1REMifT75ZJ6ZJtDHQb4ha-VCt7hIKTqIRQ_pjoUEwdR4-vWEezkspSYj2gJBCyqJvB6jckZ395Rcq3T2MaQLoEkvS8DvHDLgpV6endPF6cA-_XhzBqsK_2t02WIDVEEtkZzgfqoFImy0M5MBNlJ3RIG0vS7lOAbWiJkOd6R3dZXV6I-Q6FzELCtrhckBd-CqnUcdh3wxwlqlICR4kfSkXKY745gvNDVtuAvFRlJqQcLVFGL3ATCMLSXcBpAE6Ul7fTvEsUC0pKr9Z7RuslnXhlfgq92HzxAYGqH9-Y6rhBUNfdY617hI-OjF7b2sZvmgcDNZ58xdU6IE7MHhM2SeWblyiNg7YoyDHgsIiSjGzBCIs7I6k8Qc_nf1ek8LovWE7M21znUMKIgwiVKLIy7wdH2eqh7t8xHdX_-xXArRqI6o2d2Z96eTUdCio2WM2c_ZBPF1LjIuBQABAQCUjaP2nAs6CWYt1F9GiLGoGOruyKTPezT4hgq-eSuOPkowBfEdSD9hkKeUC7vYb636tlkdb1s5SGYC1DLwpXRrlpSJXRiiIHlMSfHlDHNFoslExa5wpTXqa4_bvIcAmh0mREgF3BhnHNCVWf0vEi1TsGk6fWpfSisOROLeR8KhKb7KcXlTbTNy9Sj21x4igWnXe5exHpBFvkhz-WnfnNwyAHvo9qNi7Ic4XuvlNWxN0b_wsT0l64c-8u5bIVaCL0zpgm8UtEVVvAvgQIzg10u4AzpuV2uXbD1eq8sUCSDZ5UeOIFmMYowsd7PUt6KMaUEZTTgn7oJOy0XrhKi3CBreEBLrWuzrv595LXcGqEU6ksMAAFP7lqIX-n5IfUUtLla0jKwP6YA9mZCdMuZKwX_4kjw9STgHSnb17diTxc8yoZJwYhP0NoCX0RY3uYHulylkRJj1tvBOu01TZOPuRCuW8AkR8a2fCQ6PpYyV7_vJ2HEjFNaUmdDROcfZe0lRO9yjd9dBmNJwWCRGZYB6GCM84GKA7lVyecBe-xVlwg28bHhu5r7FvQVI-oDZVUzuuP6Pkx6R1VBTFyVr4lJwGxq2Ww6itZ-Vc0qEGHC_YuAZh7HgawPfbN_26chY8NRcB1pYm_vAn1J7Z6XGoxC2RMjQn_b_kLkvnUN81G6HxKuJBIrrqLldxTwDrofL4eqpgcGZl9SQ9xPtnqNRYh4l04ifx0iN57DKC9-a3vhi5kn2mWAS8O-tMEHE-HQc67xKRzpMkLiyduhwrnTph7fgXrbyNvFjb3MrfUE8cQmq0DJUXEYJK9TwYCpr9_JNzVLVbA5gu53xeE05VMWQ8Ib4Wt6K2wvvZx0toUmMBN7QOixkvsj10Q29A_Kjabqx-dHEEzBLJT-Om2S3T-hKF4BCad3qX-NFepNJa1IvOKUDDD3bZd5QFfCTbLIFtidbAuUZsAPN7tYmUxTTWOlR5e5aeUS7Z51QQ6W3TuE8F_f9OcHECHW4eUuLj4tmyAEcXTZOknKdQvM_1Hz98Ef22fEA_AoX_NrFPsIzphDS5C9O6RmdhGsr2mj_ocQOZaK_ojKStMhFzs193SM35RjIh5uVhLbaDNoQF07xOB-g4pKBnsPrnE8Auo4Mtk01DAatO41_Ecp6d5dIjmdHd4OYVXALCQ5uOPohEjvLwgxwk4UKsQyqrn-t-1nOsVjsx_hUQe-lyoQmIZ3NamRbb0ccQgjhyXJna0z9Wq1fK_CkjwhBN7mqW-8QxfSKMslnUKS6gzQgjLykDW2vkHetjRMDCinWD5o4b6uqEw3tePycBzV4o2PRr-FxNsg7aVk8xRIhs_leYkNBNp6OCSQVxqe4u6UrJqCn1ipjQbVj4lmnSxc3WPH27DSnvxQenmDFpv_O_6S6-qKWfcWF4TV3UB1Q9OGLb3Ch30umnNbb7nw_E3ac6mjFFbWN2rjyjIJOEAyBUDeCplY-TEQp8KEFPoLfJ9AYTpW6H7NNW9-_kFjVaW9toeIunV-TlZzc85lFvVc9HouWiwL_xLV7IMhPlgqUNOBqbkcQx13DsFtm4BtuffOc0SvD5c3fu3qpCpcQQY1jvZD-PUHzJiZRZSobbFKgSr84Irexa_BLW5RV1AVZT7utwLdMc9v9-yxv7mFUIumnHBUdIQNZZiO3nnaqxRPYCTgXFlq42Ry-BfHzXVl_RLm16AkCcVqoDLAJ78Gqk62XtMgXA1zb8dWLpYcpgXCWkVI5j0lAlq7z0doKP8wTqHvVlV-7_7fDtjn76nA_bhpihCThMgnuPiy_Nrg0iKb6ese515qwBAPL9QSrqBThFOY1jEPnJjr5RVhBPgxyjYlMG5kgB3QQ_mquep7gcJELps39R69k9HsZZJG2dDJoiF7OhYVi3cipFIBOGHjeSdTSOm9FeXYNasYZgndwVs-BKZ9DFYSdEsH_eQG7mTuNcOMUvm6XIakh27xS2zGX4vg_or1KHd9EnfCLarxSHiP1AuOQY7jUizyXyknZvs5eN-hIQPkv-CeRTjjcT75aiEiXlWvaVLFej_Y4VT2IDkfRmwkd_MWQ7l6LFWXFgXsvsKhFZqnu_ak7LYout4cV69MM7Eiq6BZYkAb9XLlP50TcfizTwnb7K5yJVl8cRYCMgNykiQaECpOUSgtC--4B9kGozgPn0vxbZb5F4qRM40V4PEVlO_LN2U7C6-5WBmRHudf7sAx2hHuBKtZJJB9FUwgFTeDqSp5bYJQQmCaf5lxYhP3qMy4X5mYpIGu86BprCKU',
+ TermUrl: null,
+ MD: 'M2RzMi5mMzY1YjU3OWQ0Y2MxYjI2OGUwNmZjNGIwMjc0YWUxMmQ3ZWYyODExOWJiOTNkYzI5ZWQ0YzAzNTQ4ZDI1YTcw',
+ },
+ method: 'POST',
+ url: 'https://checkoutshopper-test.adyen.com/checkoutshopper/threeDS2.shtml',
+ },
+})
diff --git a/extension/test/unit/fixtures/adyen-make-payment-error-response.js b/extension/test/unit/fixtures/adyen-make-payment-error-response.js
new file mode 100644
index 000000000..4f3b44f00
--- /dev/null
+++ b/extension/test/unit/fixtures/adyen-make-payment-error-response.js
@@ -0,0 +1,14 @@
+export default JSON.stringify({
+ additionalData: {
+ cvcResult: '1 Matches',
+ avsResult: '4 AVS not supported for this card type',
+ authorisationMid: '1000',
+ cardHolderName: 'Checkout Shopper PlaceHolder',
+ acquirerAccountCode: 'TestPmmAcquirerAccount',
+ },
+ pspReference: '852587371926074H',
+ refusalReason: 'Acquirer Error',
+ resultCode: 'Error',
+ refusalReasonCode: '4',
+ merchantReference: '933',
+})
diff --git a/extension/test/unit/fixtures/adyen-make-payment-refused-response.js b/extension/test/unit/fixtures/adyen-make-payment-refused-response.js
new file mode 100644
index 000000000..9dacd3f29
--- /dev/null
+++ b/extension/test/unit/fixtures/adyen-make-payment-refused-response.js
@@ -0,0 +1,14 @@
+export default JSON.stringify({
+ additionalData: {
+ cvcResult: '1 Matches',
+ avsResult: '4 AVS not supported for this card type',
+ authorisationMid: '1000',
+ cardHolderName: 'Checkout Shopper PlaceHolder',
+ acquirerAccountCode: 'TestPmmAcquirerAccount',
+ },
+ pspReference: '882587368372271B',
+ refusalReason: 'Expired Card',
+ resultCode: 'Refused',
+ refusalReasonCode: '6',
+ merchantReference: '933',
+})
diff --git a/extension/test/unit/fixtures/adyen-make-payment-success-response.js b/extension/test/unit/fixtures/adyen-make-payment-success-response.js
new file mode 100644
index 000000000..3bbed3367
--- /dev/null
+++ b/extension/test/unit/fixtures/adyen-make-payment-success-response.js
@@ -0,0 +1,18 @@
+export default JSON.stringify({
+ additionalData: {
+ cvcResult: '1 Matches',
+ authCode: '010800',
+ avsResult: '4 AVS not supported for this card type',
+ cardHolderName: 'Checkout Shopper PlaceHolder',
+ paymentMethod: 'visa',
+ authorisationMid: '1000',
+ acquirerAccountCode: 'TestPmmAcquirerAccount',
+ },
+ pspReference: '853587031437598F',
+ resultCode: 'Authorised',
+ amount: {
+ currency: 'EUR',
+ value: 1000,
+ },
+ merchantReference: 'YOUR_REFERENCE',
+})
diff --git a/extension/test/unit/fixtures/adyen-make-payment-validation-failed-response.js b/extension/test/unit/fixtures/adyen-make-payment-validation-failed-response.js
new file mode 100644
index 000000000..adef7ee4a
--- /dev/null
+++ b/extension/test/unit/fixtures/adyen-make-payment-validation-failed-response.js
@@ -0,0 +1,6 @@
+export default JSON.stringify({
+ status: 422,
+ errorCode: '172',
+ message: 'Encrypted data used outside of valid time period',
+ errorType: 'validation',
+})
diff --git a/extension/test/unit/fixtures/adyen-submit-payment-details-challenge-shopper-response.js b/extension/test/unit/fixtures/adyen-submit-payment-details-challenge-shopper-response.js
new file mode 100644
index 000000000..eadff9197
--- /dev/null
+++ b/extension/test/unit/fixtures/adyen-submit-payment-details-challenge-shopper-response.js
@@ -0,0 +1,24 @@
+/* eslint-disable max-len */
+export default JSON.stringify({
+ resultCode: 'ChallengeShopper',
+ action: {
+ paymentData:
+ 'Ab02b4c0!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',
+ paymentMethodType: 'scheme',
+ token:
+ 'eyJhY3NSZWZlcmVuY2VOdW1iZXIiOiJBRFlFTi1BQ1MtU0lNVUxBVE9SIiwiYWNzVHJhbnNJRCI6IjcyZWVjMTdkLTAyYjEtNDJkMC1iOTcwLTE4ZjgzN2IxMWY3MyIsImFjc1VSTCI6Imh0dHBzOlwvXC9wYWwtdGVzdC5hZHllbi5jb21cL3RocmVlZHMyc2ltdWxhdG9yXC9hY3NcL2NoYWxsZW5nZS5zaHRtbCIsIm1lc3NhZ2VWZXJzaW9uIjoiMi4xLjAiLCJ0aHJlZURTTm90aWZpY2F0aW9uVVJMIjoiaHR0cHM6XC9cL2NoZWNrb3V0c2hvcHBlci10ZXN0LmFkeWVuLmNvbVwvY2hlY2tvdXRzaG9wcGVyXC8zZG5vdGlmLnNodG1sP29yaWdpbktleT1wdWIudjIuODAxNTQ3NTU5OTA1MTgwMy5hSFIwY0RvdkwyeHZZMkZzYUc5emREbzJNek0wTWcua1NEMzFidTRfM3N5c1NPdjFOZFFUS0RoWUZGNl9XTWszSUM1UDFJb1QyTSIsInRocmVlRFNTZXJ2ZXJUcmFuc0lEIjoiZTMxOTAwYmEtMGQxYi00NWUxLTg4NTEtYTgzNmQ5MzA0MzJmIn0=',
+ type: 'threeDS2Challenge',
+ },
+ authentication: {
+ 'threeds2.challengeToken':
+ 'eyJhY3NSZWZlcmVuY2VOdW1iZXIiOiJBRFlFTi1BQ1MtU0lNVUxBVE9SIiwiYWNzVHJhbnNJRCI6IjcyZWVjMTdkLTAyYjEtNDJkMC1iOTcwLTE4ZjgzN2IxMWY3MyIsImFjc1VSTCI6Imh0dHBzOlwvXC9wYWwtdGVzdC5hZHllbi5jb21cL3RocmVlZHMyc2ltdWxhdG9yXC9hY3NcL2NoYWxsZW5nZS5zaHRtbCIsIm1lc3NhZ2VWZXJzaW9uIjoiMi4xLjAiLCJ0aHJlZURTTm90aWZpY2F0aW9uVVJMIjoiaHR0cHM6XC9cL2NoZWNrb3V0c2hvcHBlci10ZXN0LmFkeWVuLmNvbVwvY2hlY2tvdXRzaG9wcGVyXC8zZG5vdGlmLnNodG1sP29yaWdpbktleT1wdWIudjIuODAxNTQ3NTU5OTA1MTgwMy5hSFIwY0RvdkwyeHZZMkZzYUc5emREbzJNek0wTWcua1NEMzFidTRfM3N5c1NPdjFOZFFUS0RoWUZGNl9XTWszSUM1UDFJb1QyTSIsInRocmVlRFNTZXJ2ZXJUcmFuc0lEIjoiZTMxOTAwYmEtMGQxYi00NWUxLTg4NTEtYTgzNmQ5MzA0MzJmIn0=',
+ },
+ details: [
+ {
+ key: 'threeds2.challengeResult',
+ type: 'text',
+ },
+ ],
+ paymentData:
+ 'Ab02b4c0!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',
+})
diff --git a/extension/test/unit/fixtures/adyen-submit-payment-details-success-response.js b/extension/test/unit/fixtures/adyen-submit-payment-details-success-response.js
new file mode 100644
index 000000000..e0891e8a4
--- /dev/null
+++ b/extension/test/unit/fixtures/adyen-submit-payment-details-success-response.js
@@ -0,0 +1,18 @@
+export default JSON.stringify({
+ pspReference: '852588749855524C',
+ resultCode: 'Authorised',
+ amount: {
+ currency: 'EUR',
+ value: 1000,
+ },
+ merchantReference: '98',
+ additionalData: {
+ cvcResult: '1 Matches',
+ 'checkoutThreeD.selectedBrand': 'mc',
+ authCode: '040715',
+ avsResult: '4 AVS not supported for this card type',
+ 'checkoutThreeD.merchantReference': '600',
+ authorisationMid: '1000',
+ acquirerAccountCode: 'TestPmmAcquirerAccount',
+ },
+})
diff --git a/extension/test/unit/fixtures/ctp-payment-make-payment.json b/extension/test/unit/fixtures/ctp-payment-make-payment.json
new file mode 100644
index 000000000..9f09c89f3
--- /dev/null
+++ b/extension/test/unit/fixtures/ctp-payment-make-payment.json
@@ -0,0 +1,58 @@
+{
+ "amountPlanned": {
+ "type": "centPrecision",
+ "currencyCode": "EUR",
+ "centAmount": 795,
+ "fractionDigits": 2
+ },
+ "paymentMethodInfo": {
+ "paymentInterface": "ctp-adyen-integration"
+ },
+ "custom": {
+ "type": {
+ "typeId": "type",
+ "key": "ctp-adyen-integration-web-components-payment-type"
+ },
+ "fields": {}
+ },
+ "transactions": [
+ {
+ "id": "6b1d4c68-8e31-4528-9af8-dc09ad653d91",
+ "type": "Authorization",
+ "amount": {
+ "type": "centPrecision",
+ "currencyCode": "EUR",
+ "centAmount": 495,
+ "fractionDigits": 2
+ },
+ "interactionId": "8835513921644842",
+ "state": "Success"
+ }
+ ],
+ "interfaceInteractions": [
+ {
+ "type": {
+ "typeId": "type",
+ "key": "ctp-adyen-integration-interaction-payment-type"
+ },
+ "fields": {
+ "request": "{\n \"riskData\": {\n \"clientData\": \"eyJ2ZXJzaW9uIjoiMS4wLjAiLCJkZXZpY2VGaW5nZXJwcmludCI6ImRmLXRpbWVkT3V0In0=\"\n },\n \"paymentMethod\": {\n \"type\": \"scheme\",\n \"encryptedCardNumber\": \"adyenjs_0_1_25$6JqHXl7+6iLItETn95i1GvE2t4QYomj5HW3llssV/cvgpiMohtAWyafHGCxC27sBj4dwunNQzcwrMwNP30G1QvGu4cbID5Wy3D2IOa2OvG+9W4OqxifZXTo2Txr7Xo4DT7y5vgCyH3r4kgQOzfc0TgHyYjCDdXsPGMYJ3QUs180eDMeYuQ82YR/dDjFuO3ea5QQJxF5j2gVzgcTmU3U8HYIhtExboPnb55FB3cOFqDfrGHPb2s8Tue2dOeEWO6RAbZiUGtf93o4p0PoILC8WRP9l6VrtTiSzqoJhng+i2a59sK6HSlIXuynk1i7uI0RZ1ZJDfnvZo5GjSUTHnNkN6A==$dHvwPh/el9VNWkBXkCL8bUdUvoXSGw1sJ0aoJro/wq0YOE7TXmOr8fbQa2E4RjaFjgB4lHxsx7EiMeWer3EEa9u/EXNTm2IHsdN3AVN3L2e8VZupSq8z0Vb73P6jeVOhAh30eXEZLWL/F5KOL37+8451eaf2L8L7Ct2uIoz/HNbpYSdAvoMHP+hed2v5Bjktr6mOs7eYtDiI2U3BIHsMBUqefThHyu1c9052CiiRuPJeigYFQs1FzQWKzAPBLCl1pAs6um2L7twCPf3gF9f3j6Ik5k+138lbAo75eqq0f/njYp1TDijdjEFSv/o66krTCO/VomUJ+xo6MnMEbuyrR+SI2FwCpna9r8Dc8mbjl00uxb6ykJP3Qee+sV6wRS7jaks17NDH8nVYZZIdDena7vPaJCa0lNH422uCVsgzR8MEaMtyQqzhLLm4UPjxz0R2ASM8ytoWM9fb941x/JTNuTk/m9zeBqGXYam95owFnUCXiVLGfQ7lX6PeTQ45KbNTctQh0lXJUFJeo7H7ksb2muNt6+baQaIXl+7DhZX86LSkF/EJb053fgolby+UqQxiLcMbWuQkYpSJ0VOCKkueVi0HoQYZtRcXNQA5/pjIknUHhXi3JxxkYMQfihvx1M/4fVhNOkn8RHO17tXgI0YCAmq2A/OKWcblVjGe3kkqMYG4dvpSnnaHIOCudc6H7jQ8XML1xtiVKR0rtRM+yMXIYe1YWedY7qPGGLo/tI0pxGsJhTbZ2g==\",\n \"encryptedExpiryMonth\": \"adyenjs_0_1_25$zEhFejVFdXvYW0pzs5VvAGOecupNu9qJ4BEoKjikJ71ce7nLmGkSYPabhOMECLtrAKa2UaU8+gdFDTggTA4jk7TXpkaAN+ESYZg6PaSnXjq1wJszQGGJEplRExgqATM/pZ6srzHoBb8GbzzdFFcMebXqTO9cqQbjNSaY6jHUvCZouqQWy8MLadAcMcWcLD8OuJ7rnamd7GCo+bsCRI080FF60cNMxI3eNesK+IV376WwWc4aHb16jFbf6RkSWi4u+wain/fG8pwM0+PBsQ4eU1jPGBFuBUpfdhDqj1cbw4nwbvSrI5BEqIp5wKVd8CHE0gsEE/sOL1xrIJSohku74g==$ff7q2k7FcGq4OB2oCQUj3buakz417VvYXVmNjM/vkNRRvYWtfi+KE29L1xskvOAcw7QIabftML7mtQSzEdGsm3v0IVYhKfdrefjrnFq6osRgBizSNoQbp+xUIzzhSUJAKN22LjPCJhxr/XB143EtQU9V6sxUigxV6rPejqs7LHKTGK/EOgLT6TaxWYtPlU0b8aPI2M15Vy11X+/oZDk7fXKqezAixscyxxYko5qrLHpijgPKZmTgPPSOYWmCKSmab2msz0PAM/SR/nVhP5ehqXbn/nNWEBlfRXjvZXSt7KzM/gOImomlnilnBdzGIdNeFVL2bt0qvPqNhf1AcESh80mMA6gpR1xQowQ6kMHGK24LjIZPsx6IQQZmoWtyJ5FcEjYVklmWI4GJBZJX/U2bs48/iEvBXcWX8E6lu6JjvG5/WkBYa1SOAMwZnuPWIbwJQAmo2vzlxloNRG4B9xpOZEE=\",\n \"encryptedExpiryYear\": \"adyenjs_0_1_25$Rvlqat8+CfPGYSs2q0wH36xxmVizEofbjrVpMhgqCGwfvOJWbQJSkpK41OhoK1C/cECg3bPFw6eEQkUeBxznKyU+lk5VQ8l0MaeyJpYxVe4bRw0NpbrjnZjlp1CQWg+MD/mW8b7wCKLNQ2xupiYscA05lGchHvPOc+rriYUTX7WHt3jZI5Gx6LW8rkDY8nCE3VzcY0VpRryOHZA0q5ftGc4/3KB3TZHuVFHLUNebAKi8gfy+HtChRAL4Y/ZGjn39z3oX6vHALg4GKIX7krd9Xq9ksTtHCaWE/yIZdQkdUXfR31aR88h3XPP2iCN0sC5uGdyrYNHIcz6pRSHZwlvwiw==$19q6x0aWFsYVYl8DPUjn/D43t9xux42pddujnp5nqauyZwEj98co3k5kvM7LwWAVflNACS7e2N9DCUOgAwqgimOQ4lrSn5OJ7b+BCCoHA/UlzZfFzs57rSvy2vbdUpsG79k0LPFxilmN0uhRfjrjPb9do630cPZIiLGboZup5bc1VMWv3jHFiEWLU0Nk1NLv58SmzmDyLHPEqx3iviA5MOo2IBpCvzMcy8CYLTfL8NTCoaPW4+fNOPD7zRzd/IApqEeALEA3XrBIYweHoyEgHTdUt98bMzvDEyxaBIGo9j8+s5Zqro1qRRhPeidt2AcEpgo19ITEX/oTmu2fE8cF7vo/U9J85bxppwhJWbTaQ826jPz2k2NonSu1spbHxqnV+Mzu23LDXfXZAbU9Uqgp8l5id99fQUIuB2TEkH2VL2VDlGgWMjbyRGXXpf2q0a6W1V5zNs8THjGH3Y7pvnBNCwNs\",\n \"encryptedSecurityCode\": \"adyenjs_0_1_25$YQyNBPAVY3gCS42K1pGqu3oSOfk61EOkrjNB2CEANMQK/VPlJKBW0jv325fyyrJ4EJw2D7Pk6RuAi5VZ+r4pyNtn1nJ+UdZd7w6gNxs0g1hdaYE5nst+DcV85SZxNzvwVByiyaAG/IIVRr1WPMIkn3g6RosDRuS8p9PRNKRqd4W0S6+Pf2GYq6X8k4K9+YiO4dvKTldHnhX6PRQE0KrQm75CDnStSyVrUooD7IjX2FNx4YCnHx5uthzH0CDbDQiwqQfqBkC8OpgSi2QoWP9n/Lcsyc+s6HOS9kDeHsaD/zMq6wxWfwlGIdB9Kw2axYxXr1C1sjZ3Adu+JbRCjtDIZg==$Pxw7HKD3Qa98EnQTVvjpKZyLMW4aiL+uA/p/fy9vmZml5cKKs0qUrD0dKOSOz8DYpnmV2J1qZ/8NyG3rd0cE63bkpvpZX79klLDmYodexhAQTWL0M7efoNDYyIxxmEg0LQvdeVDsrboQDM0bNCpDcdIJ2QDoj9QJ14QM91fntgJPxFHcshhIG6BESXrWeI9bNM/uhqyuMag2qzR60AFpaabHWT92v3VqEZbh21KRXe/6VDnRlWCF6659VIW1cVZUp2QemofK9YKSkHMe117t3Xy5mwgmKj7onPhDlANL+tV2LJgubvJAqwi5VY/QJ5sa6jVLGkUEjztFwNmXwhQDYTHFT8hUyDxh24wC+9O8PxeFXAFmJ/0J0Q42jeu/wVUjV3FMT2qhzd3uhLy+8VefJardgvv7irgeMjoux5qsbTtGCkZKF+xnEDf5/n41+DAB5kt6tsJa03KIHA==\"\n },\n \"browserInfo\": {\n \"acceptHeader\": \"*/*\",\n \"colorDepth\": 24,\n \"language\": \"en-US\",\n \"javaEnabled\": false,\n \"screenHeight\": 1050,\n \"screenWidth\": 1680,\n \"userAgent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36\",\n \"timeZoneOffset\": -120\n },\n \"reference\": 98,\n \"amount\": {\n \"currency\": \"EUR\",\n \"value\": 795\n }\n}",
+ "createdAt": "2019-02-28T22:16:04.246Z",
+ "response": "response=",
+ "type": "makePayment"
+ }
+ },
+ {
+ "type": {
+ "typeId": "type",
+ "key": "ctp-adyen-integration-interaction-payment-type"
+ },
+ "fields": {
+ "request": "request",
+ "createdAt": "2019-02-28T22:16:19.086Z",
+ "response": "response",
+ "type": "completePayment"
+ }
+ }
+ ]
+}
diff --git a/extension/test/unit/make-payment.handler.spec.js b/extension/test/unit/make-payment.handler.spec.js
new file mode 100644
index 000000000..4ea7415c7
--- /dev/null
+++ b/extension/test/unit/make-payment.handler.spec.js
@@ -0,0 +1,419 @@
+import nock from 'nock'
+import { expect } from 'chai'
+import _ from 'lodash'
+import config from '../../src/config/config.js'
+import makePaymentHandler from '../../src/paymentHandler/make-payment.handler.js'
+import paymentSuccessResponse from './fixtures/adyen-make-payment-success-response.js'
+import paymentErrorResponse from './fixtures/adyen-make-payment-error-response.js'
+import paymentRefusedResponse from './fixtures/adyen-make-payment-refused-response.js'
+import paymentRedirectResponse from './fixtures/adyen-make-payment-3ds-redirect-response.js'
+import paymentValidationFailedResponse from './fixtures/adyen-make-payment-validation-failed-response.js'
+import utils from '../../src/utils.js'
+
+const { execute } = makePaymentHandler
+
+describe('make-payment::execute', () => {
+ let scope
+
+ let ctpPayment
+
+ /* eslint-disable max-len */
+ const makePaymentRequest = {
+ reference: 'YOUR_REFERENCE',
+ riskData: {
+ clientData:
+ 'eyJ2ZXJzaW9uIjoiMS4wLjAiLCJkZXZpY2VGaW5nZXJwcmludCI6ImRmLXRpbWVkT3V0In0=',
+ },
+ paymentMethod: {
+ type: 'scheme',
+ encryptedCardNumber:
+ 'adyenjs_0_1_25$DUEzRRqi0giNphYds8B1/AhDcp1Mx9fM6uFW/fxx7HcO2vd8Lt/tT0IgODmu16duCco+vnB+HFJVV5t3m6yD93AZxA/ugFU7uzggh7UNAKPd3khkpReBRoHSzLwyj9dRnOxTDYWRX+K/3ozT+9RXvfHoPL1nWhU6A0DIKAdjiurDRHZA657XNZKd2M090R1yhqmIGH7rkHNJ3yV7/Ox/qTi5KLB4TmiEpGKD/sbuy18hG69om+66+BttPglSwPFZIy8zNXuqetYQaLY+cVlYfdKcRgEvoKJay85AtZgbxGvpp6pB+AXzIR55HM+KYykxHcHc/7O6KRMWjoxOkd9vpA==$FkH2iICmazYvxCLQ9Cu8zmFHfoXwcCZ/tMGuPyulvuyRar9Z5SMAx9GyTQvSHzTYNhglA4wQJNoEP9Vt9hnOUWdonWLMbLBDt4QhuB6c2kGcGRU7As6cBkDqllTNjItFY/19bpHIfWs50s+KbJSraAQNihpBkhRjKcXZeuxleOkpZK8Ta6B3jOLLcVXkjAYtcGl220KlOaT760UsqQpxD2UuM8UQVZEFml2WDZe1dmbzpebZVCDcy07KBy5ClsxqIErFwZkr22GXzrgg4Vg+WUg3XpAWGibRZvAsIlZO7AcVWgRzb1Sc/eA9lML0TkwS1+43vh6Z4FsVtgBI7lNFuqN9tsFba4UW1pKM3cnjL/BKH69rXO5dMZWLs0HSiGpGoOAudJTrq7T3SfiuNDignlWkww2MYuDFPxXw7H7n1gXXwczwQCVCPxh3y8xv2OM/67/Re5+zQ88N3qn+rjXssCjEtIsl5SqLJ8k675ULrS1oW8z9NoxfkE8RsKx9drrWOp37TtkRqAgqhUMgFqQM8yXo8jkujBc4EGQMysNY46lDGZkapfNIyoT6ncAJOCj8Sy8toVBrhfRqAEOFxF8weHas69EZEGxqgIlccaBKec1/TyjkUEvscST4gNcO29b3e9wV4TO73XTziqKNY5akG2uqY1YYyzQkE+AwjpDxw5JTUuuSh14bXr853W5/dy7rw9qrr3P+1iONsIqcgW7mVb596hD0J86Zc+hfbsQn1e/sFaalCX18n/PyOD9c1Fqqox2KVJQ53OuUM0HfzErb8LW0SOhD6Rcban9cWsdLmIOPpIv1d5VPUpmDb+Sa5PLXgP+IPtS5yms/i/GGn1B/ykak9t1UBe1zR71r4CqKey5lRbjYV3lOlO4WAIjLyIYYBNAjZuH3qd/KGjHTaK88DnZM6Rpr+TTNlTO1c0fs/nm8xy0rmE9QBM5bkn0LfIwli4fkRefI3rQz1doJ+plBHl4w8lk5Oe45+L01R9NhCoKYH8INwski5FxIt0SdiTD8nYnIdRzgBVrnLqf6EHvFMIwo+mcMPRv3+bbSlgLfeVvqxLSZcyz73vnP/stQPt80Q79Ru7FwUNkrXCELyMCd7CKATK5TysWN',
+ encryptedExpiryMonth:
+ 'adyenjs_0_1_25$Idi+jcq3qQgNx/Fqr4MTpy2e6vnktdvn8jTqrqWTmAgYWvnx7Pmler69ANWLRuW54xL0BQ2r7WRYVZsuVun2oNuxij54CLUhsn7b++0CJjYAhwmH6Bu3s5edgKv/2DqraLB7gq8Cjw2gbc6HQFgxLpeWfJzosLQFPRLW9nVgrceXwGS42sm3u9IwXFidwyVTQq6WSxUEE+WFtfPUe0eJGaEGqwx2sgMm2o/oUgvyxiuTNu361oIYWHfNSsVsmqjLOPKX7ztmM0Y90AbE/+PLlg5ZC9jFZUReNy9o0IB2DbR7R1oCk5obLp0uJZrywwk0fCLLAI4niKWVeFy3wVUpeQ==$B/xn3zwyW+l7qqyd3mmZbRzAG36MG1DNbKAyqE8y0fkvEg0eXY3lsclmx34vM940EAz05QdkyYT6JTLfzTrkKstmj9nV0SnTGy0DMxY0BsOzYfkPVQC7oYkOGLk+EGZUgsKwfJBdVAcIgPIZmi4y+DVYlxLJTR6fXgh5Kvsg5w6DZriYtdjlyfS6nmRHCubBgdk9HHAjAHI4DI7tVpgowdP3Q7rGdlknjFIUbdgfXPpVj6RClti+Ku+sKSSucGpofOq/sjlG5UOM2MkxJ4P0ABSIoJGb6xpGyBFQ+yFpH1Cp3p5QsVfI/NEqvD8fKHgD0q0UJDhVzmpv8Zp3vExWB1H2+9vtEyOMwSoKuUwT0VgxT0saDu0/KZrTvw34q/6IruXQ/+qrMmw9dt4+cAJroV0ubdy1KZNy3QQ1Wvb5FZK6eXvmyF4UZuBzGxJTupjdLim9UwLGPGA2mUZZn1OW2pIp76PYv5CucqAet3bT4+k51A==',
+ encryptedExpiryYear:
+ 'adyenjs_0_1_25$De1eSM6gNsQqfG81q9Qsz6jYKz2fk0/I4wfMz+ZKI4FQciwFD4TSHvxl6tJ90GvwDDEQpZVpZWWCfeWjqeFiCzAiP3iO+oM/Wa6jEjjFWI8pZ2QwVKhqemdPc37DbZxyNtlj7zMHL2BxNj7M1iuxIBm6pnUZVSnqXOMOLVzIbJDf0hVHAhp60i1L73msD7kmdeajZLlJqhZTTSoXFrmNSqHjis5bzLitpZrh4kOAMDTnMMs8ED/9KCYlXVQm1i5MWa/KPMi80psSI2Fzmq/nU8Ub5NzKF0VYdHbvsXOdXq6MAjnFu5QD6gRoNzowx4QkktwPckoBYKi70+FHKsX9Ew==$eEs0op2EAcFq9uIyUSRkTYqhgGT4r6YQWsCSN0aWVicCCO04FgxZ3uPh/xF3XsHOlqxz2PpBNiQRB0BNflrWvnwHAUDD/Fbvqhejadogx4wuH3WRqf+3nvxZKHW5ik7cDuE+BhIlAI075PY8q/RowWaDUDO8ZgjoQUIwuQuFmvjEbsu/nu9wTShM1//nNTIofc0jk1gc5u1hmqkIgA+MZWQYZ925wF2A5KQk+ncyBNC15c8z5ao03uAha872TWy6y7avUwv1ugXk/zgvBCCG87SRuu2G+R4rpro0fLjP5pxE8RTimWexRwDoPYPcztEUZcjv80jdEDPULciw9fdfefQzpgj1wieWtZTUsBm8lHG1mM6c+nJkS7JDrh12RbP3gDBUvJkBtmZBsTp3oevW6torfqTekUsBh6e2I1ual9xCmXLkKM4fusWWm7clGXe49fvqDJ3JZRezhvdqYR3GO2eKT3OvdHjtT1Rqul0Ewifo17Y=',
+ encryptedSecurityCode:
+ 'adyenjs_0_1_25$d/OMm3cbLOV4Dk9+a7rmJjZBaOsDCka6bTiuDG/JeeBuKYNjocNRSFnlPk2oV2+zUZWCHVpiFNx2ws9hDiNz9uxllO3TUpRk3whZpj30a7WUjXYOEXYgynXVtqpULXB5zP8Ro+1jP1Dr2Zbt00N8WPfehz/BnfsOu4fn2DsMhrIDlldYucsB50q1HzyLCa5IUFIsPaGQFV6TwzInLbhvEt4Nq7P1WZ/KaqJhwYURNgU89cjLUtboqIw/NGoDxG8XzdfCyGQE4yJYItgq71kpzdpIl0onxoaOaBBOaAo6ig3zNVDXl667LYiCdO049zIysS0eqJrTfzyw3qQDbiz1OQ==$5lxjuK68dSuG9jXL+sGufH2X1m2P8Z+MpswXgz28Z9OMuXIqUUFStx0EGA5WFCCZyEjknIdMd6FzbUTSji2mVYbbZWLvcxuHb/xsERQeltO1W7joM0co6GHZcVg+Vr8wp4foUCthX1y5zcIcLDVw4L73CZeRjBZyyGGTeOFV4zAr0UAYKhjQbWHQ7d/SyZuRSZGk2NL6u/eY8bf5ogSHnkYSRtVJYd95WCBhpiZR1AWrjpZBJtDqh2q53zHZuWtwmIJiGO7lZZdDdc/TpbOsLpJWcOXq6UYPiwyfIuE8U6xKyLTumkHNOBv8vDHT2PRlyvufUcrp7XthYBpHJqADjD0jvb8Kn80tL7/RDmbCtTUzHq1GFlzB25nF4JErDPgUZ+BiSI7SlMGmeJdoy9RKP2DIKZEEiw1/w3dA/4ea5D7hZ3D53LKBJTEO7uyqTJI+/M1NuAKSeeFLZE9X95+AFpx3RDGKCG380PAH',
+ },
+ browserInfo: {
+ acceptHeader: '*/*',
+ colorDepth: 24,
+ language: 'en-US',
+ javaEnabled: false,
+ screenHeight: 1050,
+ screenWidth: 1680,
+ userAgent:
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36',
+ timeZoneOffset: -120,
+ },
+ amount: {
+ currency: 'EUR',
+ value: 1000,
+ },
+ }
+ const adyenMerchantAccount = config.getAllAdyenMerchantAccounts()[0]
+
+ before(async () => {
+ ctpPayment = await utils.readAndParseJsonFile(
+ 'test/unit/fixtures/ctp-payment-make-payment.json'
+ )
+ })
+
+ beforeEach(() => {
+ const adyenConfig = config.getAdyenConfig(adyenMerchantAccount)
+ scope = nock(`${adyenConfig.apiBaseUrl}`)
+ })
+
+ afterEach(() => {
+ nock.cleanAll()
+ })
+
+ it(
+ 'when resultCode from Adyen is "Authorized", ' +
+ 'then it should return actions "addInterfaceInteraction", "setCustomField", "setKey" and "addTransaction"',
+ async () => {
+ scope.post('/payments').reply(200, paymentSuccessResponse)
+
+ const ctpPaymentClone = _.cloneDeep(ctpPayment)
+ ctpPaymentClone.custom.fields.makePaymentRequest =
+ JSON.stringify(makePaymentRequest)
+ ctpPaymentClone.custom.fields.adyenMerchantAccount = adyenMerchantAccount
+
+ const response = await execute(ctpPaymentClone)
+
+ expect(response.actions).to.have.lengthOf(6)
+
+ const setMethodInfoMethod = response.actions.find(
+ (a) => a.action === 'setMethodInfoMethod'
+ )
+ expect(setMethodInfoMethod.method).to.equal('scheme')
+
+ const setMethodInfoName = response.actions.find(
+ (a) => a.action === 'setMethodInfoName'
+ )
+ expect(setMethodInfoName.name).to.eql({ en: 'Credit Card' })
+
+ const addInterfaceInteraction = response.actions.find(
+ (a) => a.action === 'addInterfaceInteraction'
+ )
+ expect(addInterfaceInteraction.fields.type).to.equal('makePayment')
+ expect(addInterfaceInteraction.fields.request).to.be.a('string')
+ expect(addInterfaceInteraction.fields.response).to.be.a('string')
+ expect(addInterfaceInteraction.fields.createdAt).to.be.a('string')
+
+ const request = JSON.parse(addInterfaceInteraction.fields.request)
+ const requestBody = JSON.parse(request.body)
+ expect(requestBody.reference).to.deep.equal(makePaymentRequest.reference)
+ expect(requestBody.riskData).to.deep.equal(makePaymentRequest.riskData)
+ expect(requestBody.paymentMethod).to.deep.equal(
+ makePaymentRequest.paymentMethod
+ )
+ expect(requestBody.browserInfo).to.deep.equal(
+ makePaymentRequest.browserInfo
+ )
+ expect(requestBody.amount).to.deep.equal(makePaymentRequest.amount)
+ expect(requestBody.merchantAccount).to.equal(adyenMerchantAccount)
+
+ const setCustomFieldAction = response.actions.find(
+ (a) => a.action === 'setCustomField'
+ )
+ expect(setCustomFieldAction.name).to.equal('makePaymentResponse')
+ expect(setCustomFieldAction.value).to.be.a('string')
+ expect(setCustomFieldAction.value).to.equal(
+ addInterfaceInteraction.fields.response
+ )
+
+ const setKeyAction = response.actions.find((a) => a.action === 'setKey')
+ expect(setKeyAction.key).to.equal(
+ JSON.parse(paymentSuccessResponse).pspReference
+ )
+
+ const addTransaction = response.actions.find(
+ (a) => a.action === 'addTransaction'
+ )
+ expect(addTransaction.transaction).to.be.a('object')
+ expect(addTransaction.transaction.type).to.equal('Authorization')
+ expect(addTransaction.transaction.state).to.equal('Success')
+ expect(addTransaction.transaction.interactionId).to.equal(
+ JSON.parse(paymentSuccessResponse).pspReference
+ )
+ }
+ )
+
+ it(
+ 'when resultCode from Adyen is "RedirectShopper", ' +
+ 'then it should return actions "addInterfaceInteraction", "setCustomField" and "setKey"',
+ async () => {
+ scope.post('/payments').reply(200, paymentRedirectResponse)
+
+ const ctpPaymentClone = _.cloneDeep(ctpPayment)
+ ctpPaymentClone.custom.fields.makePaymentRequest =
+ JSON.stringify(makePaymentRequest)
+ ctpPaymentClone.custom.fields.adyenMerchantAccount = adyenMerchantAccount
+
+ const response = await execute(ctpPaymentClone)
+
+ expect(response.actions).to.have.lengthOf(5)
+
+ const addInterfaceInteraction = response.actions.find(
+ (a) => a.action === 'addInterfaceInteraction'
+ )
+ expect(addInterfaceInteraction.fields.type).to.equal('makePayment')
+ expect(addInterfaceInteraction.fields.request).to.be.a('string')
+ expect(addInterfaceInteraction.fields.response).to.be.a('string')
+ expect(addInterfaceInteraction.fields.createdAt).to.be.a('string')
+
+ const request = JSON.parse(addInterfaceInteraction.fields.request)
+ const requestBody = JSON.parse(request.body)
+ expect(requestBody.reference).to.deep.equal(makePaymentRequest.reference)
+ expect(requestBody.riskData).to.deep.equal(makePaymentRequest.riskData)
+ expect(requestBody.paymentMethod).to.deep.equal(
+ makePaymentRequest.paymentMethod
+ )
+ expect(requestBody.browserInfo).to.deep.equal(
+ makePaymentRequest.browserInfo
+ )
+ expect(requestBody.amount).to.deep.equal(makePaymentRequest.amount)
+ expect(requestBody.merchantAccount).to.equal(adyenMerchantAccount)
+
+ const setCustomFieldAction = response.actions.find(
+ (a) => a.action === 'setCustomField'
+ )
+ expect(setCustomFieldAction.name).to.equal('makePaymentResponse')
+ expect(setCustomFieldAction.value).to.be.a('string')
+ expect(setCustomFieldAction.value).to.equal(
+ addInterfaceInteraction.fields.response
+ )
+
+ const setKeyAction = response.actions.find((a) => a.action === 'setKey')
+ expect(setKeyAction.key).to.equal(makePaymentRequest.reference) // no pspReference until submitting additional details in redirect flow
+ }
+ )
+
+ it(
+ 'when adyen validation failed, ' +
+ 'then it should return actions "addInterfaceInteraction", "setCustomField" and "setKey"',
+ async () => {
+ scope.post('/payments').reply(422, paymentValidationFailedResponse)
+
+ const ctpPaymentClone = _.cloneDeep(ctpPayment)
+ ctpPaymentClone.custom.fields.makePaymentRequest =
+ JSON.stringify(makePaymentRequest)
+ ctpPaymentClone.custom.fields.adyenMerchantAccount = adyenMerchantAccount
+
+ const response = await execute(ctpPaymentClone)
+
+ expect(response.actions).to.have.lengthOf(5)
+
+ const addInterfaceInteraction = response.actions.find(
+ (a) => a.action === 'addInterfaceInteraction'
+ )
+ expect(addInterfaceInteraction.fields.type).to.equal('makePayment')
+ expect(addInterfaceInteraction.fields.request).to.be.a('string')
+ expect(addInterfaceInteraction.fields.response).to.be.a('string')
+ expect(addInterfaceInteraction.fields.createdAt).to.be.a('string')
+
+ const request = JSON.parse(addInterfaceInteraction.fields.request)
+ const requestBody = JSON.parse(request.body)
+ expect(requestBody.reference).to.deep.equal(makePaymentRequest.reference)
+ expect(requestBody.riskData).to.deep.equal(makePaymentRequest.riskData)
+ expect(requestBody.paymentMethod).to.deep.equal(
+ makePaymentRequest.paymentMethod
+ )
+ expect(requestBody.browserInfo).to.deep.equal(
+ makePaymentRequest.browserInfo
+ )
+ expect(requestBody.amount).to.deep.equal(makePaymentRequest.amount)
+ expect(requestBody.merchantAccount).to.equal(adyenMerchantAccount)
+
+ const setCustomFieldAction = response.actions.find(
+ (a) => a.action === 'setCustomField'
+ )
+ expect(setCustomFieldAction.name).to.equal('makePaymentResponse')
+ expect(setCustomFieldAction.value).to.be.a('string')
+ expect(setCustomFieldAction.value).to.equal(
+ addInterfaceInteraction.fields.response
+ )
+
+ const setKeyAction = response.actions.find((a) => a.action === 'setKey')
+ expect(setKeyAction.key).to.equal(makePaymentRequest.reference)
+ }
+ )
+
+ it(
+ 'when resultCode from Adyen is "Refused"' +
+ 'then it should return actions "addInterfaceInteraction", "setCustomField", ' +
+ '"setKey" and "addTransaction"',
+ async () => {
+ scope.post('/payments').reply(422, paymentRefusedResponse)
+
+ const ctpPaymentClone = _.cloneDeep(ctpPayment)
+ ctpPaymentClone.custom.fields.makePaymentRequest =
+ JSON.stringify(makePaymentRequest)
+ ctpPaymentClone.custom.fields.adyenMerchantAccount = adyenMerchantAccount
+
+ const response = await execute(ctpPaymentClone)
+
+ expect(response.actions).to.have.lengthOf(6)
+
+ const addInterfaceInteraction = response.actions.find(
+ (a) => a.action === 'addInterfaceInteraction'
+ )
+ expect(addInterfaceInteraction.fields.type).to.equal('makePayment')
+ expect(addInterfaceInteraction.fields.request).to.be.a('string')
+ expect(addInterfaceInteraction.fields.response).to.be.a('string')
+ expect(addInterfaceInteraction.fields.createdAt).to.be.a('string')
+
+ const request = JSON.parse(addInterfaceInteraction.fields.request)
+ const requestBody = JSON.parse(request.body)
+ expect(requestBody.reference).to.deep.equal(makePaymentRequest.reference)
+ expect(requestBody.riskData).to.deep.equal(makePaymentRequest.riskData)
+ expect(requestBody.paymentMethod).to.deep.equal(
+ makePaymentRequest.paymentMethod
+ )
+ expect(requestBody.browserInfo).to.deep.equal(
+ makePaymentRequest.browserInfo
+ )
+ expect(requestBody.amount).to.deep.equal(makePaymentRequest.amount)
+ expect(requestBody.merchantAccount).to.equal(adyenMerchantAccount)
+
+ const setCustomFieldAction = response.actions.find(
+ (a) => a.action === 'setCustomField'
+ )
+ expect(setCustomFieldAction.name).to.equal('makePaymentResponse')
+ expect(setCustomFieldAction.value).to.be.a('string')
+ expect(setCustomFieldAction.value).to.equal(
+ addInterfaceInteraction.fields.response
+ )
+
+ const setKeyAction = response.actions.find((a) => a.action === 'setKey')
+ expect(setKeyAction.key).to.equal(
+ JSON.parse(paymentRefusedResponse).pspReference
+ )
+
+ const addTransaction = response.actions.find(
+ (a) => a.action === 'addTransaction'
+ )
+ expect(addTransaction.transaction).to.be.a('object')
+ expect(addTransaction.transaction.type).to.equal('Authorization')
+ expect(addTransaction.transaction.state).to.equal('Failure')
+ expect(addTransaction.transaction.interactionId).to.equal(
+ JSON.parse(paymentRefusedResponse).pspReference
+ )
+ }
+ )
+
+ it(
+ 'when resultCode from Adyen is "Error", ' +
+ 'then it should return actions "addInterfaceInteraction", "setCustomField", ' +
+ '"setKey" and "addTransaction"',
+ async () => {
+ scope.post('/payments').reply(422, paymentErrorResponse)
+
+ const ctpPaymentClone = _.cloneDeep(ctpPayment)
+ ctpPaymentClone.custom.fields.makePaymentRequest =
+ JSON.stringify(makePaymentRequest)
+ ctpPaymentClone.custom.fields.adyenMerchantAccount = adyenMerchantAccount
+
+ const response = await execute(ctpPaymentClone)
+
+ expect(response.actions).to.have.lengthOf(6)
+
+ const addInterfaceInteraction = response.actions.find(
+ (a) => a.action === 'addInterfaceInteraction'
+ )
+ expect(addInterfaceInteraction.fields.type).to.equal('makePayment')
+ expect(addInterfaceInteraction.fields.request).to.be.a('string')
+ expect(addInterfaceInteraction.fields.response).to.be.a('string')
+ expect(addInterfaceInteraction.fields.createdAt).to.be.a('string')
+
+ const request = JSON.parse(addInterfaceInteraction.fields.request)
+ const requestBody = JSON.parse(request.body)
+ expect(requestBody.reference).to.deep.equal(makePaymentRequest.reference)
+ expect(requestBody.riskData).to.deep.equal(makePaymentRequest.riskData)
+ expect(requestBody.paymentMethod).to.deep.equal(
+ makePaymentRequest.paymentMethod
+ )
+ expect(requestBody.browserInfo).to.deep.equal(
+ makePaymentRequest.browserInfo
+ )
+ expect(requestBody.amount).to.deep.equal(makePaymentRequest.amount)
+ expect(requestBody.merchantAccount).to.equal(adyenMerchantAccount)
+
+ const setCustomFieldAction = response.actions.find(
+ (a) => a.action === 'setCustomField'
+ )
+ expect(setCustomFieldAction.name).to.equal('makePaymentResponse')
+ expect(setCustomFieldAction.value).to.be.a('string')
+ expect(setCustomFieldAction.value).to.equal(
+ addInterfaceInteraction.fields.response
+ )
+
+ const setKeyAction = response.actions.find((a) => a.action === 'setKey')
+ expect(setKeyAction.key).to.equal(
+ JSON.parse(paymentErrorResponse).pspReference
+ )
+
+ const addTransaction = response.actions.find(
+ (a) => a.action === 'addTransaction'
+ )
+ expect(addTransaction.transaction).to.be.a('object')
+ expect(addTransaction.transaction.type).to.equal('Authorization')
+ expect(addTransaction.transaction.state).to.equal('Failure')
+ expect(addTransaction.transaction.interactionId).to.equal(
+ JSON.parse(paymentErrorResponse).pspReference
+ )
+ }
+ )
+
+ it(
+ 'when payment method is not in the adyenPaymentMethodsToNames map, ' +
+ 'then it should return setMethodInfoMethodAction with payment method name',
+ async () => {
+ scope.post('/payments').reply(200, paymentSuccessResponse)
+
+ const ctpPaymentClone = _.cloneDeep(ctpPayment)
+ const makePaymentRequestClone = _.cloneDeep(makePaymentRequest)
+ makePaymentRequestClone.paymentMethod.type = 'new payment method'
+ ctpPaymentClone.custom.fields.makePaymentRequest = JSON.stringify(
+ makePaymentRequestClone
+ )
+ ctpPaymentClone.custom.fields.adyenMerchantAccount = adyenMerchantAccount
+
+ const response = await execute(ctpPaymentClone)
+
+ const setMethodInfoMethod = response.actions.find(
+ (a) => a.action === 'setMethodInfoMethod'
+ )
+ expect(setMethodInfoMethod.method).to.equal('new payment method')
+
+ const setMethodInfoName = response.actions.find(
+ (a) => a.action === 'setMethodInfoName'
+ )
+ expect(setMethodInfoName).to.be.undefined
+ }
+ )
+
+ it(
+ 'when payment method is null, ' +
+ 'then it should not return setMethodInfoMethodAction action',
+ async () => {
+ scope.post('/payments').reply(200, paymentSuccessResponse)
+
+ const ctpPaymentClone = _.cloneDeep(ctpPayment)
+ const makePaymentRequestClone = _.cloneDeep(makePaymentRequest)
+ delete makePaymentRequestClone.paymentMethod.type
+ ctpPaymentClone.custom.fields.makePaymentRequest = JSON.stringify(
+ makePaymentRequestClone
+ )
+ ctpPaymentClone.custom.fields.adyenMerchantAccount = adyenMerchantAccount
+
+ const response = await execute(ctpPaymentClone)
+
+ const setMethodInfoMethod = response.actions.find(
+ (a) => a.action === 'setMethodInfoMethod'
+ )
+ expect(setMethodInfoMethod).to.be.undefined
+ }
+ )
+})
diff --git a/extension/test/unit/payment-handler.spec.js b/extension/test/unit/payment-handler.spec.js
index f1e283dc9..29f950cda 100644
--- a/extension/test/unit/payment-handler.spec.js
+++ b/extension/test/unit/payment-handler.spec.js
@@ -64,7 +64,7 @@ describe('payment-handler::execute', () => {
expect(response.errors).to.have.lengthOf.above(0)
expect(response.errors[0].message).to.equal(
- errorMessage.AMOUNT_PLANNED_NOT_SAME
+ errorMessage.CREATE_SESSION_AMOUNT_PLANNED_NOT_SAME
)
}
)
@@ -119,7 +119,7 @@ describe('payment-handler::execute', () => {
const response = await paymentHandler.handlePayment(ctpPaymentClone)
expect(response.errors[0].message).to.equal(
- errorMessage.AMOUNT_PLANNED_NOT_SAME
+ errorMessage.CREATE_SESSION_AMOUNT_PLANNED_NOT_SAME
)
})
})
diff --git a/extension/test/unit/submit-payment-details.handler.spec.js b/extension/test/unit/submit-payment-details.handler.spec.js
new file mode 100644
index 000000000..233e3c958
--- /dev/null
+++ b/extension/test/unit/submit-payment-details.handler.spec.js
@@ -0,0 +1,391 @@
+import nock from 'nock'
+import _ from 'lodash'
+import { expect } from 'chai'
+import sinon from 'sinon'
+import submitPaymentDetailsSuccessResponse from './fixtures/adyen-submit-payment-details-success-response.js'
+import submitPaymentDetailsChallengeRes from './fixtures/adyen-submit-payment-details-challenge-shopper-response.js'
+import makePaymentRedirectResponse from './fixtures/adyen-make-payment-3ds-redirect-response.js'
+import paymentDetailsHandler from '../../src/paymentHandler/submit-payment-details.handler.js'
+import config from '../../src/config/config.js'
+import c from '../../src/config/constants.js'
+import utils from '../../src/utils.js'
+
+const { execute } = paymentDetailsHandler
+
+describe('submit-additional-payment-details::execute', () => {
+ let ctpPayment
+ let scope
+ /* eslint-disable max-len */
+ const submitPaymentDetailsRequest = {
+ details: {
+ MD: 'M2RzMi40YTdmZGFjNTIzMzQyNDllNWY1YmQyMWQ5OGZlMGI0YjRiYzViNjkyYmEzODZiNDE0NmE5YjgzYTMzNjQwODFh',
+ PaRes:
+ 'BQABAgAl9h9S78IkDwV92Fa_O3ZdxU3sHIIa23Z0MT20jM5P74jtujtyy_dMqUL5IPiF_wb1tMvWJago_mdKRL6vQkBxxzg51N9M57akADykaDEm3NXLAPRWu7BIopC8zePXHxVte7Ui966ghJ5XFIZ_FfLaji1E_KFzzMqOOp72NxlTQGCwF8Usx2lCBvzfVhINGwuzu2pnjzxZatquaISb1epY82jrcDNAjL9JqkgJV_hodMkx0sRoVKqnjqsUiaVH0Gj3ppojuHrRNXmK0b4w2wYhlwRpczmNxN8UrMTXHX3lrROQXAq78d3xBMDmoV61H6KNPrFjFoAtcdz0dWMMk97JdtuWyBzqJ5kkCX1G2VrzXm4CnBNwogZrVvsd_KBssVgEmd4uNcYYYheSFA64FqTKL1_OEvnz6iOJ3lGdmE3UFA2rtQ7h16VUGXX9V0vUeibJXW1yAUP1GCjNnVNbdX9vTYfZubZ28pZZD09qkcnkPmnrgc9ElC2dwa1R2mOwYePco9OUH7YN0jBPt9Mp_l_5f4imIalclKtLya-VFmbbn0NFOY8UAsotl3Vs3otZa1tWxQ4Qkf9u7SG_biQORIUltlCgwCJG9ZAC6qSZgV1IOU9KB2uDPlF2MxFINYYJGtar4aSADc7lJtD-6aGz2Iev068snyqAkyvejzJUPuCsoBBV7DTmTcao_YSOzGKHhd3AAEp7ImtleSI6IkFGMEFBQTEwM0NBNTM3RUFFRDg3QzI0REQ1MzkwOUI4MEE3OEE5MjNFMzgyM0Q2OERBQ0M5NEI5RkY4MzA1REMiferf2airaRV3vVaz6LGdyZzAUFUTMy1y3rxMZfNiLU4b2QFrRmbamSpQ5d6KU43eBFnCBOUpEmX956Jv5dFWEfbQ8aiAN_rTLjIuBQABAQBeihZONltVS1RSkIq15Ncaf0TJP_RUw_nS4u2B48E7RdTAp8Fha3zkO5pVQtcOxmNEnD6MgSrt-8S9ctb52NzzpGkUuI372lqiQ4D0MCn49uuT8RZ2sl9ommdChKCfjuPsPnMMxUQGITmOPNUliE8BX280OeNQ4DHw6rV7Qv4al1KXw1c2DwETC-bxKRKsG7U22j5qA1JcraRhCFGZ0q43_6HZbWaa6-A_2PQlwFVL_8ywbjnR7orHJo91SWbDaFL99hnnGwHOFXdOeOaRgfgJsMoDssDaCLvba4XFOpF0crgj8-BMeto6wIYsB_e2E36UMMCg3U7ZR7L64m37v09JEAeTzYHTOwPrJTKuHNoZlYAAANyzlMKxcO1IcS5Eg7p7MRY4OPS88PlW2MWHK31YErfu2KQChEmJV16-zuQuT871Sq0VDirRXBCQ881TUlFd584SvO3PGSoAoibAAW73Anc7NvWlRe1bOoPgyA55uTD6FvElntRPFEg4w9Wc_8BU6y5PQ2nrqQmKroLRYjfxR2RT3xAjyV5gg0lEHt-WAqx30BXMFJct2hufvaliS5yEqZ6X4YjbGNT_UHUTu2QlnjOwik98E-HaZcctB7U4eARLCUDnFsK7KVoixdeXOmYwEZRXyfgNdW97PEvVvZNqFVcMlxSQo7-taaJwaOCUtTj__ynIfCsmonjWXNC_sIELyGSOEScxbSO8p8QXPVlrEbh58APJykjEncC9nSBj4nOWagN61cCBCfG_jGsPULkGmkLh-vXCee0PX_juY6bhw5-pZ7Juz-q8nGHYN5YML4txMHqTHb46O7HXZ3_QpZxEfvptp_HYcWotnSdnpjLQW-aF3V3IuaNNJIWWXGSZeeuAfnuBQMzbxFzfkJthUeQytt-TJXjrybTmzr_BpTT-6u9w3At4EnNcxUIYkgtwYvUerc0foK6rVjWqWJXMlv_zSjVzhbtXBMnACQEtbU6jlp0TRM8xgd-_XCwVjYHX9o7CXEqB_kx2IVPlAW5T2UR9U6ZKOctxJ_m7p46CZONpj29lGzDIWwp6syvKUazOmzPo9-VHaG0zoCv62824prcEQRM779jiU2k0zDnf7VNBjoode8o9Af_RBeEST02El5G_9J5cVrBPRNRNS9o1mxBPJO--KlBkg_FbwEq7M1y5MIHacqtBZN3UGq2esEB7xh3FJWiwL5O0XxLn41CI0FiQ0csXdzUaLOU96my467gNEZGk00p_B9Dl-3a8Gm6fIR5OEQNpeh7FBWMGF5UoBG20pyXvn8nJhnGmSMB4ARh8PWCEZCeEMwAAnTTy0aXTg8uJBc6u8pcbB4fOhj92mXyudgMJnxwHnIC3qMGbOvBJs3HTGakUObChVbzaAH27R9rVBtHhmr2P1BZNF68dnlNTuLil3_PlhlJuNwQepxooyGuTZMqPUx9Vu7ufLoOKEo9gtY4b_hmqSKuVNbENeHNxPhwdwg-ifGJSjqfxAot9X4S8NvJv3AQEJn5xha_grAh7Pu6T4I0AF_EuS6o14Wm1hZ4dobkABgfDXpXKvB98wr9HL8W23wkiAImMvLgefd5OmnO0P4vS7hf0vyOzwpfrgMcHG4QrBI2U2DRRJmIqPFICTuwNPXX3nQ3l1YfhtGIzmToW6EXVdnkGXvIjHmfbgF0XCcCP5M2YTf-89HJ6fV81vFRnd74gB-fjQ42_14JvnaOIbAIKehw1oe-QdSgfjuz9bp_zzzytF1xD4al4m1yeZ6ObKF-v_7L0yYnWfjZwcQZGJqWhQLOOtnv8D9sDjX2HwLi_A36WaqvyNFk3DKTt35le2fOgCXT9o_k3p-olYkyOVSRTfIWhdVth5iKYxmF6aKzSZByrFnMbVlFuwh1bTW9p12g258MQ3nWe4OvpMRzP5ymRnsjPk0enmVJf-ynrFcjzUmMXZlT-6kv5zDdyFVwBRwVyK2ro_uJ2wf9VqvGmjxhwPe33kYnqcb58_AnNyMR7tZVSIlTV3JdImAh9zk0HTzWw1nkRpYakYz79U0o6tZIsTL5q1VxvUk5N2bQB9tMyxw9gpxC1rzlTQWKHh4qRFfFztIE3OEA1jjnz2GUCPg4Omu9lAUZqjTnIjBavNsoa2zAxk4WaPPWbulChMXPN5k08DYKqJno5to6FU79CDhbPyDC2FD49bxjm_Z_zG2my5hzNVWoN_bM',
+ },
+ }
+ /* eslint-enable max-len */
+ const adyenMerchantAccount = config.getAllAdyenMerchantAccounts()[0]
+
+ before(async () => {
+ ctpPayment = await utils.readAndParseJsonFile(
+ 'test/unit/fixtures/ctp-payment-make-payment.json'
+ )
+ })
+
+ beforeEach(() => {
+ const adyenConfig = config.getAdyenConfig(adyenMerchantAccount)
+ scope = nock(`${adyenConfig.apiBaseUrl}`)
+ })
+
+ afterEach(() => {
+ nock.cleanAll()
+ })
+
+ it(
+ 'when resultCode from Adyen is "Authorized", ' +
+ 'then it should return actions "addTransaction", "addInterfaceInteraction" and "setCustomField"',
+ async () => {
+ scope
+ .post('/payments/details')
+ .reply(200, submitPaymentDetailsSuccessResponse)
+
+ const ctpPaymentClone = _.cloneDeep(ctpPayment)
+ ctpPaymentClone.custom.fields.submitAdditionalPaymentDetailsRequest =
+ JSON.stringify(submitPaymentDetailsRequest)
+ ctpPaymentClone.custom.fields.makePaymentResponse = JSON.stringify(
+ makePaymentRedirectResponse
+ )
+ ctpPaymentClone.custom.fields.adyenMerchantAccount = adyenMerchantAccount
+
+ const response = await execute(ctpPaymentClone)
+
+ expect(response.actions).to.have.lengthOf(4)
+ const addInterfaceInteraction = response.actions.find(
+ (a) => a.action === 'addInterfaceInteraction'
+ )
+ expect(addInterfaceInteraction.fields.type).to.equal(
+ c.CTP_INTERACTION_TYPE_SUBMIT_ADDITIONAL_PAYMENT_DETAILS
+ )
+ expect(addInterfaceInteraction.fields.request).to.be.a('string')
+ expect(addInterfaceInteraction.fields.response).to.be.a('string')
+ expect(addInterfaceInteraction.fields.createdAt).to.be.a('string')
+
+ const request = JSON.parse(addInterfaceInteraction.fields.request)
+ const requestBody = JSON.parse(request.body)
+ expect(requestBody.reference).to.deep.equal(
+ submitPaymentDetailsRequest.reference
+ )
+ expect(requestBody.riskData).to.deep.equal(
+ submitPaymentDetailsRequest.riskData
+ )
+ expect(requestBody.paymentMethod).to.deep.equal(
+ submitPaymentDetailsRequest.paymentMethod
+ )
+ expect(requestBody.browserInfo).to.deep.equal(
+ submitPaymentDetailsRequest.browserInfo
+ )
+ expect(requestBody.amount).to.deep.equal(
+ submitPaymentDetailsRequest.amount
+ )
+ expect(requestBody.merchantAccount).to.equal(adyenMerchantAccount)
+
+ const setCustomFieldAction = response.actions.find(
+ (a) => a.action === 'setCustomField'
+ )
+ expect(setCustomFieldAction.name).to.equal(
+ c.CTP_CUSTOM_FIELD_SUBMIT_ADDITIONAL_PAYMENT_DETAILS_RESPONSE
+ )
+ expect(setCustomFieldAction.value).to.be.a('string')
+ const expectedCustomFieldValue = JSON.parse(
+ _.cloneDeep(submitPaymentDetailsSuccessResponse)
+ )
+ delete expectedCustomFieldValue.additionalData
+ expect(setCustomFieldAction.value).to.equal(
+ JSON.stringify(expectedCustomFieldValue)
+ )
+ expect(setCustomFieldAction.value).to.equal(
+ addInterfaceInteraction.fields.response
+ )
+
+ const addTransaction = response.actions.find(
+ (a) => a.action === 'addTransaction'
+ )
+ expect(addTransaction.transaction).to.be.a('object')
+ expect(addTransaction.transaction.type).to.equal('Authorization')
+ expect(addTransaction.transaction.state).to.equal('Success')
+ expect(addTransaction.transaction.interactionId).to.equal(
+ JSON.parse(submitPaymentDetailsSuccessResponse).pspReference
+ )
+ const setKeyAction = response.actions.find((a) => a.action === 'setKey')
+ expect(setKeyAction.key).to.equal(
+ JSON.parse(submitPaymentDetailsSuccessResponse).pspReference
+ )
+ }
+ )
+
+ it(
+ 'when resultCode from Adyen is "ChallengeShopper", ' +
+ 'then it should return actions "addInterfaceInteraction" and "setCustomField"',
+ async () => {
+ scope
+ .post('/payments/details')
+ .reply(200, submitPaymentDetailsChallengeRes)
+
+ const ctpPaymentClone = _.cloneDeep(ctpPayment)
+ ctpPaymentClone.custom.fields.submitAdditionalPaymentDetailsRequest =
+ JSON.stringify(submitPaymentDetailsRequest)
+ ctpPaymentClone.custom.fields.makePaymentResponse = JSON.stringify(
+ makePaymentRedirectResponse
+ )
+ ctpPaymentClone.custom.fields.adyenMerchantAccount = adyenMerchantAccount
+
+ const response = await execute(ctpPaymentClone)
+
+ expect(response.actions).to.have.lengthOf(2)
+
+ const addInterfaceInteraction = response.actions.find(
+ (a) => a.action === 'addInterfaceInteraction'
+ )
+ expect(addInterfaceInteraction.fields.type).to.equal(
+ c.CTP_INTERACTION_TYPE_SUBMIT_ADDITIONAL_PAYMENT_DETAILS
+ )
+ expect(addInterfaceInteraction.fields.request).to.be.a('string')
+ expect(addInterfaceInteraction.fields.response).to.be.a('string')
+ expect(addInterfaceInteraction.fields.createdAt).to.be.a('string')
+
+ const request = JSON.parse(addInterfaceInteraction.fields.request)
+ const requestBody = JSON.parse(request.body)
+ expect(requestBody.reference).to.deep.equal(
+ submitPaymentDetailsRequest.reference
+ )
+ expect(requestBody.riskData).to.deep.equal(
+ submitPaymentDetailsRequest.riskData
+ )
+ expect(requestBody.paymentMethod).to.deep.equal(
+ submitPaymentDetailsRequest.paymentMethod
+ )
+ expect(requestBody.browserInfo).to.deep.equal(
+ submitPaymentDetailsRequest.browserInfo
+ )
+ expect(requestBody.amount).to.deep.equal(
+ submitPaymentDetailsRequest.amount
+ )
+ expect(requestBody.merchantAccount).to.equal(adyenMerchantAccount)
+
+ const setCustomFieldAction = response.actions.find(
+ (a) => a.action === 'setCustomField'
+ )
+ expect(setCustomFieldAction.name).to.equal(
+ c.CTP_CUSTOM_FIELD_SUBMIT_ADDITIONAL_PAYMENT_DETAILS_RESPONSE
+ )
+ expect(setCustomFieldAction.value).to.be.a('string')
+ expect(setCustomFieldAction.value).to.equal(
+ submitPaymentDetailsChallengeRes
+ )
+ expect(setCustomFieldAction.value).to.equal(
+ addInterfaceInteraction.fields.response
+ )
+ }
+ )
+
+ it(
+ 'when "submitPaymentDetailsRequest" does not contain "paymentData", ' +
+ 'then it should add paymentData to the request',
+ async () => {
+ scope
+ .post('/payments/details')
+ .reply(200, submitPaymentDetailsChallengeRes)
+
+ const requestWithoutPaymentData = _.cloneDeep(submitPaymentDetailsRequest)
+ delete requestWithoutPaymentData.paymentData
+
+ const ctpPaymentClone = _.cloneDeep(ctpPayment)
+ ctpPaymentClone.custom.fields.submitAdditionalPaymentDetailsRequest =
+ JSON.stringify(requestWithoutPaymentData)
+ ctpPaymentClone.custom.fields.makePaymentResponse =
+ makePaymentRedirectResponse
+ ctpPaymentClone.custom.fields.adyenMerchantAccount = adyenMerchantAccount
+
+ const response = await execute(ctpPaymentClone)
+
+ const addInterfaceInteraction = response.actions.find(
+ (a) => a.action === 'addInterfaceInteraction'
+ )
+ const request = JSON.parse(addInterfaceInteraction.fields.request)
+ const requestBody = JSON.parse(request.body)
+ expect(requestBody.paymentData).to.equal(
+ JSON.parse(makePaymentRedirectResponse).paymentData
+ )
+ }
+ )
+
+ it(
+ 'when "submitPaymentDetailsRequest" contains "paymentData", ' +
+ 'then it should NOT add paymentData to the request',
+ async () => {
+ scope
+ .post('/payments/details')
+ .reply(200, submitPaymentDetailsChallengeRes)
+ const testPaymentData = 'paymentDataFromSubmitPaymentDetailsRequest'
+
+ const requestWithoutPaymentData = _.cloneDeep(submitPaymentDetailsRequest)
+ requestWithoutPaymentData.paymentData = testPaymentData
+
+ const ctpPaymentClone = _.cloneDeep(ctpPayment)
+ ctpPaymentClone.custom.fields.submitAdditionalPaymentDetailsRequest =
+ JSON.stringify(requestWithoutPaymentData)
+ ctpPaymentClone.custom.fields.makePaymentResponse =
+ makePaymentRedirectResponse
+ ctpPaymentClone.custom.fields.adyenMerchantAccount = adyenMerchantAccount
+
+ const response = await execute(ctpPaymentClone)
+
+ const addInterfaceInteraction = response.actions.find(
+ (a) => a.action === 'addInterfaceInteraction'
+ )
+ const request = JSON.parse(addInterfaceInteraction.fields.request)
+ const requestBody = JSON.parse(request.body)
+ expect(requestBody.paymentData).to.equal(testPaymentData)
+ }
+ )
+
+ it(
+ 'when "removeSensitiveData" is false, ' +
+ 'then it should not remove sensitive data',
+ async () => {
+ const extensionDummyConfig = {
+ removeSensitiveData: false,
+ port: 8080,
+ logLevel: 'debug',
+ apiExtensionBaseUrl: 'testUrl',
+ basicAuth: false,
+ keepAliveTimeout: 10,
+ }
+
+ const sandbox = sinon.createSandbox()
+ sandbox.stub(config, 'getModuleConfig').returns(extensionDummyConfig)
+
+ scope
+ .post('/payments/details')
+ .reply(200, submitPaymentDetailsSuccessResponse)
+
+ const ctpPaymentClone = _.cloneDeep(ctpPayment)
+ ctpPaymentClone.custom.fields.submitAdditionalPaymentDetailsRequest =
+ JSON.stringify(submitPaymentDetailsRequest)
+ ctpPaymentClone.custom.fields.makePaymentResponse = JSON.stringify(
+ makePaymentRedirectResponse
+ )
+ ctpPaymentClone.custom.fields.adyenMerchantAccount = adyenMerchantAccount
+
+ const response = await execute(ctpPaymentClone)
+
+ expect(response.actions).to.have.lengthOf(4)
+ const addInterfaceInteractionAction = response.actions.find(
+ (a) => a.action === 'addInterfaceInteraction'
+ )
+ expect(addInterfaceInteractionAction.fields.response).to.include(
+ 'additionalData'
+ )
+ const setCustomFieldAction = response.actions.find(
+ (a) => a.action === 'setCustomField'
+ )
+ expect(setCustomFieldAction.value).to.include('additionalData')
+ const setKeyAction = response.actions.find((a) => a.action === 'setKey')
+ expect(setKeyAction.key).to.equal(
+ JSON.parse(submitPaymentDetailsSuccessResponse).pspReference
+ )
+ sandbox.restore()
+ }
+ )
+
+ it(
+ 'when "removeSensitiveData" is true, ' +
+ 'then it should remove sensitive data',
+ async () => {
+ const extensionDummyConfig = {
+ removeSensitiveData: true,
+ port: 8080,
+ logLevel: 'debug',
+ apiExtensionBaseUrl: 'testUrl',
+ basicAuth: false,
+ keepAliveTimeout: 10,
+ }
+
+ const sandbox = sinon.createSandbox()
+ sandbox.stub(config, 'getModuleConfig').returns(extensionDummyConfig)
+
+ scope
+ .post('/payments/details')
+ .reply(200, submitPaymentDetailsSuccessResponse)
+
+ const ctpPaymentClone = _.cloneDeep(ctpPayment)
+ ctpPaymentClone.custom.fields.submitAdditionalPaymentDetailsRequest =
+ JSON.stringify(submitPaymentDetailsRequest)
+ ctpPaymentClone.custom.fields.makePaymentResponse = JSON.stringify(
+ makePaymentRedirectResponse
+ )
+ ctpPaymentClone.custom.fields.adyenMerchantAccount = adyenMerchantAccount
+
+ const response = await execute(ctpPaymentClone)
+
+ expect(response.actions).to.have.lengthOf(4)
+ const addInterfaceInteractionAction = response.actions.find(
+ (a) => a.action === 'addInterfaceInteraction'
+ )
+ expect(addInterfaceInteractionAction.fields.response).to.not.include(
+ 'additionalData'
+ )
+ const setCustomFieldAction = response.actions.find(
+ (a) => a.action === 'setCustomField'
+ )
+ expect(setCustomFieldAction.value).to.not.include('additionalData')
+ const setKeyAction = response.actions.find((a) => a.action === 'setKey')
+ expect(setKeyAction.key).to.equal(
+ JSON.parse(submitPaymentDetailsSuccessResponse).pspReference
+ )
+ sandbox.restore()
+ }
+ )
+
+ it(
+ 'when transaction already exists in the payment, ' +
+ 'then it should not add new transaction',
+ async () => {
+ scope
+ .post('/payments/details')
+ .reply(200, submitPaymentDetailsSuccessResponse)
+
+ const ctpPaymentClone = _.cloneDeep(ctpPayment)
+ ctpPaymentClone.custom.fields.submitAdditionalPaymentDetailsRequest =
+ JSON.stringify(submitPaymentDetailsRequest)
+ ctpPaymentClone.custom.fields.makePaymentResponse = JSON.stringify(
+ makePaymentRedirectResponse
+ )
+ ctpPaymentClone.custom.fields.adyenMerchantAccount = adyenMerchantAccount
+ ctpPaymentClone.transactions.push({
+ id: 'e759b1a7-3666-46d0-80fa-051849144087',
+ type: 'Authorization',
+ amount: {
+ type: 'centPrecision',
+ currencyCode: 'EUR',
+ centAmount: 1000,
+ fractionDigits: 2,
+ },
+ interactionId: '852588749855524C',
+ state: 'Success',
+ })
+
+ const response = await execute(ctpPaymentClone)
+
+ expect(response.actions).to.have.lengthOf(3)
+ const addTransaction = response.actions.find(
+ (a) => a.action === 'addTransaction'
+ )
+ expect(addTransaction).to.be.undefined
+ const setKeyAction = response.actions.find((a) => a.action === 'setKey')
+ expect(setKeyAction.key).to.equal(
+ JSON.parse(submitPaymentDetailsSuccessResponse).pspReference
+ )
+ }
+ )
+})
diff --git a/extension/test/unit/validator-builder.spec.js b/extension/test/unit/validator-builder.spec.js
index 20c42a85b..9fed75e83 100644
--- a/extension/test/unit/validator-builder.spec.js
+++ b/extension/test/unit/validator-builder.spec.js
@@ -4,7 +4,7 @@ import errorMessages from '../../src/validator/error-messages.js'
const {
CREATE_SESSION_REQUEST_INVALID_JSON,
- AMOUNT_PLANNED_NOT_SAME,
+ CREATE_SESSION_AMOUNT_PLANNED_NOT_SAME,
CREATE_SESSION_REQUEST_MISSING_REFERENCE,
MISSING_REQUIRED_FIELDS_ADYEN_MERCHANT_ACCOUNT,
MISSING_REQUIRED_FIELDS_CTP_PROJECT_KEY,
@@ -59,7 +59,9 @@ describe('Validator builder', () => {
const errorObject = withPayment(payment)
.validateAmountPlanned()
.getErrors()
- expect(errorObject[0].message).to.equal(AMOUNT_PLANNED_NOT_SAME)
+ expect(errorObject[0].message).to.equal(
+ CREATE_SESSION_AMOUNT_PLANNED_NOT_SAME
+ )
}
)
@@ -127,7 +129,9 @@ describe('Validator builder', () => {
const errorObject = withPayment(payment)
.validateAmountPlanned()
.getErrors()
- expect(errorObject[0].message).to.equal(AMOUNT_PLANNED_NOT_SAME)
+ expect(errorObject[0].message).to.equal(
+ CREATE_SESSION_AMOUNT_PLANNED_NOT_SAME
+ )
}
)
diff --git a/notification/src/handler/notification/notification.handler.js b/notification/src/handler/notification/notification.handler.js
index dc455bcc9..96d5b423f 100644
--- a/notification/src/handler/notification/notification.handler.js
+++ b/notification/src/handler/notification/notification.handler.js
@@ -54,7 +54,7 @@ async function processNotification(
originalReference || pspReference,
ctpClient
)
- if (payment !== null)
+ if (payment)
await updatePaymentWithRepeater(payment, notification, ctpClient, logger)
else
logger.error(