Skip to content

Commit

Permalink
PICT-207: Cancel Refund via Refunds API
Browse files Browse the repository at this point in the history
  • Loading branch information
NghiaDTr committed Jul 22, 2024
1 parent 4d5f503 commit a5e06f0
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 16 deletions.
9 changes: 9 additions & 0 deletions processor/src/commercetools/action.commercetools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,12 @@ export const changeTransactionState = (id: string, newState: CTTransactionState)
state: newState,
};
};

export const setTransactionCustomField = (transactionId: string, name: string, value: string) => {
return {
action: 'setTransactionCustomField',
transactionId: transactionId,
name: name,
value: value,
};
};
71 changes: 71 additions & 0 deletions processor/src/commercetools/customFields.commercetools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,74 @@ export async function createCustomPaymentInterfaceInteractionType(): Promise<voi
})
.execute();
}

export async function createCustomPaymentTransactionCancelRefundType(): Promise<void> {
const apiRoot = createApiRoot();

const customFieldName = CustomFields.paymentCancelRefund;

const {
body: { results: types },
} = await createApiRoot()
.types()
.get({
queryArgs: {
where: `key = "${customFieldName}"`,
},
})
.execute();

if (types.length > 0) {
const type = types[0];

await apiRoot
.types()
.withKey({ key: customFieldName })
.delete({
queryArgs: {
version: type.version,
},
})
.execute();
}

await apiRoot
.types()
.post({
body: {
key: customFieldName,
name: {
en: 'SCTM - Payment Cancel Refund on Transaction custom fields',
de: 'SCTM - Zahlung stornieren Rückerstattung bei benutzerdefinierten Transaktionsfeldern',
},
resourceTypeIds: ['transaction'],
fieldDefinitions: [
{
name: 'reasonText',
label: {
en: 'The reason of cancelling the refund, include the user name',
de: 'Der Grund für die Stornierung der Rückerstattung, den Benutzernamen einschließen',
},
required: false,
type: {
name: 'String',
},
inputHint: 'MultiLine',
},
{
name: 'statusText',
label: {
en: 'To differentiate between the “failure” from CommerceTools and the real status',
de: 'Um zwischen dem „Fehler“ von CommerceTools und dem tatsächlichen Status zu unterscheiden',
},
required: false,
type: {
name: 'String',
},
inputHint: 'MultiLine',
},
],
},
})
.execute();
}
1 change: 1 addition & 0 deletions processor/src/commercetools/extensions.commercetools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export async function createPaymentExtension(applicationUrl: string): Promise<vo
actions: ['Create', 'Update'],
},
],
timeoutInMs: 10000,
},
})
.execute();
Expand Down
2 changes: 2 additions & 0 deletions processor/src/connector/post-deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { assertError, assertString } from '../utils/assert.utils';
import { createPaymentExtension } from '../commercetools/extensions.commercetools';
import {
createCustomPaymentInterfaceInteractionType,
createCustomPaymentTransactionCancelRefundType,
createCustomPaymentType,
} from '../commercetools/customFields.commercetools';

Expand All @@ -18,6 +19,7 @@ async function postDeploy(properties: Map<string, unknown>): Promise<void> {
await createPaymentExtension(applicationUrl);
await createCustomPaymentType();
await createCustomPaymentInterfaceInteractionType();
await createCustomPaymentTransactionCancelRefundType();
}

