Skip to content

Commit

Permalink
Add support for advanced flow
Browse files Browse the repository at this point in the history
ISSUE: CS0CC-12
  • Loading branch information
goran-stamenkovski-logeecom committed Sep 21, 2023
1 parent 1d216af commit 7ce2f84
Show file tree
Hide file tree
Showing 40 changed files with 2,437 additions and 184 deletions.
2 changes: 1 addition & 1 deletion docs/adr/0012-remove-obsoleted-api-call.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 25 additions & 0 deletions docs/adr/0013-add-support-for-advanced-checkout-flow.md
Original file line number Diff line number Diff line change
@@ -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.
44 changes: 44 additions & 0 deletions extension/resources/web-components-payment-type.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
6 changes: 6 additions & 0 deletions extension/src/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',

Expand Down
110 changes: 110 additions & 0 deletions extension/src/paymentHandler/line-items-utils.js
Original file line number Diff line number Diff line change
@@ -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 }
28 changes: 28 additions & 0 deletions extension/src/paymentHandler/make-lineitems-payment.handler.js
Original file line number Diff line number Diff line change
@@ -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 }
64 changes: 64 additions & 0 deletions extension/src/paymentHandler/make-payment.handler.js
Original file line number Diff line number Diff line change
@@ -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 }
36 changes: 25 additions & 11 deletions extension/src/paymentHandler/payment-handler.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading

0 comments on commit 7ce2f84

Please sign in to comment.