Skip to content

Commit

Permalink
Merge branch 'main' of github.com:mollie/commercetools-connector
Browse files Browse the repository at this point in the history
  • Loading branch information
Tung-Huynh-Shopmacher committed Aug 7, 2024
2 parents e667866 + 1fd0061 commit 148c1b8
Show file tree
Hide file tree
Showing 12 changed files with 6,730 additions and 6,230 deletions.
12,299 changes: 6,150 additions & 6,149 deletions processor/package-lock.json

Large diffs are not rendered by default.

8 changes: 3 additions & 5 deletions processor/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "shopmacher-mollie-processor",
"description": "Integration between commercetools and mollie payment service provider",
"version": "0.0.22",
"version": "0.0.24",
"main": "index.js",
"private": true,
"scripts": {
Expand Down Expand Up @@ -32,14 +32,12 @@
"engines": {
"node": ">=18.0.0 <=20.9.0"
},
"resolutions": {
"axios": "1.6.8"
},
"devDependencies": {
"@tsconfig/recommended": "^1.0.7",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.12",
"@types/node": "^18.19.39",
"@types/node-fetch": "^2.6.11",
"@types/validator": "^13.12.0",
"@typescript-eslint/eslint-plugin": "7.13.1",
"@typescript-eslint/parser": "7.13.1",
Expand Down Expand Up @@ -67,10 +65,10 @@
"@commercetools/sdk-client-v2": "^2.5.0",
"@mollie/api-client": "^3.7.0",
"@types/uuid": "^10.0.0",
"axios": "^1.7.2",
"body-parser": "^1.20.2",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"node-fetch": "^2.7.0",
"proxy-from-env": "^1.1.0",
"uuid": "^10.0.0",
"validator": "^13.12.0"
Expand Down
8 changes: 2 additions & 6 deletions processor/src/controllers/webhook.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,16 @@ export const post = async (request: Request, response: Response, next: NextFunct
return;
}

const isSucceeded = await handlePaymentWebhook(id);
const result = await handlePaymentWebhook(id);

if (isSucceeded === true) {
if (result) {
response.status(200).send();

logger.info(`Webhook with id ${id} is handled successfully.`);

return;
} else {
response.status(400).send();

logger.warn(`Webhook with id ${id} is handled unsuccessfully, retry will be take place automatically.`);

return;
}
} catch (error: any) {
logger.error(`Error processing webhook event`, error);
Expand Down
42 changes: 30 additions & 12 deletions processor/src/mollie/payment.mollie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import {
import { initMollieClient } from '../client/mollie.client';
import CustomError from '../errors/custom.error';
import { logger } from '../utils/logger.utils';
import axios, { AxiosError } from 'axios';
import { readConfiguration } from '../utils/config.utils';
import { LIBRARY_NAME, LIBRARY_VERSION } from '../utils/constant.utils';
import { CustomPayment } from '../types/mollie.types';
import fetch from 'node-fetch';

/**
* Creates a Mollie payment using the provided payment parameters.
Expand Down Expand Up @@ -104,29 +104,47 @@ export const cancelPayment = async (paymentId: string): Promise<void> => {
};

export const createPaymentWithCustomMethod = async (paymentParams: PaymentCreateParams): Promise<CustomPayment> => {
let errorMessage;

try {
const { mollie } = readConfiguration();

const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${mollie.apiKey}`,
versionStrings: `${LIBRARY_NAME}/${LIBRARY_VERSION}`,
};

const response = await axios.post('https://api.mollie.com/v2/payments', paymentParams, { headers });
const response = await fetch('https://api.mollie.com/v2/payments', {
method: 'POST',
headers,
body: JSON.stringify(paymentParams),
});

return response.data;
} catch (error: unknown) {
let errorMessage;
const data = await response.json();

if (error instanceof AxiosError) {
errorMessage = `SCTM - createPaymentWithCustomMethod - error: ${error.response?.data?.detail}, field: ${error.response?.data?.field}`;
} else {
errorMessage = 'SCTM - createPaymentWithCustomMethod - Failed to create a payment with unknown errors';
if (response.status !== 201) {
if (response.status === 422 || response.status === 503) {
errorMessage = `SCTM - createPaymentWithCustomMethod - error: ${data?.detail}, field: ${data?.field}`;
} else {
errorMessage = 'SCTM - createPaymentWithCustomMethod - Failed to create a payment with unknown errors';
}

logger.error(errorMessage, {
response: data,
});

throw new CustomError(400, errorMessage);
}

logger.error(errorMessage, {
error,
});
return data;
} catch (error: unknown) {
if (!errorMessage) {
errorMessage = 'SCTM - createPaymentWithCustomMethod - Failed to create a payment with unknown errors';
logger.error(errorMessage, {
error,
});
}

throw new CustomError(400, errorMessage);
}
Expand Down
80 changes: 75 additions & 5 deletions processor/src/service/payment.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ControllerResponseType } from '../types/controller.types';
import { CancelStatusText, ConnectorActions, CustomFields, PAY_LATER_ENUMS } from '../utils/constant.utils';
import { List, Method, Payment as MPayment, PaymentMethod, PaymentStatus } from '@mollie/api-client';
import { List, Method, Payment as MPayment, PaymentMethod, PaymentStatus, Refund } from '@mollie/api-client';
import { logger } from '../utils/logger.utils';
import {
createMollieCreatePaymentParams,
Expand All @@ -16,14 +16,21 @@ import {
listPaymentMethods,
} from '../mollie/payment.mollie';
import {
AddTransaction,
ChangeTransactionState,
CTTransaction,
CTTransactionState,
CTTransactionType,
molliePaymentToCTStatusMap,
mollieRefundToCTStatusMap,
UpdateActionKey,
} from '../types/commercetools.types';
import { makeCTMoney, makeMollieAmount, shouldPaymentStatusUpdate } from '../utils/mollie.utils';
import {
makeCTMoney,
makeMollieAmount,
shouldPaymentStatusUpdate,
shouldRefundStatusUpdate,
} from '../utils/mollie.utils';
import { getPaymentByMolliePaymentId, updatePayment } from '../commercetools/payment.commercetools';
import {
PaymentUpdateAction,
Expand Down Expand Up @@ -60,7 +67,9 @@ export const handleListPaymentMethodsByPayment = async (ctPayment: Payment): Pro
try {
const mollieOptions = await mapCommercetoolsPaymentCustomFieldsToMollieListParams(ctPayment);
const methods: List<Method> = await listPaymentMethods(mollieOptions);
const enableCardComponent = toBoolean(readConfiguration().mollie.cardComponent, true);
const enableCardComponent =
toBoolean(readConfiguration().mollie.cardComponent, true) &&
methods.filter((method: Method) => method.id === PaymentMethod.creditcard).length > 0;
const ctUpdateActions: UpdateAction[] = [];

if (enableCardComponent) {
Expand Down Expand Up @@ -134,8 +143,16 @@ export const handlePaymentWebhook = async (paymentId: string): Promise<boolean>

const action = getPaymentStatusUpdateAction(ctPayment.transactions as CTTransaction[], molliePayment);

// If refunds are present, update their status
const refunds = molliePayment._embedded?.refunds;
if (refunds?.length) {
const refundUpdateActions = getRefundStatusUpdateActions(ctPayment.transactions as CTTransaction[], refunds);
action.push(...refundUpdateActions);
}

if (action.length === 0) {
logger.debug(`handlePaymentWebhook - No actions needed`);

return true;
}

Expand Down Expand Up @@ -396,9 +413,10 @@ export const handlePaymentCancelRefund = async (ctPayment: Payment): Promise<Con
* Retrieves the payment cancel actions based on the provided pending refund transaction.
* Would be used for cancel a payment or cancel a refund
*
* @param {Transaction} transaction - The pending refund transaction.
* @return {Action[]} An array of actions including updating the transaction state and setting the transaction custom field value.
* @throws {CustomError} If the JSON string from the custom field cannot be parsed.
* @param targetTransaction
* @param triggerTransaction
*/
export const getPaymentCancelActions = (targetTransaction: Transaction, triggerTransaction: Transaction) => {
const transactionCustomFieldName = CustomFields.paymentCancelReason;
Expand All @@ -421,6 +439,58 @@ export const getPaymentCancelActions = (targetTransaction: Transaction, triggerT
];
};

/**
* @param ctTransactions
* @param mollieRefunds
*
* Process mollie refunds and match to corresponding commercetools transaction
* Update the existing transactions if the status has changed
* If there is a refund and no corresponding transaction, add it to commercetools
*/
export const getRefundStatusUpdateActions = (
ctTransactions: CTTransaction[],
mollieRefunds: Refund[],
): (ChangeTransactionState | AddTransaction)[] => {
const updateActions: (ChangeTransactionState | AddTransaction)[] = [];
const refundTransactions = ctTransactions?.filter((ctTransaction) => ctTransaction.type === CTTransactionType.Refund);

mollieRefunds.forEach((mollieRefund) => {
const { id: mollieRefundId, status: mollieRefundStatus } = mollieRefund;
const matchingCTTransaction = refundTransactions.find((rt) => rt.interactionId === mollieRefundId);

if (matchingCTTransaction) {
const shouldUpdate = shouldRefundStatusUpdate(
mollieRefundStatus,
matchingCTTransaction.state as CTTransactionState,
);

if (shouldUpdate) {
const updateAction = changeTransactionState(
matchingCTTransaction.id as string,
mollieRefundToCTStatusMap[mollieRefundStatus],
) as ChangeTransactionState;

updateActions.push(updateAction);
}
} else {
const updateAction: AddTransaction = {
action: UpdateActionKey.AddTransaction,
transaction: {
type: CTTransactionType.Refund,

amount: makeCTMoney(mollieRefund.amount),
interactionId: mollieRefundId,
state: mollieRefundToCTStatusMap[mollieRefundStatus],
},
};

updateActions.push(updateAction);
}
});

return updateActions;
};

/**
* Handles the cancellation of a payment.
*
Expand All @@ -438,7 +508,7 @@ export const handleCancelPayment = async (ctPayment: Payment): Promise<Controlle

const molliePayment = await getPaymentById(successAuthorizationTransaction?.interactionId as string);

if (molliePayment.isCancelable === false) {
if (!molliePayment.isCancelable) {
logger.error(`SCTM - handleCancelPayment - Payment is not cancelable, Mollie Payment ID: ${molliePayment.id}`, {
molliePaymentId: molliePayment.id,
commerceToolsPaymentId: ctPayment.id,
Expand Down
8 changes: 8 additions & 0 deletions processor/src/types/commercetools.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ export const molliePaymentToCTStatusMap: StatusMap = {
pending: CTTransactionState.Pending,
};

export const mollieRefundToCTStatusMap: StatusMap = {
refunded: CTTransactionState.Success,
failed: CTTransactionState.Failure,
queued: CTTransactionState.Pending,
pending: CTTransactionState.Pending,
processing: CTTransactionState.Pending,
};

export type WebhookRequest = {
id: string;
};
1 change: 1 addition & 0 deletions processor/src/utils/app.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function createDateNowString(): string {
*
* @param {string} targetedString - The string to be parsed.
* @param {string} fieldName - The name of the custom field.
* @param errorPrefix
* @param {string} commerceToolsId - CommerceTools Payment ID or Transaction ID.
* @returns {object} - The parsed JSON object.
* @throws {CustomError} - If the string cannot be parsed into a JSON object.
Expand Down
8 changes: 2 additions & 6 deletions processor/src/utils/map.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { makeMollieAmount } from './mollie.utils';
import { CustomPaymentMethod, ParsedMethodsRequestType } from '../types/mollie.types';
import { Payment } from '@commercetools/platform-sdk';
import CustomError from '../errors/custom.error';
import { PaymentCreateParams, MethodsListParams, PaymentMethod } from '@mollie/api-client';
import { MethodsListParams, PaymentCreateParams, PaymentMethod } from '@mollie/api-client';
import { parseStringToJsonObject, removeEmptyProperties } from './app.utils';

const extractMethodsRequest = (ctPayment: Payment): ParsedMethodsRequestType | undefined => {
Expand Down Expand Up @@ -128,9 +128,5 @@ export const createMollieCreatePaymentParams = (payment: Payment, extensionUrl:
...getSpecificPaymentParams(method as PaymentMethod, paymentRequest),
};

const validatedCreatePaymentParams: PaymentCreateParams = removeEmptyProperties(
createPaymentParams,
) as PaymentCreateParams;

return validatedCreatePaymentParams;
return removeEmptyProperties(createPaymentParams) as PaymentCreateParams;
};
35 changes: 34 additions & 1 deletion processor/src/utils/mollie.utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CentPrecisionMoney } from '@commercetools/platform-sdk';
import { Amount } from '@mollie/api-client/dist/types/src/data/global';
import { CTMoney, CTTransactionState } from '../types/commercetools.types';
import { PaymentStatus } from '@mollie/api-client';
import { PaymentStatus, RefundStatus } from '@mollie/api-client';

const convertCTToMollieAmountValue = (ctValue: number, fractionDigits = 2): string => {
const divider = Math.pow(10, fractionDigits);
Expand Down Expand Up @@ -61,3 +61,36 @@ export const shouldPaymentStatusUpdate = (
}
return shouldUpdate;
};

/**
* Returns true if mollie refund status has changed and the CT Transaction should be updated
* @param mollieRefundStatus
* @param ctTransactionStatus
*/
export const shouldRefundStatusUpdate = (
mollieRefundStatus: RefundStatus,
ctTransactionStatus: CTTransactionState,
): boolean => {
let shouldUpdate: boolean;

switch (mollieRefundStatus) {
case RefundStatus.queued:
case RefundStatus.pending:
case RefundStatus.processing:
shouldUpdate = ctTransactionStatus !== CTTransactionState.Pending;
break;

case RefundStatus.refunded:
shouldUpdate = ctTransactionStatus !== CTTransactionState.Success;
break;

case RefundStatus.failed:
shouldUpdate = ctTransactionStatus !== CTTransactionState.Failure;
break;

default:
shouldUpdate = false;
break;
}
return shouldUpdate;
};
Loading

0 comments on commit 148c1b8

Please sign in to comment.