@mojaloop/sdk-scheme-adapter 24.10.7 → 24.10.9-snapshot.0

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 (57) hide show
  1. package/.yarn/cache/{@babel-core-npm-7.28.0-2c03249042-1c86eec8d7.zip → @babel-core-npm-7.28.3-fb967e901c-0faded84ed.zip} +0 -0
  2. package/.yarn/cache/@babel-generator-npm-7.28.3-1529434ded-d00d1e6b51.zip +0 -0
  3. package/.yarn/cache/@babel-helper-create-class-features-plugin-npm-7.28.3-de056c24da-32d01bdd60.zip +0 -0
  4. package/.yarn/cache/@babel-helper-module-transforms-npm-7.28.3-7b69ec189a-598fdd8aa5.zip +0 -0
  5. package/.yarn/cache/{@babel-helpers-npm-7.28.2-20c7a44ade-09fd7965e8.zip → @babel-helpers-npm-7.28.3-8e4849da45-6d39031bf0.zip} +0 -0
  6. package/.yarn/cache/@babel-parser-npm-7.28.3-8acaa30019-9fa08282e3.zip +0 -0
  7. package/.yarn/cache/@babel-plugin-bugfix-v8-static-class-fields-redefine-readonly-npm-7.28.3-19e1b3699f-eeacdb7fa5.zip +0 -0
  8. package/.yarn/cache/@babel-plugin-transform-class-static-block-npm-7.28.3-13af84b676-c0ba8f0cbf.zip +0 -0
  9. package/.yarn/cache/{@babel-plugin-transform-classes-npm-7.28.0-3815bda6ff-1a812a02f6.zip → @babel-plugin-transform-classes-npm-7.28.3-22fe11bcef-0aefcabe68.zip} +0 -0
  10. package/.yarn/cache/{@babel-plugin-transform-regenerator-npm-7.28.1-2cc4798981-45e3a63bf2.zip → @babel-plugin-transform-regenerator-npm-7.28.3-36bb1b5a59-f95e929d41.zip} +0 -0
  11. package/.yarn/cache/{@babel-preset-env-npm-7.28.0-964e29aeee-8814453ffe.zip → @babel-preset-env-npm-7.28.3-ec87d1a73a-b09991276a.zip} +0 -0
  12. package/.yarn/cache/@babel-traverse-npm-7.28.3-7786c501c7-fe521591b7.zip +0 -0
  13. package/.yarn/cache/{@redis-bloom-npm-5.8.0-436cff1687-a4ab9add75.zip → @redis-bloom-npm-5.8.1-139b45b8e4-f691b1dce2.zip} +0 -0
  14. package/.yarn/cache/{@redis-client-npm-5.8.0-3bdc0e2d9d-7db93f3e6f.zip → @redis-client-npm-5.8.1-66d46a9ca1-329d76de06.zip} +0 -0
  15. package/.yarn/cache/{@redis-json-npm-5.8.0-c6aea5063f-3347bfd1ec.zip → @redis-json-npm-5.8.1-1374d9e2de-9eabbf9a2c.zip} +0 -0
  16. package/.yarn/cache/{@redis-search-npm-5.8.0-cd8c59f8e9-055c9804f9.zip → @redis-search-npm-5.8.1-ebc7760a31-a5e12dd2c7.zip} +0 -0
  17. package/.yarn/cache/{@redis-time-series-npm-5.8.0-a8eada922a-98a8123a5b.zip → @redis-time-series-npm-5.8.1-1f5e30ede4-c9440ce935.zip} +0 -0
  18. package/.yarn/cache/@redocly-openapi-core-npm-1.34.5-6e131049a1-9ecce1975b.zip +0 -0
  19. package/.yarn/cache/{@typescript-eslint-eslint-plugin-npm-8.39.0-7cc58b0ab6-31f879990a.zip → @typescript-eslint-eslint-plugin-npm-8.39.1-8ad46b0385-446050aa43.zip} +0 -0
  20. package/.yarn/cache/{@typescript-eslint-parser-npm-8.39.0-c138f72ca9-9785994ff0.zip → @typescript-eslint-parser-npm-8.39.1-e931b25728-ff45ce7635.zip} +0 -0
  21. package/.yarn/cache/{@typescript-eslint-project-service-npm-8.39.0-4cecf00a1b-990ae23308.zip → @typescript-eslint-project-service-npm-8.39.1-f6db73ca22-1970633d1a.zip} +0 -0
  22. package/.yarn/cache/{@typescript-eslint-scope-manager-npm-8.39.0-45f3f86773-c2b232a172.zip → @typescript-eslint-scope-manager-npm-8.39.1-bf78e0253c-8874f74790.zip} +0 -0
  23. package/.yarn/cache/{@typescript-eslint-tsconfig-utils-npm-8.39.0-444fac8997-3457da49e7.zip → @typescript-eslint-tsconfig-utils-npm-8.39.1-e46dac00aa-38c1e19825.zip} +0 -0
  24. package/.yarn/cache/{@typescript-eslint-type-utils-npm-8.39.0-02f1fd51a1-3efe4001b6.zip → @typescript-eslint-type-utils-npm-8.39.1-41cbec8085-1195d65970.zip} +0 -0
  25. package/.yarn/cache/{@typescript-eslint-types-npm-8.39.0-c051b2516d-b08a42e8b5.zip → @typescript-eslint-types-npm-8.39.1-8cea531133-8013f4f48a.zip} +0 -0
  26. package/.yarn/cache/{@typescript-eslint-typescript-estree-npm-8.39.0-73bf7427a0-7e9dc461fe.zip → @typescript-eslint-typescript-estree-npm-8.39.1-eb0cf5436f-07ed9d7ab4.zip} +0 -0
  27. package/.yarn/cache/{@typescript-eslint-utils-npm-8.39.0-26129b3d3c-ed340f36fa.zip → @typescript-eslint-utils-npm-8.39.1-a6c63e4cf7-39bb105f26.zip} +0 -0
  28. package/.yarn/cache/{@typescript-eslint-visitor-keys-npm-8.39.0-76eaf78702-2eb89b9e4d.zip → @typescript-eslint-visitor-keys-npm-8.39.1-d0b0654c5b-6d4e4d0b19.zip} +0 -0
  29. package/.yarn/cache/openapi-backend-npm-5.14.0-231377503b-1bd3e6cb71.zip +0 -0
  30. package/.yarn/cache/openapi-typescript-npm-7.9.1-753f55d26c-1d2d967e40.zip +0 -0
  31. package/.yarn/cache/{redis-npm-5.8.0-e751103f9d-8a645185df.zip → redis-npm-5.8.1-201a0a72a3-26d97c6ddf.zip} +0 -0
  32. package/.yarn/cache/supports-color-npm-10.1.0-48517f80a7-28d191c4ad.zip +0 -0
  33. package/.yarn/install-state.gz +0 -0
  34. package/CHANGELOG.md +12 -0
  35. package/docker/k6/package.json +17 -0
  36. package/docker/k6/src/test.js +43 -0
  37. package/docker-compose.yml +1 -0
  38. package/modules/api-svc/package.json +5 -5
  39. package/modules/api-svc/src/InboundServer/handlers.js +1 -1
  40. package/modules/api-svc/src/lib/cache.js +106 -0
  41. package/modules/api-svc/src/lib/model/InboundTransfersModel.js +5 -3
  42. package/modules/api-svc/src/lib/model/OutboundTransfersModel.js +93 -118
  43. package/modules/api-svc/src/lib/model/common/TimeoutError.js +41 -0
  44. package/modules/api-svc/src/lib/model/common/index.js +2 -0
  45. package/modules/api-svc/src/lib/model/lib/requests/backendRequests.js +2 -2
  46. package/modules/api-svc/test/unit/api/transfers/transfers.test.js +1 -1
  47. package/modules/api-svc/test/unit/inboundApi/handlers.test.js +2 -2
  48. package/modules/api-svc/test/unit/lib/cache.test.js +150 -0
  49. package/modules/api-svc/test/unit/lib/model/InboundTransfersModel.test.js +16 -32
  50. package/modules/api-svc/test/unit/lib/model/mockedLibRequests.js +2 -2
  51. package/modules/outbound-command-event-handler/package.json +5 -5
  52. package/modules/outbound-domain-event-handler/package.json +5 -5
  53. package/modules/private-shared-lib/package.json +4 -4
  54. package/package.json +3 -3
  55. package/{sbom-v24.10.6.csv → sbom-v24.10.8.csv} +36 -36
  56. package/.yarn/cache/@babel-plugin-bugfix-v8-static-class-fields-redefine-readonly-npm-7.27.1-424bedd466-dfa68da5f6.zip +0 -0
  57. package/.yarn/cache/@babel-plugin-transform-class-static-block-npm-7.27.1-a1a8a0d79f-2d49de0f5f.zip +0 -0
