account-lookup-service 17.7.0-snapshot.5 → 17.8.0-snapshot.6

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,13 @@
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.7.0](https://github.com/mojaloop/account-lookup-service/compare/v17.6.0...v17.7.0) (2025-03-26)
6
+
7
+
8
+ ### Features
9
+
10
+ * update dependencies to latest versions ([#542](https://github.com/mojaloop/account-lookup-service/issues/542)) ([8867742](https://github.com/mojaloop/account-lookup-service/commit/8867742e5e599f9a0de05eae095e2bd12e37d149))
11
+
5
12
  ## [17.6.0](https://github.com/mojaloop/account-lookup-service/compare/v17.5.0...v17.6.0) (2025-03-19)
6
13
 
7
14
 
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.7.0-snapshot.5",
4
+ "version": "17.8.0-snapshot.6",
5
5
  "license": "Apache-2.0",
6
6
  "author": "ModusBox",
7
7
  "contributors": [
@@ -106,7 +106,7 @@
106
106
  "ajv-keywords": "5.1.0",
107
107
  "blipp": "4.0.2",
108
108
  "commander": "13.1.0",
109
- "cron": "4.1.0",
109
+ "cron": "4.1.1",
110
110
  "fast-safe-stringify": "^2.1.1",
111
111
  "hapi-auth-bearer-token": "8.0.0",
112
112
  "joi": "17.13.3",
@@ -186,9 +186,8 @@ class BasePartiesService {
186
186
  }
187
187
 
188
188
  static overrideDestinationHeader (headers, destination) {
189
- const { [Headers.FSPIOP.DESTINATION]: _, ...restHeaders } = headers || {}
190
189
  return {
191
- ...restHeaders,
190
+ ...BasePartiesService.headersWithoutDestination(headers),
192
191
  ...(destination && { [Headers.FSPIOP.DESTINATION]: destination })
193
192
  }
194
193
  }
@@ -204,6 +203,13 @@ class BasePartiesService {
204
203
  })
205
204
  }
206
205
 
206
+ static createHubErrorCallbackHeaders (hubName, destination) {
207
+ return {
208
+ [Headers.FSPIOP.SOURCE]: hubName,
209
+ [Headers.FSPIOP.DESTINATION]: destination
210
+ }
211
+ }
212
+
207
213
  static enums () {
208
214
  return {
209
215
  FspEndpointTypes,
@@ -0,0 +1,84 @@
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2020-2025 Mojaloop Foundation
5
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
10
+
11
+ Contributors
12
+ --------------
13
+ This is the official list of the Mojaloop project contributors for this file.
14
+ Names of the original copyright holders (individuals or organizations)
15
+ should be listed with a '*' in the first column. People who have
16
+ contributed from an organization can be listed under the organization
17
+ that actually holds the copyright for their contributions (see the
18
+ Mojaloop Foundation for an example). Those individuals should have
19
+ their names indented and be marked with a '-'. Email address can be added
20
+ optionally within square brackets <email>.
21
+
22
+ * Mojaloop Foundation
23
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
24
+
25
+ --------------
26
+ ******/
27
+
28
+ const ErrorHandler = require('@mojaloop/central-services-error-handling')
29
+ const { AuditEventAction } = require('@mojaloop/event-sdk')
30
+ const createSpan = require('../../timeout/createSpan') // todo: think, how to avoid this external deps
31
+ const PutPartiesErrorService = require('./PutPartiesErrorService')
32
+
33
+ class TimeoutPartiesService extends PutPartiesErrorService {
34
+ /**
35
+ * Should be used to get TimeoutPartiesService instance
36
+ *
37
+ * @param deps {PartiesDeps}
38
+ * @param cacheKey {string}
39
+ * @param spanName {string}
40
+ * @returns {TimeoutPartiesService}
41
+ */
42
+ static createInstance (deps, cacheKey, spanName) {
43
+ const { destination, partyType, partyId } = TimeoutPartiesService.parseExpiredKey(cacheKey)
44
+ const headers = TimeoutPartiesService.createHubErrorCallbackHeaders(deps.config.HUB_NAME, destination)
45
+ const params = { Type: partyType, ID: partyId } // todo: think, if we need to handle party SubId
46
+ const childSpan = createSpan(spanName, headers, params)
47
+
48
+ return new TimeoutPartiesService({ ...deps, childSpan }, { headers, params })
49
+ }
50
+
51
+ async handleExpiredKey () {
52
+ const { errorInfo, headers, params } = await this.prepareErrorInformation()
53
+ this.#spanAuditStart(errorInfo)
54
+
55
+ await this.identifyDestinationForErrorCallback()
56
+ return super.sendErrorCallback({ errorInfo, headers, params })
57
+ }
58
+
59
+ async prepareErrorInformation () {
60
+ const { headers, params } = this.inputs
61
+ const error = TimeoutPartiesService.createFSPIOPExpiredError()
62
+ const errorInfo = await this.deps.partiesUtils.makePutPartiesErrorPayload(
63
+ this.deps.config, error, headers, params
64
+ )
65
+ this.log.verbose('prepareErrorInformation is done', { errorInfo, headers, params })
66
+ return { errorInfo, headers, params }
67
+ }
68
+
69
+ #spanAuditStart (errorInformation) {
70
+ const { headers } = this.inputs
71
+ this.deps.childSpan?.audit({ errorInformation, headers }, AuditEventAction.start)
72
+ }
73
+
74
+ static parseExpiredKey (cacheKey) {
75
+ const [destination, partyType, partyId] = cacheKey.split(':').slice(-3)
76
+ return { destination, partyType, partyId }
77
+ }
78
+
79
+ static createFSPIOPExpiredError () {
80
+ return ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.EXPIRED_ERROR)
81
+ }
82
+ }
83
+
84
+ module.exports = TimeoutPartiesService
@@ -28,9 +28,11 @@
28
28
  const GetPartiesService = require('./GetPartiesService')
29
29
  const PutPartiesService = require('./PutPartiesService')
30
30
  const PutPartiesErrorService = require('./PutPartiesErrorService')
31
+ const TimeoutPartiesService = require('./TimeoutPartiesService')
31
32
 
32
33
  module.exports = {
33
34
  GetPartiesService,
34
35
  PutPartiesService,
35
- PutPartiesErrorService
36
+ PutPartiesErrorService,
37
+ TimeoutPartiesService
36
38
  }
@@ -0,0 +1,55 @@
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2020-2025 Mojaloop Foundation
5
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
10
+
11
+ Contributors
12
+ --------------
13
+ This is the official list of the Mojaloop project contributors for this file.
14
+ Names of the original copyright holders (individuals or organizations)
15
+ should be listed with a '*' in the first column. People who have
16
+ contributed from an organization can be listed under the organization
17
+ that actually holds the copyright for their contributions (see the
18
+ Mojaloop Foundation for an example). Those individuals should have
19
+ their names indented and be marked with a '-'. Email address can be added
20
+ optionally within square brackets <email>.
21
+
22
+ * Mojaloop Foundation
23
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
24
+
25
+ --------------
26
+ ******/
27
+
28
+ const {
29
+ Events: { Event },
30
+ Tags: { QueryTags }
31
+ } = require('@mojaloop/central-services-shared').Enum
32
+ const { Tracer } = require('@mojaloop/event-sdk')
33
+ const EventFrameworkUtil = require('@mojaloop/central-services-shared').Util.EventFramework
34
+
35
+ const { getSpanTags } = require('../../lib/util')
36
+
37
+ const createSpan = (spanName, headers, params) => {
38
+ const span = Tracer.createSpan(spanName, { headers })
39
+ const spanTags = getSpanTags({ headers }, Event.Type.PARTY, Event.Action.PUT)
40
+ span.setTags(spanTags)
41
+ const queryTags = EventFrameworkUtil.Tags.getQueryTags(
42
+ QueryTags.serviceName.accountLookupService,
43
+ QueryTags.auditType.transactionFlow,
44
+ QueryTags.contentType.httpRequest,
45
+ QueryTags.operation.timeoutInterschemePartiesLookups,
46
+ {
47
+ partyIdType: params.Type,
48
+ partyIdentifier: params.ID
49
+ }
50
+ )
51
+ span.setTags(queryTags)
52
+ return span
53
+ }
54
+
55
+ module.exports = createSpan
@@ -31,73 +31,56 @@
31
31
  ******/
32
32
  'use strict'
33
33
 
34
- const {
35
- Factory: { createFSPIOPError, reformatFSPIOPError },
36
- Enums: { FSPIOPErrorCodes }
37
- } = require('@mojaloop/central-services-error-handling')
38
- const {
39
- EventStateMetadata,
40
- EventStatusType,
41
- AuditEventAction
42
- } = require('@mojaloop/event-sdk')
34
+ const { Factory: { reformatFSPIOPError } } = require('@mojaloop/central-services-error-handling')
35
+ const { EventStateMetadata, EventStatusType } = require('@mojaloop/event-sdk')
43
36
  const Metrics = require('@mojaloop/central-services-metrics')
44
37
 
45
- const Participant = require('../../models/participantEndpoint/facade')
46
- const { ERROR_MESSAGES } = require('../../constants')
47
38
  const { logger } = require('../../lib')
48
39
  const { countFspiopError } = require('../../lib/util')
49
- const { timeoutCallbackDto } = require('./dto')
40
+ const { createDeps } = require('../parties/deps')
41
+ const { TimeoutPartiesService } = require('../parties/services')
50
42
 
51
43
  const timeoutInterschemePartiesLookups = async ({ proxyCache, batchSize }) => {
52
- logger.info('timeoutInterschemePartiesLookups start...', { batchSize })
53
- return proxyCache.processExpiredAlsKeys(sendTimeoutCallback, batchSize)
44
+ const operation = timeoutInterschemePartiesLookups.name
45
+ logger.info(`${operation} start...`, { batchSize })
46
+ return proxyCache.processExpiredAlsKeys(
47
+ (key) => sendTimeoutCallback(key, proxyCache, operation), batchSize
48
+ )
54
49
  }
55
50
 
56
51
  const timeoutProxyGetPartiesLookups = async ({ proxyCache, batchSize }) => {
57
- logger.info('timeoutProxyGetPartiesLookups start...', { batchSize })
58
- return proxyCache.processExpiredProxyGetPartiesKeys(sendTimeoutCallback, batchSize)
52
+ const operation = timeoutProxyGetPartiesLookups.name
53
+ logger.info(`${operation} start...`, { batchSize })
54
+ return proxyCache.processExpiredProxyGetPartiesKeys(
55
+ (key) => sendTimeoutCallback(key, proxyCache, operation), batchSize
56
+ )
59
57
  }
60
58
 
61
- const sendTimeoutCallback = async (cacheKey) => {
59
+ const sendTimeoutCallback = async (cacheKey, proxyCache, operation) => {
62
60
  const histTimerEnd = Metrics.getHistogram(
63
61
  'eg_timeoutInterschemePartiesLookups',
64
62
  'Egress - Interscheme parties lookup timeout callback',
65
63
  ['success']
66
64
  ).startTimer()
67
- let step
68
- const [destination, partyType, partyId] = parseCacheKey(cacheKey)
69
- const { errorInformation, params, headers, endpointType, span } = await timeoutCallbackDto({ destination, partyId, partyType })
70
- const log = logger.child({ destination, partyId })
71
- log.verbose('sendTimeoutCallback details:', { errorInformation, cacheKey, partyType })
65
+ const log = logger.child({ cacheKey, operation })
66
+ const deps = createDeps({ proxyCache, log })
67
+ const service = TimeoutPartiesService.createInstance(deps, cacheKey, operation)
68
+ const span = service.deps.childSpan
72
69
 
73
70
  try {
74
- step = 'validateParticipant-1'
75
- await validateParticipant(destination, log)
76
- await span.audit({ headers, errorInformation }, AuditEventAction.start)
77
- step = 'sendErrorToParticipant-2'
78
- await Participant.sendErrorToParticipant(destination, endpointType, errorInformation, headers, params, undefined, span)
71
+ await service.handleExpiredKey()
79
72
  histTimerEnd({ success: true })
80
73
  } catch (err) {
81
74
  log.warn('error in sendTimeoutCallback: ', err)
82
75
  histTimerEnd({ success: false })
83
76
  const fspiopError = reformatFSPIOPError(err)
84
- countFspiopError(fspiopError, { operation: 'sendTimeoutCallback', step })
77
+ countFspiopError(fspiopError, { operation, step: service?.currenStep })
85
78
 
86
79
  await finishSpan(span, fspiopError)
87
80
  throw fspiopError
88
81
  }
89
82
  }
90
83
 
91
- const validateParticipant = async (fspId, log) => {
92
- const participant = await Participant.validateParticipant(fspId)
93
- if (!participant) {
94
- const errMessage = ERROR_MESSAGES.partyDestinationFspNotFound
95
- log.error(`error in validateParticipant: ${errMessage}`)
96
- throw createFSPIOPError(FSPIOPErrorCodes.DESTINATION_FSP_ERROR, errMessage)
97
- }
98
- return participant
99
- }
100
-
101
84
  const finishSpan = async (span, err) => {
102
85
  if (!span.isFinished) {
103
86
  const state = new EventStateMetadata(
@@ -110,11 +93,6 @@ const finishSpan = async (span, err) => {
110
93
  }
111
94
  }
112
95
 
113
- const parseCacheKey = (cacheKey) => {
114
- const [destination, partyType, partyId] = cacheKey.split(':').slice(-3)
115
- return [destination, partyType, partyId]
116
- }
117
-
118
96
  module.exports = {
119
97
  timeoutInterschemePartiesLookups,
120
98
  timeoutProxyGetPartiesLookups,
@@ -163,6 +163,13 @@ const mockAlsRequestDto = (sourceId, type, partyId) => ({
163
163
  partyId
164
164
  })
165
165
 
166
+ const expiredCacheKeyDto = ({
167
+ sourceId = 'sourceId',
168
+ type = 'MSISDN',
169
+ partyId = 'partyId-123',
170
+ prefix = 'prefix'
171
+ } = {}) => `${prefix}:${sourceId}:${type}:${partyId}`
172
+
166
173
  const mockHapiRequestDto = ({ // https://hapi.dev/api/?v=21.3.3#request-properties
167
174
  method = 'GET',
168
175
  traceid = randomUUID(),
@@ -185,6 +192,7 @@ module.exports = {
185
192
  putPartiesSuccessResponseDto,
186
193
  postParticipantsPayloadDto,
187
194
  errorCallbackResponseDto,
195
+ expiredCacheKeyDto,
188
196
  mockAlsRequestDto,
189
197
  protocolVersionsDto,
190
198
  mockHapiRequestDto,
@@ -1,18 +1,50 @@
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2020-2025 Mojaloop Foundation
5
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
10
+
11
+ Contributors
12
+ --------------
13
+ This is the official list of the Mojaloop project contributors for this file.
14
+ Names of the original copyright holders (individuals or organizations)
15
+ should be listed with a '*' in the first column. People who have
16
+ contributed from an organization can be listed under the organization
17
+ that actually holds the copyright for their contributions (see the
18
+ Mojaloop Foundation for an example). Those individuals should have
19
+ their names indented and be marked with a '-'. Email address can be added
20
+ optionally within square brackets <email>.
21
+
22
+ * Mojaloop Foundation
23
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
24
+
25
+ --------------
26
+ ******/
27
+
1
28
  const { createProxyCache } = require('@mojaloop/inter-scheme-proxy-cache-lib')
2
- const { PAYER_DFSP, PARTY_ID_TYPE, PROXY_NAME } = require('../../constants')
29
+ const { RedisProxyCache } = require('@mojaloop/inter-scheme-proxy-cache-lib/dist/lib/storages/RedisProxyCache')
30
+ const config = require('#src/lib/config')
3
31
  const fixtures = require('../../../fixtures')
4
32
  const { ProxyApiClient } = require('../../../util')
5
- const config = require('../../../../src/lib/config')
33
+ const { PAYER_DFSP, PARTY_ID_TYPE, PROXY_NAME } = require('../../constants')
6
34
 
7
35
  const wait = (sec) => new Promise(resolve => setTimeout(resolve, sec * 1000))
8
36
 
9
37
  const CRON_TIMEOUT_SEC = 15 // see TIMEXP
10
38
 
39
+ jest.setTimeout(60_000)
40
+
11
41
  describe('Timeout Handler', () => {
12
42
  const { type, proxyConfig } = config.PROXY_CACHE_CONFIG
13
43
  const proxyCache = createProxyCache(type, proxyConfig)
14
44
  const proxyClient = new ProxyApiClient()
15
45
 
46
+ const checkKeysExistence = async (keys) => Promise.all(keys.map(key => proxyCache.redisClient.exists(key)))
47
+
16
48
  beforeAll(async () => {
17
49
  await proxyCache.connect()
18
50
  const redisClient = proxyCache.redisClient
@@ -22,6 +54,11 @@ describe('Timeout Handler', () => {
22
54
  }))
23
55
  })
24
56
 
57
+ beforeEach(async () => {
58
+ const history = await proxyClient.deleteHistory()
59
+ expect(history).toEqual([])
60
+ })
61
+
25
62
  afterAll(async () => {
26
63
  return Promise.all([
27
64
  proxyClient.deleteHistory(),
@@ -29,47 +66,65 @@ describe('Timeout Handler', () => {
29
66
  ])
30
67
  })
31
68
 
32
- it('test', async () => {
33
- let history = await proxyClient.deleteHistory()
34
- expect(history).toEqual([])
35
-
36
- // send a couple of keys to redis
37
- const partyIds = ['1234567', '7654321']
38
- const keys = [
39
- `'als:${PAYER_DFSP}:${PARTY_ID_TYPE}:${partyIds[0]}:expiresAt'`,
40
- `'als:${PAYER_DFSP}:${PARTY_ID_TYPE}:${partyIds[1]}:expiresAt'`
41
- ]
69
+ it('should pass timeoutInterschemePartiesLookups flow', async () => {
70
+ const partyIds = [`isp1-${Date.now()}`, `isp2-${Date.now()}`]
42
71
  const proxies = [PROXY_NAME]
43
72
  const alsReq1 = fixtures.mockAlsRequestDto(PAYER_DFSP, PARTY_ID_TYPE, partyIds[0])
44
73
  const alsReq2 = fixtures.mockAlsRequestDto(PAYER_DFSP, PARTY_ID_TYPE, partyIds[1])
74
+ const keys = [
75
+ RedisProxyCache.formatAlsCacheExpiryKey(alsReq1),
76
+ RedisProxyCache.formatAlsCacheExpiryKey(alsReq2)
77
+ ]
78
+ // send a couple of keys to redis
45
79
  const results = await Promise.all([
46
80
  proxyCache.setSendToProxiesList(alsReq1, proxies, CRON_TIMEOUT_SEC),
47
81
  proxyCache.setSendToProxiesList(alsReq2, proxies, CRON_TIMEOUT_SEC)
48
82
  ])
49
- expect(results.includes(false)).toBe(false)
83
+ expect(results).toEqual([true, true])
84
+ expect(await checkKeysExistence(keys)).toEqual([1, 1])
50
85
 
51
86
  // wait for the timeout handler to process the keys
52
87
  await wait(CRON_TIMEOUT_SEC * 1.5)
88
+ const history = await proxyClient.waitForNHistoryCalls(2)
53
89
 
54
90
  // check that the keys are no longer in redis
55
- const exists = await Promise.all(keys.map(key => proxyCache.redisClient.exists(key)))
56
- expect(exists.includes(1)).toBe(false)
57
-
58
- // check that the callbacks are sent and received at the FSP
59
- // for test resilience, we will retry the history check a few times
60
- const retryMaxCount = 20
61
- const retryIntervalSec = 2
62
- let retryCount = 0
63
-
64
- while (history.length < 2 && retryCount < retryMaxCount) {
65
- await wait(retryIntervalSec)
66
- history = await proxyClient.getHistory()
67
- retryCount++
68
- }
91
+ expect(await checkKeysExistence(keys)).toEqual([0, 0])
92
+
69
93
  expect(history.length).toBe(2)
70
94
  const path0 = history.find(h => h.path.includes(partyIds[0])).path
71
95
  const path1 = history.find(h => h.path.includes(partyIds[1])).path
72
96
  expect(path0).toBe(`/parties/${PARTY_ID_TYPE}/${partyIds[0]}/error`)
73
97
  expect(path1).toBe(`/parties/${PARTY_ID_TYPE}/${partyIds[1]}/error`)
74
- }, 60_000)
98
+ })
99
+
100
+ it('should pass timeoutProxyGetPartiesLookups flow', async () => {
101
+ const partyId1 = `pgp1-${Date.now()}`
102
+ const partyId2 = `pgp2-${Date.now()}`
103
+ const alsReq1 = fixtures.mockAlsRequestDto(PAYER_DFSP, PARTY_ID_TYPE, partyId1)
104
+ const alsReq2 = fixtures.mockAlsRequestDto(PAYER_DFSP, PARTY_ID_TYPE, partyId2)
105
+ const keys = [
106
+ RedisProxyCache.formatProxyGetPartiesExpiryKey(alsReq1, PROXY_NAME),
107
+ RedisProxyCache.formatProxyGetPartiesExpiryKey(alsReq2, PROXY_NAME)
108
+ ]
109
+ // send a couple of keys to redis
110
+ const results = await Promise.all([
111
+ proxyCache.setProxyGetPartiesTimeout(alsReq1, PROXY_NAME, CRON_TIMEOUT_SEC),
112
+ proxyCache.setProxyGetPartiesTimeout(alsReq2, PROXY_NAME, CRON_TIMEOUT_SEC)
113
+ ])
114
+ expect(results).toEqual([true, true])
115
+ expect(await checkKeysExistence(keys)).toEqual([1, 1])
116
+
117
+ // wait for the timeout handler to process the keys
118
+ await wait(CRON_TIMEOUT_SEC * 1.5)
119
+ const history = await proxyClient.waitForNHistoryCalls(2)
120
+
121
+ // check that the keys are no longer in redis
122
+ expect(await checkKeysExistence(keys)).toEqual([0, 0])
123
+
124
+ expect(history.length).toBe(2)
125
+ const path1 = history.find(h => h.path.includes(partyId1)).path
126
+ const path2 = history.find(h => h.path.includes(partyId2)).path
127
+ expect(path1).toBe(`/parties/${PARTY_ID_TYPE}/${partyId1}/error`)
128
+ expect(path2).toBe(`/parties/${PARTY_ID_TYPE}/${partyId2}/error`)
129
+ })
75
130
  })
@@ -26,7 +26,7 @@
26
26
  ******/
27
27
 
28
28
  const { createMockDeps, participantMock } = require('./deps')
29
- // should be first require to mock external deps
29
+ // should be first require to mock external deps
30
30
  const BasePartiesService = require('#src/domain/parties/services/BasePartiesService')
31
31
  const config = require('#src/lib/config')
32
32
  const { API_TYPES } = require('#src/constants')
@@ -26,7 +26,7 @@
26
26
  ******/
27
27
 
28
28
  const { createMockDeps, createProxyCacheMock, oracleMock, participantMock } = require('./deps')
29
- // should be first require to mock external deps
29
+ // should be first require to mock external deps
30
30
  const { GetPartiesService } = require('#src/domain/parties/services/index')
31
31
  const fixtures = require('#test/fixtures/index')
32
32
 
@@ -26,6 +26,7 @@
26
26
  ******/
27
27
 
28
28
  const { createMockDeps, oracleMock } = require('./deps')
29
+ // ↑ should be first require to mock external deps ↑
29
30
  const { PutPartiesErrorService } = require('#src/domain/parties/services/index')
30
31
  const fixtures = require('#test/fixtures/index')
31
32
 
@@ -0,0 +1,72 @@
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2020-2025 Mojaloop Foundation
5
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
10
+
11
+ Contributors
12
+ --------------
13
+ This is the official list of the Mojaloop project contributors for this file.
14
+ Names of the original copyright holders (individuals or organizations)
15
+ should be listed with a '*' in the first column. People who have
16
+ contributed from an organization can be listed under the organization
17
+ that actually holds the copyright for their contributions (see the
18
+ Mojaloop Foundation for an example). Those individuals should have
19
+ their names indented and be marked with a '-'. Email address can be added
20
+ optionally within square brackets <email>.
21
+
22
+ * Mojaloop Foundation
23
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
24
+
25
+ --------------
26
+ ******/
27
+
28
+ const { createMockDeps, createProxyCacheMock, participantMock } = require('./deps')
29
+ // ↑ should be first require to mock external deps ↑
30
+ const { TimeoutPartiesService } = require('#src/domain/parties/services/index')
31
+ const { API_TYPES } = require('#src/constants')
32
+ const fixtures = require('#test/fixtures/index')
33
+
34
+ describe('TimeoutPartiesService Tests -->', () => {
35
+ const { config } = createMockDeps()
36
+
37
+ beforeEach(() => {
38
+ jest.clearAllMocks()
39
+ })
40
+
41
+ test('should send error callback to external participant through proxy', async () => {
42
+ participantMock.validateParticipant = jest.fn().mockResolvedValue(null)
43
+ const proxy = 'proxyAB'
44
+ const proxyCache = createProxyCacheMock({
45
+ lookupProxyByDfspId: jest.fn().mockResolvedValue(proxy)
46
+ })
47
+ const deps = createMockDeps({ proxyCache })
48
+ const cacheKey = fixtures.expiredCacheKeyDto()
49
+ const service = TimeoutPartiesService.createInstance(deps, cacheKey, 'test')
50
+
51
+ await service.handleExpiredKey()
52
+ expect(participantMock.sendErrorToParticipant).toHaveBeenCalledTimes(1)
53
+ expect(participantMock.sendErrorToParticipant.mock.lastCall[0]).toBe(proxy)
54
+ })
55
+
56
+ test('should send error callback in ISO20022 format', async () => {
57
+ participantMock.validateParticipant = jest.fn().mockResolvedValue({})
58
+ const deps = {
59
+ ...createMockDeps(),
60
+ config: { ...config, API_TYPE: API_TYPES.iso20022 }
61
+ }
62
+ const sourceId = 'sourceFsp'
63
+ const cacheKey = fixtures.expiredCacheKeyDto({ sourceId })
64
+ const service = TimeoutPartiesService.createInstance(deps, cacheKey, 'test')
65
+
66
+ await service.handleExpiredKey()
67
+ expect(participantMock.sendErrorToParticipant).toHaveBeenCalledTimes(1)
68
+ const { Assgnr, Assgne } = participantMock.sendErrorToParticipant.mock.lastCall[2].Assgnmt
69
+ expect(Assgnr.Agt.FinInstnId.Othr.Id).toBe(config.HUB_NAME)
70
+ expect(Assgne.Agt.FinInstnId.Othr.Id).toBe(sourceId)
71
+ })
72
+ })
@@ -31,11 +31,13 @@
31
31
 
32
32
  'use strict'
33
33
 
34
- const Participant = require('../../../../src/models/participantEndpoint/facade')
35
- const TimeoutDomain = require('../../../../src/domain/timeout')
36
34
  const Metrics = require('@mojaloop/central-services-metrics')
35
+ const Participant = require('#src/models/participantEndpoint/facade')
36
+ const TimeoutDomain = require('#src/domain/timeout/index')
37
+ const { mockDeps } = require('#test/util/index')
37
38
 
38
39
  describe('Timeout Domain', () => {
40
+ const proxyCache = mockDeps.createProxyCacheMock()
39
41
  // Initialize Metrics for testing
40
42
  Metrics.getCounter(
41
43
  'errorCount',
@@ -56,9 +58,10 @@ describe('Timeout Domain', () => {
56
58
  describe('timeoutInterschemePartiesLookups', () => {
57
59
  describe('timeoutInterschemePartiesLookups', () => {
58
60
  it('should process expired ALS keys', async () => {
59
- const mockCache = { processExpiredAlsKeys: jest.fn() }
60
- await TimeoutDomain.timeoutInterschemePartiesLookups({ proxyCache: mockCache, batchSize: 10 })
61
- expect(mockCache.processExpiredAlsKeys).toHaveBeenCalledWith(TimeoutDomain.sendTimeoutCallback, 10)
61
+ const batchSize = 10
62
+ await TimeoutDomain.timeoutInterschemePartiesLookups({ proxyCache, batchSize })
63
+ expect(proxyCache.processExpiredAlsKeys).toHaveBeenCalledWith(expect.any(Function), batchSize)
64
+ expect(Participant.sendErrorToParticipant).toHaveBeenCalled()
62
65
  })
63
66
  })
64
67
 
@@ -69,7 +72,7 @@ describe('Timeout Domain', () => {
69
72
 
70
73
  const cacheKey = `als:${SOURCE_ID}:2:3` // ':expiresAt' part is removed inside proxyCache.processExpiryKey()
71
74
 
72
- await TimeoutDomain.sendTimeoutCallback(cacheKey)
75
+ await TimeoutDomain.sendTimeoutCallback(cacheKey, proxyCache)
73
76
 
74
77
  expect(Participant.validateParticipant).toHaveBeenCalledWith(SOURCE_ID)
75
78
  expect(Participant.sendErrorToParticipant).toHaveBeenCalledWith(
@@ -82,7 +85,9 @@ describe('Timeout Domain', () => {
82
85
 
83
86
  it('should throw error if participant validation fails', async () => {
84
87
  Participant.validateParticipant.mockResolvedValue(null)
85
- await expect(TimeoutDomain.sendTimeoutCallback('als:sourceId:2:3:expiresAt')).rejects.toThrow()
88
+ await expect(
89
+ TimeoutDomain.sendTimeoutCallback('als:sourceId:2:3:expiresAt', proxyCache)
90
+ ).rejects.toThrow()
86
91
  })
87
92
  })
88
93
  })
@@ -1,16 +1,43 @@
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2020-2025 Mojaloop Foundation
5
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
10
+
11
+ Contributors
12
+ --------------
13
+ This is the official list of the Mojaloop project contributors for this file.
14
+ Names of the original copyright holders (individuals or organizations)
15
+ should be listed with a '*' in the first column. People who have
16
+ contributed from an organization can be listed under the organization
17
+ that actually holds the copyright for their contributions (see the
18
+ Mojaloop Foundation for an example). Those individuals should have
19
+ their names indented and be marked with a '-'. Email address can be added
20
+ optionally within square brackets <email>.
21
+
22
+ * Mojaloop Foundation
23
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
24
+
25
+ --------------
26
+ ******/
27
+
1
28
  const axiosLib = require('axios')
2
- const lib = require('../../../src/lib')
3
- const fixtures = require('../../fixtures')
29
+ const { logger } = require('#src/lib/index')
30
+ const fixtures = require('#test/fixtures/index')
4
31
 
5
32
  class BasicApiClient {
6
33
  constructor ({
7
34
  baseURL,
8
35
  axios = axiosLib.create({ baseURL }),
9
- logger = lib.logger.child(this.constructor.name)
36
+ log = logger.child({ component: this.constructor.name })
10
37
  } = {}) {
11
38
  this.baseURL = baseURL
12
39
  this.axios = axios
13
- this.logger = logger
40
+ this.log = log
14
41
  this.fixtures = fixtures
15
42
  }
16
43
 
@@ -22,10 +49,10 @@ class BasicApiClient {
22
49
  headers,
23
50
  data: body
24
51
  })
25
- this.logger.info('sendRequest is done:', { method, url, body, headers, response: { status, data } })
52
+ this.log.info(`sendRequest is done [${method} ${url}]:`, { method, url, body, headers, response: { status, data } })
26
53
  return { data, status }
27
54
  } catch (err) {
28
- this.logger.error('error in sendRequest: ', err)
55
+ this.log.error('error in sendRequest: ', err)
29
56
  throw err
30
57
  }
31
58
  }
@@ -1,4 +1,32 @@
1
- const { PROXY_PORT } = require('../../integration/constants')
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2020-2025 Mojaloop Foundation
5
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
10
+
11
+ Contributors
12
+ --------------
13
+ This is the official list of the Mojaloop project contributors for this file.
14
+ Names of the original copyright holders (individuals or organizations)
15
+ should be listed with a '*' in the first column. People who have
16
+ contributed from an organization can be listed under the organization
17
+ that actually holds the copyright for their contributions (see the
18
+ Mojaloop Foundation for an example). Those individuals should have
19
+ their names indented and be marked with a '-'. Email address can be added
20
+ optionally within square brackets <email>.
21
+
22
+ * Mojaloop Foundation
23
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
24
+
25
+ --------------
26
+ ******/
27
+
28
+ const { setTimeout: sleep } = require('node:timers/promises')
29
+ const { PROXY_PORT } = require('#test/integration/constants')
2
30
  const BasicApiClient = require('./BasicApiClient')
3
31
 
4
32
  const baseURL = `http://localhost:${PROXY_PORT}`
@@ -10,6 +38,7 @@ class ProxyApiClient extends BasicApiClient {
10
38
 
11
39
  async getHistory () {
12
40
  const { data } = await this.sendRequest({ url: '/history' })
41
+ this.log.verbose('getHistory response data: ', { data })
13
42
  return data.history
14
43
  }
15
44
 
@@ -20,6 +49,22 @@ class ProxyApiClient extends BasicApiClient {
20
49
  })
21
50
  return data.history
22
51
  }
52
+
53
+ async waitForNHistoryCalls (N, retryMaxCount = 20, retryIntervalSec = 2) {
54
+ // check that the callbacks are sent and received at the FSP
55
+ // for test resilience, we will retry the history check a few times
56
+ let retryCount = 0
57
+ let history = []
58
+
59
+ while (history.length < N && retryCount < retryMaxCount) {
60
+ await sleep(retryIntervalSec * 1000)
61
+ history = await this.getHistory()
62
+ retryCount++
63
+ }
64
+ this.log.info('waitForNHistoryCalls is done: ', { history })
65
+
66
+ return history
67
+ }
23
68
  }
24
69
 
25
70
  module.exports = ProxyApiClient
@@ -25,10 +25,17 @@
25
25
  --------------
26
26
  ******/
27
27
 
28
+ const fixtures = require('../fixtures')
29
+
30
+ // eslint-disable-next-line n/no-callback-literal
31
+ const processExpierdKeysFn = async (cb) => cb(fixtures.expiredCacheKeyDto())
32
+
28
33
  const createProxyCacheMock = ({
29
34
  addDfspIdToProxyMapping = jest.fn(async () => true),
30
35
  isPendingCallback = jest.fn(async () => false),
31
36
  lookupProxyByDfspId = jest.fn(async () => null),
37
+ processExpiredAlsKeys = jest.fn(processExpierdKeysFn),
38
+ processExpiredProxyGetPartiesKeys = jest.fn(processExpierdKeysFn),
32
39
  receivedErrorResponse = jest.fn(async () => false),
33
40
  receivedSuccessResponse = jest.fn(async () => true),
34
41
  removeDfspIdFromProxyMapping = jest.fn(async () => true),
@@ -39,6 +46,8 @@ const createProxyCacheMock = ({
39
46
  addDfspIdToProxyMapping,
40
47
  isPendingCallback,
41
48
  lookupProxyByDfspId,
49
+ processExpiredAlsKeys,
50
+ processExpiredProxyGetPartiesKeys,
42
51
  receivedErrorResponse,
43
52
  receivedSuccessResponse,
44
53
  removeDfspIdFromProxyMapping,
@@ -1,54 +0,0 @@
1
- const {
2
- Factory: { createFSPIOPError },
3
- Enums: { FSPIOPErrorCodes }
4
- } = require('@mojaloop/central-services-error-handling')
5
- const {
6
- Http: { Headers: { FSPIOP: FSPIOPHeaders } },
7
- Events: { Event: { Type: EventType, Action: EventAction } },
8
- EndPoints: { FspEndpointTypes },
9
- Tags: { QueryTags: QueryTagsEnum }
10
- } = require('@mojaloop/central-services-shared').Enum
11
- const { Tracer } = require('@mojaloop/event-sdk')
12
- const EventFrameworkUtil = require('@mojaloop/central-services-shared').Util.EventFramework
13
-
14
- const LibUtil = require('../../lib/util')
15
- const Config = require('../../lib/config')
16
- const partiesUtils = require('../parties/partiesUtils')
17
-
18
- const timeoutCallbackDto = async ({ destination, partyId, partyType }) => {
19
- const headers = {
20
- [FSPIOPHeaders.SOURCE]: Config.HUB_NAME,
21
- [FSPIOPHeaders.DESTINATION]: destination
22
- }
23
- const params = {
24
- ID: partyId,
25
- Type: partyType
26
- }
27
- const error = createFSPIOPError(FSPIOPErrorCodes.EXPIRED_ERROR)
28
-
29
- const dto = {
30
- errorInformation: await partiesUtils.makePutPartiesErrorPayload(Config, error, headers, params),
31
- headers,
32
- params,
33
- endpointType: FspEndpointTypes.FSPIOP_CALLBACK_URL_PARTIES_PUT_ERROR
34
- }
35
- const span = Tracer.createSpan('timeoutInterschemePartiesLookups', { headers: dto.headers })
36
- const spanTags = LibUtil.getSpanTags({ headers: dto.headers }, EventType.PARTY, EventAction.PUT)
37
- span.setTags(spanTags)
38
- const queryTags = EventFrameworkUtil.Tags.getQueryTags(
39
- QueryTagsEnum.serviceName.accountLookupService,
40
- QueryTagsEnum.auditType.transactionFlow,
41
- QueryTagsEnum.contentType.httpRequest,
42
- QueryTagsEnum.operation.timeoutInterschemePartiesLookups,
43
- {
44
- partyIdType: params.Type,
45
- partyIdentifier: params.ID
46
- }
47
- )
48
- span.setTags(queryTags)
49
- return { ...dto, span }
50
- }
51
-
52
- module.exports = {
53
- timeoutCallbackDto
54
- }
@@ -1,24 +0,0 @@
1
- const { API_TYPES } = require('@mojaloop/central-services-shared').Util.Hapi
2
- const { timeoutCallbackDto } = require('../../../../src/domain/timeout/dto')
3
- const config = require('../../../../src/lib/config')
4
-
5
- const realApiType = config.API_TYPE
6
-
7
- describe('timeoutCallbackDto Tests -->', () => {
8
- afterAll(() => {
9
- config.API_TYPE = realApiType
10
- })
11
-
12
- test('should produce ISO payload', async () => {
13
- config.API_TYPE = API_TYPES.iso20022
14
- const destination = 'D1'
15
- const partyId = 'P1'
16
- const partyType = 'XXX'
17
- const dto = await timeoutCallbackDto({ destination, partyId, partyType })
18
- expect(dto.errorInformation).toBeTruthy()
19
-
20
- const { Assgnr, Assgne } = dto.errorInformation.Assgnmt
21
- expect(Assgnr.Agt.FinInstnId.Othr.Id).toBe(config.HUB_NAME)
22
- expect(Assgne.Agt.FinInstnId.Othr.Id).toBe(destination)
23
- })
24
- })