account-lookup-service 17.8.0-snapshot.9 → 17.9.0-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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [17.8.0](https://github.com/mojaloop/account-lookup-service/compare/v17.7.1...v17.8.0) (2025-04-03)
6
+
7
+
8
+ ### Features
9
+
10
+ * **csi-1266:** reset party mappings in inter-scheme scenario ([#541](https://github.com/mojaloop/account-lookup-service/issues/541)) ([279fac6](https://github.com/mojaloop/account-lookup-service/commit/279fac6bff5e9a30c0583e1bcfb724787590516c))
11
+
12
+ ### [17.7.1](https://github.com/mojaloop/account-lookup-service/compare/v17.7.0...v17.7.1) (2025-03-27)
13
+
14
+
15
+ ### Chore
16
+
17
+ * **csi-1248:** update transform-lib, others ([#543](https://github.com/mojaloop/account-lookup-service/issues/543)) ([7a0a5c4](https://github.com/mojaloop/account-lookup-service/commit/7a0a5c402e8f8f28661540ea5982b1ac9a97a8dd))
18
+
5
19
  ## [17.7.0](https://github.com/mojaloop/account-lookup-service/compare/v17.6.0...v17.7.0) (2025-03-26)
6
20
 
7
21
 
@@ -38,24 +38,26 @@ app.get('/health', (req: Request, res: Response) => {
38
38
  app.get('/parties/:type/:id', (req: Request, res: Response) => {
39
39
  const { type, id } = req.params;
40
40
  const headers = hubCbHeaders(req.headers);
41
- console.log('parties request details:', { type, id, headers, CL_HOST, CL_PORT });
41
+ const statusCode = detectStatusCode(req);
42
+ console.log('parties request details:', { type, id, headers, CL_HOST, CL_PORT, statusCode });
42
43
 
43
44
  // todo: reply to CL with party info
44
45
 
45
46
  res
46
47
  .set(headers)
47
- .status(202)
48
+ .status(statusCode)
48
49
  .json({ success: true });
49
50
  });
50
51
 
51
52
  app.put('/parties/:type/:id/error', (req: Request, res: Response) => {
52
53
  const { type, id } = req.params;
53
54
  const headers = dfspCbHeaders(req.headers);
54
- console.log('parties put error request details:', { type, id, headers, CL_HOST, CL_PORT });
55
+ const statusCode = detectStatusCode(req);
56
+ console.log('parties put error request details:', { type, id, headers, CL_HOST, CL_PORT, statusCode });
55
57
 
56
58
  res
57
59
  .set(headers)
58
- .status(200)
60
+ .status(statusCode)
59
61
  .json({ success: true });
60
62
  });
61
63
 
@@ -92,3 +94,10 @@ const httpsServer = http.createServer(app);
92
94
  httpsServer.listen(PROXY_PORT, () => {
93
95
  console.log(`Mock proxyAdapter "${PROXY_NAME}" is running on port ${PROXY_PORT}...`);
94
96
  });
97
+
98
+ const X_RESPONSE_STATUS_HEADER = 'x-response-status';
99
+
100
+ const detectStatusCode = (req: Request) =>
101
+ typeof req.headers[X_RESPONSE_STATUS_HEADER] === 'string'
102
+ ? parseInt(req.headers[X_RESPONSE_STATUS_HEADER], 10)
103
+ : (req.method === 'GET' ? 202 : 200);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "account-lookup-service",
3
3
  "description": "Account Lookup Service is used to validate Party and Participant lookups.",
4
- "version": "17.8.0-snapshot.9",
4
+ "version": "17.9.0-snapshot.0",
5
5
  "license": "Apache-2.0",
6
6
  "author": "ModusBox",
7
7
  "contributors": [
@@ -92,21 +92,21 @@
92
92
  "@hapi/vision": "7.0.3",
93
93
  "@mojaloop/central-services-error-handling": "13.0.7",
94
94
  "@mojaloop/central-services-health": "15.0.4",
95
- "@mojaloop/central-services-logger": "11.8.0",
95
+ "@mojaloop/central-services-logger": "11.8.1",
96
96
  "@mojaloop/central-services-metrics": "12.5.0",
97
- "@mojaloop/central-services-shared": "18.23.1",
97
+ "@mojaloop/central-services-shared": "18.23.2",
98
98
  "@mojaloop/central-services-stream": "11.5.2",
99
99
  "@mojaloop/database-lib": "11.1.4",
100
100
  "@mojaloop/event-sdk": "14.4.0",
101
- "@mojaloop/inter-scheme-proxy-cache-lib": "2.4.0",
102
- "@mojaloop/ml-schema-transformer-lib": "2.7.0",
101
+ "@mojaloop/inter-scheme-proxy-cache-lib": "2.5.0",
102
+ "@mojaloop/ml-schema-transformer-lib": "2.7.1",
103
103
  "@mojaloop/sdk-standard-components": "19.11.3-snapshot.0",
104
104
  "@now-ims/hapi-now-auth": "2.1.0",
105
105
  "ajv": "8.17.1",
106
106
  "ajv-keywords": "5.1.0",
107
107
  "blipp": "4.0.2",
108
108
  "commander": "13.1.0",
109
- "cron": "4.1.1",
109
+ "cron": "4.1.4",
110
110
  "fast-safe-stringify": "^2.1.1",
111
111
  "hapi-auth-bearer-token": "8.0.0",
112
112
  "joi": "17.13.3",
package/src/constants.js CHANGED
@@ -29,12 +29,14 @@ const { API_TYPES } = require('@mojaloop/central-services-shared').Util.Hapi
29
29
 
30
30
  const ERROR_MESSAGES = Object.freeze({
31
31
  emptyFilteredPartyList: 'Empty oracle partyList, filtered based on callbackEndpointType',
32
+ externalPartyError: 'External party error', // todo: think better message
32
33
  failedToCacheSendToProxiesList: 'Failed to cache sendToProxiesList',
33
34
  noDiscoveryRequestsForwarded: 'No discovery requests forwarded to participants',
34
35
  sourceFspNotFound: 'Requester FSP not found',
35
36
  partyDestinationFspNotFound: 'Destination FSP not found',
36
37
  partyProxyNotFound: 'Proxy not found',
37
- proxyConnectionError: 'Proxy connection error - no successful requests sent to proxies'
38
+ proxyConnectionError: 'Proxy connection error - no successful requests sent to proxies',
39
+ noSuccessfulProxyDiscoveryResponses: 'No successful proxy discovery responses'
38
40
  })
39
41
 
40
42
  const HANDLER_TYPES = Object.freeze({
@@ -97,15 +97,7 @@ const putPartiesErrorByTypeAndID = async (headers, params, payload, dataUri, spa
97
97
  let fspiopError
98
98
 
99
99
  try {
100
- const needDiscovery = await service.handleRequest()
101
- if (needDiscovery) {
102
- const getPartiesService = new services.GetPartiesService(deps, inputs)
103
- await getPartiesService.triggerInterSchemeDiscoveryFlow(
104
- services.GetPartiesService.headersWithoutDestination(headers)
105
- )
106
- // think, if we need to start the whole processing with getPartiesService.handleRequest() ?
107
- }
108
-
100
+ await service.handleRequest()
109
101
  logger.info('putPartiesErrorByTypeAndID is done')
110
102
  histTimerEnd({ success: true })
111
103
  } catch (error) {
@@ -30,6 +30,7 @@ const { Enum } = require('@mojaloop/central-services-shared')
30
30
  const { decodePayload } = require('@mojaloop/central-services-shared').Util.StreamingProtocol
31
31
  const { initStepState } = require('../../../lib/util')
32
32
  const { createCallbackHeaders } = require('../../../lib/headers')
33
+ const { ERROR_MESSAGES } = require('../../../constants')
33
34
 
34
35
  const { FspEndpointTypes, FspEndpointTemplates } = Enum.EndPoints
35
36
  const { Headers, RestMethods } = Enum.Http
@@ -100,16 +101,17 @@ class BasePartiesService {
100
101
  get state () { return this.#state }
101
102
 
102
103
  async handleError (error) {
103
- const { headers, params } = this.inputs
104
+ const { params } = this.inputs
104
105
  const log = this.log.child({ method: 'handleError' })
105
106
  try {
106
107
  log.error('error in processing parties request: ', error)
107
108
  const fspiopError = ErrorHandler.Factory.reformatFSPIOPError(error)
108
- const errorInfo = await this.deps.partiesUtils.makePutPartiesErrorPayload(this.deps.config, fspiopError, headers, params)
109
+ const callbackHeaders = BasePartiesService.createErrorCallbackHeaders(this.inputs.headers, params)
110
+ const errorInfo = await this.deps.partiesUtils.makePutPartiesErrorPayload(this.deps.config, fspiopError, callbackHeaders, params)
109
111
 
110
112
  await this.sendErrorCallback({
111
113
  errorInfo,
112
- headers: BasePartiesService.createErrorCallbackHeaders(headers, params),
114
+ headers: callbackHeaders,
113
115
  params
114
116
  })
115
117
  log.info('handleError in done')
@@ -122,13 +124,39 @@ class BasePartiesService {
122
124
  }
123
125
 
124
126
  async validateParticipant (participantId) {
125
- this.stepInProgress('validateParticipant')
126
- return this.deps.participant.validateParticipant(participantId)
127
+ try {
128
+ this.stepInProgress('validateParticipant')
129
+ return this.deps.participant.validateParticipant(participantId)
130
+ } catch (err) {
131
+ this.log.warn(`error in validateParticipant ${participantId}: `, err)
132
+ return null
133
+ }
134
+ }
135
+
136
+ async identifyDestinationForCallback () {
137
+ this.stepInProgress('identifyDestinationForCallback')
138
+ const { destination } = this.state
139
+
140
+ const schemeParticipant = await this.validateParticipant(destination)
141
+ if (schemeParticipant) {
142
+ this.state.requester = destination
143
+ return
144
+ }
145
+
146
+ const proxyName = this.state.proxyEnabled && await this.deps.proxyCache.lookupProxyByDfspId(destination)
147
+ if (proxyName) {
148
+ this.state.requester = proxyName
149
+ return
150
+ }
151
+
152
+ const errMessage = ERROR_MESSAGES.partyDestinationFspNotFound
153
+ this.log.warn(`${errMessage} and no proxy`, { destination })
154
+ throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.DESTINATION_FSP_ERROR, errMessage)
127
155
  }
128
156
 
129
157
  async sendErrorCallback ({ errorInfo, headers, params }) {
130
158
  this.stepInProgress('sendErrorCallback')
131
- const sendTo = this.state.requester || this.state.source
159
+ const sendTo = this.state.requester || headers[Headers.FSPIOP.DESTINATION] /* || this.state.source */
132
160
  const endpointType = this.deps.partiesUtils.errorPartyCbType(params.SubId)
133
161
 
134
162
  await this.deps.participant.sendErrorToParticipant(
@@ -143,6 +171,7 @@ class BasePartiesService {
143
171
  }
144
172
 
145
173
  async removeProxyGetPartiesTimeoutCache (alsReq) {
174
+ this.stepInProgress('removeProxyGetPartiesTimeoutCache')
146
175
  const isRemoved = await this.deps.proxyCache.removeProxyGetPartiesTimeout(alsReq, this.state.proxy)
147
176
  this.log.debug('removeProxyGetPartiesTimeoutCache is done', { isRemoved, alsReq })
148
177
  return isRemoved
@@ -153,6 +182,16 @@ class BasePartiesService {
153
182
  return ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
154
183
  }
155
184
 
185
+ createFspiopPartyNotFoundError (errMessage, log = this.log) {
186
+ log.warn(errMessage)
187
+ return ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.PARTY_NOT_FOUND, errMessage)
188
+ }
189
+
190
+ createFspiopServiceUnavailableError (errMessage, log = this.log) {
191
+ log.warn(errMessage)
192
+ return ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.SERVICE_CURRENTLY_UNAVAILABLE, errMessage)
193
+ }
194
+
156
195
  stepInProgress (stepName) {
157
196
  this.log.debug('step is in progress', { stepName })
158
197
  this.state.stepState?.inProgress(stepName)
@@ -192,8 +231,8 @@ class BasePartiesService {
192
231
  }
193
232
  }
194
233
 
195
- static createErrorCallbackHeaders (headers, params) {
196
- return createCallbackHeaders({
234
+ static createErrorCallbackHeaders (headers, params, overrideDestination = '') {
235
+ const cbHeaders = createCallbackHeaders({
197
236
  requestHeaders: headers,
198
237
  partyIdType: params.Type,
199
238
  partyIdentifier: params.ID,
@@ -201,6 +240,10 @@ class BasePartiesService {
201
240
  ? FspEndpointTemplates.PARTIES_SUB_ID_PUT_ERROR
202
241
  : FspEndpointTemplates.PARTIES_PUT_ERROR
203
242
  })
243
+ if (overrideDestination) {
244
+ cbHeaders[Headers.FSPIOP.DESTINATION] = overrideDestination
245
+ }
246
+ return cbHeaders
204
247
  }
205
248
 
206
249
  static createHubErrorCallbackHeaders (hubName, destination) {
@@ -148,10 +148,13 @@ class GetPartiesService extends BasePartiesService {
148
148
  }
149
149
 
150
150
  const alsReq = await this.#setProxyListToCache(proxyNames, source, params)
151
- const sentList = await this.#sendOutProxyRequests({ proxyNames, alsReq, headers, params })
151
+ const { sentList, wasLast } = await this.#sendOutProxyRequests({ proxyNames, alsReq, headers, params })
152
152
  if (sentList.length === 0) {
153
153
  throw super.createFspiopIdNotFoundError(ERROR_MESSAGES.proxyConnectionError, log)
154
154
  }
155
+ if (wasLast) {
156
+ throw super.createFspiopIdNotFoundError(ERROR_MESSAGES.noSuccessfulProxyDiscoveryResponses, log)
157
+ }
155
158
 
156
159
  log.info('triggerInterSchemeDiscoveryFlow is done:', { sentList, alsReq })
157
160
  return sentList
@@ -233,7 +236,7 @@ class GetPartiesService extends BasePartiesService {
233
236
  async #sendPartyNotFoundErrorCallback (headers) {
234
237
  const { params } = this.inputs
235
238
  const callbackHeaders = GetPartiesService.createErrorCallbackHeaders(headers, params)
236
- const fspiopError = super.createFspiopIdNotFoundError('No proxy found to start inter-scheme discovery flow')
239
+ const fspiopError = super.createFspiopPartyNotFoundError('No proxy found to start inter-scheme discovery flow')
237
240
  const errorInfo = await this.deps.partiesUtils.makePutPartiesErrorPayload(
238
241
  this.deps.config, fspiopError, callbackHeaders, params
239
242
  )
@@ -262,21 +265,23 @@ class GetPartiesService extends BasePartiesService {
262
265
  async #sendOutProxyRequests ({ proxyNames, alsReq, headers, params }) {
263
266
  this.stepInProgress('#sendOutProxyRequests')
264
267
  const sentList = []
268
+ let wasLast = false // if any failed proxy request was last
265
269
 
266
270
  const sendProxyRequest = (sendTo) => this.#forwardGetPartiesRequest({ sendTo, headers, params })
267
271
  .then(() => { sentList.push(sendTo) })
268
272
  .catch(err => {
269
273
  this.log.error(`error in sending request to proxy ${sendTo}: `, err)
270
- this.log.verbose(`remove proxy ${sendTo} from proxyCache...`)
274
+ this.log.verbose(`removing proxy ${sendTo} from proxyCache...`)
271
275
  return this.deps.proxyCache.receivedErrorResponse(alsReq, sendTo)
272
276
  })
277
+ .then((isLast) => { wasLast = isLast })
273
278
  .catch(err => {
274
279
  this.log.error(`failed to remove proxy ${sendTo} from proxyCache: `, err)
275
280
  })
276
281
  await Promise.all(proxyNames.map(sendProxyRequest))
277
282
 
278
- this.log.verbose('#sendOutProxyRequests is done:', { sentList, proxyNames })
279
- return sentList
283
+ this.log.verbose('#sendOutProxyRequests is done:', { sentList, wasLast, proxyNames })
284
+ return { sentList, wasLast }
280
285
  }
281
286
 
282
287
  async #getFilteredProxyList (proxy) {
@@ -25,35 +25,36 @@
25
25
  --------------
26
26
  ******/
27
27
 
28
- const ErrorHandler = require('@mojaloop/central-services-error-handling')
29
- const { ERROR_MESSAGES } = require('../../../constants')
30
28
  const BasePartiesService = require('./BasePartiesService')
29
+ const { ERROR_MESSAGES } = require('../../../constants')
31
30
 
32
31
  class PutPartiesErrorService extends BasePartiesService {
33
- /** @returns {Promise<true | undefined>} - If true, need to trigger inter-scheme discovery. */
34
32
  async handleRequest () {
35
33
  if (this.state.proxyEnabled && this.state.proxy) {
36
34
  const alsReq = this.deps.partiesUtils.alsRequestDto(this.state.destination, this.inputs.params) // or source?
37
- const isPending = await this.deps.proxyCache.isPendingCallback(alsReq)
38
-
39
- if (!isPending) {
40
- // not initial inter-scheme discovery case. Cleanup oracle and trigger inter-scheme discovery
41
- this.log.warn('Need to cleanup oracle and trigger new inter-scheme discovery flow')
42
- await this.cleanupOracle()
43
- await this.removeProxyGetPartiesTimeoutCache(alsReq)
44
- return true // need to trigger inter-scheme discovery
45
- }
46
-
47
- const isLast = await this.checkLastProxyCallback(alsReq)
48
- if (!isLast) {
49
- this.log.verbose('putPartiesErrorByTypeAndID proxy callback was processed')
50
- return
35
+ const isInterSchemeDiscoveryCase = await this.deps.proxyCache.isPendingCallback(alsReq)
36
+
37
+ if (isInterSchemeDiscoveryCase) {
38
+ const isLast = await this.checkLastProxyCallback(alsReq)
39
+ if (!isLast) {
40
+ this.log.verbose('proxy error callback was processed (not last)')
41
+ return
42
+ }
43
+ } else {
44
+ const schemeParticipant = await this.validateParticipant(this.state.destination)
45
+ if (schemeParticipant) {
46
+ this.log.info('Need to cleanup oracle and forward SERVICE_CURRENTLY_UNAVAILABLE error')
47
+ await this.cleanupOracle()
48
+ await this.removeProxyGetPartiesTimeoutCache(alsReq)
49
+ await this.forwardServiceUnavailableErrorCallback()
50
+ return
51
+ }
51
52
  }
52
53
  }
53
54
 
54
- await this.identifyDestinationForErrorCallback()
55
+ await super.identifyDestinationForCallback()
55
56
  await this.sendErrorCallbackToParticipant()
56
- this.log.info('putPartiesByTypeAndID is done')
57
+ this.log.info('handleRequest is done')
57
58
  }
58
59
 
59
60
  async cleanupOracle () {
@@ -72,32 +73,26 @@ class PutPartiesErrorService extends BasePartiesService {
72
73
  return isLast
73
74
  }
74
75
 
75
- async identifyDestinationForErrorCallback () {
76
- this.stepInProgress('identifyDestinationForErrorCallback')
77
- const { destination } = this.state
78
- const schemeParticipant = await super.validateParticipant(destination)
79
- if (schemeParticipant) {
80
- this.state.requester = destination
81
- return
82
- }
83
-
84
- this.stepInProgress('lookupProxyDestination-4')
85
- const proxyName = this.state.proxyEnabled && await this.deps.proxyCache.lookupProxyByDfspId(destination)
86
- if (proxyName) {
87
- this.state.requester = proxyName
88
- return
89
- }
90
-
91
- const errMessage = ERROR_MESSAGES.partyDestinationFspNotFound
92
- this.log.warn(errMessage, { destination })
93
- throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.DESTINATION_FSP_ERROR, errMessage)
94
- }
95
-
96
76
  async sendErrorCallbackToParticipant () {
97
77
  const { headers, params, dataUri } = this.inputs
98
78
  const errorInfo = PutPartiesErrorService.decodeDataUriPayload(dataUri)
99
79
  return super.sendErrorCallback({ errorInfo, headers, params })
100
80
  }
81
+
82
+ async forwardServiceUnavailableErrorCallback () {
83
+ this.stepInProgress('forwardServiceUnavailableErrorCallback')
84
+ const { headers, params } = this.inputs
85
+ const error = super.createFspiopServiceUnavailableError(ERROR_MESSAGES.externalPartyError)
86
+ const callbackHeaders = BasePartiesService.createErrorCallbackHeaders(headers, params, this.state.destination)
87
+ const errorInfo = await this.deps.partiesUtils.makePutPartiesErrorPayload(this.deps.config, error, callbackHeaders, params)
88
+
89
+ await super.sendErrorCallback({
90
+ errorInfo,
91
+ headers: callbackHeaders,
92
+ params
93
+ })
94
+ this.log.verbose('#forwardServiceUnavailableErrorCallback is done', { callbackHeaders, errorInfo })
95
+ }
101
96
  }
102
97
 
103
98
  module.exports = PutPartiesErrorService
@@ -25,7 +25,6 @@
25
25
  --------------
26
26
  ******/
27
27
 
28
- const ErrorHandler = require('@mojaloop/central-services-error-handling')
29
28
  const { ERROR_MESSAGES } = require('../../../constants')
30
29
  const BasePartiesService = require('./BasePartiesService')
31
30
 
@@ -40,7 +39,7 @@ class PutPartiesService extends BasePartiesService {
40
39
  if (proxy) {
41
40
  await this.checkProxySuccessResponse()
42
41
  }
43
- await this.identifyDestinationForSuccessCallback()
42
+ await this.identifyDestinationForCallback()
44
43
  await this.sendSuccessCallback()
45
44
  }
46
45
 
@@ -85,25 +84,6 @@ class PutPartiesService extends BasePartiesService {
85
84
  }
86
85
  }
87
86
 
88
- async identifyDestinationForSuccessCallback () {
89
- const { destination } = this.state
90
- this.stepInProgress('identifyDestinationForSuccessCallback')
91
- const destinationParticipant = await super.validateParticipant(destination)
92
- if (destinationParticipant) {
93
- this.state.requester = destinationParticipant.name
94
- return
95
- }
96
-
97
- const proxyName = this.state.proxyEnabled && await this.deps.proxyCache.lookupProxyByDfspId(destination)
98
- if (!proxyName) {
99
- const errMessage = ERROR_MESSAGES.partyDestinationFspNotFound
100
- this.log.warn(`${errMessage} and no proxy`, { destination })
101
- throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.DESTINATION_FSP_ERROR, errMessage)
102
- }
103
-
104
- this.state.requester = proxyName
105
- }
106
-
107
87
  async sendSuccessCallback () {
108
88
  const { headers, params, dataUri } = this.inputs
109
89
  const sendTo = this.state.requester
@@ -52,7 +52,7 @@ class TimeoutPartiesService extends PutPartiesErrorService {
52
52
  const { errorInfo, headers, params } = await this.prepareErrorInformation()
53
53
  this.#spanAuditStart(errorInfo)
54
54
 
55
- await this.identifyDestinationForErrorCallback()
55
+ await this.identifyDestinationForCallback()
56
56
  return super.sendErrorCallback({ errorInfo, headers, params })
57
57
  }
58
58
 
@@ -38,12 +38,12 @@ const { HANDLER_TYPES } = require('../constants')
38
38
  const Config = require('../lib/config')
39
39
  const log = require('../lib').logger.child('ALS-timeout-handler')
40
40
 
41
- process.on('uncaughtExceptionMonitor', (err) => {
42
- log.error(`uncaughtException: ${err?.message}`, err)
41
+ process.on('uncaughtException', (err, origin) => {
42
+ log.error(`uncaughtException event [origin: ${origin}]: `, err)
43
43
  process.exit(2)
44
44
  })
45
45
  process.on('unhandledRejection', (err) => {
46
- log.error(`unhandledRejection: ${err?.message}`, err)
46
+ log.error('unhandledRejection event: ', err)
47
47
  process.exit(3)
48
48
  })
49
49
 
package/src/index.js CHANGED
@@ -24,7 +24,8 @@
24
24
 
25
25
  'use strict'
26
26
 
27
- process.env.HTTP_DEFAULT_RETRY_DELAY = process.env.HTTP_DEFAULT_RETRY_DELAY || '0'
27
+ process.env.HTTP_DEFAULT_RETRIES = process.env.HTTP_DEFAULT_RETRIES || '0'
28
+ // todo: think better way to avoid reties
28
29
 
29
30
  const Server = require('./server')
30
31
  const PJson = require('../package.json')
package/src/lib/util.js CHANGED
@@ -97,8 +97,8 @@ const rethrowDatabaseError = (error) => {
97
97
  }
98
98
 
99
99
  const countFspiopError = (error, options) => {
100
- options.loggerOverride = logger
101
- rethrow.countFspiopError(error, options)
100
+ options.loggerOverride = options?.log || logger
101
+ return rethrow.countFspiopError(error, options)
102
102
  }
103
103
 
104
104
  /**
@@ -29,18 +29,19 @@
29
29
  const Mustache = require('mustache')
30
30
  const request = require('@mojaloop/central-services-shared').Util.Request
31
31
  const Enums = require('@mojaloop/central-services-shared').Enum
32
- const Logger = require('@mojaloop/central-services-logger')
33
32
  const ErrorHandler = require('@mojaloop/central-services-error-handling')
34
33
  const Metrics = require('@mojaloop/central-services-metrics')
35
34
 
36
35
  const Config = require('../../lib/config')
37
- const oracleEndpointCached = require('../oracle/oracleEndpointCached')
36
+ const { logger } = require('../../lib')
37
+ const { countFspiopError } = require('../../lib/util')
38
38
  const { hubNameRegex } = require('../../lib/util').hubNameConfig
39
+ const oracleEndpointCached = require('../oracle/oracleEndpointCached')
40
+
41
+ const { Headers, RestMethods, ReturnCodes } = Enums.Http
39
42
 
40
43
  /**
41
- * @function oracleRequest
42
- *
43
- * @description This sends a request to the oracles that are registered to the ALS
44
+ * Sends a request to the oracles that are registered to the ALS
44
45
  *
45
46
  * @param {object} headers - incoming http request headers
46
47
  * @param {string} method - incoming http request method
@@ -49,130 +50,134 @@ const { hubNameRegex } = require('../../lib/util').hubNameConfig
49
50
  * @param {object} payload - payload of the request being sent out
50
51
  * @param {object} assertPendingAcquire - flag to check DB pool pending acquire limit
51
52
  *
52
- * @returns {object} returns the response from the oracle
53
+ * @returns {object} - response from the oracle
53
54
  */
54
- exports.oracleRequest = async (headers, method, params = {}, query = {}, payload = undefined, cache, assertPendingAcquire) => {
55
+ const oracleRequest = async (headers, method, params = {}, query = {}, payload = undefined, cache, assertPendingAcquire) => {
56
+ const operation = oracleRequest.name
57
+ const log = logger.child({ component: operation, params })
58
+ let step = 'start'
59
+
55
60
  try {
56
- let url
57
- const partyIdType = params.Type
58
- const partyIdentifier = params.ID
59
- const currency = (payload && payload.currency) ? payload.currency : (query && query.currency) ? query.currency : undefined
60
- const partySubIdOrType = (params && params.SubId) ? params.SubId : (query && query.partySubIdOrType) ? query.partySubIdOrType : undefined
61
- const isGetRequest = method.toUpperCase() === Enums.Http.RestMethods.GET
62
- const isDeleteRequest = method.toUpperCase() === Enums.Http.RestMethods.DELETE
61
+ const source = headers[Headers.FSPIOP.SOURCE]
62
+ const destination = headers[Headers.FSPIOP.DESTINATION] || Config.HUB_NAME
63
+ const partySubIdOrType = params?.SubId || query?.partySubIdOrType
64
+ log.info('oracleRequest start...', { method, source, destination })
63
65
 
64
- if (currency && partySubIdOrType && isGetRequest) {
65
- url = await _getOracleEndpointByTypeCurrencyAndSubId(partyIdType, partyIdentifier, currency, partySubIdOrType, assertPendingAcquire)
66
- } else if (currency && isGetRequest) {
67
- url = await _getOracleEndpointByTypeAndCurrency(partyIdType, partyIdentifier, currency, assertPendingAcquire)
68
- } else if (partySubIdOrType && isGetRequest) {
69
- url = await _getOracleEndpointByTypeAndSubId(partyIdType, partyIdentifier, partySubIdOrType, assertPendingAcquire)
70
- } else {
71
- url = await _getOracleEndpointByType(partyIdType, partyIdentifier, assertPendingAcquire)
72
- if (partySubIdOrType) {
73
- payload = { ...payload, partySubIdOrType }
74
- }
66
+ step = 'determineOracleEndpoint'
67
+ const url = await determineOracleEndpoint({
68
+ method, params, query, payload, assertPendingAcquire
69
+ })
70
+ log.verbose(`Oracle endpoint: ${url}`)
71
+
72
+ if (method.toUpperCase() === RestMethods.GET) {
73
+ step = 'sendOracleGetRequest'
74
+ return await sendOracleGetRequest({
75
+ url, source, destination, headers, method, params, cache
76
+ })
75
77
  }
76
- Logger.isDebugEnabled && Logger.debug(`Oracle endpoints: ${url}`)
77
- const histTimerEnd = Metrics.getHistogram(
78
- 'egress_oracleRequest',
79
- 'Egress: oracleRequest',
80
- ['success', 'hit']
81
- ).startTimer()
82
- try {
83
- if (isGetRequest) {
84
- let cachedOracleFspResponse
85
- cachedOracleFspResponse = cache && cache.get(cache.createKey(`oracleSendRequest_${url}`))
86
- if (!cachedOracleFspResponse) {
87
- cachedOracleFspResponse = await request.sendRequest({
88
- url,
89
- headers,
90
- source: headers[Enums.Http.Headers.FSPIOP.SOURCE],
91
- destination: headers[Enums.Http.Headers.FSPIOP.DESTINATION] || Config.HUB_NAME,
92
- method: method.toUpperCase(),
93
- payload,
94
- hubNameRegex
95
- })
96
- // Trying to cache the whole response object will fail because it contains circular references
97
- // so we'll just cache the data property of the response.
98
- cachedOracleFspResponse = {
99
- data: cachedOracleFspResponse.data
100
- }
101
- cache && cache.set(
102
- cache.createKey(`oracleSendRequest_${url}`),
103
- cachedOracleFspResponse
104
- )
105
- histTimerEnd({ success: true, hit: false })
106
- } else {
107
- cachedOracleFspResponse = cachedOracleFspResponse.item
108
- histTimerEnd({ success: true, hit: true })
109
- Logger.isDebugEnabled && Logger.debug(`${new Date().toISOString()}, [oracleRequest]: cache hit for fsp for partyId lookup`)
110
- }
111
78
 
112
- return cachedOracleFspResponse
113
- }
79
+ if (partySubIdOrType && payload) payload.partySubIdOrType = partySubIdOrType
114
80
 
115
- if (isDeleteRequest && Config.DELETE_PARTICIPANT_VALIDATION_ENABLED) {
116
- // If the request is a DELETE request, we need to ensure that the participant belongs to the requesting FSP
117
- const getOracleResponse = await request.sendRequest({
118
- url,
119
- headers,
120
- source: headers[Enums.Http.Headers.FSPIOP.SOURCE],
121
- destination: headers[Enums.Http.Headers.FSPIOP.DESTINATION] || Config.HUB_NAME,
122
- method: Enums.Http.RestMethods.GET,
123
- payload,
124
- hubNameRegex
125
- })
81
+ if (method.toUpperCase() === RestMethods.DELETE && Config.DELETE_PARTICIPANT_VALIDATION_ENABLED) {
82
+ step = 'validatePartyDeletion'
83
+ await validatePartyDeletion({
84
+ url, source, destination, headers, method, params, payload
85
+ })
86
+ }
126
87
 
127
- if (getOracleResponse.status === Enums.Http.ReturnCodes.OK.CODE) {
128
- const participant = getOracleResponse.data
129
- if (participant?.partyList?.length > 0) {
130
- const party = participant.partyList[0]
131
- if (party.fspId === headers[Enums.Http.Headers.FSPIOP.SOURCE]) {
132
- return await request.sendRequest({
133
- url,
134
- headers,
135
- source: headers[Enums.Http.Headers.FSPIOP.SOURCE],
136
- destination: headers[Enums.Http.Headers.FSPIOP.DESTINATION] || Config.HUB_NAME,
137
- method: method.toUpperCase(),
138
- payload,
139
- hubNameRegex
140
- })
141
- } else {
142
- throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.DELETE_PARTY_INFO_ERROR, `The party ${partyIdType}:${partyIdentifier} does not belong to the requesting FSP`)
143
- }
144
- }
145
- }
88
+ step = 'sendRequest'
89
+ return await request.sendRequest({
90
+ url,
91
+ headers,
92
+ source,
93
+ destination,
94
+ method,
95
+ payload,
96
+ hubNameRegex
97
+ })
98
+ } catch (err) {
99
+ log.error('error in oracleRequest: ', err)
100
+ throw countFspiopError(err, { operation, step, log })
101
+ }
102
+ }
146
103
 
147
- throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.PARTY_NOT_FOUND)
148
- }
104
+ const determineOracleEndpoint = async ({
105
+ method, params, query, payload, assertPendingAcquire
106
+ }) => {
107
+ const partyIdType = params.Type
108
+ const partyIdentifier = params.ID
109
+ const partySubIdOrType = params?.SubId || query?.partySubIdOrType
110
+ const currency = payload?.currency || query?.currency
111
+ const isGetRequest = method.toUpperCase() === RestMethods.GET
112
+ let url
149
113
 
150
- return await request.sendRequest({
114
+ if (currency && partySubIdOrType && isGetRequest) {
115
+ url = await _getOracleEndpointByTypeCurrencyAndSubId(partyIdType, partyIdentifier, currency, partySubIdOrType, assertPendingAcquire)
116
+ } else if (currency && isGetRequest) {
117
+ url = await _getOracleEndpointByTypeAndCurrency(partyIdType, partyIdentifier, currency, assertPendingAcquire)
118
+ } else if (partySubIdOrType && isGetRequest) {
119
+ url = await _getOracleEndpointByTypeAndSubId(partyIdType, partyIdentifier, partySubIdOrType, assertPendingAcquire)
120
+ } else {
121
+ url = await _getOracleEndpointByType(partyIdType, partyIdentifier, assertPendingAcquire)
122
+ }
123
+ return url
124
+ }
125
+
126
+ const sendOracleGetRequest = async ({
127
+ url, source, destination, headers, method, params, cache
128
+ }) => {
129
+ const histTimerEnd = Metrics.getHistogram(
130
+ 'egress_oracleRequest',
131
+ 'Egress: oracleRequest',
132
+ ['success', 'hit']
133
+ ).startTimer()
134
+ const log = logger.child({ component: 'sendOracleGetRequest', params })
135
+
136
+ try {
137
+ let cachedOracleFspResponse
138
+ cachedOracleFspResponse = cache && cache.get(cache.createKey(`oracleSendRequest_${url}`))
139
+
140
+ if (!cachedOracleFspResponse) {
141
+ cachedOracleFspResponse = await request.sendRequest({
151
142
  url,
152
143
  headers,
153
- source: headers[Enums.Http.Headers.FSPIOP.SOURCE],
154
- destination: headers[Enums.Http.Headers.FSPIOP.DESTINATION] || Config.HUB_NAME,
155
- method: method.toUpperCase(),
156
- payload,
144
+ source,
145
+ destination,
146
+ method,
157
147
  hubNameRegex
158
148
  })
159
- } catch (err) {
160
- histTimerEnd({ success: false, hit: false })
161
- throw err
149
+ // Trying to cache the whole response object will fail because it contains circular references
150
+ // so we'll just cache the data property of the response.
151
+ cachedOracleFspResponse = {
152
+ data: cachedOracleFspResponse.data
153
+ }
154
+ cache && cache.set(
155
+ cache.createKey(`oracleSendRequest_${url}`),
156
+ cachedOracleFspResponse
157
+ )
158
+ histTimerEnd({ success: true, hit: false })
159
+ } else {
160
+ cachedOracleFspResponse = cachedOracleFspResponse.item
161
+ histTimerEnd({ success: true, hit: true })
162
+ logger.debug('[oracleRequest]: cache hit for fsp for partyId lookup')
162
163
  }
164
+
165
+ return cachedOracleFspResponse
163
166
  } catch (err) {
164
- const extensions = [{
165
- key: 'system',
166
- value: '["@hapi/catbox-memory","http"]'
167
- }]
168
- Logger.isErrorEnabled && Logger.error(`error in oracleRequest: ${err?.stack}`)
167
+ log.warn('error in sendOracleGetRequest: ', err)
168
+ histTimerEnd({ success: false, hit: false })
169
+
169
170
  // If the error was a 400 from the Oracle, we'll modify the error to generate a response to the
170
171
  // initiator of the request.
171
172
  if (
172
173
  err.name === 'FSPIOPError' &&
173
174
  err.apiErrorCode.code === ErrorHandler.Enums.FSPIOPErrorCodes.DESTINATION_COMMUNICATION_ERROR.code
174
175
  ) {
175
- if (err.extensions.some(ext => (ext.key === 'status' && ext.value === Enums.Http.ReturnCodes.BADREQUEST.CODE))) {
176
+ const extensions = [{
177
+ key: 'system',
178
+ value: '["@hapi/catbox-memory","http"]'
179
+ }]
180
+ if (err.extensions.some(ext => (ext.key === 'status' && ext.value === ReturnCodes.BADREQUEST.CODE))) {
176
181
  throw ErrorHandler.Factory.createFSPIOPError(
177
182
  ErrorHandler.Enums.FSPIOPErrorCodes.PARTY_NOT_FOUND,
178
183
  undefined,
@@ -180,10 +185,11 @@ exports.oracleRequest = async (headers, method, params = {}, query = {}, payload
180
185
  undefined,
181
186
  extensions
182
187
  )
183
- // Added error 404 to cover a special case of the Mowali implementation
184
- // which uses mojaloop/als-oracle-pathfinder and currently returns 404
185
- // and in which case the Mowali implementation expects back `DESTINATION_FSP_ERROR`.
186
- } else if (err.extensions.some(ext => (ext.key === 'status' && ext.value === Enums.Http.ReturnCodes.NOTFOUND.CODE))) {
188
+ }
189
+ // Added error 404 to cover a special case of the Mowali implementation
190
+ // which uses mojaloop/als-oracle-pathfinder and currently returns 404
191
+ // and in which case the Mowali implementation expects back `DESTINATION_FSP_ERROR`.
192
+ if (err.extensions.some(ext => (ext.key === 'status' && ext.value === ReturnCodes.NOTFOUND.CODE))) {
187
193
  throw ErrorHandler.Factory.createFSPIOPError(
188
194
  ErrorHandler.Enums.FSPIOPErrorCodes.DESTINATION_FSP_ERROR,
189
195
  undefined,
@@ -193,13 +199,47 @@ exports.oracleRequest = async (headers, method, params = {}, query = {}, payload
193
199
  )
194
200
  }
195
201
  }
196
- throw ErrorHandler.Factory.reformatFSPIOPError(
197
- err,
198
- undefined,
199
- undefined,
200
- extensions
201
- )
202
+
203
+ throw err
204
+ }
205
+ }
206
+
207
+ const validatePartyDeletion = async ({
208
+ url, source, destination, headers, method, params, payload
209
+ }) => {
210
+ const log = logger.child({ component: 'validatePartyDeletion', params })
211
+ // If the request is a DELETE request, we need to ensure that the participant belongs to the requesting FSP
212
+ const getParticipantResponse = await request.sendRequest({
213
+ url,
214
+ headers,
215
+ source,
216
+ destination,
217
+ method,
218
+ payload,
219
+ hubNameRegex
220
+ })
221
+
222
+ if (getParticipantResponse.status !== ReturnCodes.OK.CODE) {
223
+ const errMessage = `Invalid getOracleResponse status code: ${getParticipantResponse.status}`
224
+ log.warn(errMessage)
225
+ throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.PARTY_NOT_FOUND, errMessage)
226
+ // todo: clarify if we need to throw PARTY_NOT_FOUND
227
+ }
228
+
229
+ const participant = getParticipantResponse.data
230
+ if (!Array.isArray(participant?.partyList) || participant.partyList.length === 0) {
231
+ const errMessage = 'No participant found for the party'
232
+ log.warn(errMessage)
233
+ throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.DELETE_PARTY_INFO_ERROR, errMessage)
234
+ }
235
+
236
+ const party = participant.partyList[0] // todo: clarify why we check only the first party?
237
+ if (party.fspId !== source) {
238
+ const errMessage = `The party ${params.Type}:${params.ID} does not belong to the requesting FSP`
239
+ log.warn(errMessage)
240
+ throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.DELETE_PARTY_INFO_ERROR, errMessage)
202
241
  }
242
+ return true
203
243
  }
204
244
 
205
245
  /**
@@ -233,8 +273,10 @@ const _getOracleEndpointByTypeAndCurrency = async (partyIdType, partyIdentifier,
233
273
  )
234
274
  }
235
275
  } else {
236
- Logger.isErrorEnabled && Logger.error(`Oracle type:${partyIdType} and currency:${currency} not found`)
237
- throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ADD_PARTY_INFO_ERROR, `Oracle type:${partyIdType} and currency:${currency} not found`).toApiErrorObject(Config.ERROR_HANDLING)
276
+ const errMessage = `Oracle type:${partyIdType} and currency:${currency} not found`
277
+ logger.error(errMessage)
278
+ throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ADD_PARTY_INFO_ERROR, errMessage)
279
+ .toApiErrorObject(Config.ERROR_HANDLING)
238
280
  }
239
281
  return url
240
282
  }
@@ -269,8 +311,9 @@ const _getOracleEndpointByType = async (partyIdType, partyIdentifier, assertPend
269
311
  )
270
312
  }
271
313
  } else {
272
- Logger.isErrorEnabled && Logger.error(`Oracle type:${partyIdType} not found`)
273
- throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ADD_PARTY_INFO_ERROR, `Oracle type: ${partyIdType} not found`)
314
+ const errMessage = `Oracle type:${partyIdType} not found`
315
+ logger.error(errMessage)
316
+ throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ADD_PARTY_INFO_ERROR, errMessage)
274
317
  }
275
318
  return url
276
319
  }
@@ -306,8 +349,10 @@ const _getOracleEndpointByTypeAndSubId = async (partyIdType, partyIdentifier, pa
306
349
  )
307
350
  }
308
351
  } else {
309
- Logger.isErrorEnabled && Logger.error(`Oracle type: ${partyIdType} and subId: ${partySubIdOrType} not found`)
310
- throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ADD_PARTY_INFO_ERROR, `Oracle type: ${partyIdType} and subId: ${partySubIdOrType} not found`).toApiErrorObject(Config.ERROR_HANDLING)
352
+ const errMessage = `Oracle type: ${partyIdType} and subId: ${partySubIdOrType} not found`
353
+ logger.error(errMessage)
354
+ throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ADD_PARTY_INFO_ERROR, errMessage)
355
+ .toApiErrorObject(Config.ERROR_HANDLING)
311
356
  }
312
357
  return url
313
358
  }
@@ -344,16 +389,16 @@ const _getOracleEndpointByTypeCurrencyAndSubId = async (partyIdType, partyIdenti
344
389
  )
345
390
  }
346
391
  } else {
347
- Logger.isErrorEnabled && Logger.error(`Oracle type: ${partyIdType}, currency: ${currency}, and subId: ${partySubIdOrType} not found`)
348
- throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ADD_PARTY_INFO_ERROR, `Oracle type:${partyIdType}, currency:${currency} and subId: ${partySubIdOrType} not found`).toApiErrorObject(Config.ERROR_HANDLING)
392
+ const errMessage = `Oracle type: ${partyIdType}, currency: ${currency} and subId: ${partySubIdOrType} not found`
393
+ logger.error(errMessage)
394
+ throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ADD_PARTY_INFO_ERROR, errMessage)
395
+ .toApiErrorObject(Config.ERROR_HANDLING)
349
396
  }
350
397
  return url
351
398
  }
352
399
 
353
400
  /**
354
- * @function oracleBatchRequest
355
- *
356
- * @description This sends a request to the oracles that are registered to the ALS
401
+ * Sends a request to the oracles that are registered to the ALS
357
402
  *
358
403
  * @param {object} headers - incoming http request headers
359
404
  * @param {object} method - incoming http request method
@@ -361,9 +406,9 @@ const _getOracleEndpointByTypeCurrencyAndSubId = async (partyIdType, partyIdenti
361
406
  * @param {string} type - oracle type
362
407
  * @param {object} payload - the payload to send in the request
363
408
  *
364
- * @returns {object} returns the response from the oracle
409
+ * @returns {object} - response from the oracle
365
410
  */
366
- exports.oracleBatchRequest = async (headers, method, requestPayload, type, payload) => {
411
+ const oracleBatchRequest = async (headers, method, requestPayload, type, payload) => {
367
412
  try {
368
413
  let oracleEndpointModel
369
414
  let url
@@ -383,22 +428,27 @@ exports.oracleBatchRequest = async (headers, method, requestPayload, type, paylo
383
428
  } else {
384
429
  url = oracleEndpointModel[0].value + Enums.EndPoints.FspEndpointTemplates.ORACLE_PARTICIPANTS_BATCH
385
430
  }
386
- Logger.isDebugEnabled && Logger.debug(`Oracle endpoints: ${url}`)
431
+ logger.debug(`Oracle endpoints: ${url}`)
387
432
  return await request.sendRequest({
388
433
  url,
389
434
  headers,
390
- source: headers[Enums.Http.Headers.FSPIOP.SOURCE],
391
- destination: headers[Enums.Http.Headers.FSPIOP.DESTINATION] || Config.HUB_NAME,
435
+ source: headers[Headers.FSPIOP.SOURCE],
436
+ destination: headers[Headers.FSPIOP.DESTINATION] || Config.HUB_NAME,
392
437
  method,
393
438
  payload,
394
439
  hubNameRegex
395
440
  })
396
441
  } else {
397
- Logger.isErrorEnabled && Logger.error(`Oracle type:${type} not found`)
442
+ logger.error(`Oracle type:${type} not found`)
398
443
  throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ADD_PARTY_INFO_ERROR, `Oracle type:${type} not found`)
399
444
  }
400
445
  } catch (err) {
401
- Logger.isErrorEnabled && Logger.error(err)
446
+ logger.error('error in oracleBatchRequest: ', err)
402
447
  throw ErrorHandler.Factory.reformatFSPIOPError(err)
403
448
  }
404
449
  }
450
+
451
+ module.exports = {
452
+ oracleRequest,
453
+ oracleBatchRequest
454
+ }
package/src/server.js CHANGED
@@ -45,7 +45,18 @@ const OracleEndpointCache = require('./models/oracle/oracleEndpointCached')
45
45
  const Handlers = require('./handlers/register')
46
46
 
47
47
  const connectDatabase = async (dbConfig) => {
48
- return Db.connect(dbConfig)
48
+ await Db.connect(dbConfig)
49
+ logger.info('Database connected')
50
+ }
51
+
52
+ const initOpenApiBackend = async ({ isAdmin }) => {
53
+ const OpenAPISpecPath = Util.pathForInterface({ isAdmin, isMockInterface: false })
54
+ const apiHandlers = isAdmin
55
+ ? APIHandlers.AdminHandlers
56
+ : APIHandlers.ApiHandlers
57
+ const api = await OpenapiBackend.initialise(OpenAPISpecPath, apiHandlers)
58
+ logger.verbose('OpenAPI Backend initialized', { isAdmin })
59
+ return api
49
60
  }
50
61
 
51
62
  const migrate = async () => {
@@ -132,8 +143,7 @@ const initializeApi = async (appConfig) => {
132
143
  initializeInstrumentation(INSTRUMENTATION_METRICS_CONFIG)
133
144
  }
134
145
  await connectDatabase(DATABASE)
135
- const OpenAPISpecPath = Util.pathForInterface({ isAdmin: false, isMockInterface: false })
136
- const api = await OpenapiBackend.initialise(OpenAPISpecPath, APIHandlers.ApiHandlers)
146
+ const api = await initOpenApiBackend({ isAdmin: false })
137
147
 
138
148
  await Promise.all([
139
149
  Endpoints.initializeCache(CENTRAL_SHARED_ENDPOINT_CACHE_CONFIG, Util.hubNameConfig),
@@ -142,6 +152,7 @@ const initializeApi = async (appConfig) => {
142
152
  OracleEndpointCache.initialize(),
143
153
  Cache.initCache()
144
154
  ])
155
+ logger.verbose('all caches initialized')
145
156
 
146
157
  return createServer(API_PORT, api, Routes.APIRoutes(api), false, PROXY_CACHE_CONFIG)
147
158
  }
@@ -161,8 +172,8 @@ const initializeAdmin = async (appConfig) => {
161
172
  }
162
173
  await connectDatabase(DATABASE)
163
174
  RUN_MIGRATIONS && await migrate()
164
- const OpenAPISpecPath = Util.pathForInterface({ isAdmin: true, isMockInterface: false })
165
- const api = await OpenapiBackend.initialise(OpenAPISpecPath, APIHandlers.AdminHandlers)
175
+ const api = await initOpenApiBackend({ isAdmin: true })
176
+
166
177
  await Promise.all([
167
178
  OracleEndpointCache.initialize(),
168
179
  Cache.initCache()
@@ -26,10 +26,11 @@
26
26
  ******/
27
27
 
28
28
  const { randomUUID } = require('node:crypto')
29
- const { Enum } = require('@mojaloop/central-services-shared')
29
+ const { Enum, Util } = jest.requireActual('@mojaloop/central-services-shared')
30
30
  const isoFixtures = require('./iso')
31
31
 
32
32
  const { Headers } = Enum.Http
33
+ const { encodePayload } = Util.StreamingProtocol
33
34
 
34
35
  const headersDto = ({
35
36
  source = 'fromDfsp',
@@ -37,14 +38,16 @@ const headersDto = ({
37
38
  proxy = '',
38
39
  date = new Date().toUTCString(),
39
40
  accept,
40
- contentType
41
+ contentType,
42
+ addHeaders
41
43
  } = {}) => Object.freeze({
42
44
  [Headers.FSPIOP.SOURCE]: source,
43
45
  ...(destination && { [Headers.FSPIOP.DESTINATION]: destination }),
44
46
  ...(proxy && { [Headers.FSPIOP.PROXY]: proxy }),
45
47
  date,
46
48
  accept,
47
- 'content-type': contentType || accept
49
+ 'content-type': contentType || accept,
50
+ ...(addHeaders && { ...addHeaders })
48
51
  })
49
52
 
50
53
  const partiesParamsDto = ({
@@ -72,12 +75,14 @@ const partiesCallHeadersDto = ({
72
75
  source,
73
76
  destination,
74
77
  proxy,
75
- date
78
+ date,
79
+ addHeaders
76
80
  } = {}) => headersDto({
77
81
  source,
78
82
  destination,
79
83
  proxy,
80
84
  date,
85
+ addHeaders,
81
86
  accept: interopHeader('parties', '1'),
82
87
  contentType: interopHeader('parties', '1.1')
83
88
  })
@@ -141,6 +146,8 @@ const postParticipantsPayloadDto = ({
141
146
  ...(currency && { currency })
142
147
  })
143
148
 
149
+ const dataUriDto = (payload = {}) => encodePayload(JSON.stringify(payload), 'application/json')
150
+
144
151
  const errorCallbackResponseDto = ({
145
152
  errorCode = '1234',
146
153
  errorDescription = 'Error description',
@@ -191,6 +198,7 @@ module.exports = {
191
198
  oracleRequestResponseDto,
192
199
  putPartiesSuccessResponseDto,
193
200
  postParticipantsPayloadDto,
201
+ dataUriDto,
194
202
  errorCallbackResponseDto,
195
203
  expiredCacheKeyDto,
196
204
  mockAlsRequestDto,
@@ -62,6 +62,7 @@ describe('Parties Endpoints Tests -->', () => {
62
62
  partyIdType: PARTY_ID_TYPE,
63
63
  source: PAYER_DFSP,
64
64
  destination: ''
65
+ // addHeaders: { 'x-response-status': '503' }
65
66
  })
66
67
  expect(result.status).toBe(202)
67
68
 
@@ -587,20 +587,20 @@ describe('Parties Tests', () => {
587
587
  it('successfully sends the callback to the participant', async () => {
588
588
  expect.hasAssertions()
589
589
  // Arrange
590
- participant.validateParticipant = sandbox.stub().resolves({
591
- name: 'fsp1'
592
- })
590
+ participant.validateParticipant = sandbox.stub().resolves({})
593
591
  participant.sendRequest = sandbox.stub().resolves()
594
592
  const payload = JSON.stringify({ testPayload: true })
595
593
  const dataUri = encodePayload(payload, 'application/json')
594
+ const destination = 'destFsp'
595
+ const headers = fixtures.partiesCallHeadersDto({ destination })
596
596
 
597
597
  // Act
598
- await partiesDomain.putPartiesByTypeAndID(Helper.putByTypeIdRequest.headers, Helper.putByTypeIdRequest.params, 'put', payload, dataUri, null, proxyCache)
598
+ await partiesDomain.putPartiesByTypeAndID(headers, Helper.putByTypeIdRequest.params, 'put', payload, dataUri, null, proxyCache)
599
599
 
600
600
  // Assert
601
601
  expect(participant.sendRequest.callCount).toBe(1)
602
602
  const sendRequestCallArgs = participant.sendRequest.getCall(0).args
603
- expect(sendRequestCallArgs[1]).toStrictEqual('fsp1')
603
+ expect(sendRequestCallArgs[1]).toBe(destination)
604
604
  participant.sendRequest.reset()
605
605
  })
606
606
 
@@ -927,14 +927,15 @@ describe('Parties Tests', () => {
927
927
  expect(sendErrorCallArgs[1]).toBe(expectedCallbackEnpointType)
928
928
  })
929
929
 
930
- it('should handle notValidPayeeIdentifier case, and delete partyId from oracle', async () => {
930
+ it('should handle external party error callback, and delete partyId from oracle', async () => {
931
931
  Config.PROXY_CACHE_CONFIG.enabled = true
932
932
  const errorCode = MojaloopApiErrorCodes.PAYEE_IDENTIFIER_NOT_VALID.code
933
933
  const payload = fixtures.errorCallbackResponseDto({ errorCode })
934
- const source = `source-${Date.now()}`
934
+ const destination = `dest-${Date.now()}`
935
935
  const proxy = `proxy-${Date.now()}`
936
- const headers = fixtures.partiesCallHeadersDto({ source, proxy })
936
+ const headers = fixtures.partiesCallHeadersDto({ destination, proxy })
937
937
  const { params } = Helper.putByTypeIdRequest
938
+ participant.validateParticipant = sandbox.stub().resolves({})
938
939
  participant.sendRequest = sandbox.stub().resolves()
939
940
  participant.sendErrorToParticipant = sandbox.stub().resolves()
940
941
  oracleEndpointCached.getOracleEndpointByType = sandbox.stub().resolves([
@@ -944,13 +945,14 @@ describe('Parties Tests', () => {
944
945
 
945
946
  await partiesDomain.putPartiesErrorByTypeAndID(headers, params, payload, '', null, null, proxyCache)
946
947
 
947
- expect(participant.sendRequest.callCount).toBe(0)
948
948
  expect(oracle.oracleRequest.callCount).toBe(1)
949
- const [, method] = oracle.oracleRequest.getCall(0).args
950
- expect(method).toBe(RestMethods.DELETE)
951
- // todo: think, how to stub getPartiesByTypeAndID call
952
- // expect(partiesDomain.getPartiesByTypeAndID.callCount).toBe(1)
953
- // expect(participant.sendErrorToParticipant.callCount).toBe(0)
949
+ expect(oracle.oracleRequest.lastCall.args[1]).toBe(RestMethods.DELETE)
950
+ expect(participant.sendRequest.callCount).toBe(0)
951
+ expect(participant.sendErrorToParticipant.callCount).toBe(1)
952
+ // eslint-disable-next-line no-unused-vars
953
+ const [sentTo, _, data] = participant.sendErrorToParticipant.lastCall.args
954
+ expect(sentTo).toBe(destination)
955
+ expect(data.errorInformation.errorCode).toBe('2003')
954
956
  })
955
957
  })
956
958
  })
@@ -54,7 +54,8 @@ describe('BasePartiesService Tests -->', () => {
54
54
  expect(sentTo).toBe(source)
55
55
  expect(payload.Rpt.Rsn.Cd).toBe('2001')
56
56
  expect(payload.Rpt.OrgnlId).toBe(`${params.Type}/${params.ID}`)
57
- expect(payload.Assgnmt.Assgnr.Agt.FinInstnId.Othr.Id).toBe(source)
57
+ expect(payload.Assgnmt.Assgne.Agt.FinInstnId.Othr.Id).toBe(source)
58
+ expect(payload.Assgnmt.Assgnr.Agt.FinInstnId.Othr.Id).toBe(config.HUB_NAME)
58
59
  })
59
60
 
60
61
  test('should remove proxy getParties timeout cache key', async () => {
@@ -25,6 +25,7 @@
25
25
  --------------
26
26
  ******/
27
27
 
28
+ const { setTimeout: sleep } = require('node:timers/promises')
28
29
  const {
29
30
  createMockDeps,
30
31
  createProxyCacheMock,
@@ -197,6 +198,51 @@ describe('GetPartiesService Tests -->', () => {
197
198
  expect(deps.proxyCache.receivedErrorResponse).toHaveBeenCalledTimes(1)
198
199
  expect(participantMock.sendRequest.mock.lastCall[1]).toBe(proxyOk)
199
200
  })
201
+
202
+ const throwDelayedErrorOnNthCall = (N, delay = 1000, error = new Error('Nth call Delayed Error')) => {
203
+ let count = 0
204
+ return async () => {
205
+ count++
206
+ if (count !== N) return {}
207
+ await sleep(1000)
208
+ throw error
209
+ }
210
+ }
211
+
212
+ const prepareGetPartiesServiceForDelayedProxyError = () => {
213
+ participantMock.sendRequest = jest.fn(throwDelayedErrorOnNthCall(2)) // throw error on 2nd proxy call
214
+ const proxies = createProxiesUtilMock({
215
+ getAllProxiesNames: jest.fn().mockResolvedValue(['proxy1', 'proxy2'])
216
+ })
217
+ const proxyCache = createProxyCacheMock({
218
+ receivedErrorResponse: jest.fn().mockResolvedValue(true) // failed proxy request is last in inter-scheme discovery flow
219
+ })
220
+ const deps = createMockDeps({ proxies, proxyCache })
221
+ const headers = fixtures.partiesCallHeadersDto({ destination: '' })
222
+ const params = fixtures.partiesParamsDto()
223
+
224
+ return new GetPartiesService(deps, { headers, params })
225
+ }
226
+
227
+ test('should throw an error if proxyRequest failed after delay, and other proxies have already replied', async () => {
228
+ expect.assertions(1)
229
+ const service = prepareGetPartiesServiceForDelayedProxyError()
230
+ const { headers } = service.inputs
231
+
232
+ await expect(service.triggerInterSchemeDiscoveryFlow(headers))
233
+ .rejects.toThrow(ERROR_MESSAGES.noSuccessfulProxyDiscoveryResponses)
234
+ })
235
+
236
+ test('should send error callback in ISO format if proxyRequest failed after delay, and other proxies have already replied', async () => {
237
+ const service = prepareGetPartiesServiceForDelayedProxyError()
238
+ const { headers } = service.inputs
239
+ service.deps.config.API_TYPE = API_TYPES.iso20022
240
+
241
+ await service.triggerInterSchemeDiscoveryFlow(headers)
242
+ .catch(err => service.handleError(err))
243
+ expect(participantMock.sendErrorToParticipant).toHaveBeenCalledTimes(1)
244
+ expect(participantMock.sendErrorToParticipant.mock.lastCall[2].Rpt.Rsn.Cd).toBe('3200')
245
+ })
200
246
  })
201
247
 
202
248
  describe('setProxyGetPartiesTimeout Tests', () => {
@@ -25,7 +25,7 @@
25
25
  --------------
26
26
  ******/
27
27
 
28
- const { createMockDeps, oracleMock } = require('./deps')
28
+ const { createMockDeps, oracleMock, participantMock } = require('./deps')
29
29
  // ↑ should be first require to mock external deps ↑
30
30
  const { PutPartiesErrorService } = require('#src/domain/parties/services/index')
31
31
  const fixtures = require('#test/fixtures/index')
@@ -37,14 +37,41 @@ describe('PutPartiesErrorService Tests -->', () => {
37
37
  jest.clearAllMocks()
38
38
  })
39
39
 
40
- test('should cleanup oracle and trigger discovery flow for party from external dfsp', async () => {
41
- const headers = fixtures.partiesCallHeadersDto({ proxy: 'proxyA' })
40
+ test('should cleanup oracle and forward SERVICE_CURRENTLY_UNAVAILABLE error for party from external dfsp', async () => {
41
+ participantMock.validateParticipant = jest.fn().mockResolvedValue({})
42
+ const destination = 'destFsp'
43
+ const headers = fixtures.partiesCallHeadersDto({ destination, proxy: 'proxyA' })
42
44
  const params = fixtures.partiesParamsDto()
43
- const service = new PutPartiesErrorService(createMockDeps(), { headers, params })
45
+ const dataUri = fixtures.dataUriDto()
46
+ const service = new PutPartiesErrorService(createMockDeps(), { headers, params, dataUri })
44
47
 
45
- const needDiscovery = await service.handleRequest()
46
- expect(needDiscovery).toBe(true)
47
- expect(oracleMock.oracleRequest.mock.calls.length).toBe(1)
48
+ await service.handleRequest()
49
+ expect(oracleMock.oracleRequest).toHaveBeenCalledTimes(1)
48
50
  expect(oracleMock.oracleRequest.mock.lastCall[1]).toBe(RestMethods.DELETE)
51
+ expect(participantMock.sendErrorToParticipant).toHaveBeenCalledTimes(1)
52
+ // eslint-disable-next-line no-unused-vars
53
+ const [sentTo, _, payload] = participantMock.sendErrorToParticipant.mock.lastCall
54
+ expect(sentTo).toBe(destination)
55
+ expect(payload.errorInformation.errorCode).toBe('2003')
56
+ })
57
+
58
+ test('should NOT cleanup oracle if destination is external', async () => {
59
+ const destination = 'externalDfsp'
60
+ const proxyDest = 'proxyDest'
61
+ const deps = createMockDeps()
62
+ deps.participant.validateParticipant = jest.fn().mockResolvedValue(null)
63
+ deps.proxyCache.lookupProxyByDfspId = jest.fn().mockResolvedValue(proxyDest)
64
+
65
+ const headers = fixtures.partiesCallHeadersDto({
66
+ destination, proxy: 'proxyA'
67
+ })
68
+ const params = fixtures.partiesParamsDto()
69
+ const dataUri = fixtures.dataUriDto()
70
+ const service = new PutPartiesErrorService(deps, { headers, params, dataUri })
71
+
72
+ await service.handleRequest()
73
+ expect(oracleMock.oracleRequest).not.toHaveBeenCalled()
74
+ expect(participantMock.sendErrorToParticipant).toHaveBeenCalledTimes(1)
75
+ expect(participantMock.sendErrorToParticipant.mock.lastCall[0]).toBe(proxyDest)
49
76
  })
50
77
  })
@@ -8,9 +8,9 @@ class AlsProxyApiClient extends BasicApiClient {
8
8
  super({ ...deps, baseURL })
9
9
  }
10
10
 
11
- async getPartyByIdAndType ({ partyId, partyIdType, source, destination, proxy = '' }) {
11
+ async getPartyByIdAndType ({ partyId, partyIdType, source, destination, proxy = '', addHeaders = null }) {
12
12
  return this.sendPartyRequest({
13
- partyId, partyIdType, source, destination, proxy
13
+ partyId, partyIdType, source, destination, proxy, addHeaders
14
14
  })
15
15
  }
16
16
 
@@ -27,10 +27,12 @@ class AlsProxyApiClient extends BasicApiClient {
27
27
  })
28
28
  }
29
29
 
30
- async sendPartyRequest ({ partyId, partyIdType, source, destination, proxy = '', body = null, isError = false }) {
30
+ async sendPartyRequest ({
31
+ partyId, partyIdType, source, destination, proxy = '', body = null, isError = false, addHeaders = null
32
+ }) {
31
33
  const method = body ? 'PUT' : 'GET'
32
34
  const url = `/parties/${partyIdType}/${partyId}${isError ? '/error' : ''}`
33
- const headers = this.fixtures.partiesCallHeadersDto({ source, destination, proxy })
35
+ const headers = this.fixtures.partiesCallHeadersDto({ source, destination, proxy, addHeaders })
34
36
 
35
37
  return this.sendRequest({
36
38
  method,