@commercetools/checkout-payments-processor-sdk 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +42 -0
  3. package/biome.json +38 -0
  4. package/package.json +48 -0
  5. package/src/api/context/request-context.helper.test.ts +98 -0
  6. package/src/api/context/request-context.helper.ts +89 -0
  7. package/src/api/context/request-context.provider.ts +27 -0
  8. package/src/api/context/types/request-context.type.ts +18 -0
  9. package/src/api/handlers/payment-components.handler.test.ts +378 -0
  10. package/src/api/handlers/payment-components.handler.ts +80 -0
  11. package/src/api/handlers/payment-intents.handler.test.ts +475 -0
  12. package/src/api/handlers/payment-intents.handler.ts +169 -0
  13. package/src/api/handlers/status.handler.test.ts +551 -0
  14. package/src/api/handlers/status.handler.ts +216 -0
  15. package/src/api/handlers/transaction.handler.ts +86 -0
  16. package/src/api/handlers/types/handler.type.ts +49 -0
  17. package/src/api/hooks/authorize.hook.test.ts +62 -0
  18. package/src/api/hooks/authorize.hook.ts +51 -0
  19. package/src/api/hooks/jwt-auth.hook.test.ts +54 -0
  20. package/src/api/hooks/jwt-auth.hook.ts +56 -0
  21. package/src/api/hooks/oauth2-auth.hook.test.ts +173 -0
  22. package/src/api/hooks/oauth2-auth.hook.ts +46 -0
  23. package/src/api/hooks/session-header-auth.hook.ts +47 -0
  24. package/src/api/hooks/session-query-param-auth.hook.ts +46 -0
  25. package/src/api/hooks/types/hook.type.ts +12 -0
  26. package/src/api/index.ts +11 -0
  27. package/src/api/types.ts +3 -0
  28. package/src/commercetools/api/base-api.ts +28 -0
  29. package/src/commercetools/api/cart-api.test.ts +95 -0
  30. package/src/commercetools/api/cart-api.ts +55 -0
  31. package/src/commercetools/api/custom-type-api.ts +39 -0
  32. package/src/commercetools/api/order-api.ts +22 -0
  33. package/src/commercetools/api/payment-api.ts +64 -0
  34. package/src/commercetools/api/payment-method-api.ts +118 -0
  35. package/src/commercetools/api/root-api.ts +109 -0
  36. package/src/commercetools/errors/ct-api.error.ts +27 -0
  37. package/src/commercetools/helpers/currency.converter.test.ts +121 -0
  38. package/src/commercetools/helpers/currency.converter.test_data.json +2379 -0
  39. package/src/commercetools/helpers/currency.converter.ts +102 -0
  40. package/src/commercetools/helpers/taxrate.converter.test.ts +48 -0
  41. package/src/commercetools/helpers/taxrate.converter.ts +39 -0
  42. package/src/commercetools/index.ts +22 -0
  43. package/src/commercetools/services/ct-cart.service.test.ts +1616 -0
  44. package/src/commercetools/services/ct-cart.service.ts +316 -0
  45. package/src/commercetools/services/ct-custom-type.service.ts +62 -0
  46. package/src/commercetools/services/ct-order.service.test.ts +136 -0
  47. package/src/commercetools/services/ct-order.service.ts +47 -0
  48. package/src/commercetools/services/ct-payment-method.service.test.ts +878 -0
  49. package/src/commercetools/services/ct-payment-method.service.ts +223 -0
  50. package/src/commercetools/services/ct-payment.service.test.ts +1123 -0
  51. package/src/commercetools/services/ct-payment.service.ts +574 -0
  52. package/src/commercetools/types/api.type.ts +162 -0
  53. package/src/commercetools/types/cart.type.ts +83 -0
  54. package/src/commercetools/types/custom-type.type.ts +21 -0
  55. package/src/commercetools/types/order.type.ts +19 -0
  56. package/src/commercetools/types/payment-method.type.ts +130 -0
  57. package/src/commercetools/types/payment.type.ts +74 -0
  58. package/src/commercetools/types/predefined-custom-types.type.ts +240 -0
  59. package/src/commercetools/types.ts +7 -0
  60. package/src/errorx/errorx.ts +625 -0
  61. package/src/errorx/index.ts +1 -0
  62. package/src/errorx/types.ts +5 -0
  63. package/src/fetch/decorators/base.decorator.ts +13 -0
  64. package/src/fetch/decorators/monitoring.decorator.ts +41 -0
  65. package/src/fetch/fetch.test.ts +57 -0
  66. package/src/fetch/index.ts +3 -0
  67. package/src/fetch/types/fetch.type.ts +11 -0
  68. package/src/fetch/types.ts +1 -0
  69. package/src/index.ts +15 -0
  70. package/src/logger/commercetools-logger.ts +69 -0
  71. package/src/logger/index.ts +1 -0
  72. package/src/logger/types/logger.type.ts +6 -0
  73. package/src/logger/types.ts +1 -0
  74. package/src/mocks/auth.mock.ts +6 -0
  75. package/src/mocks/cart.mock.ts +239 -0
  76. package/src/mocks/ct-api-error.mock.ts +23 -0
  77. package/src/mocks/index.ts +4 -0
  78. package/src/mocks/payment.mock.ts +35 -0
  79. package/src/payment-processor.ts +317 -0
  80. package/src/security/authn/authns.ts +219 -0
  81. package/src/security/authn/bearer-utils.test.ts +35 -0
  82. package/src/security/authn/bearer-utils.ts +28 -0
  83. package/src/security/authn/jwt-authn-manager.ts +69 -0
  84. package/src/security/authn/oauth2-authn-manager.ts +105 -0
  85. package/src/security/authn/session-header-authn-manager.ts +58 -0
  86. package/src/security/authn/session-query-param-authn-manager.ts +53 -0
  87. package/src/security/authn/types/authn.type.ts +50 -0
  88. package/src/security/authz/authorization-manager.ts +39 -0
  89. package/src/security/authz/types/authz.type.ts +13 -0
  90. package/src/security/index.ts +13 -0
  91. package/src/security/services/authorization.service.ts +54 -0
  92. package/src/security/services/jwt.service.test.ts +27 -0
  93. package/src/security/services/jwt.service.ts +45 -0
  94. package/src/security/services/oauth2.service.ts +60 -0
  95. package/src/security/services/session.service.ts +141 -0
  96. package/src/security/services/types/authorization.type.ts +10 -0
  97. package/src/security/services/types/jwt.type.ts +3 -0
  98. package/src/security/services/types/oauth2.type.ts +15 -0
  99. package/src/security/services/types/session.type.ts +38 -0
  100. package/src/security/types.ts +6 -0
  101. package/tsconfig.json +28 -0
  102. package/tsconfig.prod.json +9 -0
  103. package/typedoc.json +6 -0
  104. package/vitest.config.ts +15 -0
  105. package/wiki/getting-started.md +13 -0
