@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
package/.yarnrc.yml CHANGED
@@ -4,4 +4,4 @@ enableGlobalCache: false
4
4
 
5
5
  nodeLinker: node-modules
6
6
 
7
- yarnPath: .yarn/releases/yarn-4.9.2.cjs
7
+ yarnPath: .yarn/releases/yarn-4.9.4.cjs
package/CHANGELOG.md CHANGED
@@ -1,4 +1,41 @@
1
1
  # Changelog: [mojaloop/sdk-scheme-adapter](https://github.com/mojaloop/sdk-scheme-adapter)
2
+ ### [24.10.11](https://github.com/mojaloop/sdk-scheme-adapter/compare/v24.10.10...v24.10.11) (2025-09-15)
3
+
4
+
5
+ ### Bug Fixes
6
+
7
+ * ilp decimal calculation ([#612](https://github.com/mojaloop/sdk-scheme-adapter/issues/612)) ([77cdd19](https://github.com/mojaloop/sdk-scheme-adapter/commit/77cdd19dc2543f9bd115da4da55e1d3266d21e67))
8
+
9
+
10
+ ### Chore
11
+
12
+ * grype exception ([#613](https://github.com/mojaloop/sdk-scheme-adapter/issues/613)) ([464f4fd](https://github.com/mojaloop/sdk-scheme-adapter/commit/464f4fd95feec2953106839c134296d2f51f05b3))
13
+ * **sbom:** update sbom [skip ci] ([051986c](https://github.com/mojaloop/sdk-scheme-adapter/commit/051986ccc8bb51b616200239cb1727598fd29f74))
14
+
15
+ ### [24.10.10](https://github.com/mojaloop/sdk-scheme-adapter/compare/v24.10.9...v24.10.10) (2025-08-27)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **csi-1708:** updated deps ([#607](https://github.com/mojaloop/sdk-scheme-adapter/issues/607)) ([42f7baf](https://github.com/mojaloop/sdk-scheme-adapter/commit/42f7baf2e828e4eea2eb4edc41cd847d4e15651c))
21
+
22
+
23
+ ### Chore
24
+
25
+ * **sbom:** update sbom [skip ci] ([c05eac1](https://github.com/mojaloop/sdk-scheme-adapter/commit/c05eac19196b19b1b3ff3c6cafc0744207e4b43d))
26
+
27
+ ### [24.10.9](https://github.com/mojaloop/sdk-scheme-adapter/compare/v24.10.8...v24.10.9) (2025-08-18)
28
+
29
+
30
+ ### Bug Fixes
31
+
32
+ * **csi-1680:** changed PATCH transfers/fxTransfers logic of sending notifications ([#605](https://github.com/mojaloop/sdk-scheme-adapter/issues/605)) ([749d84a](https://github.com/mojaloop/sdk-scheme-adapter/commit/749d84a785a571b7f401b2e98f2852bc2ae175d8))
33
+
34
+
35
+ ### Chore
36
+
37
+ * **sbom:** update sbom [skip ci] ([071b19e](https://github.com/mojaloop/sdk-scheme-adapter/commit/071b19e185aff5e3f5e17681c4032d85b8bc35e8))
38
+
2
39
  ### [24.10.8](https://github.com/mojaloop/sdk-scheme-adapter/compare/v24.10.7...v24.10.8) (2025-08-11)
3
40
 
4
41
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mojaloop/sdk-scheme-adapter-api-svc",
3
- "version": "21.0.0-snapshot.54",
3
+ "version": "21.0.0-snapshot.59",
4
4
  "description": "An adapter for connecting to Mojaloop API enabled switches.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -65,20 +65,20 @@
65
65
  "dependencies": {
66
66
  "@koa/cors": "5.0.0",
67
67
  "@mojaloop/api-snippets": "18.1.1",
68
- "@mojaloop/central-services-error-handling": "13.1.0",
69
- "@mojaloop/central-services-logger": "11.9.0",
70
- "@mojaloop/central-services-metrics": "12.6.0",
71
- "@mojaloop/central-services-shared": "18.30.7",
72
- "@mojaloop/event-sdk": "14.6.1",
68
+ "@mojaloop/central-services-error-handling": "13.1.2",
69
+ "@mojaloop/central-services-logger": "11.9.3",
70
+ "@mojaloop/central-services-metrics": "12.7.1",
71
+ "@mojaloop/central-services-shared": "18.33.0",
72
+ "@mojaloop/event-sdk": "14.7.0",
73
73
  "@mojaloop/logging-bc-client-lib": "0.5.8",
74
- "@mojaloop/ml-schema-transformer-lib": "2.7.7",
74
+ "@mojaloop/ml-schema-transformer-lib": "2.7.8",
75
75
  "@mojaloop/sdk-scheme-adapter-private-shared-lib": "workspace:^",
76
- "@mojaloop/sdk-standard-components": "19.16.4",
76
+ "@mojaloop/sdk-standard-components": "19.17.0",
77
77
  "ajv": "8.17.1",
78
- "axios": "1.11.0",
78
+ "axios": "1.12.2",
79
79
  "body-parser": "2.2.0",
80
80
  "co-body": "6.2.0",
81
- "dotenv": "17.2.1",
81
+ "dotenv": "17.2.2",
82
82
  "env-var": "7.5.0",
83
83
  "express": "4.21.2",
84
84
  "fast-json-patch": "3.1.1",
@@ -96,17 +96,19 @@
96
96
  "prom-client": "15.1.3",
97
97
  "promise-timeout": "1.3.0",
98
98
  "random-word-slugs": "0.1.7",
99
- "redis": "5.8.1",
99
+ "redis": "5.8.2",
100
+ "retry": "^0.13.1",
100
101
  "uuidv4": "6.2.13",
101
102
  "ws": "8.18.3"
102
103
  },
103
104
  "devDependencies": {
104
- "@babel/core": "7.28.3",
105
+ "@babel/core": "7.28.4",
105
106
  "@babel/preset-env": "7.28.3",
106
107
  "@redocly/openapi-cli": "1.0.0-beta.95",
107
108
  "@types/jest": "30.0.0",
109
+ "@types/retry": "^0",
108
110
  "axios-mock-adapter": "2.1.0",
109
- "babel-jest": "30.0.5",
111
+ "babel-jest": "30.1.2",
110
112
  "eslint": "9.15.0",
111
113
  "eslint-config-airbnb-base": "15.0.0",
112
114
  "eslint-plugin-import": "2.32.0",
@@ -729,12 +729,22 @@ const patchTransfersById = async (ctx) => {
729
729
  // use the transfers model to execute asynchronous stages with the switch
730
730
  const model = createInboundTransfersModel(ctx);
731
731
 
732
- // sends notification to the payee fsp
733
- const response = await model.sendNotificationToPayee(req.data, idValue);
732
+ // kick off an asynchronous operation to handle the request
733
+ (async () => {
734
+ try {
735
+ // sends notification to the payee fsp
736
+ const response = await model.sendNotificationToPayee(req.data, idValue);
737
+
738
+ // log the result
739
+ ctx.state.logger.isDebugEnabled && ctx.state.logger.push({ response }).
740
+ debug('Inbound transfers model handled PATCH /transfers/{ID} request');
741
+ } catch (err) {
742
+ ctx.state.logger.isErrorEnabled && ctx.state.logger.push({ err }).error('Error handling PATCH /transfers/{ID}');
743
+ }
744
+ })();
734
745
 
735
- // log the result
736
- ctx.state.logger.isDebugEnabled && ctx.state.logger.push({response}).
737
- debug('Inbound transfers model handled PATCH /transfers/{ID} request');
746
+ ctx.response.status = ReturnCodes.OK.CODE;
747
+ ctx.response.body = '';
738
748
  };
739
749
 
740
750
  /**
@@ -1080,10 +1090,18 @@ const patchFxTransfersById = async (ctx) => {
1080
1090
  const data = { ...ctx.request.body };
1081
1091
  const idValue = ctx.state.path.params.ID;
1082
1092
 
1083
- const model = createInboundTransfersModel(ctx);
1084
- const response = await model.sendFxPutNotificationToBackend(data, idValue);
1093
+ (async () => {
1094
+ const model = createInboundTransfersModel(ctx);
1095
+ try {
1096
+ const response = await model.sendFxPutNotificationToBackend(data, idValue);
1097
+ ctx.state.logger.push({ response }).debug('Inbound Transfers model handled PATCH /fxTransfers/{ID} request');
1098
+ } catch (err) {
1099
+ ctx.state.logger.push({ err }).error('Error handling PATCH /fxTransfers/{ID}');
1100
+ }
1101
+ })();
1085
1102
 
1086
- ctx.state.logger.push({ response }).debug('Inbound Transfers model handled PATCH /fxTransfers/{ID} request');
1103
+ ctx.response.status = ReturnCodes.OK.CODE;
1104
+ ctx.response.body = '';
1087
1105
  };
1088
1106
 
1089
1107
  /**
@@ -264,5 +264,21 @@ module.exports = {
264
264
 
265
265
  // Redis key ttl when stored in the cache, if value is used as zero it will
266
266
  // persist throughout the session , value used is in seconds
267
- redisCacheTtl: env.get('REDIS_CACHE_TTL').default('0').asInt()
267
+ redisCacheTtl: env.get('REDIS_CACHE_TTL').default('0').asInt(),
268
+
269
+ backendRequestRetry: {
270
+ enabled: env.get('BACKEND_REQUEST_RETRY_ENABLED').default('true').asBool(),
271
+ maxRetries: env.get('BACKEND_REQUEST_RETRY_MAX_RETRIES').default('5').asIntPositive(),
272
+ retryDelayMs: env.get('BACKEND_REQUEST_RETRY_DELAY_MS').default('1000').asIntPositive(),
273
+ maxRetryDelayMs: env.get('BACKEND_REQUEST_RETRY_MAX_DELAY_MS').default('10000').asIntPositive(),
274
+ backoffFactor: env.get('BACKEND_REQUEST_RETRY_BACKOFF_FACTOR').default('2').asIntPositive(),
275
+ },
276
+ getTransferRequestRetry: {
277
+ enabled: env.get('GET_TRANSFER_REQUEST_RETRY_ENABLED').default('false').asBool(),
278
+ maxRetries: env.get('GET_TRANSFER_REQUEST_RETRY_MAX_RETRIES').default('3').asIntPositive(),
279
+ retryDelayMs: env.get('GET_TRANSFER_REQUEST_RETRY_DELAY_MS').default('1000').asIntPositive(),
280
+ maxRetryDelayMs: env.get('GET_TRANSFER_REQUEST_RETRY_MAX_DELAY_MS').default('10000').asIntPositive(),
281
+ backoffFactor: env.get('GET_TRANSFER_REQUEST_RETRY_BACKOFF_FACTOR').default('2').asIntPositive(),
282
+ },
283
+ patchNotificationGraceTimeMs: env.get('PATCH_NOTIFICATION_GRACE_TIME_MS').default('15000').asIntPositive(),
268
284
  };
@@ -30,6 +30,7 @@ const safeStringify = require('fast-safe-stringify');
30
30
  const { MojaloopRequests, Ilp, Errors } = require('@mojaloop/sdk-standard-components');
31
31
  const FSPIOPTransferStateEnum = require('@mojaloop/central-services-shared').Enum.Transfers.TransferState;
32
32
  const FSPIOPBulkTransferStateEnum = require('@mojaloop/central-services-shared').Enum.Transfers.BulkTransferState;
33
+ const retry = require('retry');
33
34
 
34
35
  const dto = require('../dto');
35
36
  const shared = require('./lib/shared');
@@ -52,6 +53,9 @@ class InboundTransfersModel {
52
53
  this._reserveNotification = config.reserveNotification;
53
54
  this._allowDifferentTransferTransactionId = config.allowDifferentTransferTransactionId;
54
55
  this._supportedCurrencies = config.supportedCurrencies;
56
+ this._patchNotificationGraceTimeMs = config.patchNotificationGraceTimeMs;
57
+ this._getTransferRequestRetry = config.getTransferRequestRetry;
58
+ this._backendRequestRetry = config.backendRequestRetry;
55
59
 
56
60
  this._mojaloopRequests = new MojaloopRequests({
57
61
  logger: this._logger,
@@ -489,6 +493,61 @@ class InboundTransfersModel {
489
493
  this.data.currentState = response.transferState || (this._reserveNotification ? SDKStateEnum.RESERVED : SDKStateEnum.COMPLETED);
490
494
 
491
495
  await this._save();
496
+
497
+ // --- PATCH NOTIFICATION TIMER LOGIC ---
498
+ // Set a timer to trigger GET /transfers/{ID} if sendNotificationToPayee is not called in time
499
+ if (this._patchNotificationGraceTimeMs > 0) {
500
+ const transferId = prepareRequest.transferId;
501
+ const cacheKey = `patchNotificationSent_${transferId}`;
502
+ // Mark as not notified yet
503
+ await this._cache.set(cacheKey, false, Math.ceil(this._patchNotificationGraceTimeMs / 1000) + 5);
504
+
505
+ setTimeout(async () => {
506
+ try {
507
+ this._logger.isInfoEnabled && this._logger.push({ transferId }).info('Patch notification grace time expired, attempting GET /transfers/{ID}');
508
+ const notified = await this._cache.get(cacheKey);
509
+
510
+ if (!notified) {
511
+ // Subscribe to transfer callback channel
512
+ const transferKey = `tf_${transferId}`;
513
+ const unsubscribeTimeout = this._getTransferRequestRetry?.retryDelayMs || 1000;
514
+ let gotCallback = false;
515
+
516
+ // Subscribe for one message with a timeout
517
+ const messagePromise = this._cache.subscribeToOneMessageWithTimer(transferKey, Math.ceil(unsubscribeTimeout / 1000));
518
+ // Kick off GET /transfers/{ID}
519
+ await this._mojaloopRequests.getTransfers(transferId, sourceFspId, headers);
520
+
521
+ // Retry logic
522
+ let attempts = 0;
523
+ const maxAttempts = (this._getTransferRequestRetry?.maxRetries || 3);
524
+ const retryDelay = this._getTransferRequestRetry?.retryDelayMs || 1000;
525
+ while (attempts < maxAttempts && !gotCallback) {
526
+ try {
527
+ const message = await messagePromise;
528
+ if (message && message.data) {
529
+ gotCallback = true;
530
+ // Mark as notified to prevent duplicate notification
531
+ await this._cache.set(cacheKey, true, 60);
532
+ // Send notification to payee
533
+ await this.sendNotificationToPayee(message.data.body, transferId);
534
+ break;
535
+ }
536
+ } catch (err) {
537
+ this._logger.isErrorEnabled && this._logger.push({ err, transferId }).error('Error while waiting for transfer callback in patch notification grace timer logic');
538
+ }
539
+ attempts++;
540
+ if (!gotCallback && attempts < maxAttempts) {
541
+ await this._mojaloopRequests.getTransfers(transferId, sourceFspId, headers);
542
+ await new Promise(r => setTimeout(r, retryDelay));
543
+ }
544
+ }
545
+ }
546
+ } catch (err) {
547
+ this._logger.isErrorEnabled && this._logger.push({ err, transferId }).error('Error in patch notification grace timer logic');
548
+ }
549
+ }, this._patchNotificationGraceTimeMs);
550
+ }
492
551
  return res;
493
552
  } catch(err) {
494
553
  this._logger.isErrorEnabled && this._logger.push({ err }).error(`Error in prepareTransfer: ${prepareRequest?.transferId}`);
@@ -956,7 +1015,7 @@ class InboundTransfersModel {
956
1015
  async sendFxPutNotificationToBackend(body, conversionId) {
957
1016
  const log = this._logger.child({ conversionId });
958
1017
  try {
959
- log.verbose('sendFxPutNotificationToBackend - incoming payload: ', { body });
1018
+ log.verbose('sendFxPutNotificationToBackend incoming payload: ', { body });
960
1019
  this.data = await this.loadFxState(conversionId);
961
1020
 
962
1021
  if(!this.data) {
@@ -978,12 +1037,51 @@ class InboundTransfersModel {
978
1037
 
979
1038
  const responseBody = {
980
1039
  conversionState: body.conversionState, // one of ABORTED, COMMITTED, RESERVED
981
- completedTimestamp: body.completedTimestamp
1040
+ completedTimestamp: body.completedTimestamp,
982
1041
  };
983
- log.verbose('sendFxPutNotificationToBackend - body sent to cc: ', { responseBody });
984
-
1042
+ log.verbose('sendFxPutNotificationToBackend body sent to cc: ', { responseBody });
1043
+
1044
+ const { enabled, maxRetries, retryDelayMs, maxRetryDelayMs, backoffFactor } = this._backendRequestRetry || {};
1045
+ let res;
1046
+ const shouldRetry = enabled !== false; // default to true if not set
1047
+
1048
+ if (shouldRetry) {
1049
+ const operation = retry.operation({
1050
+ retries: maxRetries || 5,
1051
+ factor: backoffFactor || 2,
1052
+ minTimeout: retryDelayMs || 1000,
1053
+ maxTimeout: maxRetryDelayMs || 10000,
1054
+ });
1055
+
1056
+ await new Promise((resolve) => {
1057
+ operation.attempt(async (currentAttempt) => {
1058
+ try {
1059
+ res = await this._backendRequests.putFxTransfersNotification(responseBody, conversionId);
1060
+ if ((res && (res.status === 200 || res.statusCode === 200)) || res === true) {
1061
+ return resolve();
1062
+ }
1063
+ log.warn(`putFxTransfersNotification attempt ${currentAttempt} failed, retrying...`);
1064
+ if (!operation.retry(new Error('Non-200 response'))) {
1065
+ resolve();
1066
+ }
1067
+ } catch (err) {
1068
+ log.warn(`putFxTransfersNotification attempt ${currentAttempt} threw error, retrying...`, err);
1069
+ if (!operation.retry(err)) {
1070
+ resolve();
1071
+ }
1072
+ }
1073
+ });
1074
+ });
985
1075
 
986
- const res = await this._backendRequests.putFxTransfersNotification(responseBody, conversionId);
1076
+ if ((res && (res.status !== 200 && res.statusCode !== 200)) && res !== true) {
1077
+ log.error(`putFxTransfersNotification failed after ${operation.attempts()} attempts`);
1078
+ }
1079
+ } else {
1080
+ res = await this._backendRequests.putFxTransfersNotification(responseBody, conversionId);
1081
+ if ((res && (res.status !== 200 && res.statusCode !== 200)) && res !== true) {
1082
+ log.error('putFxTransfersNotification failed');
1083
+ }
1084
+ }
987
1085
  return res;
988
1086
  } catch (err) {
989
1087
  log.error('error in sendFxPutNotificationToBackend: ', err);
@@ -1026,7 +1124,49 @@ class InboundTransfersModel {
1026
1124
 
1027
1125
  await this._save();
1028
1126
 
1029
- const res = await this._backendRequests.putTransfersNotification(this.data, transferId);
1127
+ const { enabled, maxRetries, retryDelayMs, maxRetryDelayMs, backoffFactor } = this._backendRequestRetry || {};
1128
+ let res;
1129
+ const shouldRetry = enabled !== false; // default to true if not set
1130
+
1131
+ if (shouldRetry) {
1132
+ const operation = retry.operation({
1133
+ retries: maxRetries || 5,
1134
+ factor: backoffFactor || 2,
1135
+ minTimeout: retryDelayMs || 1000,
1136
+ maxTimeout: maxRetryDelayMs || 10000,
1137
+ });
1138
+
1139
+ await new Promise((resolve) => {
1140
+ operation.attempt(async (currentAttempt) => {
1141
+ try {
1142
+ res = await this._backendRequests.putTransfersNotification(this.data, transferId);
1143
+ if ((res && (res.status === 200 || res.statusCode === 200)) || res === true) {
1144
+ const cacheKey = `patchNotificationSent_${transferId}`;
1145
+ await this._cache.set(cacheKey, true, 60);
1146
+ return resolve();
1147
+ }
1148
+ this._logger.warn(`putTransfersNotification attempt ${currentAttempt} failed, retrying...`);
1149
+ if (!operation.retry(new Error('Non-200 response'))) {
1150
+ resolve();
1151
+ }
1152
+ } catch (err) {
1153
+ this._logger.warn(`putTransfersNotification attempt ${currentAttempt} threw error, retrying...`, err);
1154
+ if (!operation.retry(err)) {
1155
+ resolve();
1156
+ }
1157
+ }
1158
+ });
1159
+ });
1160
+
1161
+ if ((res && (res.status !== 200 && res.statusCode !== 200)) && res !== true) {
1162
+ this._logger.error(`putTransfersNotification failed after ${operation.attempts()} attempts`);
1163
+ }
1164
+ } else {
1165
+ res = await this._backendRequests.putTransfersNotification(this.data, transferId);
1166
+ if ((res && (res.status !== 200 && res.statusCode !== 200)) && res !== true) {
1167
+ this._logger.error('putTransfersNotification failed');
1168
+ }
1169
+ }
1030
1170
  return res;
1031
1171
  } catch (err) {
1032
1172
  this._logger.isErrorEnabled && this._logger.push({ err, transferId }).error(`Error notifying backend of final transfer state equal to: ${body.transferState}`);
@@ -1473,7 +1473,7 @@ class OutboundTransfersModel {
1473
1473
  log.isVerboseEnabled && log.verbose(`Transfer model state machine transition completed in state: ${this.stateMachine.state}. Recursing to handle next transition.`);
1474
1474
  return this.run();
1475
1475
  } catch (err) {
1476
- log.isErrorEnabled && log.push({ err }).error(`error running outbound transfer model: ${err?.message}`);
1476
+ log.error('error running outbound transfer model: ', err);
1477
1477
 
1478
1478
  // as this function is recursive, we dont want to error the state machine multiple times
1479
1479
  if (this.data.currentState !== States.ERRORED) {
@@ -56,8 +56,11 @@ class RedisClient extends redisMock.RedisClient {
56
56
  process.nextTick(() => this.events.emit(...args));
57
57
  }
58
58
 
59
- set(...args) {
60
- return promisify(super.set.bind(this))(...args);
59
+ /**
60
+ * Note: This Redis mock implementation does not support options like TTL (time-to-live).
61
+ */
62
+ set(key, value) {
63
+ return promisify(super.set.bind(this))(key, value);
61
64
  }
62
65
 
63
66
  get(...args) {
@@ -315,7 +315,7 @@ describe('Inbound API handlers transforming incoming ISO20022 message bodies', (
315
315
 
316
316
  });
317
317
 
318
- test('calls `prepareTransfer` with the expected arguments.', async () => {
318
+ test('calls `prepareTransfer` with the expected arguments and sets 200 status.', async () => {
319
319
  const transferRequestSpy = jest.spyOn(Model.prototype, 'sendNotificationToPayee');
320
320
 
321
321
  await expect(handlers['/transfers/{ID}'].patch(mockContext)).resolves.toBe(undefined);
@@ -324,6 +324,8 @@ describe('Inbound API handlers transforming incoming ISO20022 message bodies', (
324
324
  expect(transferRequestSpy.mock.calls[0][0]).not.toBeUndefined();
325
325
  expect(transferRequestSpy.mock.calls[0][0]).not.toEqual(isoBodies.patchTransfersRequest);
326
326
  expect(transferRequestSpy.mock.calls[0][0].transferState).toBe('COMMITTED');
327
+ expect(mockContext.response.status).toBe(200);
328
+ console.log('Response body:', mockContext.response);
327
329
  });
328
330
  });
329
331
 
@@ -624,12 +624,13 @@ describe('Inbound API handlers:', () => {
624
624
  };
625
625
  });
626
626
 
627
- test('calls `model.sendNotificationToPayee with expected arguments', async () => {
627
+ test('calls `model.sendNotificationToPayee` with expected arguments and responds 200', async () => {
628
628
  const notificationSpy = jest.spyOn(Model.prototype, 'sendNotificationToPayee');
629
629
 
630
630
  await expect(handlers['/transfers/{ID}'].patch(mockNotificationMessage)).resolves.toBe(undefined);
631
631
  expect(notificationSpy).toHaveBeenCalledTimes(1);
632
632
  expect(notificationSpy.mock.calls[0][1]).toBe(mockNotificationMessage.state.path.params.ID);
633
+ expect(mockNotificationMessage.response.status).toBe(200);
633
634
  });
634
635
 
635
636
  });
@@ -1075,12 +1076,13 @@ describe('Inbound API handlers:', () => {
1075
1076
  };
1076
1077
  });
1077
1078
 
1078
- test('calls `model.sendFxPutNotificationToBackend with expected arguments', async () => {
1079
+ test('calls `model.sendFxPutNotificationToBackend` with expected arguments and responds 200', async () => {
1079
1080
  const notificationSpy = jest.spyOn(Model.prototype, 'sendFxPutNotificationToBackend');
1080
1081
 
1081
1082
  await expect(handlers['/fxTransfers/{ID}'].patch(mockNotificationMessage)).resolves.toBe(undefined);
1082
1083
  expect(notificationSpy).toHaveBeenCalledTimes(1);
1083
1084
  expect(notificationSpy.mock.calls[0][1]).toBe(mockNotificationMessage.state.path.params.ID);
1085
+ expect(mockNotificationMessage.response.status).toBe(200);
1084
1086
  });
1085
1087
 
1086
1088
  });