@mojaloop/sdk-scheme-adapter 24.11.0-csi-1680.0 → 24.11.0-snapshot.1

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 (100) hide show
  1. package/.grype.yaml +1 -0
  2. package/.ncurc.yaml +3 -1
  3. package/.yarn/cache/{@babel-core-npm-7.28.3-fb967e901c-0faded84ed.zip → @babel-core-npm-7.28.4-cb5fd966cc-0593295241.zip} +0 -0
  4. package/.yarn/cache/{@babel-helpers-npm-7.28.3-8e4849da45-6d39031bf0.zip → @babel-helpers-npm-7.28.4-d9f7567704-5a70a82e19.zip} +0 -0
  5. package/.yarn/cache/@babel-parser-npm-7.28.4-e1b2cbaf6c-f54c46213e.zip +0 -0
  6. package/.yarn/cache/@babel-traverse-npm-7.28.4-ffade2903a-c3099364b7.zip +0 -0
  7. package/.yarn/cache/@babel-types-npm-7.28.4-7f16191818-db50bf257a.zip +0 -0
  8. package/.yarn/cache/@grpc-proto-loader-npm-0.8.0-b53ddeb647-216813bdca.zip +0 -0
  9. package/.yarn/cache/{@hapi-hapi-npm-21.4.2-b5f92c52c3-efe9025469.zip → @hapi-hapi-npm-21.4.3-3dc5e89aa0-f23bda02b4.zip} +0 -0
  10. package/.yarn/cache/@hapi-shot-npm-6.0.2-c6ccc15f52-8715f5759c.zip +0 -0
  11. package/.yarn/cache/{@hapi-subtext-npm-8.1.0-ea59196b68-3f7bf0c689.zip → @hapi-subtext-npm-8.1.1-8aefc21cfb-9ce8251d5e.zip} +0 -0
  12. package/.yarn/cache/@jest-transform-npm-30.1.2-14cfbd16d5-aec6c6a46f.zip +0 -0
  13. package/.yarn/cache/@jridgewell-remapping-npm-2.3.5-df8dacc063-c2bb01856e.zip +0 -0
  14. package/.yarn/cache/{@mojaloop-central-services-error-handling-npm-13.1.0-07ff108f3f-64e80a9ba2.zip → @mojaloop-central-services-error-handling-npm-13.1.1-990790968e-a5d8a46f3c.zip} +0 -0
  15. package/.yarn/cache/@mojaloop-central-services-error-handling-npm-13.1.2-680b8f106e-1357eefd24.zip +0 -0
  16. package/.yarn/cache/@mojaloop-central-services-logger-npm-11.9.3-7669d33c28-58cca19ace.zip +0 -0
  17. package/.yarn/cache/@mojaloop-central-services-metrics-npm-12.7.1-a82eece473-0aa6374c0e.zip +0 -0
  18. package/.yarn/cache/{@mojaloop-central-services-shared-npm-18.30.6-d528dafb13-a2f47b26cb.zip → @mojaloop-central-services-shared-npm-18.32.1-82382b67a6-6f8a3ab043.zip} +0 -0
  19. package/.yarn/cache/{@mojaloop-central-services-shared-npm-18.30.7-291fa1aea3-aee239baa2.zip → @mojaloop-central-services-shared-npm-18.33.0-eee3a0674c-98d4f0e2ce.zip} +0 -0
  20. package/.yarn/cache/{@mojaloop-event-sdk-npm-14.6.1-a36281071d-5652aa9087.zip → @mojaloop-event-sdk-npm-14.7.0-dfe9fa1933-59adbf133d.zip} +0 -0
  21. package/.yarn/cache/@mojaloop-ml-number-npm-11.3.0-9858cadff5-8435794709.zip +0 -0
  22. package/.yarn/cache/{@mojaloop-ml-schema-transformer-lib-npm-2.7.7-ad2e66700a-0325beb4f9.zip → @mojaloop-ml-schema-transformer-lib-npm-2.7.8-270774b6ee-06bb19a304.zip} +0 -0
  23. package/.yarn/cache/{@mojaloop-sdk-standard-components-npm-19.16.4-59956a0e05-3feb521c69.zip → @mojaloop-sdk-standard-components-npm-19.17.0-0519957b97-95fef19fc4.zip} +0 -0
  24. package/.yarn/cache/{@redis-bloom-npm-5.8.1-139b45b8e4-f691b1dce2.zip → @redis-bloom-npm-5.8.2-1972c61f30-99ec4f127b.zip} +0 -0
  25. package/.yarn/cache/{@redis-client-npm-5.8.1-66d46a9ca1-329d76de06.zip → @redis-client-npm-5.8.2-68c2d31768-653ba2d0ef.zip} +0 -0
  26. package/.yarn/cache/{@redis-json-npm-5.8.1-1374d9e2de-9eabbf9a2c.zip → @redis-json-npm-5.8.2-99129c657f-2877cd93b7.zip} +0 -0
  27. package/.yarn/cache/{@redis-search-npm-5.8.1-ebc7760a31-a5e12dd2c7.zip → @redis-search-npm-5.8.2-c7014522ef-6cc0499a11.zip} +0 -0
  28. package/.yarn/cache/{@redis-time-series-npm-5.8.1-1f5e30ede4-c9440ce935.zip → @redis-time-series-npm-5.8.2-94d4c69124-a775817380.zip} +0 -0
  29. package/.yarn/cache/@rollup-rollup-linux-x64-musl-npm-4.50.2-1d19f7f518-10.zip +0 -0
  30. package/.yarn/cache/@types-node-npm-24.5.1-e2de7d4e53-1dd21dffe0.zip +0 -0
  31. package/.yarn/cache/@types-retry-npm-0.12.5-f1986a76a6-3fb6bf9183.zip +0 -0
  32. package/.yarn/cache/{@typescript-eslint-eslint-plugin-npm-8.39.1-8ad46b0385-446050aa43.zip → @typescript-eslint-eslint-plugin-npm-8.44.0-3a3d745bcf-38d0491d96.zip} +0 -0
  33. package/.yarn/cache/{@typescript-eslint-parser-npm-8.39.1-e931b25728-ff45ce7635.zip → @typescript-eslint-parser-npm-8.44.0-9be86aa2f8-8c7ddabf46.zip} +0 -0
  34. package/.yarn/cache/{@typescript-eslint-project-service-npm-8.39.1-f6db73ca22-1970633d1a.zip → @typescript-eslint-project-service-npm-8.44.0-3208cc873e-400b4981e6.zip} +0 -0
  35. package/.yarn/cache/{@typescript-eslint-scope-manager-npm-8.39.1-bf78e0253c-8874f74790.zip → @typescript-eslint-scope-manager-npm-8.44.0-02a051c9d1-5dae4a8386.zip} +0 -0
  36. package/.yarn/cache/{@typescript-eslint-tsconfig-utils-npm-8.39.1-e46dac00aa-38c1e19825.zip → @typescript-eslint-tsconfig-utils-npm-8.44.0-de2d92d917-c8535d481d.zip} +0 -0
  37. package/.yarn/cache/{@typescript-eslint-type-utils-npm-8.39.1-41cbec8085-1195d65970.zip → @typescript-eslint-type-utils-npm-8.44.0-4b4c61deae-513c6d3719.zip} +0 -0
  38. package/.yarn/cache/@typescript-eslint-types-npm-8.44.0-89c4325651-9e28c95feb.zip +0 -0
  39. package/.yarn/cache/{@typescript-eslint-typescript-estree-npm-8.39.1-eb0cf5436f-07ed9d7ab4.zip → @typescript-eslint-typescript-estree-npm-8.44.0-122f9245db-e2e579b15c.zip} +0 -0
  40. package/.yarn/cache/{@typescript-eslint-utils-npm-8.39.1-a6c63e4cf7-39bb105f26.zip → @typescript-eslint-utils-npm-8.44.0-fc612e2915-436e21e3d0.zip} +0 -0
  41. package/.yarn/cache/{@typescript-eslint-visitor-keys-npm-8.39.1-d0b0654c5b-6d4e4d0b19.zip → @typescript-eslint-visitor-keys-npm-8.44.0-131d4d0e8f-09b008b14f.zip} +0 -0
  42. package/.yarn/cache/{babel-jest-npm-30.0.5-8bced40b9f-39a36b8648.zip → babel-jest-npm-30.1.2-2d68b3440b-9697119fe4.zip} +0 -0
  43. package/.yarn/cache/bignumber.js-npm-9.3.1-d784181dd0-1be0372bf0.zip +0 -0
  44. package/.yarn/cache/dotenv-npm-17.2.2-f2cdf74d0a-258210c403.zip +0 -0
  45. package/.yarn/cache/iconv-lite-npm-0.7.0-89105876e3-5bfc897fed.zip +0 -0
  46. package/.yarn/cache/{jest-haste-map-npm-30.0.5-ff2b66456e-3539359589.zip → jest-haste-map-npm-30.1.0-8189548adb-bd39053fe1.zip} +0 -0
  47. package/.yarn/cache/{jest-worker-npm-30.0.5-dcae728924-04d9a58ddb.zip → jest-worker-npm-30.1.0-b4a01545e6-cc09d2ce86.zip} +0 -0
  48. package/.yarn/cache/joi-npm-18.0.1-f286682573-d8e391c0e9.zip +0 -0
  49. package/.yarn/cache/{openapi-backend-npm-5.14.0-231377503b-1bd3e6cb71.zip → openapi-backend-npm-5.15.0-b7b193973a-f7bdc40ac6.zip} +0 -0
  50. package/.yarn/cache/{protobufjs-npm-7.5.3-a54566937a-3e412d2e2f.zip → protobufjs-npm-7.5.4-4d6f681551-88d677bb6f.zip} +0 -0
  51. package/.yarn/cache/{raw-body-npm-3.0.0-cd8403b401-2443429bbb.zip → raw-body-npm-3.0.1-cbb0b09e07-3cc63e1541.zip} +0 -0
  52. package/.yarn/cache/{redis-npm-5.8.1-201a0a72a3-26d97c6ddf.zip → redis-npm-5.8.2-9c493c0c47-a7635cedf2.zip} +0 -0
  53. package/.yarn/cache/{ts-jest-npm-29.4.1-ab76d85d32-6aed48232c.zip → ts-jest-npm-29.4.2-1fc50073bc-09494224db.zip} +0 -0
  54. package/.yarn/cache/{undici-types-npm-7.10.0-cd8324b9eb-1f3fe77793.zip → undici-types-npm-7.12.0-af0c725921-4a0f927c98.zip} +0 -0
  55. package/.yarn/install-state.gz +0 -0
  56. package/.yarn/releases/{yarn-4.9.2.cjs → yarn-4.9.4.cjs} +358 -358
  57. package/.yarnrc.yml +1 -1
  58. package/CHANGELOG.md +37 -0
  59. package/modules/api-svc/package.json +15 -13
  60. package/modules/api-svc/src/InboundServer/handlers.js +26 -8
  61. package/modules/api-svc/src/config.js +17 -1
  62. package/modules/api-svc/src/lib/model/InboundTransfersModel.js +146 -6
  63. package/modules/api-svc/src/lib/model/OutboundTransfersModel.js +1 -1
  64. package/modules/api-svc/test/__mocks__/redis.js +5 -2
  65. package/modules/api-svc/test/unit/inboundApi/handlers-iso20022.test.js +3 -1
  66. package/modules/api-svc/test/unit/inboundApi/handlers.test.js +4 -2
  67. package/modules/api-svc/test/unit/lib/model/InboundTransfersModel.test.js +481 -117
  68. package/modules/outbound-command-event-handler/package.json +8 -8
  69. package/modules/outbound-domain-event-handler/package.json +7 -7
  70. package/modules/private-shared-lib/package.json +7 -7
  71. package/package.json +6 -6
  72. package/{sbom-v24.10.8.csv → sbom-v24.10.11.csv} +224 -224
  73. package/.yarn/cache/@grpc-proto-loader-npm-0.7.15-889e15aec1-2e2b33ace8.zip +0 -0
  74. package/.yarn/cache/@hapi-hapi-npm-21.4.0-2644a983d1-d49ae44142.zip +0 -0
  75. package/.yarn/cache/@hapi-shot-npm-6.0.1-2553675f4f-6eb387f9c6.zip +0 -0
  76. package/.yarn/cache/@hapi-topo-npm-5.1.0-5e0b776809-084bfa6470.zip +0 -0
  77. package/.yarn/cache/@jest-transform-npm-30.0.5-90874ed0b8-2b3e0bc39a.zip +0 -0
  78. package/.yarn/cache/@mojaloop-central-services-metrics-npm-12.6.0-6353d00803-e55c70c0b1.zip +0 -0
  79. package/.yarn/cache/@rollup-rollup-linux-x64-musl-npm-4.45.1-255fc04506-10.zip +0 -0
  80. package/.yarn/cache/@sideway-address-npm-4.1.5-a3852745c8-c4c73ac033.zip +0 -0
  81. package/.yarn/cache/@sideway-formula-npm-3.0.1-ee371b2ddf-8d3ee7f80d.zip +0 -0
  82. package/.yarn/cache/@sideway-pinpoint-npm-2.0.0-66d94e687e-1ed2180012.zip +0 -0
  83. package/.yarn/cache/@types-node-npm-24.2.1-00ab09acd1-cdfa7b30b2.zip +0 -0
  84. package/.yarn/cache/@typescript-eslint-eslint-plugin-npm-8.32.1-4a9716e105-442205dd4e.zip +0 -0
  85. package/.yarn/cache/@typescript-eslint-parser-npm-8.32.1-4842816d93-3c2ab90fec.zip +0 -0
  86. package/.yarn/cache/@typescript-eslint-scope-manager-npm-8.32.1-7708347a5f-f81f71bd88.zip +0 -0
  87. package/.yarn/cache/@typescript-eslint-type-utils-npm-8.32.1-e98f19d598-e50a6f2a16.zip +0 -0
  88. package/.yarn/cache/@typescript-eslint-types-npm-8.32.1-ded19751b6-3a310e4baf.zip +0 -0
  89. package/.yarn/cache/@typescript-eslint-types-npm-8.39.1-8cea531133-8013f4f48a.zip +0 -0
  90. package/.yarn/cache/@typescript-eslint-typescript-estree-npm-8.32.1-5eacb17d12-8b956ce05b.zip +0 -0
  91. package/.yarn/cache/@typescript-eslint-utils-npm-8.32.1-8a5bff5552-9383cea185.zip +0 -0
  92. package/.yarn/cache/@typescript-eslint-visitor-keys-npm-8.32.1-780bd4dae9-a1cbfbdac8.zip +0 -0
  93. package/.yarn/cache/dotenv-npm-17.2.0-4ee4b4bbd1-73d57d7ed8.zip +0 -0
  94. package/.yarn/cache/dotenv-npm-17.2.1-33fbb0afbc-8fde672d1c.zip +0 -0
  95. package/.yarn/cache/joi-npm-17.13.3-866dad5bc8-4c150db0c8.zip +0 -0
  96. package/.yarn/cache/joi-npm-18.0.0-1ebac7eadf-568004d69f.zip +0 -0
  97. package/.yarn/cache/openapi-backend-npm-5.13.0-03ae1ecf2c-a8b1d0d167.zip +0 -0
  98. package/.yarn/cache/yaml-npm-2.8.0-01747dd315-7d4bd9c10d.zip +0 -0
  99. package/audit/k6/package.json +0 -17
  100. package/docker/k666/package.json +0 -17