@@ -0,0 +1,574 @@
1
+ import { randomInt } from 'node:crypto';
2
+ import type {
3
+ CustomFieldsDraft,
4
+ LocalizedString,
5
+ Payment,
6
+ PaymentAddInterfaceInteractionAction,
7
+ PaymentAddTransactionAction,
8
+ PaymentChangeTransactionInteractionIdAction,
9
+ PaymentChangeTransactionStateAction,
10
+ PaymentDraft,
11
+ PaymentSetCustomTypeAction,
12
+ PaymentSetInterfaceIdAction,
13
+ PaymentSetMethodInfoCustomTypeAction,
14
+ PaymentSetMethodInfoInterfaceAccountAction,
15
+ PaymentSetMethodInfoInterfaceAction,
16
+ PaymentSetMethodInfoMethodAction,
17
+ PaymentSetMethodInfoNameAction,
18
+ PaymentSetMethodInfoTokenAction,
19
+ PaymentUpdateAction,
20
+ Transaction,
21
+ TransactionDraft,
22
+ TransactionState,
23
+ } from '@commercetools/platform-sdk';
24
+ import type { Logger } from '@logger-types';
25
+ import { CommercetoolsAPIError } from '@/commercetools/errors/ct-api.error.js';
26
+ import type { CommercetoolsAPI } from '@/commercetools/types/api.type.js';
27
+ import type {
28
+ FindPaymentsByInterfaceId,
29
+ FindTransaction,
30
+ GetPayment,
31
+ PaymentService,
32
+ PaymentServiceOptions,
33
+ TransactionData,
34
+ UpdatePaymentInfo,
35
+ } from '@/commercetools/types/payment.type.js';
36
+
37
+ /**
38
+ * This is the default implementation of the PaymentService interface.
39
+ */
40
+ export class DefaultPaymentService implements PaymentService {
41
+ private ctAPI: CommercetoolsAPI;
42
+ private logger: Logger;
43
+
44
+ constructor(opts: PaymentServiceOptions) {
45
+ this.ctAPI = opts.ctAPI;
46
+ this.logger = opts.logger;
47
+ }
48
+
49
+ public async getPayment(opts: GetPayment): Promise<Payment> {
50
+ return await this.ctAPI.payment.getPaymentById(opts.id);
51
+ }
52
+
53
+ public async findPaymentsByInterfaceId(
54
+ opts: FindPaymentsByInterfaceId,
55
+ ): Promise<Payment[]> {
56
+ const predicate = `interfaceId="${opts.interfaceId}"`;
57
+ return (await this.ctAPI.payment.find(predicate)).results;
58
+ }
59
+
60
+ public async createPayment(draft: PaymentDraft): Promise<Payment> {
61
+ return await this.ctAPI.payment.createPayment(draft);
62
+ }
63
+
64
+ public async updatePayment(opts: UpdatePaymentInfo): Promise<Payment> {
65
+ const maxRetries = 3;
66
+ let lastError: unknown;
67
+
68
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
69
+ const payment = await this.getPayment({ id: opts.id });
70
+ const actions = this.consolidateUpdateActions(payment, opts);
71
+ this.logger.info(
72
+ {
73
+ paymentId: payment.id,
74
+ paymentVersion: payment.version,
75
+ actions,
76
+ attempt,
77
+ },
78
+ 'Updating payment with actions',
79
+ );
80
+
81
+ if (actions.length === 0) {
82
+ this.logger.info(
83
+ { paymentId: payment.id, paymentVersion: payment.version, attempt },
84
+ 'Update payment skipped, no actions to perform',
85
+ );
86
+ return payment;
87
+ }
88
+
89
+ try {
90
+ return await this.ctAPI.payment.updatePayment({
91
+ resource: {
92
+ id: payment.id,
93
+ version: payment.version,
94
+ },
95
+ actions,
96
+ });
97
+ } catch (e) {
98
+ lastError = e;
99
+
100
+ const isRetryableError =
101
+ e instanceof CommercetoolsAPIError &&
102
+ (e.httpErrorStatus === 409 ||
103
+ (e.httpErrorStatus === 400 &&
104
+ e.message.includes(
105
+ 'already used without setting a payment interface',
106
+ )));
107
+
108
+ if (isRetryableError && attempt < maxRetries - 1) {
109
+ const delayMs = this.calculateExponentialBackoff(attempt);
110
+ this.logger.warn(
111
+ {
112
+ paymentId: payment.id,
113
+ paymentVersion: payment.version,
114
+ attempt,
115
+ delayMs,
116
+ err: e,
117
+ },
118
+ 'Unable to update the payment, retrying',
119
+ );
120
+ await this.sleep(delayMs);
121
+ continue;
122
+ }
123
+
124
+ this.logger.error(
125
+ {
126
+ paymentId: payment.id,
127
+ paymentVersion: payment.version,
128
+ attempt,
129
+ err: e,
130
+ },
131
+ 'Unable to update the payment, abort',
132
+ );
133
+ throw e;
134
+ }
135
+ }
136
+
137
+ throw lastError;
138
+ }
139
+
140
+ /**
141
+ * Calculates exponential backoff delay with jitter
142
+ * @param attempt - Current attempt number (0-based)
143
+ * @returns Delay in milliseconds
144
+ */
145
+ private calculateExponentialBackoff(attempt: number): number {
146
+ const baseDelayMs = 100;
147
+ const maxDelayMs = 2000;
148
+ const jitterMs = 100;
149
+
150
+ const exponentialDelay = baseDelayMs * 2 ** attempt;
151
+ const jitter = randomInt(0, jitterMs);
152
+ return Math.min(exponentialDelay + jitter, maxDelayMs);
153
+ }
154
+
155
+ /**
156
+ * Sleep for a specified duration
157
+ * @param ms - Duration in milliseconds
158
+ */
159
+ private sleep(ms: number): Promise<void> {
160
+ return new Promise((resolve) => setTimeout(resolve, ms));
161
+ }
162
+
163
+ private consolidateUpdateActions(
164
+ payment: Payment,
165
+ updateInfo: UpdatePaymentInfo,
166
+ ): PaymentUpdateAction[] {
167
+ const actions: PaymentUpdateAction[] = [];
168
+
169
+ if (!payment.interfaceId && updateInfo.pspReference) {
170
+ actions.push(this.populateSetInterfaceIdAction(updateInfo.pspReference));
171
+ }
172
+
173
+ /**
174
+ * old way
175
+ */
176
+ if (!payment.paymentMethodInfo?.method && updateInfo.paymentMethod) {
177
+ actions.push(this.populateSetPaymentMethod(updateInfo.paymentMethod));
178
+ }
179
+
180
+ if (!payment.paymentMethodInfo?.token && updateInfo.token) {
181
+ actions.push(this.populateSetMethodInfoToken(updateInfo.token));
182
+ }
183
+
184
+ /**
185
+ * New way
186
+ */
187
+ if (
188
+ !payment.paymentMethodInfo?.method &&
189
+ updateInfo.paymentMethodInfo?.method
190
+ ) {
191
+ actions.push(
192
+ this.populateSetPaymentMethod(updateInfo.paymentMethodInfo.method),
193
+ );
194
+ }
195
+
196
+ if (
197
+ !payment.paymentMethodInfo?.token &&
198
+ updateInfo.paymentMethodInfo?.token?.value
199
+ ) {
200
+ actions.push(
201
+ this.populateSetMethodInfoToken(
202
+ updateInfo.paymentMethodInfo.token.value,
203
+ ),
204
+ );
205
+ }
206
+
207
+ if (
208
+ !payment.paymentMethodInfo?.name &&
209
+ updateInfo.paymentMethodInfo?.name
210
+ ) {
211
+ actions.push(
212
+ this.populateSetMethodInfoName(updateInfo.paymentMethodInfo.name),
213
+ );
214
+ }
215
+
216
+ if (
217
+ !payment.paymentMethodInfo?.paymentInterface &&
218
+ updateInfo.paymentMethodInfo?.paymentInterface
219
+ ) {
220
+ actions.push(
221
+ this.populateSetMethodInfoInterface(
222
+ updateInfo.paymentMethodInfo.paymentInterface,
223
+ ),
224
+ );
225
+ }
226
+
227
+ if (
228
+ !payment.paymentMethodInfo?.interfaceAccount &&
229
+ updateInfo.paymentMethodInfo?.interfaceAccount
230
+ ) {
231
+ actions.push(
232
+ this.populateSetMethodInfoInterfaceAccount(
233
+ updateInfo.paymentMethodInfo.interfaceAccount,
234
+ ),
235
+ );
236
+ }
237
+
238
+ if (updateInfo.paymentMethodInfo?.custom) {
239
+ actions.push(
240
+ this.populateSetMethodInfoCustomType(
241
+ updateInfo.paymentMethodInfo.custom,
242
+ ),
243
+ );
244
+ }
245
+
246
+ if (updateInfo.transaction) {
247
+ const transactionChanges = this.consolidateTransactionChanges(
248
+ payment,
249
+ updateInfo.transaction,
250
+ );
251
+ if (transactionChanges.length > 0) {
252
+ actions.push(...transactionChanges);
253
+ }
254
+ }
255
+
256
+ if (updateInfo.customFields) {
257
+ actions.push(this.populateSetCustomType(updateInfo.customFields));
258
+ }
259
+
260
+ if (updateInfo.pspInteractions && updateInfo.pspInteractions.length > 0) {
261
+ actions.push(
262
+ ...this.populateAddInterfaceInteractions(updateInfo.pspInteractions),
263
+ );
264
+ }
265
+
266
+ return actions;
267
+ }
268
+
269
+ private populateSetInterfaceIdAction(
270
+ interfaceId: string,
271
+ ): PaymentSetInterfaceIdAction {
272
+ return {
273
+ action: 'setInterfaceId',
274
+ interfaceId,
275
+ };
276
+ }
277
+
278
+ private populateChangeTransactionInteractionId(
279
+ txId: string,
280
+ interactionId: string,
281
+ ): PaymentChangeTransactionInteractionIdAction {
282
+ return {
283
+ action: 'changeTransactionInteractionId',
284
+ transactionId: txId,
285
+ interactionId,
286
+ };
287
+ }
288
+
289
+ private populateSetTransactionInterfaceId(
290
+ txId: string,
291
+ interfaceId: string,
292
+ ): PaymentUpdateAction {
293
+ return {
294
+ action: 'setTransactionInterfaceId',
295
+ transactionId: txId,
296
+ interfaceId,
297
+ } as unknown as PaymentUpdateAction; // TypeScript does not have this action in the SDK yet
298
+ }
299
+
300
+ private populateAddTransactionAction(
301
+ draft: TransactionDraft,
302
+ ): PaymentAddTransactionAction {
303
+ return {
304
+ action: 'addTransaction',
305
+ transaction: {
306
+ ...draft,
307
+ timestamp: new Date().toISOString(),
308
+ },
309
+ };
310
+ }
311
+
312
+ private populateChangeTransactionState(
313
+ txId: string,
314
+ state: TransactionState,
315
+ ): PaymentChangeTransactionStateAction {
316
+ return {
317
+ action: 'changeTransactionState',
318
+ transactionId: txId,
319
+ state,
320
+ };
321
+ }
322
+
323
+ private populateSetPaymentMethod(
324
+ paymentMethod: string,
325
+ ): PaymentSetMethodInfoMethodAction {
326
+ return {
327
+ action: 'setMethodInfoMethod',
328
+ method: paymentMethod,
329
+ };
330
+ }
331
+
332
+ private populateSetMethodInfoToken(
333
+ token: string,
334
+ ): PaymentSetMethodInfoTokenAction {
335
+ return {
336
+ action: 'setMethodInfoToken',
337
+ token: {
338
+ value: token,
339
+ },
340
+ };
341
+ }
342
+
343
+ private populateSetMethodInfoInterface(
344
+ paymentInterface: string,
345
+ ): PaymentSetMethodInfoInterfaceAction {
346
+ return {
347
+ action: 'setMethodInfoInterface',
348
+ interface: paymentInterface,
349
+ };
350
+ }
351
+
352
+ private populateSetMethodInfoName(
353
+ name: LocalizedString,
354
+ ): PaymentSetMethodInfoNameAction {
355
+ return {
356
+ action: 'setMethodInfoName',
357
+ name,
358
+ };
359
+ }
360
+
361
+ private populateSetMethodInfoInterfaceAccount(
362
+ interfaceAccount: string,
363
+ ): PaymentSetMethodInfoInterfaceAccountAction {
364
+ return {
365
+ action: 'setMethodInfoInterfaceAccount',
366
+ interfaceAccount,
367
+ };
368
+ }
369
+
370
+ private populateSetMethodInfoCustomType(
371
+ customType: CustomFieldsDraft,
372
+ ): PaymentSetMethodInfoCustomTypeAction {
373
+ return {
374
+ action: 'setMethodInfoCustomType',
375
+ ...customType,
376
+ };
377
+ }
378
+
379
+ private populateSetCustomType(
380
+ customFields: CustomFieldsDraft,
381
+ ): PaymentSetCustomTypeAction {
382
+ return {
383
+ action: 'setCustomType',
384
+ ...customFields,
385
+ };
386
+ }
387
+
388
+ private populateAddInterfaceInteractions(
389
+ pspInteractions: CustomFieldsDraft[],
390
+ ): PaymentAddInterfaceInteractionAction[] {
391
+ return pspInteractions.map((interaction) => {
392
+ return {
393
+ action: 'addInterfaceInteraction',
394
+ ...interaction,
395
+ };
396
+ });
397
+ }
398
+
399
+ public findMatchingTransactions(
400
+ payment: Payment,
401
+ transaction: TransactionData,
402
+ ): Transaction[] {
403
+ return payment.transactions.filter((tx: Transaction) => {
404
+ // Must match transaction type
405
+ if (tx.type !== transaction.type) {
406
+ return false;
407
+ }
408
+
409
+ // New transaction must have at least one PSP identifier. InteractionId is for retro compatibility.
410
+ if (!transaction.interactionId && !transaction.interfaceId) {
411
+ return false;
412
+ }
413
+
414
+ // Match by interactionId
415
+ if (
416
+ transaction.interactionId &&
417
+ tx.interactionId === transaction.interactionId
418
+ ) {
419
+ return true;
420
+ }
421
+
422
+ // Match by interfaceId
423
+ if (
424
+ transaction.interfaceId &&
425
+ tx.interfaceId === transaction.interfaceId
426
+ ) {
427
+ return true;
428
+ }
429
+
430
+ // Match Initial transactions missing at least one ID by amount
431
+ // This allows reusing an Initial transaction when we get a PSP response with an ID
432
+ const isInitialMissingIds =
433
+ tx.state === 'Initial' && (!tx.interactionId || !tx.interfaceId);
434
+ const hasMatchingAmount =
435
+ tx.amount.centAmount === transaction.amount.centAmount &&
436
+ tx.amount.currencyCode === transaction.amount.currencyCode;
437
+
438
+ return isInitialMissingIds && hasMatchingAmount;
439
+ });
440
+ }
441
+
442
+ public hasTransactionInState(opts: FindTransaction): boolean {
443
+ return opts.payment.transactions.some(
444
+ (tx: Transaction) =>
445
+ tx.type === opts.transactionType && opts.states.includes(tx.state),
446
+ );
447
+ }
448
+
449
+ public consolidateTransactionChanges(
450
+ payment: Payment,
451
+ newTransaction: TransactionData,
452
+ ): PaymentUpdateAction[] {
453
+ const actions: PaymentUpdateAction[] = [];
454
+
455
+ // Discard transactions in 'Initial' state without an interactionId as they provide no value
456
+ if (this.shouldDiscardTransaction(newTransaction)) {
457
+ return [];
458
+ }
459
+
460
+ // Find matching transactions in the payment
461
+ const matchingTxs = this.findMatchingTransactions(payment, newTransaction);
462
+
463
+ // Handle cases where no matching transactions are found
464
+ if (matchingTxs.length === 0) {
465
+ actions.push(this.populateAddTransactionAction(newTransaction));
466
+ return actions;
467
+ }
468
+
469
+ // Handle cases where exactly one matching transaction is found
470
+ if (matchingTxs.length === 1) {
471
+ const existingTx = matchingTxs[0];
472
+ this.handleTransactionUpdates(existingTx, newTransaction, actions);
473
+ return actions;
474
+ }
475
+
476
+ // Handle cases where multiple matching transactions are found
477
+ this.throwMultipleMatchingTransactionsError(
478
+ payment,
479
+ newTransaction,
480
+ matchingTxs,
481
+ );
482
+ }
483
+
484
+ private shouldDiscardTransaction(newTransaction: TransactionData): boolean {
485
+ return newTransaction.state === 'Initial' && !newTransaction.interactionId;
486
+ }
487
+
488
+ private handleTransactionUpdates(
489
+ existingTx: Transaction,
490
+ newTransaction: TransactionData,
491
+ actions: PaymentUpdateAction[],
492
+ ): void {
493
+ if (this.shouldUpdateTransactionState(existingTx, newTransaction)) {
494
+ actions.push(
495
+ this.populateChangeTransactionState(
496
+ existingTx.id,
497
+ newTransaction.state,
498
+ ),
499
+ );
500
+ }
501
+
502
+ if (this.shouldUpdateInteractionId(existingTx, newTransaction)) {
503
+ actions.push(
504
+ this.populateChangeTransactionInteractionId(
505
+ existingTx.id,
506
+ newTransaction.interactionId as string,
507
+ ),
508
+ );
509
+ }
510
+
511
+ if (this.shouldUpdateTransactionInterfaceId(existingTx, newTransaction)) {
512
+ actions.push(
513
+ this.populateSetTransactionInterfaceId(
514
+ existingTx.id,
515
+ newTransaction.interfaceId as string,
516
+ ),
517
+ );
518
+ }
519
+ }
520
+
521
+ private shouldUpdateTransactionState(
522
+ existingTx: Transaction,
523
+ newTransaction: TransactionData,
524
+ ): boolean {
525
+ const { state: currentState } = existingTx;
526
+ const { state: newState } = newTransaction;
527
+
528
+ // Define allowed state transitions
529
+ const allowedTransitions: Record<string, string[]> = {
530
+ Initial: ['Pending', 'Success', 'Failure'],
531
+ Pending: ['Success', 'Failure'],
532
+ Failure: ['Success'],
533
+ Success: ['Failure'],
534
+ };
535
+
536
+ return (
537
+ currentState !== newState &&
538
+ allowedTransitions[currentState]?.includes(newState)
539
+ );
540
+ }
541
+
542
+ private shouldUpdateTransactionInterfaceId(
543
+ existingTx: Transaction,
544
+ newTransaction: TransactionData,
545
+ ): boolean {
546
+ return !existingTx.interfaceId && !!newTransaction.interfaceId;
547
+ }
548
+
549
+ private shouldUpdateInteractionId(
550
+ existingTx: Transaction,
551
+ newTransaction: TransactionData,
552
+ ): boolean {
553
+ return !existingTx.interactionId && !!newTransaction.interactionId;
554
+ }
555
+
556
+ private throwMultipleMatchingTransactionsError(
557
+ payment: Payment,
558
+ newTransaction: TransactionData,
559
+ matchingTxs: Transaction[],
560
+ ): never {
561
+ this.logger.error(
562
+ {
563
+ paymentId: payment.id,
564
+ transaction: newTransaction,
565
+ matchingTransactions: matchingTxs,
566
+ },
567
+ 'Multiple matching transactions found when consolidating payment changes',
568
+ );
569
+
570
+ throw new Error(
571
+ `Multiple matching transactions found for payment ${payment.id} and transaction ${JSON.stringify(newTransaction)}`,
572
+ );
573
+ }
574
+ }