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 +14 -0
- package/docker/mock-proxy/src/server.ts +13 -4
- package/package.json +6 -6
- package/src/constants.js +3 -1
- package/src/domain/parties/putParties.js +1 -9
- package/src/domain/parties/services/BasePartiesService.js +51 -8
- package/src/domain/parties/services/GetPartiesService.js +10 -5
- package/src/domain/parties/services/PutPartiesErrorService.js +35 -40
- package/src/domain/parties/services/PutPartiesService.js +1 -21
- package/src/domain/parties/services/TimeoutPartiesService.js +1 -1
- package/src/handlers/index.js +3 -3
- package/src/index.js +2 -1
- package/src/lib/util.js +2 -2
- package/src/models/oracle/facade.js +186 -136
- package/src/server.js +16 -5
- package/test/fixtures/index.js +12 -4
- package/test/integration/api/parties.test.js +1 -0
- package/test/unit/domain/parties/parties.test.js +16 -14
- package/test/unit/domain/parties/services/BasePartiesService.test.js +2 -1
- package/test/unit/domain/parties/services/GetPartiesService.test.js +46 -0
- package/test/unit/domain/parties/services/PutPartiesErrorService.test.js +34 -7
- package/test/util/apiClients/AlsApiClient.js +6 -4
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
|
-
|
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(
|
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
|
-
|
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(
|
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.
|
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.
|
95
|
+
"@mojaloop/central-services-logger": "11.8.1",
|
96
96
|
"@mojaloop/central-services-metrics": "12.5.0",
|
97
|
-
"@mojaloop/central-services-shared": "18.23.
|
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.
|
102
|
-
"@mojaloop/ml-schema-transformer-lib": "2.7.
|
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.
|
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
|
-
|
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 {
|
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
|
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:
|
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
|
-
|
126
|
-
|
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
|
-
|
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.
|
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(`
|
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
|
38
|
-
|
39
|
-
if (
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
}
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
55
|
+
await super.identifyDestinationForCallback()
|
55
56
|
await this.sendErrorCallbackToParticipant()
|
56
|
-
this.log.info('
|
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.
|
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.
|
55
|
+
await this.identifyDestinationForCallback()
|
56
56
|
return super.sendErrorCallback({ errorInfo, headers, params })
|
57
57
|
}
|
58
58
|
|
package/src/handlers/index.js
CHANGED
@@ -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('
|
42
|
-
log.error(`uncaughtException: ${
|
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(
|
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.
|
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
|
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
|
-
*
|
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}
|
53
|
+
* @returns {object} - response from the oracle
|
53
54
|
*/
|
54
|
-
|
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
|
-
|
57
|
-
const
|
58
|
-
const
|
59
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
113
|
-
}
|
79
|
+
if (partySubIdOrType && payload) payload.partySubIdOrType = partySubIdOrType
|
114
80
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
-
|
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
|
-
|
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
|
154
|
-
destination
|
155
|
-
method
|
156
|
-
payload,
|
144
|
+
source,
|
145
|
+
destination,
|
146
|
+
method,
|
157
147
|
hubNameRegex
|
158
148
|
})
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
165
|
-
|
166
|
-
|
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
|
-
|
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
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
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
|
-
|
237
|
-
|
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
|
-
|
273
|
-
|
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
|
-
|
310
|
-
|
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
|
-
|
348
|
-
|
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
|
-
*
|
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}
|
409
|
+
* @returns {object} - response from the oracle
|
365
410
|
*/
|
366
|
-
|
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
|
-
|
431
|
+
logger.debug(`Oracle endpoints: ${url}`)
|
387
432
|
return await request.sendRequest({
|
388
433
|
url,
|
389
434
|
headers,
|
390
|
-
source: headers[
|
391
|
-
destination: headers[
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
165
|
-
|
175
|
+
const api = await initOpenApiBackend({ isAdmin: true })
|
176
|
+
|
166
177
|
await Promise.all([
|
167
178
|
OracleEndpointCache.initialize(),
|
168
179
|
Cache.initCache()
|
package/test/fixtures/index.js
CHANGED
@@ -26,10 +26,11 @@
|
|
26
26
|
******/
|
27
27
|
|
28
28
|
const { randomUUID } = require('node:crypto')
|
29
|
-
const { Enum } =
|
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,
|
@@ -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(
|
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]).
|
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
|
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
|
934
|
+
const destination = `dest-${Date.now()}`
|
935
935
|
const proxy = `proxy-${Date.now()}`
|
936
|
-
const headers = fixtures.partiesCallHeadersDto({
|
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
|
-
|
950
|
-
expect(
|
951
|
-
|
952
|
-
//
|
953
|
-
|
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.
|
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
|
41
|
-
|
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
|
45
|
+
const dataUri = fixtures.dataUriDto()
|
46
|
+
const service = new PutPartiesErrorService(createMockDeps(), { headers, params, dataUri })
|
44
47
|
|
45
|
-
|
46
|
-
expect(
|
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 ({
|
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,
|