@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,1123 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { RequestContextProvider } from '@api';
3
+ import type { Payment, Transaction } from '@commercetools/platform-sdk';
4
+ import { CommercetoolsLogger } from '@logger';
5
+ import { CommercetoolsPaymentAPI } from '@/commercetools/api/payment-api.js';
6
+ import { DefaultCommercetoolsAPI } from '@/commercetools/api/root-api.js';
7
+ import { CommercetoolsAPIError } from '@/commercetools/errors/ct-api.error.js';
8
+ import { DefaultPaymentService } from '@/commercetools/services/ct-payment.service.js';
9
+
10
+ const projectKey = 'test';
11
+ const contextProvider = new RequestContextProvider({
12
+ getContextFn: () => ({
13
+ correlationId: 'correlation-id',
14
+ requestId: 'request-id',
15
+ }),
16
+ updateContextFn: () => {},
17
+ });
18
+ const logger = new CommercetoolsLogger({ contextProvider, projectKey });
19
+
20
+ const ctAPI = new DefaultCommercetoolsAPI({
21
+ apiUrl: 'http://api.test.com',
22
+ authUrl: 'http://auth.test.com',
23
+ clientId: 'clientId',
24
+ clientSecret: 'clientSecret',
25
+ projectKey,
26
+ contextProvider,
27
+ logger,
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ httpClient: (input: string | URL | Request, init?: RequestInit) => {
30
+ return fetch(input, init);
31
+ },
32
+ });
33
+
34
+ const paymentAPI = new CommercetoolsPaymentAPI({ client: ctAPI.client });
35
+ ctAPI.payment = paymentAPI;
36
+
37
+ const ctPaymentService = new DefaultPaymentService({ ctAPI, logger });
38
+
39
+ describe('commercetools payment service', () => {
40
+ beforeEach(() => {
41
+ vi.clearAllMocks();
42
+ });
43
+
44
+ describe('getPaymentById', () => {
45
+ test('should return a payment', async () => {
46
+ //Given
47
+ const paymentId = randomUUID();
48
+ const txId = randomUUID();
49
+ const payment = {
50
+ id: paymentId,
51
+ version: 1,
52
+ amountPlanned: {
53
+ currencyCode: 'EUR',
54
+ centAmount: 10000,
55
+ },
56
+ transactions: [
57
+ {
58
+ id: txId,
59
+ type: 'Authorization',
60
+ amount: {
61
+ currencyCode: 'EUR',
62
+ centAmount: 10000,
63
+ },
64
+ state: 'Initial',
65
+ },
66
+ ],
67
+ } as Payment;
68
+
69
+ vi.spyOn(paymentAPI, 'getPaymentById').mockResolvedValueOnce(payment);
70
+
71
+ //When
72
+ const result = await ctPaymentService.getPayment({
73
+ id: paymentId,
74
+ });
75
+
76
+ //Then
77
+ expect(result).toEqual(payment);
78
+ });
79
+ });
80
+
81
+ describe('findPaymentsByInterfaceId', () => {
82
+ test('should return a payment given the interface id', async () => {
83
+ //Given
84
+ const paymentId = randomUUID();
85
+ const interfaceId = randomUUID();
86
+ const txId = randomUUID();
87
+ const payment = {
88
+ id: paymentId,
89
+ version: 1,
90
+ amountPlanned: {
91
+ currencyCode: 'EUR',
92
+ centAmount: 10000,
93
+ },
94
+ interfaceId,
95
+ transactions: [
96
+ {
97
+ id: txId,
98
+ type: 'Authorization',
99
+ amount: {
100
+ currencyCode: 'EUR',
101
+ centAmount: 10000,
102
+ },
103
+ state: 'Initial',
104
+ },
105
+ ],
106
+ } as Payment;
107
+
108
+ vi.spyOn(paymentAPI, 'find').mockResolvedValueOnce({
109
+ results: [payment],
110
+ count: 1,
111
+ total: 1,
112
+ offset: 0,
113
+ limit: 10,
114
+ });
115
+
116
+ //When
117
+ const result = await ctPaymentService.findPaymentsByInterfaceId({
118
+ interfaceId: interfaceId,
119
+ });
120
+
121
+ //Then
122
+ expect(result).toHaveLength(1);
123
+ expect(result[0]).toMatchObject(payment);
124
+ });
125
+ });
126
+
127
+ describe('updatePayment', () => {
128
+ test('should update the payment in the first attempt', async () => {
129
+ //Given
130
+ const paymentId = randomUUID();
131
+ const txId = randomUUID();
132
+ const interfaceId = randomUUID();
133
+ const initialPayment = {
134
+ id: paymentId,
135
+ version: 1,
136
+ amountPlanned: {
137
+ currencyCode: 'EUR',
138
+ centAmount: 10000,
139
+ },
140
+ transactions: [
141
+ {
142
+ id: txId,
143
+ type: 'Authorization',
144
+ amount: {
145
+ currencyCode: 'EUR',
146
+ centAmount: 10000,
147
+ },
148
+ state: 'Initial',
149
+ },
150
+ ],
151
+ } as Payment;
152
+
153
+ const updatedPayment = {
154
+ id: paymentId,
155
+ version: 2,
156
+ amountPlanned: {
157
+ currencyCode: 'EUR',
158
+ centAmount: 10000,
159
+ },
160
+ interfaceId,
161
+ transactions: [
162
+ {
163
+ id: txId,
164
+ type: 'Authorization',
165
+ amount: {
166
+ currencyCode: 'EUR',
167
+ centAmount: 10000,
168
+ },
169
+ state: 'Success',
170
+ interactionId: interfaceId,
171
+ },
172
+ ],
173
+ } as Payment;
174
+
175
+ vi.spyOn(paymentAPI, 'getPaymentById').mockResolvedValueOnce(
176
+ initialPayment,
177
+ );
178
+ vi.spyOn(paymentAPI, 'updatePayment').mockResolvedValueOnce(
179
+ updatedPayment,
180
+ );
181
+
182
+ //When
183
+ const result = await ctPaymentService.updatePayment({
184
+ id: paymentId,
185
+ pspReference: interfaceId,
186
+ transaction: {
187
+ type: 'Authorization',
188
+ amount: {
189
+ currencyCode: 'EUR',
190
+ centAmount: 10000,
191
+ },
192
+ state: 'Success',
193
+ interactionId: interfaceId,
194
+ },
195
+ });
196
+
197
+ //Then
198
+ expect(result).toEqual(updatedPayment);
199
+ });
200
+
201
+ test('should update the payment in the second attempt, after resolving a conflict due to invalid interfaceId', async () => {
202
+ //Given
203
+ const paymentId = randomUUID();
204
+ const txId = randomUUID();
205
+ const interfaceId = randomUUID();
206
+ const initialPayment = {
207
+ id: paymentId,
208
+ version: 1,
209
+ amountPlanned: {
210
+ currencyCode: 'EUR',
211
+ centAmount: 10000,
212
+ },
213
+ transactions: [
214
+ {
215
+ id: txId,
216
+ type: 'Authorization',
217
+ amount: {
218
+ currencyCode: 'EUR',
219
+ centAmount: 10000,
220
+ },
221
+ state: 'Initial',
222
+ },
223
+ ],
224
+ } as Payment;
225
+
226
+ const secondAttemptPayment = {
227
+ id: paymentId,
228
+ version: 2,
229
+ amountPlanned: {
230
+ currencyCode: 'EUR',
231
+ centAmount: 10000,
232
+ },
233
+ interfaceId,
234
+ transactions: [
235
+ {
236
+ id: txId,
237
+ type: 'Authorization',
238
+ amount: {
239
+ currencyCode: 'EUR',
240
+ centAmount: 10000,
241
+ },
242
+ state: 'Initial',
243
+ },
244
+ ],
245
+ } as Payment;
246
+
247
+ const updatedPayment = {
248
+ id: paymentId,
249
+ version: 3,
250
+ amountPlanned: {
251
+ currencyCode: 'EUR',
252
+ centAmount: 10000,
253
+ },
254
+ interfaceId,
255
+ transactions: [
256
+ {
257
+ id: txId,
258
+ type: 'Authorization',
259
+ amount: {
260
+ currencyCode: 'EUR',
261
+ centAmount: 10000,
262
+ },
263
+ state: 'Success',
264
+ interactionId: interfaceId,
265
+ },
266
+ ],
267
+ } as Payment;
268
+
269
+ vi.spyOn(paymentAPI, 'getPaymentById').mockResolvedValueOnce(
270
+ initialPayment,
271
+ );
272
+ vi.spyOn(paymentAPI, 'getPaymentById').mockResolvedValueOnce(
273
+ secondAttemptPayment,
274
+ );
275
+ vi.spyOn(paymentAPI, 'updatePayment').mockRejectedValueOnce(
276
+ new CommercetoolsAPIError({
277
+ body: {
278
+ message: `The interface id '${interfaceId}' was already used without setting a payment interface.`,
279
+ errors: [
280
+ {
281
+ code: 'InvalidInput',
282
+ message: `The interface id '${interfaceId}' was already used without setting a payment interface.`,
283
+ action: {
284
+ action: 'setInterfaceId',
285
+ interfaceId,
286
+ },
287
+ },
288
+ ],
289
+ },
290
+ statusCode: 400,
291
+ message: `The interface id '${interfaceId}' was already used without setting a payment interface.`,
292
+ }),
293
+ );
294
+ vi.spyOn(paymentAPI, 'updatePayment').mockResolvedValueOnce(
295
+ updatedPayment,
296
+ );
297
+
298
+ //When
299
+ const result = await ctPaymentService.updatePayment({
300
+ id: paymentId,
301
+ pspReference: interfaceId,
302
+ transaction: {
303
+ type: 'Authorization',
304
+ amount: {
305
+ currencyCode: 'EUR',
306
+ centAmount: 10000,
307
+ },
308
+ state: 'Success',
309
+ interactionId: interfaceId,
310
+ },
311
+ });
312
+
313
+ //Then
314
+ expect(result).toEqual(updatedPayment);
315
+ expect(paymentAPI.getPaymentById).toHaveBeenCalledTimes(2);
316
+ expect(paymentAPI.updatePayment).toHaveBeenCalledTimes(2);
317
+ });
318
+
319
+ test('should update the payment in the second attempt, after resolving a concurrent modification conflict', async () => {
320
+ //Given
321
+ const paymentId = randomUUID();
322
+ const txId = randomUUID();
323
+ const interfaceId = randomUUID();
324
+ const initialPayment = {
325
+ id: paymentId,
326
+ version: 1,
327
+ amountPlanned: {
328
+ currencyCode: 'EUR',
329
+ centAmount: 10000,
330
+ },
331
+ transactions: [
332
+ {
333
+ id: txId,
334
+ type: 'Authorization',
335
+ amount: {
336
+ currencyCode: 'EUR',
337
+ centAmount: 10000,
338
+ },
339
+ state: 'Initial',
340
+ },
341
+ ],
342
+ } as Payment;
343
+
344
+ const secondAttemptPayment = {
345
+ id: paymentId,
346
+ version: 2,
347
+ amountPlanned: {
348
+ currencyCode: 'EUR',
349
+ centAmount: 10000,
350
+ },
351
+ interfaceId,
352
+ transactions: [
353
+ {
354
+ id: txId,
355
+ type: 'Authorization',
356
+ amount: {
357
+ currencyCode: 'EUR',
358
+ centAmount: 10000,
359
+ },
360
+ state: 'Initial',
361
+ },
362
+ ],
363
+ } as Payment;
364
+
365
+ const updatedPayment = {
366
+ id: paymentId,
367
+ version: 3,
368
+ amountPlanned: {
369
+ currencyCode: 'EUR',
370
+ centAmount: 10000,
371
+ },
372
+ interfaceId,
373
+ transactions: [
374
+ {
375
+ id: txId,
376
+ type: 'Authorization',
377
+ amount: {
378
+ currencyCode: 'EUR',
379
+ centAmount: 10000,
380
+ },
381
+ state: 'Success',
382
+ interactionId: interfaceId,
383
+ },
384
+ ],
385
+ } as Payment;
386
+
387
+ vi.spyOn(paymentAPI, 'getPaymentById').mockResolvedValueOnce(
388
+ initialPayment,
389
+ );
390
+ vi.spyOn(paymentAPI, 'getPaymentById').mockResolvedValueOnce(
391
+ secondAttemptPayment,
392
+ );
393
+ vi.spyOn(paymentAPI, 'updatePayment').mockRejectedValueOnce(
394
+ new CommercetoolsAPIError({
395
+ body: {
396
+ message: `Object ${paymentId} has a different version than expected. Expected: 3 - Actual: 2`,
397
+ errors: [
398
+ {
399
+ code: 'ConcurrentModification',
400
+ message: `Object ${paymentId} has a different version than expected. Expected: 3 - Actual: 2`,
401
+ },
402
+ ],
403
+ },
404
+ statusCode: 409,
405
+ message: `Object ${paymentId} has a different version than expected. Expected: 3 - Actual: 2`,
406
+ }),
407
+ );
408
+ vi.spyOn(paymentAPI, 'updatePayment').mockResolvedValueOnce(
409
+ updatedPayment,
410
+ );
411
+
412
+ //When
413
+ const result = await ctPaymentService.updatePayment({
414
+ id: paymentId,
415
+ pspReference: interfaceId,
416
+ transaction: {
417
+ type: 'Authorization',
418
+ amount: {
419
+ currencyCode: 'EUR',
420
+ centAmount: 10000,
421
+ },
422
+ state: 'Success',
423
+ interactionId: interfaceId,
424
+ },
425
+ });
426
+
427
+ //Then
428
+ expect(result).toEqual(updatedPayment);
429
+ expect(paymentAPI.getPaymentById).toHaveBeenCalledTimes(2);
430
+ expect(paymentAPI.updatePayment).toHaveBeenCalledTimes(2);
431
+ });
432
+
433
+ test('should not update the payment when there is an unexpected error', async () => {
434
+ //Given
435
+ const paymentId = randomUUID();
436
+ const txId = randomUUID();
437
+ const interfaceId = randomUUID();
438
+ const initialPayment = {
439
+ id: paymentId,
440
+ version: 1,
441
+ amountPlanned: {
442
+ currencyCode: 'EUR',
443
+ centAmount: 10000,
444
+ },
445
+ transactions: [
446
+ {
447
+ id: txId,
448
+ type: 'Authorization',
449
+ amount: {
450
+ currencyCode: 'EUR',
451
+ centAmount: 10000,
452
+ },
453
+ state: 'Initial',
454
+ },
455
+ ],
456
+ } as Payment;
457
+
458
+ vi.spyOn(paymentAPI, 'getPaymentById').mockResolvedValueOnce(
459
+ initialPayment,
460
+ );
461
+ vi.spyOn(paymentAPI, 'updatePayment').mockRejectedValueOnce(
462
+ new CommercetoolsAPIError({
463
+ body: {
464
+ message: `General error`,
465
+ },
466
+ statusCode: 500,
467
+ message: `General error`,
468
+ }),
469
+ );
470
+
471
+ //When
472
+ const resultPromise = ctPaymentService.updatePayment({
473
+ id: paymentId,
474
+ pspReference: interfaceId,
475
+ transaction: {
476
+ type: 'Authorization',
477
+ amount: {
478
+ currencyCode: 'EUR',
479
+ centAmount: 10000,
480
+ },
481
+ state: 'Success',
482
+ interactionId: interfaceId,
483
+ },
484
+ });
485
+
486
+ //Then
487
+ await expect(resultPromise).rejects.toThrow('General error');
488
+ expect(paymentAPI.getPaymentById).toHaveBeenCalledTimes(1);
489
+ expect(paymentAPI.updatePayment).toHaveBeenCalledTimes(1);
490
+ });
491
+ });
492
+
493
+ describe('findMatchingTransactions', () => {
494
+ test('should find a match when an initial tx without interaction id exists ', () => {
495
+ //Given
496
+ const expectedTxId = randomUUID();
497
+ const payment = {
498
+ id: randomUUID(),
499
+ amountPlanned: {
500
+ currencyCode: 'EUR',
501
+ centAmount: 10000,
502
+ },
503
+ transactions: [
504
+ {
505
+ id: expectedTxId,
506
+ type: 'Authorization',
507
+ amount: {
508
+ currencyCode: 'EUR',
509
+ centAmount: 10000,
510
+ },
511
+ state: 'Initial',
512
+ },
513
+ ],
514
+ } as Payment;
515
+
516
+ const transaction = {
517
+ type: 'Authorization',
518
+ amount: {
519
+ currencyCode: 'EUR',
520
+ centAmount: 10000,
521
+ },
522
+ state: 'Success',
523
+ interactionId: 'interactionId',
524
+ } as Transaction;
525
+
526
+ //When
527
+ const result = ctPaymentService.findMatchingTransactions(
528
+ payment,
529
+ transaction,
530
+ );
531
+
532
+ //Then
533
+ expect(result).toHaveLength(1);
534
+ expect(result[0].id).toEqual(expectedTxId);
535
+ });
536
+
537
+ test('should find a match when the type and interaction id is the same', () => {
538
+ //Given
539
+ const expectedTxId = randomUUID();
540
+ const payment = {
541
+ id: randomUUID(),
542
+ amountPlanned: {
543
+ currencyCode: 'EUR',
544
+ centAmount: 10000,
545
+ },
546
+ transactions: [
547
+ {
548
+ id: randomUUID(),
549
+ type: 'Authorization',
550
+ amount: {
551
+ currencyCode: 'EUR',
552
+ centAmount: 10000,
553
+ },
554
+ state: 'Failure',
555
+ interactionId: 'interactionId1',
556
+ },
557
+ {
558
+ id: expectedTxId,
559
+ type: 'Authorization',
560
+ amount: {
561
+ currencyCode: 'EUR',
562
+ centAmount: 10000,
563
+ },
564
+ state: 'Pending',
565
+ interactionId: 'interactionId2',
566
+ },
567
+ ],
568
+ } as Payment;
569
+
570
+ const transaction = {
571
+ type: 'Authorization',
572
+ amount: {
573
+ currencyCode: 'EUR',
574
+ centAmount: 10000,
575
+ },
576
+ state: 'Success',
577
+ interactionId: 'interactionId2',
578
+ } as Transaction;
579
+
580
+ //When
581
+ const result = ctPaymentService.findMatchingTransactions(
582
+ payment,
583
+ transaction,
584
+ );
585
+
586
+ //Then
587
+ expect(result).toHaveLength(1);
588
+ expect(result[0].id).toEqual(expectedTxId);
589
+ });
590
+
591
+ test('should not find any match when the type and interaction id is different', () => {
592
+ //Given
593
+ const payment = {
594
+ id: randomUUID(),
595
+ amountPlanned: {
596
+ currencyCode: 'EUR',
597
+ centAmount: 10000,
598
+ },
599
+ transactions: [
600
+ {
601
+ id: randomUUID(),
602
+ type: 'Authorization',
603
+ amount: {
604
+ currencyCode: 'EUR',
605
+ centAmount: 10000,
606
+ },
607
+ state: 'Failure',
608
+ interactionId: 'interactionId1',
609
+ },
610
+ {
611
+ id: randomUUID(),
612
+ type: 'Authorization',
613
+ amount: {
614
+ currencyCode: 'EUR',
615
+ centAmount: 10000,
616
+ },
617
+ state: 'Pending',
618
+ interactionId: 'interactionId2',
619
+ },
620
+ ],
621
+ } as Payment;
622
+
623
+ const transaction = {
624
+ type: 'Authorization',
625
+ amount: {
626
+ currencyCode: 'EUR',
627
+ centAmount: 10000,
628
+ },
629
+ state: 'Success',
630
+ interactionId: 'interactionId3',
631
+ } as Transaction;
632
+
633
+ //When
634
+ const result = ctPaymentService.findMatchingTransactions(
635
+ payment,
636
+ transaction,
637
+ );
638
+
639
+ //Then
640
+ expect(result).toHaveLength(0);
641
+ });
642
+ });
643
+
644
+ describe('findTransaction', () => {
645
+ test('should return true if a transaction with specified filters exists', () => {
646
+ const payment = {
647
+ id: randomUUID(),
648
+ amountPlanned: {
649
+ currencyCode: 'EUR',
650
+ centAmount: 10000,
651
+ },
652
+ transactions: [
653
+ {
654
+ id: randomUUID(),
655
+ type: 'Authorization',
656
+ amount: {
657
+ currencyCode: 'EUR',
658
+ centAmount: 10000,
659
+ },
660
+ state: 'Success',
661
+ },
662
+ ],
663
+ } as Payment;
664
+
665
+ // When
666
+ const result = ctPaymentService.hasTransactionInState({
667
+ payment,
668
+ transactionType: 'Authorization',
669
+ states: ['Success', 'Pending'],
670
+ });
671
+
672
+ // Then
673
+ expect(result).toBe(true);
674
+ });
675
+
676
+ test('should return false because transaction with condition does not exist', () => {
677
+ const payment = {
678
+ id: randomUUID(),
679
+ amountPlanned: {
680
+ currencyCode: 'EUR',
681
+ centAmount: 10000,
682
+ },
683
+ transactions: [
684
+ {
685
+ id: randomUUID(),
686
+ type: 'Authorization',
687
+ amount: {
688
+ currencyCode: 'EUR',
689
+ centAmount: 10000,
690
+ },
691
+ state: 'Initial',
692
+ },
693
+ {
694
+ id: randomUUID(),
695
+ type: 'Charge',
696
+ amount: {
697
+ currencyCode: 'EUR',
698
+ centAmount: 10000,
699
+ },
700
+ state: 'Success',
701
+ },
702
+ ],
703
+ } as Payment;
704
+
705
+ // When
706
+ const result = ctPaymentService.hasTransactionInState({
707
+ payment,
708
+ transactionType: 'Authorization',
709
+ states: ['Success'],
710
+ });
711
+
712
+ // Then
713
+ expect(result).toBe(false);
714
+ });
715
+ });
716
+
717
+ describe('consolidateTransactionChanges', () => {
718
+ test('should return an empty array when the transaction is in initial state without interactionId', () => {
719
+ //Given
720
+ const payment = {
721
+ id: randomUUID(),
722
+ version: 1,
723
+ amountPlanned: {
724
+ currencyCode: 'EUR',
725
+ centAmount: 10000,
726
+ },
727
+ transactions: [
728
+ {
729
+ id: randomUUID(),
730
+ type: 'Authorization',
731
+ amount: {
732
+ currencyCode: 'EUR',
733
+ centAmount: 10000,
734
+ },
735
+ state: 'Initial',
736
+ },
737
+ ],
738
+ } as Payment;
739
+
740
+ const transaction = {
741
+ type: 'Authorization',
742
+ amount: {
743
+ currencyCode: 'EUR',
744
+ centAmount: 10000,
745
+ },
746
+ state: 'Initial',
747
+ } as Transaction;
748
+
749
+ //When
750
+ const result = ctPaymentService.consolidateTransactionChanges(
751
+ payment,
752
+ transaction,
753
+ );
754
+
755
+ //Then
756
+ expect(result).toHaveLength(0);
757
+ });
758
+
759
+ test('should return an add transaction action when there is no matching transaction', () => {
760
+ //Given
761
+ const payment = {
762
+ id: randomUUID(),
763
+ version: 1,
764
+ amountPlanned: {
765
+ currencyCode: 'EUR',
766
+ centAmount: 10000,
767
+ },
768
+ transactions: [
769
+ {
770
+ id: randomUUID(),
771
+ type: 'Authorization',
772
+ amount: {
773
+ currencyCode: 'EUR',
774
+ centAmount: 10000,
775
+ },
776
+ state: 'Failure',
777
+ },
778
+ ],
779
+ } as Payment;
780
+
781
+ const transaction = {
782
+ type: 'Authorization',
783
+ amount: {
784
+ currencyCode: 'EUR',
785
+ centAmount: 10000,
786
+ },
787
+ state: 'Success',
788
+ interactionId: randomUUID(),
789
+ } as Transaction;
790
+
791
+ //When
792
+ const result = ctPaymentService.consolidateTransactionChanges(
793
+ payment,
794
+ transaction,
795
+ );
796
+
797
+ //Then
798
+ expect(result).toHaveLength(1);
799
+ expect(result[0]).toMatchObject({
800
+ action: 'addTransaction',
801
+ transaction,
802
+ });
803
+ });
804
+
805
+ test('should return a change state and change interaction id actions when an initial transaction to reuse already exists', () => {
806
+ //Given
807
+ const txId = randomUUID();
808
+ const interactionId = randomUUID();
809
+ const payment = {
810
+ id: randomUUID(),
811
+ version: 1,
812
+ amountPlanned: {
813
+ currencyCode: 'EUR',
814
+ centAmount: 10000,
815
+ },
816
+ transactions: [
817
+ {
818
+ id: txId,
819
+ type: 'Authorization',
820
+ amount: {
821
+ currencyCode: 'EUR',
822
+ centAmount: 10000,
823
+ },
824
+ state: 'Initial',
825
+ },
826
+ ],
827
+ } as Payment;
828
+
829
+ const transaction = {
830
+ type: 'Authorization',
831
+ amount: {
832
+ currencyCode: 'EUR',
833
+ centAmount: 10000,
834
+ },
835
+ state: 'Success',
836
+ interactionId: interactionId,
837
+ } as Transaction;
838
+
839
+ //When
840
+ const result = ctPaymentService.consolidateTransactionChanges(
841
+ payment,
842
+ transaction,
843
+ );
844
+
845
+ //Then
846
+ expect(result).toHaveLength(2);
847
+ expect(result).toMatchObject([
848
+ {
849
+ action: 'changeTransactionState',
850
+ state: 'Success',
851
+ transactionId: txId,
852
+ },
853
+ {
854
+ action: 'changeTransactionInteractionId',
855
+ interactionId,
856
+ transactionId: txId,
857
+ },
858
+ ]);
859
+ });
860
+
861
+ test('should return a change state when the transaction was pending and now it is success', () => {
862
+ //Given
863
+ const txId = randomUUID();
864
+ const interactionId = randomUUID();
865
+ const payment = {
866
+ id: randomUUID(),
867
+ version: 1,
868
+ amountPlanned: {
869
+ currencyCode: 'EUR',
870
+ centAmount: 10000,
871
+ },
872
+ transactions: [
873
+ {
874
+ id: txId,
875
+ type: 'Authorization',
876
+ amount: {
877
+ currencyCode: 'EUR',
878
+ centAmount: 10000,
879
+ },
880
+ state: 'Pending',
881
+ interactionId,
882
+ },
883
+ ],
884
+ } as Payment;
885
+
886
+ const transaction = {
887
+ type: 'Authorization',
888
+ amount: {
889
+ currencyCode: 'EUR',
890
+ centAmount: 10000,
891
+ },
892
+ state: 'Success',
893
+ interactionId: interactionId,
894
+ } as Transaction;
895
+
896
+ //When
897
+ const result = ctPaymentService.consolidateTransactionChanges(
898
+ payment,
899
+ transaction,
900
+ );
901
+
902
+ //Then
903
+ expect(result).toHaveLength(1);
904
+ expect(result).toMatchObject([
905
+ {
906
+ action: 'changeTransactionState',
907
+ state: 'Success',
908
+ transactionId: txId,
909
+ },
910
+ ]);
911
+ });
912
+
913
+ test('should return a change state when the transaction was failure and now it is success', () => {
914
+ //Given
915
+ const txId = randomUUID();
916
+ const interactionId = randomUUID();
917
+ const payment = {
918
+ id: randomUUID(),
919
+ version: 1,
920
+ amountPlanned: {
921
+ currencyCode: 'EUR',
922
+ centAmount: 10000,
923
+ },
924
+ transactions: [
925
+ {
926
+ id: txId,
927
+ type: 'Authorization',
928
+ amount: {
929
+ currencyCode: 'EUR',
930
+ centAmount: 10000,
931
+ },
932
+ state: 'Failure',
933
+ interactionId,
934
+ },
935
+ ],
936
+ } as Payment;
937
+
938
+ const transaction = {
939
+ type: 'Authorization',
940
+ amount: {
941
+ currencyCode: 'EUR',
942
+ centAmount: 10000,
943
+ },
944
+ state: 'Success',
945
+ interactionId: interactionId,
946
+ } as Transaction;
947
+
948
+ //When
949
+ const result = ctPaymentService.consolidateTransactionChanges(
950
+ payment,
951
+ transaction,
952
+ );
953
+
954
+ //Then
955
+ expect(result).toHaveLength(1);
956
+ expect(result).toMatchObject([
957
+ {
958
+ action: 'changeTransactionState',
959
+ state: 'Success',
960
+ transactionId: txId,
961
+ },
962
+ ]);
963
+ });
964
+
965
+ test('should return a change state when the transaction was success and now it is failure', () => {
966
+ //Given
967
+ const txId = randomUUID();
968
+ const interactionId = randomUUID();
969
+ const payment = {
970
+ id: randomUUID(),
971
+ version: 1,
972
+ amountPlanned: {
973
+ currencyCode: 'EUR',
974
+ centAmount: 10000,
975
+ },
976
+ transactions: [
977
+ {
978
+ id: txId,
979
+ type: 'Authorization',
980
+ amount: {
981
+ currencyCode: 'EUR',
982
+ centAmount: 10000,
983
+ },
984
+ state: 'Success',
985
+ interactionId,
986
+ },
987
+ ],
988
+ } as Payment;
989
+
990
+ const transaction = {
991
+ type: 'Authorization',
992
+ amount: {
993
+ currencyCode: 'EUR',
994
+ centAmount: 10000,
995
+ },
996
+ state: 'Failure',
997
+ interactionId: interactionId,
998
+ } as Transaction;
999
+
1000
+ //When
1001
+ const result = ctPaymentService.consolidateTransactionChanges(
1002
+ payment,
1003
+ transaction,
1004
+ );
1005
+
1006
+ //Then
1007
+ expect(result).toHaveLength(1);
1008
+ expect(result).toMatchObject([
1009
+ {
1010
+ action: 'changeTransactionState',
1011
+ state: 'Failure',
1012
+ transactionId: txId,
1013
+ },
1014
+ ]);
1015
+ });
1016
+
1017
+ test('should return a change state when the transaction was pending and now it is failure', () => {
1018
+ //Given
1019
+ const txId = randomUUID();
1020
+ const interactionId = randomUUID();
1021
+ const payment = {
1022
+ id: randomUUID(),
1023
+ version: 1,
1024
+ amountPlanned: {
1025
+ currencyCode: 'EUR',
1026
+ centAmount: 10000,
1027
+ },
1028
+ transactions: [
1029
+ {
1030
+ id: txId,
1031
+ type: 'Authorization',
1032
+ amount: {
1033
+ currencyCode: 'EUR',
1034
+ centAmount: 10000,
1035
+ },
1036
+ state: 'Pending',
1037
+ interactionId,
1038
+ },
1039
+ ],
1040
+ } as Payment;
1041
+
1042
+ const transaction = {
1043
+ type: 'Authorization',
1044
+ amount: {
1045
+ currencyCode: 'EUR',
1046
+ centAmount: 10000,
1047
+ },
1048
+ state: 'Failure',
1049
+ interactionId: interactionId,
1050
+ } as Transaction;
1051
+
1052
+ //When
1053
+ const result = ctPaymentService.consolidateTransactionChanges(
1054
+ payment,
1055
+ transaction,
1056
+ );
1057
+
1058
+ //Then
1059
+ expect(result).toHaveLength(1);
1060
+ expect(result).toMatchObject([
1061
+ {
1062
+ action: 'changeTransactionState',
1063
+ state: 'Failure',
1064
+ transactionId: txId,
1065
+ },
1066
+ ]);
1067
+ });
1068
+
1069
+ test('should return an error when more than one transaction matches the criteria', () => {
1070
+ //Given
1071
+ const txId1 = randomUUID();
1072
+ const txId2 = randomUUID();
1073
+ const interactionId = randomUUID();
1074
+ const payment = {
1075
+ id: randomUUID(),
1076
+ version: 1,
1077
+ amountPlanned: {
1078
+ currencyCode: 'EUR',
1079
+ centAmount: 10000,
1080
+ },
1081
+ transactions: [
1082
+ {
1083
+ id: txId1,
1084
+ type: 'Authorization',
1085
+ amount: {
1086
+ currencyCode: 'EUR',
1087
+ centAmount: 10000,
1088
+ },
1089
+ state: 'Pending',
1090
+ interactionId,
1091
+ },
1092
+ {
1093
+ id: txId2,
1094
+ type: 'Authorization',
1095
+ amount: {
1096
+ currencyCode: 'EUR',
1097
+ centAmount: 10000,
1098
+ },
1099
+ state: 'Pending',
1100
+ interactionId,
1101
+ },
1102
+ ],
1103
+ } as Payment;
1104
+
1105
+ const transaction = {
1106
+ type: 'Authorization',
1107
+ amount: {
1108
+ currencyCode: 'EUR',
1109
+ centAmount: 10000,
1110
+ },
1111
+ state: 'Success',
1112
+ interactionId,
1113
+ } as Transaction;
1114
+
1115
+ //When
1116
+ const result = () =>
1117
+ ctPaymentService.consolidateTransactionChanges(payment, transaction);
1118
+
1119
+ //Then
1120
+ expect(result).toThrow(/Multiple matching transactions found/);
1121
+ });
1122
+ });
1123
+ });