diff --git a/processor/src/service/payment.service.ts b/processor/src/service/payment.service.ts index c0f5cd7..22fbf9d 100644 --- a/processor/src/service/payment.service.ts +++ b/processor/src/service/payment.service.ts @@ -71,6 +71,7 @@ import { convertCentToEUR, parseStringToJsonObject, roundSurchargeAmountToCent, + sortTransactionsByLatestCreationTime, } from '../utils/app.utils'; import ApplePaySession from '@mollie/api-client/dist/types/src/data/applePaySession/ApplePaySession'; import { getMethodConfigObjects, getSingleMethodConfigObject } from '../commercetools/customObjects.commercetools'; @@ -422,10 +423,6 @@ export const handleCreatePayment = async (ctPayment: Payment): Promise => { - const successChargeTransaction = ctPayment.transactions.find( - (transaction) => transaction.type === CTTransactionType.Charge && transaction.state === CTTransactionState.Success, - ); + let successChargeTransaction; + const updateActions = [] as UpdateAction[]; const initialRefundTransaction = ctPayment.transactions.find( (transaction) => transaction.type === CTTransactionType.Refund && transaction.state === CTTransactionState.Initial, ); + if (initialRefundTransaction?.custom?.fields[CustomFields.transactionRefundForMolliePayment]) { + logger.debug('SCTM - handleCreateRefund - creating a refund with specific payment id'); + + successChargeTransaction = ctPayment.transactions.find( + (transaction) => + transaction.type === CTTransactionType.Charge && + transaction.state === CTTransactionState.Success && + transaction.interactionId === + initialRefundTransaction?.custom?.fields[CustomFields.transactionRefundForMolliePayment], + ); + } else { + logger.debug('SCTM - handleCreateRefund - creating a refund for the latest success charge transaction'); + + const latestTransactions = sortTransactionsByLatestCreationTime(ctPayment.transactions); + + successChargeTransaction = latestTransactions.find( + (transaction) => + transaction.type === CTTransactionType.Charge && transaction.state === CTTransactionState.Success, + ); + + updateActions.push( + setTransactionCustomType(initialRefundTransaction?.id as string, CustomFields.transactionRefundForMolliePayment, { + [CustomFields.transactionRefundForMolliePayment]: successChargeTransaction?.interactionId, + }), + ); + } + + if (!successChargeTransaction) { + throw new CustomError(400, 'SCTM - handleCreateRefund - Cannot find valid success charge transaction'); + } + const paymentCreateRefundParams: CreateParameters = { paymentId: successChargeTransaction?.interactionId as string, amount: makeMollieAmount(initialRefundTransaction?.amount as CentPrecisionMoney), @@ -545,12 +572,14 @@ export const handleCreateRefund = async (ctPayment: Payment): Promise => { - 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, - ); + let pendingRefundTransaction: any; + let successChargeTransaction: any; const initialCancelAuthorization = ctPayment.transactions.find( (transaction) => transaction.type === CTTransactionType.CancelAuthorization && transaction.state === CTTransactionState.Initial, ); + if (initialCancelAuthorization?.interactionId) { + pendingRefundTransaction = ctPayment.transactions.find( + (transaction) => + transaction.type === CTTransactionType.Refund && + transaction.state === CTTransactionState.Pending && + transaction?.interactionId === initialCancelAuthorization.interactionId, + ) as Transaction; + + if (pendingRefundTransaction) { + successChargeTransaction = ctPayment.transactions.find( + (transaction) => + transaction.type === CTTransactionType.Charge && + transaction.state === CTTransactionState.Success && + transaction.interactionId === + pendingRefundTransaction?.custom?.fields[CustomFields.transactionRefundForMolliePayment], + ) as Transaction; + } + + if (!successChargeTransaction) { + throw new CustomError( + 400, + 'SCTM - handlePaymentCancelRefund - Cannot find the valid Success Charge transaction.', + ); + } + } + + /** + * @deprecated v1.2 - Will be remove in the next version + */ + if (!pendingRefundTransaction || !successChargeTransaction) { + const latestTransactions = sortTransactionsByLatestCreationTime(ctPayment.transactions); + + pendingRefundTransaction = latestTransactions.find( + (transaction) => + transaction.type === CTTransactionType.Refund && transaction.state === CTTransactionState.Pending, + ); + + successChargeTransaction = latestTransactions.find( + (transaction) => + transaction.type === CTTransactionType.Charge && transaction.state === CTTransactionState.Success, + ); + } + /** + * end deprecated + */ + const paymentGetRefundParams: CancelParameters = { paymentId: successChargeTransaction?.interactionId as string, }; @@ -763,4 +833,4 @@ export const handleGetApplePaySession = async (ctPayment: Payment): Promise { return Math.round(surchargeAmountInEur * Math.pow(10, fractionDigits)); }; + +export const sortTransactionsByLatestCreationTime = (transactions: Transaction[]): Transaction[] => { + const clonedTransactions = Object.assign([], transactions); + + return clonedTransactions.sort((a: Transaction, b: Transaction) => { + const timeA = a.timestamp as string; + const timeB = b.timestamp as string; + + if (timeA < timeB) { + return 1; + } + + if (timeA > timeB) { + return -1; + } + + return 0; + }); +}; diff --git a/processor/src/utils/constant.utils.ts b/processor/src/utils/constant.utils.ts index 3e39cee..aab693f 100644 --- a/processor/src/utils/constant.utils.ts +++ b/processor/src/utils/constant.utils.ts @@ -39,6 +39,7 @@ export const CustomFields = { }, }, transactionSurchargeCost: 'sctm_transaction_surcharge_cost', + transactionRefundForMolliePayment: 'sctm_transaction_refund_for_mollie_payment', }; export enum ConnectorActions { @@ -69,4 +70,4 @@ export const MOLLIE_SURCHARGE_CUSTOM_LINE_ITEM = 'mollie-surcharge-line-item'; export const MOLLIE_SURCHARGE_LINE_DESCRIPTION = 'Total surcharge amount'; -export const MOLLIE_SHIPPING_LINE_DESCRIPTION = 'Shipping amount'; +export const MOLLIE_SHIPPING_LINE_DESCRIPTION = 'Shipping amount'; \ No newline at end of file diff --git a/processor/tests/service/payment.service.spec.ts b/processor/tests/service/payment.service.spec.ts index 11d56f6..bf2e5ed 100644 --- a/processor/tests/service/payment.service.spec.ts +++ b/processor/tests/service/payment.service.spec.ts @@ -1814,7 +1814,7 @@ describe('Test handleCreatePayment', () => { ], interfaceInteractions: [], paymentMethodInfo: { - method: 'googlepay', + method: 'ideal', }, custom: { type: { @@ -1904,7 +1904,7 @@ describe('Test handleCreatePayment', () => { }); (createMollieCreatePaymentParams as jest.Mock).mockReturnValueOnce({ - method: 'googlepay', + method: 'ideal', }); (changeTransactionState as jest.Mock).mockReturnValueOnce({ @@ -1961,7 +1961,7 @@ describe('Test handleCreatePayment', () => { sctm_id: '5c8b0375-305a-4f19-ae8e-07806b101999', sctm_action_type: 'createPayment', sctm_created_at: '2024-03-20T09:13:37+00:00', - sctm_request: '{"transactionId":"5c8b0375-305a-4f19-ae8e-07806b101999","paymentMethod":"googlepay"}', + sctm_request: '{"transactionId":"5c8b0375-305a-4f19-ae8e-07806b101999","paymentMethod":"ideal"}', sctm_response: '{"molliePaymentId":"tr_7UhSN1zuXS","checkoutUrl":"https://www.mollie.com/checkout/select-method/7UhSN1zuXS","transactionId":"5c8b0375-305a-4f19-ae8e-07806b101999"}', }, @@ -2001,7 +2001,7 @@ describe('Test handleCreatePayment', () => { }); describe('Test handleCreateRefund', () => { - it('should return status code and array of actions', async () => { + it('should return status code and array of actions (1 success charge transaction)', async () => { const CTPayment: Payment = { id: '5c8b0375-305a-4f19-ae8e-07806b101999', version: 1, @@ -2065,6 +2065,227 @@ describe('Test handleCreateRefund', () => { const result = await handleCreateRefund(CTPayment); + expect(createPaymentRefund).toBeCalledTimes(1); + expect(createPaymentRefund).toBeCalledWith(paymentCreateRefundParams); + expect(result.statusCode).toBe(201); + expect(result.actions).toStrictEqual([ + { + action: 'setTransactionCustomType', + type: { + key: CustomFieldName.transactionRefundForMolliePayment, + }, + transactionId: 'test_refund', + fields: { + [CustomFieldName.transactionRefundForMolliePayment]: 'tr_123123', + }, + }, + { + action: 'changeTransactionInteractionId', + transactionId: 'test_refund', + interactionId: 'fake_refund_id', + }, + { + action: 'changeTransactionState', + transactionId: 'test_refund', + state: 'Pending', + }, + ]); + }); + + it('should return status code and array of actions (more than 1 success charge transaction, with Mollie payment that need to be refunded is not specified)', async () => { + const targetedMolliePaymentId = 'tr_123456'; + + const CTPayment: Payment = { + id: '5c8b0375-305a-4f19-ae8e-07806b101999', + version: 1, + createdAt: '2024-07-04T14:07:35.625Z', + lastModifiedAt: '2024-07-04T14:07:35.625Z', + amountPlanned: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + paymentStatus: {}, + transactions: [ + { + id: uuid, + timestamp: '2024-06-24T08:28:43.474Z', + type: 'Charge', + interactionId: 'tr_123123', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Success', + }, + { + id: 'test-123', + timestamp: '2024-06-24T08:30:43.474Z', + type: 'Charge', + interactionId: targetedMolliePaymentId, + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Success', + }, + { + id: 'test_refund', + type: 'Refund', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Initial', + }, + ], + interfaceInteractions: [], + paymentMethodInfo: { + method: 'creditcard', + }, + }; + + (changeTransactionState as jest.Mock).mockReturnValueOnce({ + action: 'changeTransactionState', + state: 'Pending', + transactionId: 'test_refund', + }); + + (createPaymentRefund as jest.Mock).mockReturnValue({ + id: 'fake_refund_id', + }); + + const paymentCreateRefundParams: CreateParameters = { + paymentId: targetedMolliePaymentId, + amount: { + value: '10.00', + currency: 'EUR', + }, + }; + + const result = await handleCreateRefund(CTPayment); + + expect(createPaymentRefund).toBeCalledTimes(1); + expect(createPaymentRefund).toBeCalledWith(paymentCreateRefundParams); + expect(result.statusCode).toBe(201); + expect(result.actions).toStrictEqual([ + { + action: 'setTransactionCustomType', + type: { + key: CustomFieldName.transactionRefundForMolliePayment, + }, + transactionId: 'test_refund', + fields: { + [CustomFieldName.transactionRefundForMolliePayment]: targetedMolliePaymentId, + }, + }, + { + action: 'changeTransactionInteractionId', + transactionId: 'test_refund', + interactionId: 'fake_refund_id', + }, + { + action: 'changeTransactionState', + transactionId: 'test_refund', + state: 'Pending', + }, + ]); + }); + + it('should return status code and array of actions (more than 1 success charge transaction, with Mollie payment that need to be refunded is specified)', async () => { + const targetedMolliePaymentId = 'tr_123123'; + + const CTPayment: Payment = { + id: '5c8b0375-305a-4f19-ae8e-07806b101999', + version: 1, + createdAt: '2024-07-04T14:07:35.625Z', + lastModifiedAt: '2024-07-04T14:07:35.625Z', + amountPlanned: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + paymentStatus: {}, + transactions: [ + { + id: uuid, + type: 'Charge', + interactionId: targetedMolliePaymentId, + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Success', + }, + { + id: 'test-123', + type: 'Charge', + interactionId: 'tr_123456', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Success', + }, + { + id: 'test_refund', + type: 'Refund', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Initial', + custom: { + type: { + typeId: 'type', + id: 'custom-type-id', + }, + fields: { + [CustomFieldName.transactionRefundForMolliePayment]: targetedMolliePaymentId, + }, + }, + }, + ], + interfaceInteractions: [], + paymentMethodInfo: { + method: 'creditcard', + }, + }; + + (changeTransactionState as jest.Mock).mockReturnValueOnce({ + action: 'changeTransactionState', + state: 'Pending', + transactionId: 'test_refund', + }); + + (createPaymentRefund as jest.Mock).mockReturnValue({ + id: 'fake_refund_id', + }); + + const paymentCreateRefundParams: CreateParameters = { + paymentId: targetedMolliePaymentId, + amount: { + value: '10.00', + currency: 'EUR', + }, + }; + + const result = await handleCreateRefund(CTPayment); + expect(createPaymentRefund).toBeCalledTimes(1); expect(createPaymentRefund).toBeCalledWith(paymentCreateRefundParams); expect(result.statusCode).toBe(201); @@ -2237,7 +2458,6 @@ describe('Test handlePaymentCancelRefund', () => { { id: '5c8b0375-305a-4f19-ae8e-07806b102000', type: 'CancelAuthorization', - interactionId: 're_4qqhO89gsT', amount: { type: 'centPrecision', currencyCode: 'EUR', @@ -2312,7 +2532,7 @@ describe('Test handlePaymentCancelRefund', () => { } }); - it('should return status code and array of actions', async () => { + it('should return status code and array of actions (interactionId is not defined in the Initial CancelAuthorization transaction)', async () => { const mollieRefund: Refund = { resource: 'refund', id: CTPayment.transactions[1].interactionId, @@ -2358,6 +2578,297 @@ describe('Test handlePaymentCancelRefund', () => { paymentId: CTPayment.transactions[0].interactionId, }); }); + + it('should return status code and array of actions (interactionId is defined in the Initial CancelAuthorization transaction)', async () => { + const CTPaymentMocked: Payment = { + id: '5c8b0375-305a-4f19-ae8e-07806b101999', + version: 1, + createdAt: '2024-07-04T14:07:35.625Z', + lastModifiedAt: '2024-07-04T14:07:35.625Z', + amountPlanned: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + paymentStatus: {}, + transactions: [ + { + id: '5c8b0375-305a-4f19-ae8e-07806b101992', + type: 'Charge', + interactionId: 'tr_test', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Success', + }, + { + id: '5c8b0375-305a-4f19-ae8e-07806b101999', + type: 'Charge', + interactionId: 'tr_123123', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Success', + }, + { + id: '5c8b0375-305a-4f19-ae8e-07806b102011', + type: 'Refund', + interactionId: 're_TEST', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Pending', + }, + { + id: '5c8b0375-305a-4f19-ae8e-07806b102000', + type: 'Refund', + interactionId: 're_4qqhO89gsT', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Pending', + custom: { + type: { + typeId: 'type', + id: 'custom-type', + }, + fields: { + [CustomFieldName.transactionRefundForMolliePayment]: 'tr_123123', + }, + }, + }, + { + id: '5c8b0375-305a-4f19-ae8e-07806b102000', + type: 'CancelAuthorization', + interactionId: 're_4qqhO89gsT', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Initial', + custom: { + type: { + typeId: 'type', + id: 'sctm_payment_cancel_reason', + }, + fields: { + reasonText: 'dummy reason', + }, + }, + }, + ], + interfaceInteractions: [], + paymentMethodInfo: { + method: 'creditcard', + }, + }; + + const mollieRefund: Refund = { + resource: 'refund', + id: CTPaymentMocked.transactions[3].interactionId, + description: 'Order', + amount: { + currency: 'EUR', + value: '5.95', + }, + status: 'pending', + metadata: '{"bookkeeping_id":12345}', + paymentId: 'tr_7UhSN1zuXS', + createdAt: '2023-03-14T17:09:02.0Z', + _links: { + self: { + href: '...', + type: 'application/hal+json', + }, + payment: { + href: 'https://api.mollie.com/v2/payments/tr_7UhSN1zuXS', + type: 'application/hal+json', + }, + documentation: { + href: '...', + type: 'text/html', + }, + }, + } as Refund; + + (getPaymentRefund as jest.Mock).mockReturnValueOnce(mollieRefund); + + (cancelPaymentRefund as jest.Mock).mockReturnValueOnce(true); + + (getPaymentCancelActions as jest.Mock).mockReturnValueOnce([]); + + await handlePaymentCancelRefund(CTPaymentMocked); + + expect(getPaymentRefund).toBeCalledTimes(1); + expect(getPaymentRefund).toBeCalledWith(CTPaymentMocked.transactions[3].interactionId, { + paymentId: CTPaymentMocked.transactions[1].interactionId, + }); + expect(cancelPaymentRefund).toBeCalledTimes(1); + expect(cancelPaymentRefund).toBeCalledWith(CTPaymentMocked.transactions[3].interactionId, { + paymentId: CTPaymentMocked.transactions[1].interactionId, + }); + }); + + it('should throw error if valid Success Charge transaction was not found (interactionId is defined in the Initial CancelAuthorization transaction)', async () => { + const CTPaymentMocked: Payment = { + id: '5c8b0375-305a-4f19-ae8e-07806b101999', + version: 1, + createdAt: '2024-07-04T14:07:35.625Z', + lastModifiedAt: '2024-07-04T14:07:35.625Z', + amountPlanned: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + paymentStatus: {}, + transactions: [ + { + id: '5c8b0375-305a-4f19-ae8e-07806b101992', + type: 'Charge', + interactionId: 'tr_test123123', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Success', + }, + { + id: '5c8b0375-305a-4f19-ae8e-07806b101999', + type: 'Charge', + interactionId: 'tr_dummy', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Success', + }, + { + id: '5c8b0375-305a-4f19-ae8e-07806b102011', + type: 'Refund', + interactionId: 're_TEST', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Pending', + }, + { + id: '5c8b0375-305a-4f19-ae8e-07806b102000', + type: 'Refund', + interactionId: 're_4qqhO89gsT', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Pending', + custom: { + type: { + typeId: 'type', + id: 'custom-type', + }, + fields: { + [CustomFieldName.transactionRefundForMolliePayment]: 'tr_123123', + }, + }, + }, + { + id: '5c8b0375-305a-4f19-ae8e-07806b102000', + type: 'CancelAuthorization', + interactionId: 're_4qqhO89gsT', + amount: { + type: 'centPrecision', + currencyCode: 'EUR', + centAmount: 1000, + fractionDigits: 2, + }, + state: 'Initial', + custom: { + type: { + typeId: 'type', + id: 'sctm_payment_cancel_reason', + }, + fields: { + reasonText: 'dummy reason', + }, + }, + }, + ], + interfaceInteractions: [], + paymentMethodInfo: { + method: 'creditcard', + }, + }; + + const mollieRefund: Refund = { + resource: 'refund', + id: CTPaymentMocked.transactions[3].interactionId, + description: 'Order', + amount: { + currency: 'EUR', + value: '5.95', + }, + status: 'pending', + metadata: '{"bookkeeping_id":12345}', + paymentId: 'tr_7UhSN1zuXS', + createdAt: '2023-03-14T17:09:02.0Z', + _links: { + self: { + href: '...', + type: 'application/hal+json', + }, + payment: { + href: 'https://api.mollie.com/v2/payments/tr_7UhSN1zuXS', + type: 'application/hal+json', + }, + documentation: { + href: '...', + type: 'text/html', + }, + }, + } as Refund; + + (getPaymentRefund as jest.Mock).mockReturnValueOnce(mollieRefund); + + (cancelPaymentRefund as jest.Mock).mockReturnValueOnce(true); + + (getPaymentCancelActions as jest.Mock).mockReturnValueOnce([]); + + try { + await handlePaymentCancelRefund(CTPaymentMocked); + } catch (error: any) { + expect(getPaymentRefund).toBeCalledTimes(0); + expect(cancelPaymentRefund).toBeCalledTimes(0); + + expect(error).toBeInstanceOf(CustomError); + expect((error as CustomError).message).toBe( + 'SCTM - handlePaymentCancelRefund - Cannot find the valid Success Charge transaction.', + ); + } + }); }); describe('Test handlePaymentWebhook', () => { @@ -3028,4 +3539,4 @@ describe('Test handleGetApplePaySession', () => { expect(mockMakeCTMoney).toHaveBeenCalledWith({ currency: 'EUR', value: '20.00' }); }); }); -}); +}); \ No newline at end of file diff --git a/processor/tests/utils/app.utils.spec.ts b/processor/tests/utils/app.utils.spec.ts index 60eda64..f76db6c 100644 --- a/processor/tests/utils/app.utils.spec.ts +++ b/processor/tests/utils/app.utils.spec.ts @@ -6,11 +6,12 @@ import { parseStringToJsonObject, removeEmptyProperties, roundSurchargeAmountToCent, + sortTransactionsByLatestCreationTime, validateEmail, } from '../../src/utils/app.utils'; import { logger } from '../../src/utils/logger.utils'; import CustomError from '../../src/errors/custom.error'; -import { Payment } from '@commercetools/platform-sdk'; +import { Payment, Transaction } from '@commercetools/platform-sdk'; import { SurchargeCost } from '../../src/types/commercetools.types'; describe('Test createDateNowString', () => { @@ -145,3 +146,63 @@ describe('Test roundSurchargeAmountToCent', () => { expect(roundSurchargeAmountToCent(surchargeAmountInEur, fractionDigits)).toBe(30100); }); }); + +describe('Test sortTransactionsByLatestCreationTime', () => { + it('should return the correct order', () => { + const data = [ + { + id: '39c1eae1-e9b4-45f0-ac18-7d83ec429cc8', + timestamp: '2024-06-24T08:28:43.474Z', + type: 'Authorization', + amount: { + type: 'centPrecision', + currencyCode: 'GBP', + centAmount: 61879, + fractionDigits: 2, + }, + interactionId: '12789fae-d6d6-4b66-9739-3a420dbda2a8', + state: 'Failure', + }, + { + id: '39c1eae1-e9b4-45f0-ac18-7d83ec429cde', + timestamp: '2024-06-24T08:29:43.474Z', + type: 'Authorization', + amount: { + type: 'centPrecision', + currencyCode: 'GBP', + centAmount: 61879, + fractionDigits: 2, + }, + interactionId: '12789fae-d6d6-4b66-9739-3a420dbda2a8', + state: 'Failure', + }, + { + id: '39c1eae1-e9b4-45f0-ac18-7d83ec429cd9', + timestamp: '2024-06-24T08:30:43.474Z', + type: 'Authorization', + amount: { + type: 'centPrecision', + currencyCode: 'GBP', + centAmount: 61879, + fractionDigits: 2, + }, + interactionId: '12789fae-d6d6-4b66-9739-3a420dbda2a8', + state: 'Failure', + }, + { + id: '39c1eae1-e9b4-45f0-ac18-7d83ec429111', + type: 'Authorization', + amount: { + type: 'centPrecision', + currencyCode: 'GBP', + centAmount: 61879, + fractionDigits: 2, + }, + interactionId: '12789fae-d6d6-4b66-9739-3a420dbda2a8', + state: 'Failure', + }, + ] as Transaction[]; + + expect(sortTransactionsByLatestCreationTime(data)).toStrictEqual([data[2], data[1], data[0], data[3]]); + }); +});