async function run(): Promise<void> {
Expand Down
8 changes: 7 additions & 1 deletion processor/src/controllers/payment.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { determinePaymentAction } from '../utils/paymentAction.utils';
import { ControllerResponseType } from '../types/controller.types';
import { handleCreatePayment, handleListPaymentMethodsByPayment } from '../service/payment.service';
import {
handleCreatePayment,
handleListPaymentMethodsByPayment,
handlePaymentCancelRefund,
} from '../service/payment.service';
import { PaymentReference, Payment } from '@commercetools/platform-sdk';
import { ConnectorActions } from '../utils/constant.utils';
import { validateCommerceToolsPaymentPayload } from '../validators/payment.validators';
Expand Down Expand Up @@ -37,6 +41,8 @@ export const paymentController = async (
return await handleListPaymentMethodsByPayment(ctPayment);
case ConnectorActions.CreatePayment:
return await handleCreatePayment(ctPayment);
case ConnectorActions.CancelRefund:
return await handlePaymentCancelRefund(ctPayment);
default:
if (controllerAction.errorMessage === '') {
throw new SkipError('SCTM - No payment actions matched');
Expand Down
5 changes: 5 additions & 0 deletions processor/src/mollie/payment.mollie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@mollie/api-client';
import { initMollieClient } from '../client/mollie.client';
import CustomError from '../errors/custom.error';
import { CancelParameters } from '@mollie/api-client/dist/types/src/binders/payments/refunds/parameters';

/**
* Creates a Mollie payment using the provided payment parameters.
Expand Down Expand Up @@ -47,3 +48,7 @@ export const listPaymentMethods = async (options: MethodsListParams): Promise<Li
return {} as List<Method>;
}
};

export const cancelPaymentRefund = async (paymentId: string, params: CancelParameters): Promise<boolean> => {
return await initMollieClient().paymentRefunds.cancel(paymentId, params);
};
81 changes: 77 additions & 4 deletions processor/src/service/payment.service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { ControllerResponseType } from '../types/controller.types';
import { ConnectorActions, CustomFields, PAY_LATER_ENUMS } from '../utils/constant.utils';
import { List, Method, Payment as MPayment, PaymentMethod } from '@mollie/api-client';
import { CancelRefundStatusText, ConnectorActions, CustomFields, PAY_LATER_ENUMS } from '../utils/constant.utils';
import { List, Method, Payment as MPayment, PaymentMethod, PaymentStatus } from '@mollie/api-client';
import { logger } from '../utils/logger.utils';
import {
createMollieCreatePaymentParams,
mapCommercetoolsPaymentCustomFieldsToMollieListParams,
} from '../utils/map.utils';
import { Payment, UpdateAction } from '@commercetools/platform-sdk';
import CustomError from '../errors/custom.error';
import { createMolliePayment, getPaymentById, listPaymentMethods } from '../mollie/payment.mollie';
import { cancelPaymentRefund, createMolliePayment, getPaymentById, listPaymentMethods } from '../mollie/payment.mollie';
import {
AddTransaction,
ChangeTransactionState,
Expand All @@ -20,16 +20,21 @@ import {
} from '../types/commercetools.types';
import { makeCTMoney, shouldPaymentStatusUpdate } from '../utils/mollie.utils';
import { getPaymentByMolliePaymentId, updatePayment } from '../commercetools/payment.commercetools';
import { PaymentUpdateAction } from '@commercetools/platform-sdk/dist/declarations/src/generated/models/payment';
import {
PaymentUpdateAction,
Transaction,
} from '@commercetools/platform-sdk/dist/declarations/src/generated/models/payment';
import { v4 as uuid } from 'uuid';
import {
addInterfaceInteraction,
changeTransactionInteractionId,
changeTransactionState,
changeTransactionTimestamp,
setCustomFields,
setTransactionCustomField,
} from '../commercetools/action.commercetools';
import { readConfiguration } from '../utils/config.utils';
import { CancelParameters } from '@mollie/api-client/dist/types/src/binders/payments/refunds/parameters';

/**
* Handles listing payment methods by payment.
Expand Down Expand Up @@ -205,3 +210,71 @@ export const getCreatePaymentUpdateAction = async (molliePayment: MPayment, CTPa
return Promise.reject(error);
}
};

export const handlePaymentCancelRefund = async (ctPayment: Payment): Promise<ControllerResponseType> => {
const successChargeTransaction = ctPayment.transactions.find(
(transaction) => transaction.type === CTTransactionType.Charge && transaction.state === CTTransactionState.Success,
);

const pendingRefundTransaction = ctPayment.transactions.find(
(transaction) => transaction.type === CTTransactionType.Refund && transaction.state === CTTransactionState.Pending,
);

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

if (molliePayment.status !== PaymentStatus.pending) {
logger.error('SCTM - handleCancelRefund - Mollie Payment status must be pending, payment ID: ' + molliePayment.id);
throw new CustomError(
400,
'SCTM - handleCancelRefund - Mollie Payment status must be pending, payment ID: ' + molliePayment.id,
);
}

const paymentCancelRefundParams: CancelParameters = {
paymentId: molliePayment.id,
};

await cancelPaymentRefund(pendingRefundTransaction?.id as string, paymentCancelRefundParams);

const ctActions: UpdateAction[] = getPaymentCancelRefundActions(pendingRefundTransaction as Transaction);

return {
statusCode: 200,
actions: ctActions,
};
};

export const getPaymentCancelRefundActions = (pendingRefundTransaction: Transaction) => {
const transactionCustomFieldName = CustomFields.paymentCancelRefund;

let transactionCustomFieldValue;
try {
transactionCustomFieldValue = !pendingRefundTransaction.custom?.fields.customFieldName
? {}
: JSON.parse(pendingRefundTransaction.custom?.fields.customFieldName);
} catch (error: unknown) {
logger.error(
`SCTM - handleCancelRefund - Failed to parse the JSON string from the custom field ${transactionCustomFieldName}.`,
);
throw new CustomError(
400,
`SCTM - handleCancelRefund - Failed to parse the JSON string from the custom field ${transactionCustomFieldName}.`,
);
}

const newTransactionCustomFieldValue = {
reasonText: transactionCustomFieldValue.reasonText,
statusText: CancelRefundStatusText,
};

return [
// Update transaction state
changeTransactionState(pendingRefundTransaction.id, CTTransactionState.Failure),
// Set transaction custom field value
setTransactionCustomField(
pendingRefundTransaction.id,
transactionCustomFieldName,
JSON.stringify(newTransactionCustomFieldValue),
),
];
};
4 changes: 4 additions & 0 deletions processor/src/utils/constant.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ export const CustomFields = {
request: 'sctm_create_payment_request',
interfaceInteraction: 'sctm_interface_interaction_type',
},
paymentCancelRefund: 'sctm_payment_cancel_refund',
};

export enum ConnectorActions {
GetPaymentMethods = 'getPaymentMethods',
CreatePayment = 'createPayment',
CancelRefund = 'cancelRefund',
NoAction = 'noAction',
}

Expand All @@ -29,3 +31,5 @@ export const ErrorMessages = {
};

export const PAY_LATER_ENUMS = [PaymentMethod.klarnapaylater, PaymentMethod.klarnasliceit];

export const CancelRefundStatusText = 'Cancelled from shop side';
28 changes: 17 additions & 11 deletions processor/src/utils/paymentAction.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,25 @@ export const determinePaymentAction = (ctPayment?: Payment): DeterminePaymentAct

const { id, key, transactions } = ctPayment;

const initialTransactions: CTTransaction[] = [];
const initialChargeTransactions: CTTransaction[] = [];
const pendingChargeTransactions: CTTransaction[] = [];
const successChargeTransactions: CTTransaction[] = [];
const pendingRefundTransactions: CTTransaction[] = [];

const chargeTransactions =
transactions?.filter(
/* eslint-disable @typescript-eslint/no-explicit-any */
(transaction: any) => transaction.type === CTTransactionType.Charge,
) ?? [];
chargeTransactions?.forEach((transaction: any) => {
if (transaction.state === CTTransactionState.Initial) initialChargeTransactions.push(transaction);
if (transaction.state === CTTransactionState.Pending) pendingChargeTransactions.push(transaction);
if (transaction.state === CTTransactionState.Success) successChargeTransactions.push(transaction);
});
transactions?.forEach((transaction: any) => {
if (transaction.state === CTTransactionState.Initial) {
initialTransactions.push(transaction);
}

const initialTransactions = transactions?.filter(({ state }) => state === CTTransactionState.Initial) ?? [];
if (transaction.type === CTTransactionType.Charge) {
if (transaction.state === CTTransactionState.Initial) initialChargeTransactions.push(transaction);
if (transaction.state === CTTransactionState.Pending) pendingChargeTransactions.push(transaction);
if (transaction.state === CTTransactionState.Success) successChargeTransactions.push(transaction);
} else if (transaction.type === CTTransactionType.Refund) {
if (transaction.state === CTTransactionState.Pending) pendingRefundTransactions.push(transaction);
}
});

let action;
let errorMessage = '';
Expand Down Expand Up @@ -76,6 +79,9 @@ export const determinePaymentAction = (ctPayment?: Payment): DeterminePaymentAct
!pendingChargeTransactions.length:
action = ConnectorActions.CreatePayment;
break;
case successChargeTransactions.length === 1 && pendingRefundTransactions.length === 1:
action = ConnectorActions.CancelRefund;
break;
default:
action = ConnectorActions.NoAction;
logger.warn('SCTM - No payment actions matched');
Expand Down

0 comments on commit a5e06f0

Please sign in to comment.