@@ -295,44 +295,46 @@ describe('inboundModel', () => {
295
295
  MojaloopRequests.__putTransfersError.mockClear();
296
296
  BackendRequests.__postTransfers = jest.fn().mockReturnValue(Promise.resolve({}));
297
297
  MojaloopRequests.__putTransfers = jest.fn().mockReturnValue(Promise.resolve({
298
- originalRequest: {
299
- headers: {},
300
- body: {},
301
- }
298
+ originalRequest: {
299
+ headers: {},
300
+ body: {},
301
+ }
302
302
  }));
303
303
 
304
304
  cache = new Cache({
305
- cacheUrl: 'redis://dummy:1234',
306
- logger,
307
- unsubscribeTimeoutMs: 5000
305
+ cacheUrl: 'redis://dummy:1234',
306
+ logger,
307
+ unsubscribeTimeoutMs: 5000
308
308
  });
309
309
  await cache.connect();
310
310
  });
311
311
 
312
312
  afterEach(async () => {
313
313
  await cache.disconnect();
314
+ jest.clearAllTimers();
315
+ jest.useRealTimers();
314
316
  });
315
317
 
316
318
  test('fail on quote `expiration` deadline.', async () => {
317
319
  const TRANSFER_ID = 'fake-transfer-id';
318
320
  const model = new Model({
319
- ...config,
320
- cache,
321
- logger,
322
- rejectTransfersOnExpiredQuotes: true,
321
+ ...config,
322
+ cache,
323
+ logger,
324
+ rejectTransfersOnExpiredQuotes: true,
323
325
  });
324
326
  cache.set(`transferModel_in_${TRANSFER_ID}`, {
325
- transferId: TRANSFER_ID,
326
- quote: {
327
- mojaloopResponse: {
328
- expiration: new Date(new Date().getTime() - 1000).toISOString(),
329
- }
327
+ transferId: TRANSFER_ID,
328
+ quote: {
329
+ mojaloopResponse: {
330
+ expiration: new Date(new Date().getTime() - 1000).toISOString(),
330
331
  }
332
+ }
331
333
  });
332
334
  const args = {
333
- body: {
334
- transferId: TRANSFER_ID,
335
- }
335
+ body: {
336
+ transferId: TRANSFER_ID,
337
+ }
336
338
  };
337
339
 
338
340
  await model.prepareTransfer(args, mockArgs.fspId);
@@ -351,9 +353,9 @@ describe('inboundModel', () => {
351
353
  BackendRequests.__getTransfers = jest.fn().mockReturnValue(Promise.resolve(backendResponse));
352
354
 
353
355
  const model = new Model({
354
- ...config,
355
- cache,
356
- logger,
356
+ ...config,
357
+ cache,
358
+ logger,
357
359
  });
358
360
 
359
361
  await model.getTransfer(TRANSFER_ID, mockArgs.fspId);
@@ -373,9 +375,9 @@ describe('inboundModel', () => {
373
375
  BackendRequests.__getTransfers = jest.fn().mockReturnValue(Promise.resolve(backendResponse));
374
376
 
375
377
  const model = new Model({
376
- ...config,
377
- cache,
378
- logger,
378
+ ...config,
379
+ cache,
380
+ logger,
379
381
  });
380
382
 
381
383
  await model.getTransfer(TRANSFER_ID, mockArgs.fspId);
@@ -390,18 +392,18 @@ describe('inboundModel', () => {
390
392
  const TRANSFER_ID = 'fake-transfer-id';
391
393
 
392
394
  BackendRequests.__getTransfers = jest.fn().mockReturnValue(
393
- Promise.reject(new HTTPResponseError({
394
- res: {
395
- data: {
396
- statusCode: '3208'
397
- },
398
- }
399
- })));
395
+ Promise.reject(new HTTPResponseError({
396
+ res: {
397
+ data: {
398
+ statusCode: '3208'
399
+ },
400
+ }
401
+ })));
400
402
 
401
403
  const model = new Model({
402
- ...config,
403
- cache,
404
- logger,
404
+ ...config,
405
+ cache,
406
+ logger,
405
407
  });
406
408
 
407
409
  await model.getTransfer(TRANSFER_ID, mockArgs.fspId);
@@ -415,22 +417,22 @@ describe('inboundModel', () => {
415
417
  test('fail on transfer without quote.', async () => {
416
418
  const TRANSFER_ID = 'without_quote-transfer-id';
417
419
  const args = {
418
- body: {
419
- transferId: TRANSFER_ID,
420
- amount: {
421
- currency: 'USD',
422
- amount: 20.13
423
- },
424
- ilpPacket: 'mockBase64encodedIlpPacket',
425
- condition: 'mockGeneratedCondition'
426
- }
420
+ body: {
421
+ transferId: TRANSFER_ID,
422
+ amount: {
423
+ currency: 'USD',
424
+ amount: 20.13
425
+ },
426
+ ilpPacket: 'mockBase64encodedIlpPacket',
427
+ condition: 'mockGeneratedCondition'
428
+ }
427
429
  };
428
430
 
429
431
  const model = new Model({
430
- ...config,
431
- cache,
432
- logger,
433
- allowTransferWithoutQuote: false,
432
+ ...config,
433
+ cache,
434
+ logger,
435
+ allowTransferWithoutQuote: false,
434
436
  });
435
437
 
436
438
  await model.prepareTransfer(args, mockArgs.fspId);
@@ -448,38 +450,38 @@ describe('inboundModel', () => {
448
450
 
449
451
  // mock response from dfsp acting as payee
450
452
  BackendRequests.__postTransfers = jest.fn().mockReturnValueOnce(Promise.resolve({
451
- homeTransactionId: HOME_TRANSACTION_ID,
452
- transferId: TRANSFER_ID
453
+ homeTransactionId: HOME_TRANSACTION_ID,
454
+ transferId: TRANSFER_ID
453
455
  }));
454
456
 
455
457
  const args = {
456
- body: {
457
- transferId: TRANSFER_ID,
458
- amount: {
459
- currency: 'USD',
460
- amount: 20.13
461
- },
462
- ilpPacket: 'mockBase64encodedIlpPacket',
463
- condition: 'mockGeneratedCondition'
464
- }
458
+ body: {
459
+ transferId: TRANSFER_ID,
460
+ amount: {
461
+ currency: 'USD',
462
+ amount: 20.13
463
+ },
464
+ ilpPacket: 'mockBase64encodedIlpPacket',
465
+ condition: 'mockGeneratedCondition'
466
+ }
465
467
  };
466
468
 
467
469
  const model = new Model({
468
- ...config,
469
- cache,
470
- logger,
471
- checkIlp: false,
472
- rejectTransfersOnExpiredQuotes: false
470
+ ...config,
471
+ cache,
472
+ logger,
473
+ checkIlp: false,
474
+ rejectTransfersOnExpiredQuotes: false
473
475
  });
474
476
 
475
477
  cache.set(`transferModel_in_${TRANSFER_ID}`, {
476
- transferId: TRANSFER_ID,
477
- quote: {
478
- fulfilment: 'mockFulfilment',
479
- mojaloopResponse: {
480
- condition: 'mockCondition',
481
- }
478
+ transferId: TRANSFER_ID,
479
+ quote: {
480
+ fulfilment: 'mockFulfilment',
481
+ mojaloopResponse: {
482
+ condition: 'mockCondition',
482
483
  }
484
+ }
483
485
  });
484
486
 
485
487
  await model.prepareTransfer(args, mockArgs.fspId);
@@ -488,36 +490,36 @@ describe('inboundModel', () => {
488
490
  expect(BackendRequests.__postTransfers).toHaveBeenCalledTimes(1);
489
491
  expect(MojaloopRequests.__putTransfers).toHaveBeenCalledTimes(1);
490
492
  expect((await cache.get(`transferModel_in_${TRANSFER_ID}`)).homeTransactionId)
491
- .toEqual(HOME_TRANSACTION_ID);
493
+ .toEqual(HOME_TRANSACTION_ID);
492
494
  });
493
495
 
494
496
  test('pass on transfer without quote.', async () => {
495
497
  const TRANSFER_ID = 'without_quote-transfer-id';
496
498
  cache.set(`transferModel_in_${TRANSFER_ID}`, {
497
- fulfilment: '',
498
- mojaloopResponse: {
499
- response: ''
500
- },
501
- quote: null
499
+ fulfilment: '',
500
+ mojaloopResponse: {
501
+ response: ''
502
+ },
503
+ quote: null
502
504
  });
503
505
 
504
506
  const args = {
505
- body: {
506
- transferId: TRANSFER_ID,
507
- amount: {
508
- currency: 'USD',
509
- amount: 20.13
510
- },
511
- ilpPacket: 'mockBase64encodedIlpPacket',
512
- condition: 'mockGeneratedCondition'
513
- }
507
+ body: {
508
+ transferId: TRANSFER_ID,
509
+ amount: {
510
+ currency: 'USD',
511
+ amount: 20.13
512
+ },
513
+ ilpPacket: 'mockBase64encodedIlpPacket',
514
+ condition: 'mockGeneratedCondition'
515
+ }
514
516
  };
515
517
 
516
518
  const model = new Model({
517
- ...config,
518
- cache,
519
- logger,
520
- allowTransferWithoutQuote: true,
519
+ ...config,
520
+ cache,
521
+ logger,
522
+ allowTransferWithoutQuote: true,
521
523
  });
522
524
 
523
525
  await model.prepareTransfer(args, mockArgs.fspId);
@@ -533,36 +535,36 @@ describe('inboundModel', () => {
533
535
  shared.mojaloopPrepareToInternalTransfer = jest.fn().mockReturnValueOnce({});
534
536
 
535
537
  cache.set(`transferModel_in_${transactionId}`, {
536
- fulfilment: '',
538
+ fulfilment: '',
539
+ mojaloopResponse: {
540
+ response: ''
541
+ },
542
+ quote: {
543
+ fulfilment: 'mockFulfilment',
537
544
  mojaloopResponse: {
538
- response: ''
539
- },
540
- quote: {
541
- fulfilment: 'mockFulfilment',
542
- mojaloopResponse: {
543
- condition: 'mockCondition',
544
- }
545
+ condition: 'mockCondition',
545
546
  }
547
+ }
546
548
  });
547
549
 
548
550
  const args = {
549
- body: {
550
- transferId: TRANSFER_ID,
551
- amount: {
552
- currency: 'USD',
553
- amount: 20.13
554
- },
555
- ilpPacket: 'mockIlpPacket',
556
- condition: 'mockGeneratedCondition'
557
- }
551
+ body: {
552
+ transferId: TRANSFER_ID,
553
+ amount: {
554
+ currency: 'USD',
555
+ amount: 20.13
556
+ },
557
+ ilpPacket: 'mockIlpPacket',
558
+ condition: 'mockGeneratedCondition'
559
+ }
558
560
  };
559
561
 
560
562
  const model = new Model({
561
- ...config,
562
- cache,
563
- logger,
564
- allowDifferentTransferTransactionId: true,
565
- checkIlp: false,
563
+ ...config,
564
+ cache,
565
+ logger,
566
+ allowDifferentTransferTransactionId: true,
567
+ checkIlp: false,
566
568
  });
567
569
 
568
570
  await model.prepareTransfer(args, mockArgs.fspId);
@@ -571,6 +573,173 @@ describe('inboundModel', () => {
571
573
  expect(BackendRequests.__postTransfers).toHaveBeenCalledTimes(1);
572
574
  expect(MojaloopRequests.__putTransfers).toHaveBeenCalledTimes(1);
573
575
  });
576
+
577
+ // --- Additional tests for patch notification retry logic ---
578
+
579
+ test('should getTransfer if patch notification is not called within grace time', async () => {
580
+ jest.useFakeTimers();
581
+ const TRANSFER_ID = 'patch-notify-transfer-id';
582
+ const PATCH_GRACE_MS = 100;
583
+ const backendResponse = JSON.parse(JSON.stringify(getTransfersBackendResponse));
584
+ backendResponse.to.fspId = config.dfspId;
585
+ BackendRequests.__getTransfers = jest.fn().mockResolvedValue(backendResponse);
586
+ // Mock shared.mojaloopPrepareToInternalTransfer to avoid undefined property errors
587
+ shared.mojaloopPrepareToInternalTransfer = jest.fn().mockReturnValue({
588
+ transferId: TRANSFER_ID,
589
+ amount: { currency: 'USD', amount: 20.13 },
590
+ ilpPacket: 'mockBase64encodedIlpPacket',
591
+ condition: 'mockCondition'
592
+ });
593
+
594
+ // Mock cache.set for patch notification logic to avoid real Redis calls and allow timer logic to proceed
595
+ jest.spyOn(cache, 'set').mockImplementation(async (key, value, ttl) => {
596
+ // For patchNotificationSent_ keys, simulate not notified yet
597
+ if (key.startsWith('patchNotificationSent_')) {
598
+ return true;
599
+ }
600
+ // For other keys, fallback to original set
601
+ return Cache.prototype.set.call(cache, key, value, ttl);
602
+ });
603
+ // Also mock cache.get for patch notification logic to always return false (not notified)
604
+ jest.spyOn(cache, 'get').mockImplementation(async (key) => {
605
+ if (key.startsWith('patchNotificationSent_')) {
606
+ return false;
607
+ }
608
+ return Cache.prototype.get.call(cache, key);
609
+ });
610
+ // Mock subscribeToOneMessageWithTimer to immediately resolve with a message to simulate successful notification
611
+ jest.spyOn(cache, 'subscribeToOneMessageWithTimer').mockImplementation(async () => {
612
+ // Simulate a successful message received
613
+ return { data: { transferState: 'COMMITTED' } };
614
+ });
615
+
616
+ // Mock backendRequests.putTransfersNotification to return 200
617
+ jest.spyOn(BackendRequests, '__putTransfersNotification').mockResolvedValue({ status: 200 });
618
+
619
+ // Prepare model with patch notification grace time enabled
620
+ const model = new Model({
621
+ ...config,
622
+ cache,
623
+ logger,
624
+ patchNotificationGraceTimeMs: PATCH_GRACE_MS,
625
+ });
626
+
627
+ // Mock _load to avoid actual cache access
628
+ jest.spyOn(model, '_load').mockImplementation(async (transferId) => {
629
+ return {
630
+ transferId,
631
+ quote: {
632
+ fulfilment: 'mockFulfilment',
633
+ mojaloopResponse: {
634
+ condition: 'mockCondition',
635
+ expiration: new Date(Date.now() + 10000).toISOString(),
636
+ }
637
+ }
638
+ };
639
+ });
640
+
641
+ // Mock _save to avoid actual cache writes
642
+ jest.spyOn(model, '_save').mockImplementation(async () => {});
643
+
644
+ // Set up cache with quote for transfer
645
+ cache.set(`transferModel_in_${TRANSFER_ID}`, {
646
+ transferId: TRANSFER_ID,
647
+ quote: {
648
+ fulfilment: 'mockFulfilment',
649
+ mojaloopResponse: {
650
+ condition: 'mockCondition',
651
+ expiration: new Date(Date.now() + 10000).toISOString(),
652
+ }
653
+ }
654
+ });
655
+
656
+ const args = {
657
+ body: {
658
+ transferId: TRANSFER_ID,
659
+ amount: {
660
+ currency: 'USD',
661
+ amount: 20.13
662
+ },
663
+ ilpPacket: 'mockBase64encodedIlpPacket',
664
+ condition: 'mockCondition'
665
+ }
666
+ };
667
+
668
+ // Spy on getTransfer to check if it's called by timer
669
+ const getTransfersSpy = jest.spyOn(model._mojaloopRequests, 'getTransfers').mockResolvedValue();
670
+
671
+ await model.prepareTransfer(args, mockArgs.fspId);
672
+
673
+ // Advance timers to simulate PATCH_GRACE_MS passing
674
+ jest.advanceTimersByTime(PATCH_GRACE_MS + 50);
675
+
676
+ // Wait for any pending promises
677
+ await Promise.resolve();
678
+ expect(getTransfersSpy).toHaveBeenCalled();
679
+
680
+ expect(getTransfersSpy).toHaveBeenCalledWith(TRANSFER_ID, mockArgs.fspId, undefined);
681
+ getTransfersSpy.mockRestore();
682
+ jest.useRealTimers();
683
+ });
684
+
685
+ test('should not set up patch notification timer if patchNotificationGraceTimeMs is 0', async () => {
686
+ const TRANSFER_ID = 'patch-notify-transfer-id-no-timer';
687
+ shared.mojaloopPrepareToInternalTransfer = jest.fn().mockReturnValue({
688
+ transferId: TRANSFER_ID,
689
+ amount: { currency: 'USD', amount: 20.13 },
690
+ ilpPacket: 'mockBase64encodedIlpPacket',
691
+ condition: 'mockCondition'
692
+ });
693
+
694
+ const model = new Model({
695
+ ...config,
696
+ cache,
697
+ logger,
698
+ patchNotificationGraceTimeMs: 0,
699
+ });
700
+
701
+ jest.spyOn(model._mojaloopRequests, 'getTransfers');
702
+ jest.spyOn(model, '_load').mockImplementation(async (transferId) => {
703
+ return {
704
+ transferId,
705
+ quote: {
706
+ fulfilment: 'mockFulfilment',
707
+ mojaloopResponse: {
708
+ condition: 'mockCondition',
709
+ expiration: new Date(Date.now() + 10000).toISOString(),
710
+ }
711
+ }
712
+ };
713
+ });
714
+ jest.spyOn(model, '_save').mockImplementation(async () => {});
715
+
716
+ cache.set(`transferModel_in_${TRANSFER_ID}`, {
717
+ transferId: TRANSFER_ID,
718
+ quote: {
719
+ fulfilment: 'mockFulfilment',
720
+ mojaloopResponse: {
721
+ condition: 'mockCondition',
722
+ expiration: new Date(Date.now() + 10000).toISOString(),
723
+ }
724
+ }
725
+ });
726
+
727
+ const args = {
728
+ body: {
729
+ transferId: TRANSFER_ID,
730
+ amount: {
731
+ currency: 'USD',
732
+ amount: 20.13
733
+ },
734
+ ilpPacket: 'mockBase64encodedIlpPacket',
735
+ condition: 'mockCondition'
736
+ }
737
+ };
738
+
739
+ await model.prepareTransfer(args, mockArgs.fspId);
740
+ // getTransfers should not be called at all
741
+ expect(model._mojaloopRequests.getTransfers).not.toHaveBeenCalled();
742
+ });
574
743
  });
575
744
 
576
745
  describe('prepareBulkTransfer:', () => {
@@ -774,7 +943,7 @@ describe('inboundModel', () => {
774
943
  });
775
944
 
776
945
  test('sends notification to fsp backend', async () => {
777
- BackendRequests.__putTransfersNotification = jest.fn().mockReturnValue(Promise.resolve({}));
946
+ BackendRequests.__putTransfersNotification = jest.fn().mockReturnValue(Promise.resolve({ status: 200 }));
778
947
  const notif = JSON.parse(JSON.stringify(notificationToPayee));
779
948
 
780
949
  const expectedRequest = {
@@ -796,7 +965,7 @@ describe('inboundModel', () => {
796
965
  });
797
966
 
798
967
  test('sends ABORTED notification to fsp backend', async () => {
799
- BackendRequests.__putTransfersNotification = jest.fn().mockReturnValue(Promise.resolve({}));
968
+ BackendRequests.__putTransfersNotification = jest.fn().mockReturnValue(Promise.resolve({ status: 200 }));
800
969
  const notif = JSON.parse(JSON.stringify(notificationAbortedToPayee));
801
970
 
802
971
  const expectedRequest = {
@@ -818,7 +987,7 @@ describe('inboundModel', () => {
818
987
  });
819
988
 
820
989
  test('sends RESERVED notification to fsp backend', async () => {
821
- BackendRequests.__putTransfersNotification = jest.fn().mockReturnValue(Promise.resolve({}));
990
+ BackendRequests.__putTransfersNotification = jest.fn().mockReturnValue(Promise.resolve({ status: 200 }));
822
991
  const notif = JSON.parse(JSON.stringify(notificationReservedToPayee));
823
992
 
824
993
  const expectedRequest = {
@@ -840,6 +1009,50 @@ describe('inboundModel', () => {
840
1009
  expect(call[1]).toEqual(transferId);
841
1010
  });
842
1011
 
1012
+ test('retries notification to fsp backend on failure', async () => {
1013
+ // Simulate failure for first 2 attempts, then success
1014
+ const mockFn = jest.fn()
1015
+ .mockRejectedValueOnce(new Error('fail1'))
1016
+ .mockRejectedValueOnce(new Error('fail2'))
1017
+ .mockResolvedValue({ status: 200 });
1018
+ BackendRequests.__putTransfersNotification = mockFn;
1019
+
1020
+ const notif = JSON.parse(JSON.stringify(notificationToPayee));
1021
+ const model = new Model({
1022
+ ...config,
1023
+ cache,
1024
+ logger,
1025
+ backendRequestRetry: {
1026
+ enabled: true,
1027
+ maxRetries: 3,
1028
+ retryDelayMs: 10,
1029
+ maxRetryDelayMs: 20,
1030
+ backoffFactor: 1
1031
+ }
1032
+ });
1033
+
1034
+ await model.sendNotificationToPayee(notif.data, transferId);
1035
+ expect(BackendRequests.__putTransfersNotification).toHaveBeenCalledTimes(3);
1036
+ });
1037
+
1038
+ test('does not retry notification to fsp backend if disabled', async () => {
1039
+ const mockFn = jest.fn().mockResolvedValue({ status: 200 });
1040
+ BackendRequests.__putTransfersNotification = mockFn;
1041
+
1042
+ const notif = JSON.parse(JSON.stringify(notificationToPayee));
1043
+ const model = new Model({
1044
+ ...config,
1045
+ cache,
1046
+ logger,
1047
+ backendRequestRetry: {
1048
+ enabled: false
1049
+ }
1050
+ });
1051
+
1052
+ await model.sendNotificationToPayee(notif.data, transferId);
1053
+ expect(BackendRequests.__putTransfersNotification).toHaveBeenCalledTimes(1);
1054
+ });
1055
+
843
1056
  });
844
1057
 
845
1058
  describe('sendFxPutNotificationToBackend:', () => {
@@ -860,7 +1073,7 @@ describe('inboundModel', () => {
860
1073
  });
861
1074
 
862
1075
  test('sends notification to fsp backend', async () => {
863
- BackendRequests.__putFxTransfersNotification = jest.fn().mockReturnValue(Promise.resolve({}));
1076
+ BackendRequests.__putFxTransfersNotification = jest.fn().mockReturnValue(Promise.resolve({ status: 200 }));
864
1077
  const notif = JSON.parse(JSON.stringify(fxNotificationToBackend));
865
1078
 
866
1079
  const model = new Model({
@@ -878,7 +1091,7 @@ describe('inboundModel', () => {
878
1091
  });
879
1092
 
880
1093
  test('sends ABORTED notification to fsp backend', async () => {
881
- BackendRequests.__putFxTransfersNotification = jest.fn().mockReturnValue(Promise.resolve({}));
1094
+ BackendRequests.__putFxTransfersNotification = jest.fn().mockReturnValue(Promise.resolve({ status: 200 }));
882
1095
  const notif = JSON.parse(JSON.stringify(fxNotificationAbortedToBackend));
883
1096
 
884
1097
  const model = new Model({
@@ -896,7 +1109,7 @@ describe('inboundModel', () => {
896
1109
  });
897
1110
 
898
1111
  test('sends RESERVED notification to fsp backend', async () => {
899
- BackendRequests.__putFxTransfersNotification = jest.fn().mockReturnValue(Promise.resolve({}));
1112
+ BackendRequests.__putFxTransfersNotification = jest.fn().mockReturnValue(Promise.resolve({ status: 200 }));
900
1113
  const notif = JSON.parse(JSON.stringify(fxNotificationReservedToBackend));
901
1114
 
902
1115
  const model = new Model({
@@ -913,6 +1126,157 @@ describe('inboundModel', () => {
913
1126
  expect(call[1]).toEqual(conversionId);
914
1127
  });
915
1128
 
1129
+ test('retries notification to backend on failure for sendFxPutNotificationToBackend if enabled', async () => {
1130
+ // Simulate failure for first 2 attempts, then success
1131
+ const mockFn = jest.fn()
1132
+ .mockRejectedValueOnce(new Error('fail1'))
1133
+ .mockRejectedValueOnce(new Error('fail2'))
1134
+ .mockResolvedValue({ status: 200 });
1135
+ BackendRequests.__putFxTransfersNotification = mockFn;
1136
+
1137
+ const notif = JSON.parse(JSON.stringify(fxNotificationToBackend));
1138
+ const model = new Model({
1139
+ ...config,
1140
+ cache,
1141
+ logger,
1142
+ backendRequestRetry: {
1143
+ enabled: true,
1144
+ maxRetries: 3,
1145
+ retryDelayMs: 10,
1146
+ maxRetryDelayMs: 20,
1147
+ backoffFactor: 1
1148
+ }
1149
+ });
1150
+ model.saveFxState = jest.fn().mockReturnValue(Promise.resolve({}));
1151
+
1152
+ await model.sendFxPutNotificationToBackend(notif.data, conversionId);
1153
+ expect(BackendRequests.__putFxTransfersNotification).toHaveBeenCalledTimes(3);
1154
+ });
1155
+
1156
+ test('does not retry notification to backend for sendFxPutNotificationToBackend if disabled', async () => {
1157
+ const mockFn = jest.fn().mockResolvedValue({ status: 200 });
1158
+ BackendRequests.__putFxTransfersNotification = mockFn;
1159
+
1160
+ const notif = JSON.parse(JSON.stringify(fxNotificationToBackend));
1161
+ const model = new Model({
1162
+ ...config,
1163
+ cache,
1164
+ logger,
1165
+ backendRequestRetry: {
1166
+ enabled: false
1167
+ }
1168
+ });
1169
+ model.saveFxState = jest.fn().mockReturnValue(Promise.resolve({}));
1170
+
1171
+ await model.sendFxPutNotificationToBackend(notif.data, conversionId);
1172
+ expect(BackendRequests.__putFxTransfersNotification).toHaveBeenCalledTimes(1);
1173
+ });
1174
+
1175
+ test('sendFxPutNotificationToBackend handles error and still saves state', async () => {
1176
+ BackendRequests.__putFxTransfersNotification = jest.fn().mockRejectedValue(new Error('fail'));
1177
+ const notif = JSON.parse(JSON.stringify(fxNotificationToBackend));
1178
+ const model = new Model({
1179
+ ...config,
1180
+ cache,
1181
+ logger,
1182
+ backendRequestRetry: {
1183
+ enabled: false
1184
+ }
1185
+ });
1186
+ const saveFxStateSpy = jest.spyOn(model, 'saveFxState').mockResolvedValue({});
1187
+ await expect(model.sendFxPutNotificationToBackend(notif.data, conversionId)).resolves.toBeUndefined();
1188
+ expect(saveFxStateSpy).toHaveBeenCalled();
1189
+ });
1190
+
1191
+ test('sendNotificationToPayee handles error and still returns', async () => {
1192
+ BackendRequests.__putTransfersNotification = jest.fn().mockRejectedValue(new Error('fail'));
1193
+ const notif = JSON.parse(JSON.stringify(notificationToPayee));
1194
+ const model = new Model({
1195
+ ...config,
1196
+ cache,
1197
+ logger,
1198
+ backendRequestRetry: {
1199
+ enabled: false
1200
+ }
1201
+ });
1202
+ await expect(model.sendNotificationToPayee(notif.data, 'some-transfer-id')).resolves.toBeUndefined();
1203
+ });
1204
+
1205
+ test('retries notification to backend on failure for sendFxPutNotificationToBackend if enabled', async () => {
1206
+ // Simulate failure for first 2 attempts, then success
1207
+ const mockFn = jest.fn()
1208
+ .mockRejectedValueOnce(new Error('fail1'))
1209
+ .mockRejectedValueOnce(new Error('fail2'))
1210
+ .mockResolvedValue({ status: 200 });
1211
+ BackendRequests.__putFxTransfersNotification = mockFn;
1212
+
1213
+ const notif = JSON.parse(JSON.stringify(fxNotificationToBackend));
1214
+ const model = new Model({
1215
+ ...config,
1216
+ cache,
1217
+ logger,
1218
+ backendRequestRetry: {
1219
+ enabled: true,
1220
+ maxRetries: 3,
1221
+ retryDelayMs: 10,
1222
+ maxRetryDelayMs: 20,
1223
+ backoffFactor: 1
1224
+ }
1225
+ });
1226
+ model.saveFxState = jest.fn().mockReturnValue(Promise.resolve({}));
1227
+
1228
+ await model.sendFxPutNotificationToBackend(notif.data, conversionId);
1229
+ expect(BackendRequests.__putFxTransfersNotification).toHaveBeenCalledTimes(3);
1230
+ });
1231
+
1232
+ test('does not retry notification to backend for sendFxPutNotificationToBackend if disabled', async () => {
1233
+ const mockFn = jest.fn().mockResolvedValue({ status: 200 });
1234
+ BackendRequests.__putFxTransfersNotification = mockFn;
1235
+
1236
+ const notif = JSON.parse(JSON.stringify(fxNotificationToBackend));
1237
+ const model = new Model({
1238
+ ...config,
1239
+ cache,
1240
+ logger,
1241
+ backendRequestRetry: {
1242
+ enabled: false
1243
+ }
1244
+ });
1245
+ model.saveFxState = jest.fn().mockReturnValue(Promise.resolve({}));
1246
+
1247
+ await model.sendFxPutNotificationToBackend(notif.data, conversionId);
1248
+ expect(BackendRequests.__putFxTransfersNotification).toHaveBeenCalledTimes(1);
1249
+ });
1250
+
1251
+ test('sendFxPutNotificationToBackend handles error and still saves state', async () => {
1252
+ BackendRequests.__putFxTransfersNotification = jest.fn().mockRejectedValue(new Error('fail'));
1253
+ const notif = JSON.parse(JSON.stringify(fxNotificationToBackend));
1254
+ const model = new Model({
1255
+ ...config,
1256
+ cache,
1257
+ logger,
1258
+ backendRequestRetry: {
1259
+ enabled: false
1260
+ }
1261
+ });
1262
+ const saveFxStateSpy = jest.spyOn(model, 'saveFxState').mockResolvedValue({});
1263
+ await expect(model.sendFxPutNotificationToBackend(notif.data, conversionId)).resolves.toBeUndefined();
1264
+ expect(saveFxStateSpy).toHaveBeenCalled();
1265
+ });
1266
+
1267
+ test('sendNotificationToPayee handles error and still returns', async () => {
1268
+ BackendRequests.__putTransfersNotification = jest.fn().mockRejectedValue(new Error('fail'));
1269
+ const notif = JSON.parse(JSON.stringify(notificationToPayee));
1270
+ const model = new Model({
1271
+ ...config,
1272
+ cache,
1273
+ logger,
1274
+ backendRequestRetry: {
1275
+ enabled: false
1276
+ }
1277
+ });
1278
+ await expect(model.sendNotificationToPayee(notif.data, 'some-transfer-id')).resolves.toBeUndefined();
1279
+ });
916
1280
  });
917
1281
 
918
1282
  describe('error handling:', () => {