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(