Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ PAYMENT_GATEWAY_SAVED_CARD_LIMIT_FRAME =

PAYMENT_GATEWAY_DECISION_SYNC =
PAYMENT_GATEWAY_DECISION_MANAGER =
PAYMENT_GATEWAY_ENABLE_MOTO =
PAYMENT_GATEWAY_RUN_SYNC =
PAYMENT_GATEWAY_DECISION_SYNC_MULTI_MID =
PAYMENT_GATEWAY_NETWORK_TOKEN_MULTI_MID =
Expand Down
1 change: 0 additions & 1 deletion docs/Commercetools-Setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ Fields
| isv_payerEnrollStatus | String | false ||
| isv_payerEnrollHttpCode | Number | false ||
| isv_saleEnabled | Boolean | false ||
| isv_enabledMoto | Boolean | false ||
| isv_walletType | String | false ||
| isv_accountNumber | String | false ||
| isv_accountType | String | false ||
Expand Down
2 changes: 1 addition & 1 deletion docs/PayPal-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ Once your merchant Id is configured with Cybersource, you can integrate PayPal a
- Display PayPal as a payment option during the checkout process.You can get the PayPal payment Logo from
[PayPal Logo Centre](https://www.paypal.com/in/webapps/mpp/logo-center)

After selecting PayPal as the payment method, you can continue to the [Process a Payment (PayPal)](Process-a-Payment-PayPal.md) process.
After selecting PayPal as the payment method, you can continue to the [Process a Payment (PayPal)](Process-a-Payment-PayPal.md) process.
14 changes: 6 additions & 8 deletions docs/Process-a-Payment-MOTO.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,17 @@ For the Commercetools Extension to support MOTO transaction, follow the steps me
- [Process a Payment With eCheck](./Process-a-Payment-eCheck.md)


and add the following properties

| Property | Value | Required | Notes |
| ----------------------------- | -------------------------------- | -------- | ------------------------------------------------------------------------------------------------------- |
| custom.fields.isv_enabledMoto | Flag Indicating MOTO transaction | Yes | Boolean value. Used by the extention to identify whether it is a normal transaction or MOTO transaction |
and ensure the environment variable `PAYMENT_GATEWAY_ENABLE_MOTO` is set to `true` in your server configuration to enable MOTO transaction processing.


5. Add the payment to the cart

6. Update the Commercetools payment (<https://docs.commercetools.com/api/projects/payments>) with the fields mentioned in the step 5 of [Process a Card Payment Without Payer Authentication](./Process-a-Card-Payment-Without-Payer-Authentication.md) along with the below data

| Property | Value | Required | Notes |
| ----------------------------- | -------------------------------- | -------- | ------------------------------------------------------------------------------------------------------- |
| custom.fields.isv_enabledMoto | Flag Indicating MOTO transaction | Yes | Boolean value. Used by the extention to identify whether it is a normal transaction or MOTO transaction |
6. Update the Commercetools payment (<https://docs.commercetools.com/api/projects/payments>) with the fields mentioned in the step 5 of [Process a Card Payment Without Payer Authentication](./Process-a-Card-Payment-Without-Payer-Authentication.md)

> **_NOTE:_** MOTO transactions are controlled server-side via the `PAYMENT_GATEWAY_ENABLE_MOTO` environment variable. Set this to `true` in your deployment configuration to enable MOTO processing.


7. Add a transaction to the payment

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "commercetools-extension",
"version": "25.3.0",
"version": "25.3.1",
"description": "Payment Services Commercetools Extension",
"main": "build/main/index.js",
"typings": "build/main/index.d.ts",
Expand Down Expand Up @@ -84,6 +84,7 @@
"jsonwebtoken": "^9.0.2",
"jwk-to-pem": "^2.0.5",
"jwt-decode": "^4.0.0",
"node-fetch": "^3.3.2",
"moment": "^2.30.1",
"npm-run-all": "^4.1.5",
"nyc": "^15.1.0",
Expand All @@ -100,6 +101,7 @@
"@types/jsdom": "^21.1.7",
"@types/memory-cache": "^0.2.6",
"@types/node-forge": "^1.3.11",
"@types/node": "^20.11.30",
"@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0",
"ava": "6.3.0",
Expand Down
124 changes: 114 additions & 10 deletions src/apiController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import captureContext from './service/payment/CaptureContextService';
import flexKeys from './service/payment/FlexKeys';
import getCardByInstrument from './service/payment/GetCardByInstrumentId';
import keyVerification from './service/payment/GetPublicKeys';
import { ActionResponseType, AmountPlannedType } from './types/Types';
import { ActionResponseType, AmountPlannedType, CustomerTokensType } from './types/Types';
import { ApiError, errorHandler, PaymentProcessingError } from './utils/ErrorHandler';
import paymentActions from './utils/PaymentActions';
import paymentHandler from './utils/PaymentHandler';
Expand All @@ -18,6 +18,81 @@ import orderManagementHelper from './utils/helpers/OrderManagementHelper';
import payerAuthHelper from './utils/helpers/PayerAuthHelper';
import paymentHelper from './utils/helpers/PaymentHelper';

/* START GENAI */
/**
* Allowed fields for the CustomerTokensType schema.
* Only these string fields are expected in a valid token object.
*/
const ALLOWED_TOKEN_FIELDS: (keyof CustomerTokensType)[] = [
'alias', 'value', 'paymentToken', 'instrumentIdentifier',
'cardType', 'cardName', 'cardNumber', 'cardExpiryMonth',
'cardExpiryYear', 'addressId', 'timestamp', 'address',
];

/**
* Validates the parsed token object against the expected CustomerTokensType schema.
* Ensures the object is a plain object, contains only allowed fields,
* and that required fields (paymentToken, value) are non-empty strings.
*
* @param {any} tokenObj - The parsed token object to validate.
* @returns {boolean} - True if the object conforms to the expected schema.
*/
const isValidTokenSchema = (tokenObj: any): tokenObj is Partial<CustomerTokensType> => {
if (!tokenObj || typeof tokenObj !== 'object' || Array.isArray(tokenObj)) {
return false;
}
const tokenKeys = Object.keys(tokenObj);
for (const key of tokenKeys) {
if (!ALLOWED_TOKEN_FIELDS.includes(key as keyof CustomerTokensType)) {
return false;
}
}
if (!tokenObj.paymentToken || typeof tokenObj.paymentToken !== 'string' || tokenObj.paymentToken.trim().length === 0) {
return false;
}
if (!tokenObj.value || typeof tokenObj.value !== 'string' || tokenObj.value.trim().length === 0) {
return false;
}
for (const key of tokenKeys) {
if (key !== 'address' && tokenObj[key] !== undefined && typeof tokenObj[key] !== 'string') {
return false;
}
}
return true;
};

/**
* Verifies that the token identified by paymentToken belongs to the customer
* by fetching the customer's authoritative isv_tokens from the server-side
* commercetools API and validating the paymentToken against that source.
*
* @param {string} paymentToken - The paymentToken to verify ownership of.
* @param {string} customerId - The customer ID to fetch authoritative data for.
* @returns {Promise<boolean>} - True if the paymentToken is found in the customer's server-side tokens.
*/
const isTokenOwnedByCustomer = async (paymentToken: string, customerId: string): Promise<boolean> => {
if (!paymentToken || !customerId) {
return false;
}
const customerInfo = await commercetoolsApi.getCustomer(customerId);
const serverTokens = customerInfo?.custom?.fields?.isv_tokens;
if (!serverTokens || !Array.isArray(serverTokens) || serverTokens.length === 0) {
return false;
}
for (const tokenStr of serverTokens) {
try {
const token = JSON.parse(tokenStr);
if (token && token.paymentToken === paymentToken) {
return true;
}
} catch {
continue;
}
}
return false;
};
/* END GENAI */



/**
Expand Down Expand Up @@ -115,11 +190,19 @@ const customerUpdateApi = async (customerObj: any): Promise<ActionResponseType>
response = await paymentHandler.handleCardAddition(customerObj.id, customerAddress, customerObj);
} else if (isv_tokens && 0 < isv_tokens?.length) {
const tokensToUpdate = JSON.parse(customerObj.custom.fields.isv_tokens[0]);
/* START GENAI */
if (Constants.ISV_TOKEN_ACTION_DELETE === isv_tokenAction || Constants.ISV_TOKEN_ACTION_UPDATE === isv_tokenAction) {
if (!isValidTokenSchema(tokensToUpdate) || !(await isTokenOwnedByCustomer(tokensToUpdate.paymentToken as string, customerObj.id))) {
paymentUtils.logData(__filename, FunctionConstant.FUNC_CUSTOMER_UPDATE_API, Constants.LOG_ERROR, '', CustomMessages.ERROR_MSG_INVALID_TOKEN_DATA);
return response;
}
}
/* END GENAI */
switch (isv_tokenAction) {
case 'delete':
case Constants.ISV_TOKEN_ACTION_DELETE:
response = await paymentHandler.handleCardDeletion(tokensToUpdate, customerObj.id);
break;
case 'update':
case Constants.ISV_TOKEN_ACTION_UPDATE:
response = await paymentHandler.handleUpdateCard(tokensToUpdate, customerObj.id, customerObj);
break;
default:
Expand Down Expand Up @@ -238,16 +321,35 @@ const orderManagementApi = async (paymentId: string, transactionAmount: number |
pendingAmount = orderManagementHelper.getCapturedAmount(paymentObject);
break;
}
if (0 === transactionAmount) {
/* START GENAI */
if (Constants.CT_TRANSACTION_TYPE_CANCEL_AUTHORIZATION === transactionType) {
amountPlanned = { ...paymentObject.amountPlanned };
const transactionObject = paymentUtils.createTransactionObject(paymentObject.version, amountPlanned, transactionType, Constants.CT_TRANSACTION_STATE_INITIAL, undefined, undefined);
/* END GENAI */
const addTransaction = await commercetoolsApi.addTransaction(transactionObject, paymentId);
if (addTransaction && addTransaction.transactions?.length) {
const transactionLength = addTransaction.transactions.length;
const latestTransaction = addTransaction.transactions[transactionLength - 1];
if (latestTransaction && Constants.CT_TRANSACTION_STATE_SUCCESS === latestTransaction.state) {
if (latestTransaction?.type) {
apiResponse.successMessage = paymentUtils.handleOmSuccessMessage(latestTransaction.type);
}
} else {
apiResponse.errorMessage = paymentUtils.handleOMErrorMessage(2, transactionType);
}
} else {
apiResponse.errorMessage = CustomMessages.ERROR_MSG_ADD_TRANSACTION_DETAILS;
}
/* START GENAI */
} else if (!transactionAmount || transactionAmount <= 0) {
apiResponse.errorMessage = paymentUtils.handleOMErrorMessage(0, transactionType);
} else if (pendingAmount && transactionAmount && transactionAmount > pendingAmount) {
} else if (pendingAmount && transactionAmount > pendingAmount) {
apiResponse.errorMessage = paymentUtils.handleOMErrorMessage(1, transactionType);
} else {
if (transactionAmount) {
amountPlanned = paymentObject.amountPlanned;
amountPlanned.centAmount = paymentUtils.convertAmountToCent(transactionAmount, fractionDigits)
}
const transactionObject = paymentUtils.createTransactionObject(paymentObject.version, paymentObject.amountPlanned, transactionType, Constants.CT_TRANSACTION_STATE_INITIAL, undefined, undefined);
amountPlanned = { ...paymentObject.amountPlanned };
amountPlanned.centAmount = paymentUtils.convertAmountToCent(transactionAmount, fractionDigits);
const transactionObject = paymentUtils.createTransactionObject(paymentObject.version, amountPlanned, transactionType, Constants.CT_TRANSACTION_STATE_INITIAL, undefined, undefined);
/* END GENAI */
const addTransaction = await commercetoolsApi.addTransaction(transactionObject, paymentId);
if (addTransaction && addTransaction.transactions?.length) {
const transactionLength = addTransaction.transactions.length;
Expand Down Expand Up @@ -368,4 +470,6 @@ export default {
captureContextApi,
notificationApi,
orderManagementApi,
isValidTokenSchema,
isTokenOwnedByCustomer,
};
4 changes: 4 additions & 0 deletions src/constants/customMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export abstract class CustomMessages {
static readonly ERROR_MSG_ADD_TRANSACTION_DETAILS = 'There was an error while adding transaction details, please try again';
static readonly ERROR_MSG_APPLICATION_DETAILS = 'Unable to fetch transaction application details';
static readonly ERROR_MSG_APPLE_PAY_CERTIFICATES = 'Please provide certificates paths for Apple Pay in configuration file';
/* START GENAI */
static readonly ERROR_MSG_APPLE_PAY_INVALID_VALIDATION_URL = 'The Apple Pay validation URL is not allowed. Only Apple Pay gateway endpoints are permitted.';
static readonly ERROR_MSG_INVALID_TOKEN_DATA = 'Invalid token data: the token object does not conform to the expected schema or does not belong to this customer.';
/* END GENAI */
static readonly ERROR_MSG_ACCESSING_CERTIFICATES = 'An error occurred while accessing ApplePay Certificates';
static readonly ERROR_MSG_ENV_VARIABLES_NOT_FOUND = 'Missing configurations for merchant id ';
static readonly ERROR_MSG_EMPTY_PAYMENT_DATA = 'There was an error while fetching payment details';
Expand Down
6 changes: 5 additions & 1 deletion src/constants/paymentConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export abstract class Constants {
static readonly PAYMENT_GATEWAY_PRODUCT_ID = 'ctNetworkTokenSubscription';
static readonly PAYMENT_GATEWAY_NETWORK_TOKEN_EVENT_TYPE = 'tms.networktoken.updated';
static readonly PAYMENT_GATEWAY_APPLICATION_NAME = 'Commercetools(REST)';
static readonly PAYMENT_GATEWAY_APPLICATION_VERSION = '25.3.0';
static readonly PAYMENT_GATEWAY_APPLICATION_VERSION = '25.3.1';
static readonly PAYMENT_GATEWAY_AP_SESSIONS = 'AP_SESSIONS';
static readonly PAYMENT_GATEWAY_AP_STATUS = 'AP_STATUS';
static readonly PAYMENT_GATEWAY_AP_ORDER = 'AP_ORDER';
Expand Down Expand Up @@ -155,6 +155,8 @@ export abstract class Constants {
static readonly NETWORK_TOKEN_EVENT = 'tms.networktoken.updated';
static readonly ADDITIONAL_CUSTOM_TYPE_FILE_PATH = 'src/resources/isv_additonal_custom_type.json';
static readonly CERTIFICATE_PATH = '../certificates';
static readonly CACHE_CERTIFICATE_FROM_P12_FILE = 'certificateFromP12File';
static readonly CACHE_CERTIFICATE_LAST_MODIFIED_TIMESTAMP = 'certificateLastModifideTimeStamp';
static readonly STRING_TEST = 'test';
static readonly STRING_GOOGLE_PAY = 'googlePay';
static readonly STRING_VISA = 'visa';
Expand Down Expand Up @@ -198,6 +200,8 @@ export abstract class Constants {
static readonly ISV_ADDRESS_ID = 'isv_addressId';
static readonly ISV_TRANSACTION_DATA = 'isv_transaction_data';
static readonly ISV_TOKEN_ACTION = 'isv_tokenAction';
static readonly ISV_TOKEN_ACTION_DELETE = 'delete';
static readonly ISV_TOKEN_ACTION_UPDATE = 'update';
static readonly ISV_CARD_NEW_EXPIRY_MONTH = 'isv_cardNewExpiryMonth';
static readonly ISV_CARD_NEW_EXPIRY_YEAR = 'isv_cardNewExpiryYear';
static readonly ISV_FAILED_TOKENS = 'isv_failedTokens';
Expand Down
Loading