Binary file
package/CHANGELOG.md CHANGED
@@ -1,4 +1,16 @@
1
1
  # Changelog: [mojaloop/sdk-scheme-adapter](https://github.com/mojaloop/sdk-scheme-adapter)
2
+ ### [24.10.8](https://github.com/mojaloop/sdk-scheme-adapter/compare/v24.10.7...v24.10.8) (2025-08-11)
3
+
4
+
5
+ ### Bug Fixes
6
+
7
+ * concurrency issues ([#601](https://github.com/mojaloop/sdk-scheme-adapter/issues/601)) ([b693e2f](https://github.com/mojaloop/sdk-scheme-adapter/commit/b693e2fe618f256e4511fe9fad256f48d814ca01))
8
+
9
+
10
+ ### Chore
11
+
12
+ * **sbom:** update sbom [skip ci] ([63b0609](https://github.com/mojaloop/sdk-scheme-adapter/commit/63b06090017264a5d375d48bb9800ca9ae183ddd))
13
+
2
14
  ### [24.10.7](https://github.com/mojaloop/sdk-scheme-adapter/compare/v24.10.6...v24.10.7) (2025-08-08)
3
15
 
4
16
 
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "k6-tests",
3
+ "version": "1.0.0",
4
+ "description": "```bash docker run --rm -i grafana/k6 run --vus 1 --duration 10s - <./src/test.js ```",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "author": "Eugen Klymniuk",
10
+ "license": "MIT",
11
+ "dependencies": {
12
+ "k6": "0.0.0"
13
+ },
14
+ "devDependencies": {
15
+ "@types/k6": "1.1.1"
16
+ }
17
+ }
@@ -0,0 +1,43 @@
1
+ import http from 'k6/http';
2
+ import { sleep } from 'k6';
3
+ // import { Options } from 'k6/options';
4
+
5
+ export const options = {
6
+ vus: 3,
7
+ iterations: 10,
8
+ duration: '3s'
9
+ };
10
+
11
+ // // The default exported function is gonna be picked up by k6 as the entry point for the test script. It will be executed repeatedly in "iterations" for the whole duration of the test.
12
+ // export default function () {
13
+ // // Make a GET request to the target URL
14
+ // http.get('https://quickpizza.grafana.com');
15
+ //
16
+ // // Sleep for 1 second to simulate real-world usage
17
+ // sleep(1);
18
+ // }
19
+
20
+
21
+ // const url = 'http://localhost:4000/parties/MSISDN/1233641534556178';
22
+ const url = 'https://quickpizza.grafana.com/api/put';
23
+
24
+ export default function () {
25
+ // const headers = {
26
+ // 'Content-Type': 'application/json',
27
+ // Accept: 'application/vnd.interoperability.iso20022.parties+json;version=2.0'
28
+ // };
29
+ // const data = {};
30
+ const headers = { 'Content-Type': 'application/json' };
31
+ const data = { name: 'Bert' };
32
+
33
+ const res = http.put(url, JSON.stringify(data), { headers });
34
+
35
+ let body = {};
36
+ try {
37
+ body = JSON.parse(res.body)
38
+ } catch {}
39
+
40
+ console.log(body);
41
+
42
+ sleep(0.5)
43
+ }
@@ -17,6 +17,7 @@ services:
17
17
  - "4000:4000"
18
18
  - "4001:4001"
19
19
  - "4002:4002"
20
+ - "4004:4004"
20
21
  - "9229:9229"
21
22
  depends_on:
22
23
  redis:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mojaloop/sdk-scheme-adapter-api-svc",
3
- "version": "21.0.0-snapshot.49",
3
+ "version": "21.0.0-snapshot.52",
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",
@@ -96,13 +96,13 @@
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.0",
99
+ "redis": "5.8.1",
100
100
  "uuidv4": "6.2.13",
101
101
  "ws": "8.18.3"
102
102
  },
103
103
  "devDependencies": {
104
- "@babel/core": "7.28.0",
105
- "@babel/preset-env": "7.28.0",
104
+ "@babel/core": "7.28.3",
105
+ "@babel/preset-env": "7.28.3",
106
106
  "@redocly/openapi-cli": "1.0.0-beta.95",
107
107
  "@types/jest": "30.0.0",
108
108
  "axios-mock-adapter": "2.1.0",
@@ -115,7 +115,7 @@
115
115
  "jest-junit": "16.0.0",
116
116
  "npm-check-updates": "16.7.10",
117
117
  "openapi-response-validator": "12.1.3",
118
- "openapi-typescript": "7.8.0",
118
+ "openapi-typescript": "7.9.1",
119
119
  "redis-mock": "0.56.3",
120
120
  "replace": "1.2.2",
121
121
  "standard-version": "9.5.0",
@@ -1079,7 +1079,7 @@ const patchFxTransfersById = async (ctx) => {
1079
1079
 
1080
1080
  const model = createInboundTransfersModel(ctx);
1081
1081
 
1082
- const response = await model.sendFxPatchNotificationToBackend(req.data, idValue);
1082
+ const response = await model.sendFxPutNotificationToBackend(req.data, idValue);
1083
1083
 
1084
1084
  // log the result
1085
1085
  ctx.state.logger.isDebugEnabled && ctx.state.logger.push({response}).
@@ -27,6 +27,8 @@
27
27
  'use strict';
28
28
 
29
29
  const redis = require('redis');
30
+ const EventEmitter = require('events');
31
+ const { TimeoutError } = require('./model/common/TimeoutError');
30
32
 
31
33
  const CONN_ST = {
32
34
  CONNECTED: 'CONNECTED',
@@ -41,6 +43,7 @@ const CONN_ST = {
41
43
  class Cache {
42
44
  constructor(config) {
43
45
  this._config = config;
46
+ this._channelEmitter = new EventEmitter();
44
47
 
45
48
  if(!config.cacheUrl || !config.logger) {
46
49
  throw new Error('Cache config requires cacheUrl and logger properties');
@@ -196,6 +199,109 @@ class Cache {
196
199
  return id;
197
200
  }
198
201
 
202
+ /**
203
+ * Subscribes to a channel and waits for a single message with timeout support.
204
+ *
205
+ * NOTE:
206
+ * This implementation uses EventEmitter to handle Redis pub/sub concurrency issues
207
+ * that occur when multiple subscribers listen to the same channel simultaneously.
208
+ * It's designed to prevent race conditions in party lookups where concurrent requests
209
+ * for the same party ID could interfere with each other.
210
+ * Currently used for: Party lookup operations
211
+ * Future potential: This function can be extended to other scenarios and potentially
212
+ * replace the existing subscribe, unsubscribe, and subscribeToOneMessageWithTimer
213
+ * functions for a more robust and concurrency-safe pub/sub implementation.
214
+ *
215
+ * @param {string} channel - The channel name to subscribe to
216
+ * @param {number} requestProcessingTimeoutSeconds - Timeout in seconds before rejecting with TimeoutError
217
+ * @param {boolean} [needParse=true] - Whether to JSON.parse the received message
218
+ *
219
+ * @returns {Promise<any>} Promise that resolves with the message or rejects with TimeoutError/Error
220
+ */
221
+ async subscribeToOneMessageWithTimerNew(channel, requestProcessingTimeoutSeconds, needParse = true) {
222
+ return new Promise((resolve, reject) => {
223
+ let timeoutHandle = null;
224
+ let subscription = null;
225
+ let isResolved = false;
226
+
227
+ // Helper to safely unsubscribe from Redis channel
228
+ const unsubscribeFromRedis = async (reason = 'cleanup') => {
229
+ if (this._channelEmitter.listenerCount(channel) === 0 && this._subscriptionClient) {
230
+ try {
231
+ await this._subscriptionClient.unsubscribe(channel);
232
+ this._logger.push({ channel, reason }).debug('Unsubscribed from Redis channel');
233
+ } catch (unsubscribeErr) {
234
+ this._logger.push({ channel, reason }).warn('Failed to unsubscribe from Redis channel', unsubscribeErr);
235
+ }
236
+ }
237
+ };
238
+
239
+ // Helper to clean up resources and prevent multiple resolutions
240
+ const cleanup = () => {
241
+ if (timeoutHandle) {
242
+ clearTimeout(timeoutHandle);
243
+ timeoutHandle = null;
244
+ }
245
+ if (subscription) {
246
+ this._channelEmitter.removeListener(channel, subscription);
247
+ subscription = null;
248
+ }
249
+ };
250
+
251
+ // Set up timeout handler
252
+ timeoutHandle = setTimeout(async () => {
253
+ if (isResolved) return;
254
+ isResolved = true;
255
+
256
+ cleanup();
257
+ await unsubscribeFromRedis('timeout');
258
+
259
+ const errMessage = `Subscription timeout after ${requestProcessingTimeoutSeconds}s`;
260
+ this._logger.push({ channel, timeout: requestProcessingTimeoutSeconds }).warn(errMessage);
261
+ reject(new TimeoutError(errMessage));
262
+ }, requestProcessingTimeoutSeconds * 1000);
263
+
264
+ // Set up message handler
265
+ subscription = (message) => {
266
+ if (isResolved) return;
267
+ isResolved = true;
268
+
269
+ this._logger.push({ channel, needParse }).debug('Received message on subscribed channel');
270
+
271
+ cleanup();
272
+
273
+ try {
274
+ const result = needParse ? JSON.parse(message) : message;
275
+ resolve(result);
276
+ } catch (parseErr) {
277
+ this._logger.push({ channel, message }).error('Failed to parse received message', parseErr);
278
+ reject(parseErr);
279
+ }
280
+ };
281
+
282
+ // Register the one-time listener
283
+ this._channelEmitter.once(channel, subscription);
284
+
285
+ // Subscribe to Redis channel
286
+ this._subscriptionClient.subscribe(channel, (msg) => {
287
+ this._channelEmitter.emit(channel, msg);
288
+
289
+ // Auto-unsubscribe if no more listeners
290
+ unsubscribeFromRedis('auto-cleanup').catch(err => {
291
+ this._logger.push({ channel }).warn('Auto-unsubscribe failed', err);
292
+ });
293
+ })
294
+ .catch(subscribeErr => {
295
+ if (isResolved) return;
296
+ isResolved = true;
297
+
298
+ cleanup();
299
+ this._logger.push({ channel }).error('Failed to subscribe to Redis channel', subscribeErr);
300
+ reject(subscribeErr);
301
+ });
302
+ });
303
+ }
304
+
199
305
  /**
200
306
  * Subscribes to a channel for some period and always returns resolved promise
201
307
  *
@@ -953,8 +953,10 @@ class InboundTransfersModel {
953
953
  }
954
954
  }
955
955
 
956
- async sendFxPatchNotificationToBackend(body, conversionId) {
956
+ async sendFxPutNotificationToBackend(body, conversionId) {
957
+ const log = this._logger.child({ conversionId });
957
958
  try {
959
+ log.verbose('sendFxPutNotificationToBackend payload: ', { body });
958
960
  this.data = await this.loadFxState(conversionId);
959
961
 
960
962
  if(!this.data) {
@@ -974,10 +976,10 @@ class InboundTransfersModel {
974
976
 
975
977
  await this.saveFxState();
976
978
 
977
- const res = await this._backendRequests.patchFxTransfersNotification(this.data,conversionId);
979
+ const res = await this._backendRequests.putFxTransfersNotification(body, conversionId);
978
980
  return res;
979
981
  } catch (err) {
980
- this._logger.isErrorEnabled && this._logger.push({ err, conversionId }).error(`Error notifying backend of final conversionId state equal to: ${body.conversionState} `);
982
+ log.error('error in sendFxPutNotificationToBackend: ', err);
981
983
  }
982
984
  }
983
985
  /**
@@ -40,6 +40,7 @@ const PartiesModel = require('./PartiesModel');
40
40
  const {
41
41
  AmountTypes,
42
42
  BackendError,
43
+ TimeoutError,
43
44
  CacheKeyPrefixes,
44
45
  Directions,
45
46
  ErrorMessages,
@@ -321,147 +322,121 @@ class OutboundTransfersModel {
321
322
 
322
323
  let latencyTimerDone;
323
324
 
324
- // hook up a subscriber to handle response messages
325
- const subId = await this._cache.subscribe(payeeKey, (cn, msg, subId) => {
326
- try {
327
- if(latencyTimerDone) {
328
- latencyTimerDone();
329
- }
330
- this.metrics.partyLookupResponses.inc();
331
-
332
- this.data.getPartiesResponse = JSON.parse(msg);
333
- if (this.data.getPartiesResponse.body?.errorInformation) {
334
- // this is an error response to our GET /parties request
335
- const err = new BackendError(`Got an error response resolving party: ${safeStringify(this.data.getPartiesResponse.body, { depth: Infinity })}`, 500);
336
- err.mojaloopError = this.data.getPartiesResponse.body;
337
- // cancel the timeout handler
338
- clearTimeout(timeout);
339
- return reject(err);
340
- }
341
- let payee = this.data.getPartiesResponse.body;
342
-
343
- if(!payee.party) {
344
- // we should never get a non-error response without a party, but just in case...
345
- // cancel the timeout handler
346
- clearTimeout(timeout);
347
- return reject(new Error(`Resolved payee has no party object: ${safeStringify(payee)}`));
348
- }
349
-
350
- payee = payee.party;
351
-
352
- // cancel the timeout handler
353
- clearTimeout(timeout);
354
-
355
- this._logger.isVerboseEnabled && this._logger.push({ payee }).verbose('Payee resolved');
325
+ // now we have a timeout handler and a cache subscriber hooked up we can fire off
326
+ // a GET /parties request to the switch
327
+ try {
328
+ const channel = payeeKey;
329
+ const subscribing = this._cache.subscribeToOneMessageWithTimerNew(channel, this._requestProcessingTimeoutSeconds);
356
330
 
357
- // stop listening for payee resolution messages
358
- // no need to await for the unsubscribe to complete.
359
- // we dont really care if the unsubscribe fails but we should log it regardless
360
- this._cache.unsubscribe(payeeKey, subId, true).catch(e => {
361
- this._logger.isErrorEnabled && this._logger.error(`Error unsubscribing (in callback) ${payeeKey} ${subId}: ${e.stack || safeStringify(e)}`);
362
- });
331
+ latencyTimerDone = this.metrics.partyLookupLatency.startTimer();
332
+ const res = await this._requests.getParties(
333
+ this.data.to.idType,
334
+ this.data.to.idValue,
335
+ this.data.to.idSubValue,
336
+ this.data.to.fspId,
337
+ this.#createOtelHeaders()
338
+ );
363
339
 
364
- // check we got the right payee and info we need
365
- if(payee.partyIdInfo.partyIdType !== this.data.to.idType) {
366
- const err = new Error(`Expecting resolved payee party IdType to be ${this.data.to.idType} but got ${payee.partyIdInfo.partyIdType}`);
367
- return reject(err);
368
- }
340
+ this.data.getPartiesRequest = res.originalRequest;
369
341
 
370
- if(payee.partyIdInfo.partyIdentifier !== this.data.to.idValue) {
371
- const err = new Error(`Expecting resolved payee party identifier to be ${this.data.to.idValue} but got ${payee.partyIdInfo.partyIdentifier}`);
372
- return reject(err);
373
- }
342
+ this.metrics.partyLookupRequests.inc();
343
+ this._logger.push({ peer: res }).debug('Party lookup sent to peer');
374
344
 
375
- if(payee.partyIdInfo.partySubIdOrType !== this.data.to.idSubValue) {
376
- const err = new Error(`Expecting resolved payee party subTypeId to be ${this.data.to.idSubValue} but got ${payee.partyIdInfo.partySubIdOrType}`);
377
- return reject(err);
378
- }
345
+ const message = await subscribing;
379
346
 
380
- if(!payee.partyIdInfo.fspId) {
381
- const err = new Error(`Expecting resolved payee party to have an FSPID: ${safeStringify(payee.partyIdInfo)}`);
382
- return reject(err);
383
- }
347
+ if(latencyTimerDone) {
348
+ latencyTimerDone();
349
+ }
350
+ this.metrics.partyLookupResponses.inc();
384
351
 
385
- // now we got the payee, add the details to our data so we can use it
386
- // in the quote request
387
- this.data.to.fspId = payee.partyIdInfo.fspId;
388
- if(payee.partyIdInfo.extensionList) {
389
- this.data.to.extensionList = payee.partyIdInfo.extensionList.extension;
390
- }
391
- if(payee.personalInfo) {
392
- if(payee.personalInfo.complexName) {
393
- this.data.to.firstName = payee.personalInfo.complexName.firstName || this.data.to.firstName;
394
- this.data.to.middleName = payee.personalInfo.complexName.middleName || this.data.to.middleName;
395
- this.data.to.lastName = payee.personalInfo.complexName.lastName || this.data.to.lastName;
396
- }
397
- this.data.to.dateOfBirth = payee.personalInfo.dateOfBirth;
398
- }
352
+ this.data.getPartiesResponse = message;
353
+ if (this.data.getPartiesResponse.body?.errorInformation) {
354
+ // this is an error response to our GET /parties request
355
+ const err = new BackendError(`Got an error response resolving party: ${safeStringify(this.data.getPartiesResponse.body, { depth: Infinity })}`, 500);
356
+ err.mojaloopError = this.data.getPartiesResponse.body;
357
+ return reject(err);
358
+ }
359
+ let payee = this.data.getPartiesResponse.body;
399
360
 
400
- if (Array.isArray(payee.supportedCurrencies)) {
401
- if (!payee.supportedCurrencies.length) {
402
- throw new Error(ErrorMessages.noSupportedCurrencies);
403
- }
361
+ if(!payee.party) {
362
+ // we should never get a non-error response without a party, but just in case...
363
+ return reject(new Error(`Resolved payee has no party object: ${safeStringify(payee)}`));
364
+ }
404
365
 
405
- this.data.needFx = this._isFxNeeded(this._supportedCurrencies, payee.supportedCurrencies, this.data.currency, this.data.amountType);
406
- this.data.supportedCurrencies = payee.supportedCurrencies;
407
- }
366
+ payee = payee.party;
408
367
 
409
- this._logger.isVerboseEnabled && this._logger.push({
410
- transferId: this.data.transferId,
411
- homeTransactionId: this.data.homeTransactionId,
412
- needFx: this.data.needFx,
413
- }).verbose('Payee validation passed');
368
+ this._logger.push({ payee }).verbose('Payee resolved');
414
369
 
415
- return resolve(payee);
370
+ // check we got the right payee and info we need
371
+ if(payee.partyIdInfo.partyIdType !== this.data.to.idType) {
372
+ const err = new Error(`Expecting resolved payee party IdType to be ${this.data.to.idType} but got ${payee.partyIdInfo.partyIdType}`);
373
+ return reject(err);
416
374
  }
417
- catch (err) {
375
+
376
+ if(payee.partyIdInfo.partyIdentifier !== this.data.to.idValue) {
377
+ const err = new Error(`Expecting resolved payee party identifier to be ${this.data.to.idValue} but got ${payee.partyIdInfo.partyIdentifier}`);
418
378
  return reject(err);
419
379
  }
420
- });
421
380
 
422
- // set up a timeout for the resolution
423
- const timeout = setTimeout(() => {
424
- const err = new BackendError(`Timeout resolving payee for transfer ${this.data.transferId}`, 504);
381
+ if(payee.partyIdInfo.partySubIdOrType !== this.data.to.idSubValue) {
382
+ const err = new Error(`Expecting resolved payee party subTypeId to be ${this.data.to.idSubValue} but got ${payee.partyIdInfo.partySubIdOrType}`);
383
+ return reject(err);
384
+ }
425
385
 
426
- // we dont really care if the unsubscribe fails but we should log it regardless
427
- this._cache.unsubscribe(payeeKey, subId, true).catch(e => {
428
- this._logger.isErrorEnabled && this._logger.error(`Error unsubscribing (in timeout handler) ${payeeKey} ${subId}: ${e.stack || safeStringify(e)}`);
429
- });
386
+ if(!payee.partyIdInfo.fspId) {
387
+ const err = new Error(`Expecting resolved payee party to have an FSPID: ${safeStringify(payee.partyIdInfo)}`);
388
+ return reject(err);
389
+ }
430
390
 
431
- if(latencyTimerDone) {
432
- latencyTimerDone();
391
+ // now we got the payee, add the details to our data so we can use it
392
+ // in the quote request
393
+ this.data.to.fspId = payee.partyIdInfo.fspId;
394
+ if(payee.partyIdInfo.extensionList) {
395
+ this.data.to.extensionList = payee.partyIdInfo.extensionList.extension;
396
+ }
397
+ if(payee.personalInfo) {
398
+ if(payee.personalInfo.complexName) {
399
+ this.data.to.firstName = payee.personalInfo.complexName.firstName || this.data.to.firstName;
400
+ this.data.to.middleName = payee.personalInfo.complexName.middleName || this.data.to.middleName;
401
+ this.data.to.lastName = payee.personalInfo.complexName.lastName || this.data.to.lastName;
402
+ }
403
+ this.data.to.dateOfBirth = payee.personalInfo.dateOfBirth;
433
404
  }
434
405
 
435
- return reject(err);
436
- }, this._requestProcessingTimeoutSeconds * 1000);
406
+ if (Array.isArray(payee.supportedCurrencies)) {
407
+ if (!payee.supportedCurrencies.length) {
408
+ throw new Error(ErrorMessages.noSupportedCurrencies);
409
+ }
437
410
 
438
- // now we have a timeout handler and a cache subscriber hooked up we can fire off
439
- // a GET /parties request to the switch
440
- try {
441
- latencyTimerDone = this.metrics.partyLookupLatency.startTimer();
442
- const res = await this._requests.getParties(
443
- this.data.to.idType,
444
- this.data.to.idValue,
445
- this.data.to.idSubValue,
446
- this.data.to.fspId,
447
- this.#createOtelHeaders()
448
- );
411
+ this.data.needFx = this._isFxNeeded(this._supportedCurrencies, payee.supportedCurrencies, this.data.currency, this.data.amountType);
412
+ this.data.supportedCurrencies = payee.supportedCurrencies;
413
+ }
449
414
 
450
- this.data.getPartiesRequest = res.originalRequest;
415
+ this._logger.push({
416
+ transferId: this.data.transferId,
417
+ homeTransactionId: this.data.homeTransactionId,
418
+ needFx: this.data.needFx,
419
+ }).verbose('Payee validation passed');
451
420
 
452
- this.metrics.partyLookupRequests.inc();
453
- this._logger.isDebugEnabled && this._logger.push({ peer: res }).debug('Party lookup sent to peer');
421
+ return resolve(payee);
454
422
  }
455
423
  catch(err) {
456
- // cancel the timeout and unsubscribe before rejecting the promise
457
- clearTimeout(timeout);
458
-
459
- // we dont really care if the unsubscribe fails but we should log it regardless
460
- this._cache.unsubscribe(payeeKey, subId, true).catch(e => {
461
- this._logger.isErrorEnabled && this._logger.error(`Error unsubscribing ${payeeKey} ${subId}: ${e.stack || safeStringify(e)}`);
462
- });
463
-
464
- return reject(err);
424
+ this._logger.error(`Error in resolvePayee ${payeeKey}:`, err);
425
+ // If type of error is BackendError, it will be handled by the state machine
426
+ if (err instanceof BackendError) {
427
+ this.data.lastError = err;
428
+ return reject(err);
429
+ }
430
+ // Check if the error is a TimeoutError, and if so, reject with a BackendError
431
+ if (err instanceof TimeoutError) {
432
+ const error = new BackendError(`Timeout resolving payee for transfer ${this.data.transferId}`, 504);
433
+ this.data.lastError = error;
434
+ return reject(error);
435
+ }
436
+ // otherwise, just throw a generic error
437
+ const error = new BackendError(`Error resolving payee for transfer ${this.data.transferId}: ${err.message}`, 500);
438
+ this.data.lastError = error;
439
+ return reject(error);
465
440
  }
466
441
  });
467
442
  }