@mojaloop/central-ledger 17.3.2 → 17.4.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/README.md +58 -0
- package/config/default.json +29 -1
- package/package.json +2 -2
- package/src/domain/position/binProcessor.js +250 -0
- package/src/domain/position/prepare.js +251 -0
- package/src/handlers/index.js +9 -0
- package/src/handlers/positions/handlerBatch.js +232 -0
- package/src/handlers/register.js +5 -0
- package/src/models/participant/facade.js +1 -1
- package/src/models/position/batch.js +125 -0
- package/src/models/position/batchCached.js +134 -0
- package/src/models/settlement/settlementModelCached.js +2 -2
- package/src/shared/setup.js +6 -0
package/README.md
CHANGED
|
@@ -108,6 +108,45 @@ NOTE: Only POSITION.PREPARE is supported at this time, with additional event-typ
|
|
|
108
108
|
}
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
### Batch Processing Configuration Guide
|
|
112
|
+
|
|
113
|
+
Batch processing can be enabled in the transfer execution flow. Follow the steps below to enable batch processing for a more efficient transfer execution:
|
|
114
|
+
|
|
115
|
+
- **Step 1:** **Create a New Kafka Topic**
|
|
116
|
+
|
|
117
|
+
Create a new Kafka topic named `topic-transfer-position-batch` to handle batch processing events.
|
|
118
|
+
- **Step 2:** **Configure Action Type Mapping**
|
|
119
|
+
|
|
120
|
+
Point the prepare handler to the newly created topic for the action type `prepare` using the `KAFKA.EVENT_TYPE_ACTION_TOPIC_MAP` configuration as shown below:
|
|
121
|
+
```
|
|
122
|
+
"KAFKA": {
|
|
123
|
+
"EVENT_TYPE_ACTION_TOPIC_MAP" : {
|
|
124
|
+
"POSITION":{
|
|
125
|
+
"PREPARE": "topic-transfer-position-batch"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
- **Step 3:** **Run Batch Processing Handlers**
|
|
131
|
+
|
|
132
|
+
Run the position batch handler along with the existing position handler using the following configuration options:
|
|
133
|
+
- **Configure Event Type Action Topic Map for Batch Handler:**
|
|
134
|
+
|
|
135
|
+
Configure the `EVENT_TYPE_ACTION_TOPIC_MAP` parameter for the position batch handler to consume events from the new topic (topic-transfer-position-batch).
|
|
136
|
+
|
|
137
|
+
- **Set Batch Size:**
|
|
138
|
+
|
|
139
|
+
Adjust the batch size using the parameter `KAFKA.CONSUMER.TRANSFER.POSITION_BATCH.config.options.batchSize`. This parameter determines the number of messages fetched in each batch.
|
|
140
|
+
|
|
141
|
+
- **Set Consume Timeout:**
|
|
142
|
+
|
|
143
|
+
Configure the consume timeout using the parameter `KAFKA.CONSUMER.TRANSFER.POSITION_BATCH.config.options.consumeTimeout`. This parameter specifies the number of milliseconds to wait for a batch of messages to be fetched if the specified batch size of messages is not immediately available.
|
|
144
|
+
|
|
145
|
+
Example Command to Run Handlers:
|
|
146
|
+
```
|
|
147
|
+
node src/handlers/index.js handler --positionbatch
|
|
148
|
+
```
|
|
149
|
+
|
|
111
150
|
## API
|
|
112
151
|
|
|
113
152
|
For endpoint documentation, see the [API documentation](API.md).
|
|
@@ -187,6 +226,25 @@ If you want to run integration tests in a repetitive manner, you can startup the
|
|
|
187
226
|
|
|
188
227
|
If you want to run override position topic tests you can repeat the above and use `npm run test:int-override` after configuring settings found [here](#kafka-position-event-type-action-topic-map)
|
|
189
228
|
|
|
229
|
+
#### For running integration tests for batch processing interactively
|
|
230
|
+
- Run dependecies
|
|
231
|
+
```
|
|
232
|
+
docker-compose up -d mysql kafka init-kafka kafka-debug-console
|
|
233
|
+
npm run wait-4-docker
|
|
234
|
+
```
|
|
235
|
+
- Run central-ledger services
|
|
236
|
+
```
|
|
237
|
+
nvm use
|
|
238
|
+
npm run migrate
|
|
239
|
+
env "CLEDG_KAFKA__EVENT_TYPE_ACTION_TOPIC_MAP__POSITION__PREPARE=topic-transfer-position-batch" npm start
|
|
240
|
+
```
|
|
241
|
+
- Additionally, run position batch handler in a new terminal
|
|
242
|
+
```
|
|
243
|
+
env "CLEDG_KAFKA__EVENT_TYPE_ACTION_TOPIC_MAP__POSITION__PREPARE=topic-transfer-position-batch" "CLEDG_HANDLERS__API__DISABLED=true" node src/handlers/index.js handler --positionbatch
|
|
244
|
+
```
|
|
245
|
+
- Run tests using `npx tape 'test/integration-override/**/handlerBatch.test.js'`
|
|
246
|
+
|
|
247
|
+
|
|
190
248
|
If you want to just run all of the integration suite non-interactively then use npm run `test:integration`.
|
|
191
249
|
It will handle docker start up, migration, service starting and testing. Be sure to exit any previously ran handlers or docker commands.
|
|
192
250
|
|
package/config/default.json
CHANGED
|
@@ -289,7 +289,35 @@
|
|
|
289
289
|
"group.id": "cl-group-transfer-position",
|
|
290
290
|
"metadata.broker.list": "localhost:9092",
|
|
291
291
|
"socket.keepalive.enable": true,
|
|
292
|
-
"allow.auto.create.topics": true
|
|
292
|
+
"allow.auto.create.topics": true,
|
|
293
|
+
"partition.assignment.strategy": "cooperative-sticky",
|
|
294
|
+
"enable.auto.commit": false
|
|
295
|
+
},
|
|
296
|
+
"topicConf": {
|
|
297
|
+
"auto.offset.reset": "earliest"
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
"POSITION_BATCH": {
|
|
302
|
+
"config": {
|
|
303
|
+
"options": {
|
|
304
|
+
"mode": 2,
|
|
305
|
+
"batchSize": 10,
|
|
306
|
+
"pollFrequency": 10,
|
|
307
|
+
"recursiveTimeout": 100,
|
|
308
|
+
"messageCharset": "utf8",
|
|
309
|
+
"messageAsJSON": true,
|
|
310
|
+
"sync": true,
|
|
311
|
+
"consumeTimeout": 10
|
|
312
|
+
},
|
|
313
|
+
"rdkafkaConf": {
|
|
314
|
+
"client.id": "cl-con-transfer-position-batch",
|
|
315
|
+
"group.id": "cl-group-transfer-position-batch",
|
|
316
|
+
"metadata.broker.list": "localhost:9092",
|
|
317
|
+
"socket.keepalive.enable": true,
|
|
318
|
+
"allow.auto.create.topics": true,
|
|
319
|
+
"partition.assignment.strategy": "cooperative-sticky",
|
|
320
|
+
"enable.auto.commit": false
|
|
293
321
|
},
|
|
294
322
|
"topicConf": {
|
|
295
323
|
"auto.offset.reset": "earliest"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mojaloop/central-ledger",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.4.0-snapshot.0",
|
|
4
4
|
"description": "Central ledger hosted by a scheme to record and settle transfers",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "ModusBox",
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"glob": "10.3.10",
|
|
110
110
|
"hapi-auth-basic": "5.0.0",
|
|
111
111
|
"hapi-auth-bearer-token": "8.0.0",
|
|
112
|
-
"hapi-swagger": "17.
|
|
112
|
+
"hapi-swagger": "17.2.0",
|
|
113
113
|
"ilp-packet": "2.2.0",
|
|
114
114
|
"knex": "3.0.1",
|
|
115
115
|
"lodash": "4.17.21",
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/*****
|
|
2
|
+
License
|
|
3
|
+
--------------
|
|
4
|
+
Copyright © 2017 Bill & Melinda Gates Foundation
|
|
5
|
+
The Mojaloop files are made available by the Bill & Melinda Gates 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
|
+
Gates Foundation organization 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
|
+
* Gates Foundation
|
|
23
|
+
- Name Surname <name.surname@gatesfoundation.com>
|
|
24
|
+
|
|
25
|
+
* INFITX
|
|
26
|
+
- Vijay Kumar Guthi <vijaya.guthi@infitx.com>
|
|
27
|
+
- Steven Oderayi <steven.oderayi@infitx.com>
|
|
28
|
+
|
|
29
|
+
--------------
|
|
30
|
+
******/
|
|
31
|
+
'use strict'
|
|
32
|
+
|
|
33
|
+
const Logger = require('@mojaloop/central-services-logger')
|
|
34
|
+
const BatchPositionModel = require('../../models/position/batch')
|
|
35
|
+
const BatchPositionModelCached = require('../../models/position/batchCached')
|
|
36
|
+
const PositionPrepareDomain = require('./prepare')
|
|
37
|
+
const SettlementModelCached = require('../../models/settlement/settlementModelCached')
|
|
38
|
+
const Enum = require('@mojaloop/central-services-shared').Enum
|
|
39
|
+
const ErrorHandler = require('@mojaloop/central-services-error-handling')
|
|
40
|
+
// TODO: We may not need this if we optimize the participantLimit query
|
|
41
|
+
const participantFacade = require('../../models/participant/facade')
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @function processBins
|
|
45
|
+
*
|
|
46
|
+
* @async
|
|
47
|
+
* @description This is the domain function to process a list bins containing position messages grouped by participant account.
|
|
48
|
+
*
|
|
49
|
+
* @param {array} bins - a list of account-bins to process
|
|
50
|
+
* @param {object} trx - Database transaction object
|
|
51
|
+
*
|
|
52
|
+
* @returns {results} - Returns a list of bins with results or throws an error if failed
|
|
53
|
+
*/
|
|
54
|
+
const processBins = async (bins, trx) => {
|
|
55
|
+
const transferIdList = []
|
|
56
|
+
iterateThroughBins(bins, (_accountID, _action, item) => {
|
|
57
|
+
if (item.decodedPayload?.transferId) {
|
|
58
|
+
transferIdList.push(item.decodedPayload.transferId)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
// Pre fetch latest transferStates for all the transferIds in the account-bin
|
|
62
|
+
const latestTransferStateChanges = await BatchPositionModel.getLatestTransferStateChangesByTransferIdList(trx, transferIdList)
|
|
63
|
+
const latestTransferStates = {}
|
|
64
|
+
for (const key in latestTransferStateChanges) {
|
|
65
|
+
latestTransferStates[key] = latestTransferStateChanges[key].transferStateId
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const accountIds = Object.keys(bins)
|
|
69
|
+
|
|
70
|
+
// Pre fetch all settlement accounts corresponding to the position accounts
|
|
71
|
+
// Get all participantIdMap for the accountIds
|
|
72
|
+
const participantCurrencyIds = await BatchPositionModelCached.getParticipantCurrencyByIds(trx, accountIds)
|
|
73
|
+
|
|
74
|
+
// Validate that participantCurrencyIds exist for each of the accountIds
|
|
75
|
+
// i.e every unique accountId has a corresponding entry in participantCurrencyIds
|
|
76
|
+
const participantIdsHavingCurrencyIdsList = [...new Set(participantCurrencyIds.map(item => item.participantCurrencyId))]
|
|
77
|
+
const allAccountIdsHaveParticipantCurrencyIds = accountIds.every(accountId => {
|
|
78
|
+
return participantIdsHavingCurrencyIdsList.includes(Number(accountId))
|
|
79
|
+
})
|
|
80
|
+
if (!allAccountIdsHaveParticipantCurrencyIds) {
|
|
81
|
+
throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR, 'Not all accountIds have corresponding participantCurrencyIds')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const allSettlementModels = await SettlementModelCached.getAll()
|
|
85
|
+
|
|
86
|
+
// Construct objects participantIdMap, accountIdMap and currencyIdMap
|
|
87
|
+
const participantIdMap = {}
|
|
88
|
+
const accountIdMap = {}
|
|
89
|
+
const currencyIdMap = {}
|
|
90
|
+
for (const item of participantCurrencyIds) {
|
|
91
|
+
const { participantId, currencyId, participantCurrencyId } = item
|
|
92
|
+
if (!participantIdMap[participantId]) {
|
|
93
|
+
participantIdMap[participantId] = {}
|
|
94
|
+
}
|
|
95
|
+
if (!currencyIdMap[currencyId]) {
|
|
96
|
+
currencyIdMap[currencyId] = {
|
|
97
|
+
settlementModel: _getSettlementModelForCurrency(currencyId, allSettlementModels)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
participantIdMap[participantId][currencyId] = participantCurrencyId
|
|
101
|
+
accountIdMap[participantCurrencyId] = { participantId, currencyId }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Get all participantCurrencyIds for the participantIdMap
|
|
105
|
+
const allParticipantCurrencyIds = await BatchPositionModelCached.getParticipantCurrencyByParticipantIds(trx, Object.keys(participantIdMap))
|
|
106
|
+
const settlementCurrencyIds = []
|
|
107
|
+
for (const pc of allParticipantCurrencyIds) {
|
|
108
|
+
const correspondingParticipantCurrencyId = participantIdMap[pc.participantId][pc.currencyId]
|
|
109
|
+
if (correspondingParticipantCurrencyId) {
|
|
110
|
+
const settlementModel = currencyIdMap[pc.currencyId].settlementModel
|
|
111
|
+
if (pc.ledgerAccountTypeId === settlementModel.settlementAccountTypeId) {
|
|
112
|
+
settlementCurrencyIds.push(pc)
|
|
113
|
+
accountIdMap[correspondingParticipantCurrencyId].settlementCurrencyId = pc.participantCurrencyId
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Pre fetch all position account balances for the account-bin and acquire lock on position
|
|
119
|
+
const positions = await BatchPositionModel.getPositionsByAccountIdsForUpdate(trx, [
|
|
120
|
+
...accountIds,
|
|
121
|
+
...settlementCurrencyIds.map(pc => pc.participantCurrencyId)
|
|
122
|
+
])
|
|
123
|
+
|
|
124
|
+
let notifyMessages = []
|
|
125
|
+
let limitAlarms = []
|
|
126
|
+
|
|
127
|
+
// For each account-bin in the list
|
|
128
|
+
for (const accountID in bins) {
|
|
129
|
+
const accountBin = bins[accountID]
|
|
130
|
+
const actions = Object.keys(accountBin)
|
|
131
|
+
|
|
132
|
+
const settlementParticipantPosition = positions[accountIdMap[accountID].settlementCurrencyId].value
|
|
133
|
+
const settlementModel = currencyIdMap[accountIdMap[accountID].currencyId].settlementModel
|
|
134
|
+
|
|
135
|
+
// TODO: Refactor the following SQL query to optimize the performance
|
|
136
|
+
const participantLimit = await participantFacade.getParticipantLimitByParticipantCurrencyLimit(
|
|
137
|
+
accountIdMap[accountID].participantId,
|
|
138
|
+
accountIdMap[accountID].currencyId,
|
|
139
|
+
Enum.Accounts.LedgerAccountType.POSITION,
|
|
140
|
+
Enum.Accounts.ParticipantLimitType.NET_DEBIT_CAP
|
|
141
|
+
)
|
|
142
|
+
// Initialize accumulated values
|
|
143
|
+
// These values will be passed across various actions in the bin
|
|
144
|
+
let accumulatedPositionValue = positions[accountID].value
|
|
145
|
+
let accumulatedPositionReservedValue = positions[accountID].reservedValue
|
|
146
|
+
let accumulatedTransferStates = latestTransferStates
|
|
147
|
+
let accumulatedTransferStateChanges = []
|
|
148
|
+
let accumulatedPositionChanges = []
|
|
149
|
+
|
|
150
|
+
// If non-prepare action found, log error
|
|
151
|
+
// We need to remove this once we implement all the actions
|
|
152
|
+
if (actions.length > 1 || (actions.length === 1 && actions[0] !== 'prepare')) {
|
|
153
|
+
Logger.isErrorEnabled && Logger.error('Only prepare action is allowed in a batch')
|
|
154
|
+
// throw new Error('Only prepare action is allowed in a batch')
|
|
155
|
+
}
|
|
156
|
+
// If prepare action found then call processPositionPrepareBin function
|
|
157
|
+
const prepareActionResult = await PositionPrepareDomain.processPositionPrepareBin(
|
|
158
|
+
accountBin.prepare,
|
|
159
|
+
accumulatedPositionValue,
|
|
160
|
+
accumulatedPositionReservedValue,
|
|
161
|
+
accumulatedTransferStates,
|
|
162
|
+
settlementParticipantPosition,
|
|
163
|
+
settlementModel,
|
|
164
|
+
participantLimit
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
// Update accumulated values
|
|
168
|
+
accumulatedPositionValue = prepareActionResult.accumulatedPositionValue
|
|
169
|
+
accumulatedPositionReservedValue = prepareActionResult.accumulatedPositionReservedValue
|
|
170
|
+
accumulatedTransferStates = prepareActionResult.accumulatedTransferStates
|
|
171
|
+
// Append accumulated arrays
|
|
172
|
+
accumulatedTransferStateChanges = accumulatedTransferStateChanges.concat(prepareActionResult.accumulatedTransferStateChanges)
|
|
173
|
+
accumulatedPositionChanges = accumulatedPositionChanges.concat(prepareActionResult.accumulatedPositionChanges)
|
|
174
|
+
notifyMessages = notifyMessages.concat(prepareActionResult.notifyMessages)
|
|
175
|
+
|
|
176
|
+
// Update accumulated position values by calling a facade function
|
|
177
|
+
await BatchPositionModel.updateParticipantPosition(trx, positions[accountID].participantPositionId, accumulatedPositionValue, accumulatedPositionReservedValue)
|
|
178
|
+
|
|
179
|
+
// Bulk insert accumulated transferStateChanges by calling a facade function
|
|
180
|
+
await BatchPositionModel.bulkInsertTransferStateChanges(trx, accumulatedTransferStateChanges)
|
|
181
|
+
|
|
182
|
+
// Bulk get the transferStateChangeIds for transferids using select whereIn
|
|
183
|
+
const fetchedTransferStateChanges = await BatchPositionModel.getLatestTransferStateChangesByTransferIdList(trx, accumulatedTransferStateChanges.map(item => item.transferId))
|
|
184
|
+
// Mutate accumulated positionChanges with transferStateChangeIds
|
|
185
|
+
for (const positionChange of accumulatedPositionChanges) {
|
|
186
|
+
positionChange.transferStateChangeId = fetchedTransferStateChanges[positionChange.transferId].transferStateChangeId
|
|
187
|
+
positionChange.participantPositionId = positions[accountID].participantPositionId
|
|
188
|
+
delete positionChange.transferId
|
|
189
|
+
}
|
|
190
|
+
// Bulk insert accumulated positionChanges by calling a facade function
|
|
191
|
+
await BatchPositionModel.bulkInsertParticipantPositionChanges(trx, accumulatedPositionChanges)
|
|
192
|
+
|
|
193
|
+
limitAlarms = limitAlarms.concat(prepareActionResult.limitAlarms)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Return results
|
|
197
|
+
return {
|
|
198
|
+
notifyMessages,
|
|
199
|
+
limitAlarms
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @function iterateThroughBins
|
|
205
|
+
*
|
|
206
|
+
* @async
|
|
207
|
+
* @description Helper function to iterate though all messages in bins.
|
|
208
|
+
*
|
|
209
|
+
* @param {array} bins - a list of account-bins to iterate
|
|
210
|
+
* @param {async function} cb - callback function to call for each item
|
|
211
|
+
* @param {async function} errCb - callback function to call incase of any error
|
|
212
|
+
*
|
|
213
|
+
* @returns {void} - Doesn't return anything
|
|
214
|
+
*/
|
|
215
|
+
|
|
216
|
+
const iterateThroughBins = async (bins, cb, errCb) => {
|
|
217
|
+
for (const accountID in bins) {
|
|
218
|
+
const accountBin = bins[accountID]
|
|
219
|
+
for (const action in accountBin) {
|
|
220
|
+
const actionBin = accountBin[action]
|
|
221
|
+
for (const item of actionBin) {
|
|
222
|
+
try {
|
|
223
|
+
await cb(accountID, action, item)
|
|
224
|
+
} catch (err) {
|
|
225
|
+
if (errCb === undefined) {
|
|
226
|
+
Logger.isErrorEnabled && Logger.error(err)
|
|
227
|
+
} else {
|
|
228
|
+
await errCb(accountID, action, item)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const _getSettlementModelForCurrency = (currencyId, allSettlementModels) => {
|
|
237
|
+
let settlementModels = allSettlementModels.filter(model => model.currencyId === currencyId)
|
|
238
|
+
if (settlementModels.length === 0) {
|
|
239
|
+
settlementModels = allSettlementModels.filter(model => model.currencyId === null) // Default settlement model
|
|
240
|
+
if (settlementModels.length === 0) {
|
|
241
|
+
throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.GENERIC_SETTLEMENT_ERROR, 'Unable to find a matching or default, Settlement Model')
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return settlementModels.find(sm => sm.ledgerAccountTypeId === Enum.Accounts.LedgerAccountType.POSITION)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
module.exports = {
|
|
248
|
+
processBins,
|
|
249
|
+
iterateThroughBins
|
|
250
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const { Enum } = require('@mojaloop/central-services-shared')
|
|
2
|
+
const ErrorHandler = require('@mojaloop/central-services-error-handling')
|
|
3
|
+
const Config = require('../../lib/config')
|
|
4
|
+
const Utility = require('@mojaloop/central-services-shared').Util
|
|
5
|
+
const resourceVersions = require('@mojaloop/central-services-shared').Util.resourceVersions
|
|
6
|
+
const MLNumber = require('@mojaloop/ml-number')
|
|
7
|
+
const Logger = require('@mojaloop/central-services-logger')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @function processPositionPrepareBin
|
|
11
|
+
*
|
|
12
|
+
* @async
|
|
13
|
+
* @description This is the domain function to process a bin of position-prepare messages of a single participant account.
|
|
14
|
+
*
|
|
15
|
+
* @param {array} binItems - an array of objects that contain a position prepare message and its span. {message, span}
|
|
16
|
+
* @param {number} accumulatedPositionValue - value of position accumulated so far from previous bin processing
|
|
17
|
+
* @param {number} accumulatedPositionReservedValue - value of position reserved accumulated so far, not used but kept for consistency
|
|
18
|
+
* @param {object} accumulatedTransferStates - object with transfer id keys and transfer state id values. Used to check if transfer is in correct state for processing. Clone and update states for output.
|
|
19
|
+
* @param {number} settlementParticipantPosition - position value of the participants settlement account
|
|
20
|
+
* @param {object} settlementModel - settlement model object for the currency
|
|
21
|
+
* @param {object} participantLimit - participant limit object for the currency
|
|
22
|
+
* @returns {object} - Returns an object containing accumulatedPositionValue, accumulatedPositionReservedValue, accumulatedTransferStateChanges, accumulatedTransferStates, resultMessages, limitAlarms or throws an error if failed
|
|
23
|
+
*/
|
|
24
|
+
const processPositionPrepareBin = async (
|
|
25
|
+
binItems,
|
|
26
|
+
accumulatedPositionValue,
|
|
27
|
+
accumulatedPositionReservedValue,
|
|
28
|
+
accumulatedTransferStates,
|
|
29
|
+
settlementParticipantPosition,
|
|
30
|
+
settlementModel,
|
|
31
|
+
participantLimit
|
|
32
|
+
) => {
|
|
33
|
+
const transferStateChanges = []
|
|
34
|
+
const participantPositionChanges = []
|
|
35
|
+
const resultMessages = []
|
|
36
|
+
const limitAlarms = []
|
|
37
|
+
const accumulatedTransferStatesCopy = Object.assign({}, accumulatedTransferStates)
|
|
38
|
+
|
|
39
|
+
let currentPosition = new MLNumber(accumulatedPositionValue)
|
|
40
|
+
const reservedPosition = new MLNumber(accumulatedPositionReservedValue)
|
|
41
|
+
const effectivePosition = new MLNumber(currentPosition.add(reservedPosition).toFixed(Config.AMOUNT.SCALE))
|
|
42
|
+
const liquidityCover = new MLNumber(settlementParticipantPosition).multiply(-1)
|
|
43
|
+
const payerLimit = new MLNumber(participantLimit.value)
|
|
44
|
+
let availablePositionBasedOnLiquidityCover = new MLNumber(liquidityCover.subtract(effectivePosition).toFixed(Config.AMOUNT.SCALE))
|
|
45
|
+
Logger.isInfoEnabled && Logger.info(`processPositionPrepareBin::availablePositionBasedOnLiquidityCover: ${availablePositionBasedOnLiquidityCover}`)
|
|
46
|
+
let availablePositionBasedOnPayerLimit = new MLNumber(payerLimit.subtract(effectivePosition).toFixed(Config.AMOUNT.SCALE))
|
|
47
|
+
Logger.isDebugEnabled && Logger.debug(`processPositionPrepareBin::availablePositionBasedOnPayerLimit: ${availablePositionBasedOnPayerLimit}`)
|
|
48
|
+
|
|
49
|
+
if (binItems && binItems.length > 0) {
|
|
50
|
+
for (const binItem of binItems) {
|
|
51
|
+
let transferStateId
|
|
52
|
+
let reason
|
|
53
|
+
let resultMessage
|
|
54
|
+
const transfer = binItem.decodedPayload
|
|
55
|
+
Logger.isDebugEnabled && Logger.debug(`processPositionPrepareBin::transfer:processingMessage: ${JSON.stringify(transfer)}`)
|
|
56
|
+
|
|
57
|
+
// Check if transfer is in correct state for processing, produce an internal error message
|
|
58
|
+
if (accumulatedTransferStates[transfer.transferId] !== Enum.Transfers.TransferInternalState.RECEIVED_PREPARE) {
|
|
59
|
+
Logger.isDebugEnabled && Logger.debug(`processPositionPrepareBin::transferState: ${accumulatedTransferStates[transfer.transferId]} !== ${Enum.Transfers.TransferInternalState.RECEIVED_PREPARE}`)
|
|
60
|
+
|
|
61
|
+
transferStateId = Enum.Transfers.TransferInternalState.ABORTED_REJECTED
|
|
62
|
+
reason = 'Transfer in incorrect state'
|
|
63
|
+
|
|
64
|
+
const headers = Utility.Http.SwitchDefaultHeaders(
|
|
65
|
+
transfer.payerFsp,
|
|
66
|
+
Enum.Http.HeaderResources.TRANSFERS,
|
|
67
|
+
Enum.Http.Headers.FSPIOP.SWITCH.value,
|
|
68
|
+
resourceVersions[Enum.Http.HeaderResources.TRANSFERS].contentVersion
|
|
69
|
+
)
|
|
70
|
+
const fspiopError = ErrorHandler.Factory.createFSPIOPError(
|
|
71
|
+
ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR
|
|
72
|
+
).toApiErrorObject(Config.ERROR_HANDLING)
|
|
73
|
+
const state = Utility.StreamingProtocol.createEventState(
|
|
74
|
+
Enum.Events.EventStatus.FAILURE.status,
|
|
75
|
+
fspiopError.errorInformation.errorCode,
|
|
76
|
+
fspiopError.errorInformation.errorDescription
|
|
77
|
+
)
|
|
78
|
+
const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
|
|
79
|
+
transfer.transferId,
|
|
80
|
+
Enum.Kafka.Topics.NOTIFICATION,
|
|
81
|
+
Enum.Events.Event.Action.PREPARE,
|
|
82
|
+
state
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
resultMessage = Utility.StreamingProtocol.createMessage(
|
|
86
|
+
transfer.transferId,
|
|
87
|
+
transfer.payeeFsp,
|
|
88
|
+
transfer.payerFsp,
|
|
89
|
+
metadata,
|
|
90
|
+
headers,
|
|
91
|
+
fspiopError,
|
|
92
|
+
{ id: transfer.transferId },
|
|
93
|
+
'application/json'
|
|
94
|
+
)
|
|
95
|
+
binItem.result = { success: false }
|
|
96
|
+
// Check if payer has insufficient liquidity, produce an error message and abort transfer
|
|
97
|
+
} else if (availablePositionBasedOnLiquidityCover.toNumber() < transfer.amount.amount) {
|
|
98
|
+
transferStateId = Enum.Transfers.TransferInternalState.ABORTED_REJECTED
|
|
99
|
+
reason = ErrorHandler.Enums.FSPIOPErrorCodes.PAYER_FSP_INSUFFICIENT_LIQUIDITY.message
|
|
100
|
+
|
|
101
|
+
const headers = Utility.Http.SwitchDefaultHeaders(
|
|
102
|
+
transfer.payerFsp,
|
|
103
|
+
Enum.Http.HeaderResources.TRANSFERS,
|
|
104
|
+
Enum.Http.Headers.FSPIOP.SWITCH.value,
|
|
105
|
+
resourceVersions[Enum.Http.HeaderResources.TRANSFERS].contentVersion
|
|
106
|
+
)
|
|
107
|
+
const fspiopError = ErrorHandler.Factory.createFSPIOPError(
|
|
108
|
+
ErrorHandler.Enums.FSPIOPErrorCodes.PAYER_FSP_INSUFFICIENT_LIQUIDITY
|
|
109
|
+
).toApiErrorObject(Config.ERROR_HANDLING)
|
|
110
|
+
const state = Utility.StreamingProtocol.createEventState(
|
|
111
|
+
Enum.Events.EventStatus.FAILURE.status,
|
|
112
|
+
fspiopError.errorInformation.errorCode,
|
|
113
|
+
fspiopError.errorInformation.errorDescription
|
|
114
|
+
)
|
|
115
|
+
const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
|
|
116
|
+
transfer.transferId,
|
|
117
|
+
Enum.Kafka.Topics.NOTIFICATION,
|
|
118
|
+
Enum.Events.Event.Action.PREPARE,
|
|
119
|
+
state
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
resultMessage = Utility.StreamingProtocol.createMessage(
|
|
123
|
+
transfer.transferId,
|
|
124
|
+
transfer.payeeFsp,
|
|
125
|
+
transfer.payerFsp,
|
|
126
|
+
metadata,
|
|
127
|
+
headers,
|
|
128
|
+
fspiopError,
|
|
129
|
+
{ id: transfer.transferId },
|
|
130
|
+
'application/json'
|
|
131
|
+
)
|
|
132
|
+
binItem.result = { success: false }
|
|
133
|
+
// Check if payer has surpassed their limit, produce an error message and abort transfer
|
|
134
|
+
} else if (availablePositionBasedOnPayerLimit.toNumber() < transfer.amount.amount) {
|
|
135
|
+
transferStateId = Enum.Transfers.TransferInternalState.ABORTED_REJECTED
|
|
136
|
+
reason = ErrorHandler.Enums.FSPIOPErrorCodes.PAYER_LIMIT_ERROR.message
|
|
137
|
+
|
|
138
|
+
const headers = Utility.Http.SwitchDefaultHeaders(
|
|
139
|
+
transfer.payerFsp,
|
|
140
|
+
Enum.Http.HeaderResources.TRANSFERS,
|
|
141
|
+
Enum.Http.Headers.FSPIOP.SWITCH.value,
|
|
142
|
+
resourceVersions[Enum.Http.HeaderResources.TRANSFERS].contentVersion
|
|
143
|
+
)
|
|
144
|
+
const fspiopError = ErrorHandler.Factory.createFSPIOPError(
|
|
145
|
+
ErrorHandler.Enums.FSPIOPErrorCodes.PAYER_LIMIT_ERROR
|
|
146
|
+
).toApiErrorObject(Config.ERROR_HANDLING)
|
|
147
|
+
const state = Utility.StreamingProtocol.createEventState(
|
|
148
|
+
Enum.Events.EventStatus.FAILURE.status,
|
|
149
|
+
fspiopError.errorInformation.errorCode,
|
|
150
|
+
fspiopError.errorInformation.errorDescription
|
|
151
|
+
)
|
|
152
|
+
const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
|
|
153
|
+
transfer.transferId,
|
|
154
|
+
Enum.Kafka.Topics.NOTIFICATION,
|
|
155
|
+
Enum.Events.Event.Action.PREPARE,
|
|
156
|
+
state
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
resultMessage = Utility.StreamingProtocol.createMessage(
|
|
160
|
+
transfer.transferId,
|
|
161
|
+
transfer.payeeFsp,
|
|
162
|
+
transfer.payerFsp,
|
|
163
|
+
metadata,
|
|
164
|
+
headers,
|
|
165
|
+
fspiopError,
|
|
166
|
+
{ id: transfer.transferId },
|
|
167
|
+
'application/json'
|
|
168
|
+
)
|
|
169
|
+
binItem.result = { success: false }
|
|
170
|
+
// Payer has sufficient liquidity and limit
|
|
171
|
+
} else {
|
|
172
|
+
transferStateId = Enum.Transfers.TransferState.RESERVED
|
|
173
|
+
currentPosition = currentPosition.add(transfer.amount.amount)
|
|
174
|
+
availablePositionBasedOnLiquidityCover = availablePositionBasedOnLiquidityCover.add(transfer.amount.amount)
|
|
175
|
+
availablePositionBasedOnPayerLimit = availablePositionBasedOnPayerLimit.add(transfer.amount.amount)
|
|
176
|
+
|
|
177
|
+
// Are these the right headers?
|
|
178
|
+
const headers = Utility.Http.SwitchDefaultHeaders(
|
|
179
|
+
transfer.payeeFsp,
|
|
180
|
+
Enum.Http.HeaderResources.TRANSFERS,
|
|
181
|
+
transfer.payerFsp,
|
|
182
|
+
resourceVersions[Enum.Http.HeaderResources.TRANSFERS].contentVersion
|
|
183
|
+
)
|
|
184
|
+
const state = Utility.StreamingProtocol.createEventState(
|
|
185
|
+
Enum.Events.EventStatus.SUCCESS.status,
|
|
186
|
+
null,
|
|
187
|
+
null
|
|
188
|
+
)
|
|
189
|
+
const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
|
|
190
|
+
transfer.transferId,
|
|
191
|
+
Enum.Kafka.Topics.TRANSFER,
|
|
192
|
+
Enum.Events.Event.Action.PREPARE,
|
|
193
|
+
state
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
resultMessage = Utility.StreamingProtocol.createMessage(
|
|
197
|
+
transfer.transferId,
|
|
198
|
+
transfer.payeeFsp,
|
|
199
|
+
transfer.payerFsp,
|
|
200
|
+
metadata,
|
|
201
|
+
headers,
|
|
202
|
+
transfer,
|
|
203
|
+
{},
|
|
204
|
+
'application/json'
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
const participantPositionChange = {
|
|
208
|
+
transferId: transfer.transferId, // Need to delete this in bin processor while updating transferStateChangeId
|
|
209
|
+
transferStateChangeId: null, // Need to update this in bin processor while executing queries
|
|
210
|
+
value: currentPosition.toNumber(),
|
|
211
|
+
reservedValue: accumulatedPositionReservedValue
|
|
212
|
+
}
|
|
213
|
+
participantPositionChanges.push(participantPositionChange)
|
|
214
|
+
Logger.isDebugEnabled && Logger.debug(`processPositionPrepareBin::participantPositionChange: ${JSON.stringify(participantPositionChange)}`)
|
|
215
|
+
binItem.result = { success: true }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
resultMessages.push({ binItem, message: resultMessage })
|
|
219
|
+
|
|
220
|
+
const transferStateChange = {
|
|
221
|
+
transferId: transfer.transferId,
|
|
222
|
+
transferStateId,
|
|
223
|
+
reason
|
|
224
|
+
}
|
|
225
|
+
transferStateChanges.push(transferStateChange)
|
|
226
|
+
Logger.isDebugEnabled && Logger.debug(`processPositionPrepareBin::transferStateChange: ${JSON.stringify(transferStateChange)}`)
|
|
227
|
+
|
|
228
|
+
Logger.isDebugEnabled && Logger.debug(`processPositionPrepareBin::limitAlarm: ${currentPosition.toNumber()} > ${liquidityCover.multiply(participantLimit.thresholdAlarmPercentage)}`)
|
|
229
|
+
if (currentPosition.toNumber() > liquidityCover.multiply(participantLimit.thresholdAlarmPercentage).toNumber()) {
|
|
230
|
+
limitAlarms.push(participantLimit)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
accumulatedTransferStatesCopy[transfer.transferId] = transferStateId
|
|
234
|
+
Logger.isDebugEnabled && Logger.debug(`processPositionPrepareBin::accumulatedTransferStatesCopy:finalizedTransferState ${JSON.stringify(transferStateId)}`)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
accumulatedPositionValue: currentPosition.toNumber(),
|
|
240
|
+
accumulatedTransferStates: accumulatedTransferStatesCopy, // finalized transfer state after prepare processing
|
|
241
|
+
accumulatedPositionReservedValue, // not used but kept for consistency
|
|
242
|
+
accumulatedTransferStateChanges: transferStateChanges, // transfer state changes to be persisted in order
|
|
243
|
+
limitAlarms, // array of participant limits that have been breached
|
|
244
|
+
accumulatedPositionChanges: participantPositionChanges, // participant position changes to be persisted in order
|
|
245
|
+
notifyMessages: resultMessages // array of objects containing bin item and result message. {binItem, message}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = {
|
|
250
|
+
processPositionPrepareBin
|
|
251
|
+
}
|
package/src/handlers/index.js
CHANGED
|
@@ -52,6 +52,7 @@ Program.command('handler') // sub-command name, coffeeType = type, required
|
|
|
52
52
|
.description('Start a specified Handler') // command description
|
|
53
53
|
.option('--prepare', 'Start the Prepare Handler')
|
|
54
54
|
.option('--position', 'Start the Position Handler')
|
|
55
|
+
.option('--positionbatch', 'Start the Position Batch Handler')
|
|
55
56
|
.option('--get', 'Start the Transfer Get Handler')
|
|
56
57
|
.option('--fulfil', 'Start the Fulfil Handler')
|
|
57
58
|
.option('--timeout', 'Start the Timeout Handler')
|
|
@@ -81,6 +82,14 @@ Program.command('handler') // sub-command name, coffeeType = type, required
|
|
|
81
82
|
}
|
|
82
83
|
handlerList.push(handler)
|
|
83
84
|
}
|
|
85
|
+
if (args.positionbatch) {
|
|
86
|
+
Logger.isDebugEnabled && Logger.debug('CLI: Executing --positionbatch')
|
|
87
|
+
const handler = {
|
|
88
|
+
type: 'positionbatch',
|
|
89
|
+
enabled: true
|
|
90
|
+
}
|
|
91
|
+
handlerList.push(handler)
|
|
92
|
+
}
|
|
84
93
|
if (args.get) {
|
|
85
94
|
Logger.isDebugEnabled && Logger.debug('CLI: Executing --get')
|
|
86
95
|
const handler = {
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/*****
|
|
2
|
+
License
|
|
3
|
+
--------------
|
|
4
|
+
Copyright © 2017 Bill & Melinda Gates Foundation
|
|
5
|
+
The Mojaloop files are made available by the Bill & Melinda Gates 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
|
+
Gates Foundation organization 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
|
+
* Gates Foundation
|
|
23
|
+
- Name Surname <name.surname@gatesfoundation.com>
|
|
24
|
+
|
|
25
|
+
* INFITX
|
|
26
|
+
- Vijay Kumar Guthi <vijaya.guthi@infitx.com>
|
|
27
|
+
|
|
28
|
+
--------------
|
|
29
|
+
******/
|
|
30
|
+
'use strict'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @module src/handlers/positions
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const Logger = require('@mojaloop/central-services-logger')
|
|
37
|
+
const EventSdk = require('@mojaloop/event-sdk')
|
|
38
|
+
const BinProcessor = require('../../domain/position/binProcessor')
|
|
39
|
+
const SettlementModelCached = require('../../models/settlement/settlementModelCached')
|
|
40
|
+
const Utility = require('@mojaloop/central-services-shared').Util
|
|
41
|
+
const Kafka = require('@mojaloop/central-services-shared').Util.Kafka
|
|
42
|
+
const Producer = require('@mojaloop/central-services-stream').Util.Producer
|
|
43
|
+
const Consumer = require('@mojaloop/central-services-stream').Util.Consumer
|
|
44
|
+
const Enum = require('@mojaloop/central-services-shared').Enum
|
|
45
|
+
const Metrics = require('@mojaloop/central-services-metrics')
|
|
46
|
+
const Config = require('../../lib/config')
|
|
47
|
+
const Uuid = require('uuid4')
|
|
48
|
+
// const decodePayload = require('@mojaloop/central-services-shared').Util.StreamingProtocol.decodePayload
|
|
49
|
+
// const decodeMessages = require('@mojaloop/central-services-shared').Util.StreamingProtocol.decodeMessages
|
|
50
|
+
const ErrorHandler = require('@mojaloop/central-services-error-handling')
|
|
51
|
+
// const location = { module: 'PositionHandler', method: '', path: '' } // var object used as pointer
|
|
52
|
+
const BatchPositionModel = require('../../models/position/batch')
|
|
53
|
+
const decodePayload = require('@mojaloop/central-services-shared').Util.StreamingProtocol.decodePayload
|
|
54
|
+
|
|
55
|
+
const consumerCommit = true
|
|
56
|
+
// const fromSwitch = true
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @function positions
|
|
60
|
+
*
|
|
61
|
+
* @async
|
|
62
|
+
* @description This is the consumer callback function that gets registered to a topic. This then gets a list of messages.
|
|
63
|
+
*
|
|
64
|
+
* @param {error} error - error thrown if something fails within Kafka
|
|
65
|
+
* @param {array} messages - a list of messages to consume for the relevant topic
|
|
66
|
+
*
|
|
67
|
+
* @returns {object} - Returns a boolean: true if successful, or throws and error if failed
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
const positions = async (error, messages) => {
|
|
71
|
+
const histTimerEnd = Metrics.getHistogram(
|
|
72
|
+
'transfer_position_batch',
|
|
73
|
+
'Consume a batch of prepare transfer messages from the kafka topic and process them',
|
|
74
|
+
['success']
|
|
75
|
+
).startTimer()
|
|
76
|
+
|
|
77
|
+
if (error) {
|
|
78
|
+
histTimerEnd({ success: false })
|
|
79
|
+
throw ErrorHandler.Factory.reformatFSPIOPError(error)
|
|
80
|
+
}
|
|
81
|
+
let consumedMessages = []
|
|
82
|
+
|
|
83
|
+
if (Array.isArray(messages)) {
|
|
84
|
+
consumedMessages = Array.from(messages)
|
|
85
|
+
} else {
|
|
86
|
+
consumedMessages = [Object.assign({}, Utility.clone(messages))]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const firstMessageOffset = consumedMessages[0]?.offset
|
|
90
|
+
const lastMessageOffset = consumedMessages[consumedMessages.length - 1]?.offset
|
|
91
|
+
const binId = `${firstMessageOffset}-${lastMessageOffset}`
|
|
92
|
+
|
|
93
|
+
// TODO: How to handle spans and audits for batch of messages ??
|
|
94
|
+
// Currently we have to create a span and do and audit for each message
|
|
95
|
+
|
|
96
|
+
// Iterate through consumedMessages
|
|
97
|
+
const bins = {}
|
|
98
|
+
for (const message of consumedMessages) {
|
|
99
|
+
const histTimerMsgEnd = Metrics.getHistogram(
|
|
100
|
+
'transfer_position',
|
|
101
|
+
'Process a prepare transfer message',
|
|
102
|
+
['success', 'action']
|
|
103
|
+
).startTimer()
|
|
104
|
+
// Create a span for each message
|
|
105
|
+
const contextFromMessage = EventSdk.Tracer.extractContextFromMessage(message.value)
|
|
106
|
+
const span = EventSdk.Tracer.createChildSpanFromContext('cl_transfer_position', contextFromMessage)
|
|
107
|
+
span.setTags({
|
|
108
|
+
processedAsBatch: true,
|
|
109
|
+
binId
|
|
110
|
+
})
|
|
111
|
+
// 1. Assign message to account-bin by accountID and child action-bin by action
|
|
112
|
+
// (References to the messages to be stored in bins, no duplication of messages)
|
|
113
|
+
const accountID = message.key.toString()
|
|
114
|
+
const action = message.value.metadata.event.action
|
|
115
|
+
|
|
116
|
+
const accountBin = bins[accountID] || (bins[accountID] = {})
|
|
117
|
+
const actionBin = accountBin[action] || (accountBin[action] = [])
|
|
118
|
+
// Decode the payload and pass it as a separate parameter
|
|
119
|
+
const decodedPayload = decodePayload(message.value.content.payload)
|
|
120
|
+
actionBin.push({
|
|
121
|
+
message,
|
|
122
|
+
decodedPayload,
|
|
123
|
+
span,
|
|
124
|
+
result: {},
|
|
125
|
+
histTimerMsgEnd
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
await span.audit(message, EventSdk.AuditEventAction.start)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 3. Start DB Transaction
|
|
132
|
+
const trx = await BatchPositionModel.startDbTransaction()
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// 4. Call Bin Processor with the list of account-bins and trx
|
|
136
|
+
// const decodedMessages = decodeMessages(consumedMessages)
|
|
137
|
+
const result = await BinProcessor.processBins(bins, trx)
|
|
138
|
+
|
|
139
|
+
// 5. If Bin Processor processed bins successfully
|
|
140
|
+
// - 5.1. Commit Kafka offset
|
|
141
|
+
// Commit the offset of last message in the array
|
|
142
|
+
const lastMessageToCommit = consumedMessages[consumedMessages.length - 1]
|
|
143
|
+
const params = { message: lastMessageToCommit, kafkaTopic: lastMessageToCommit.topic, consumer: Consumer }
|
|
144
|
+
// We are using Kafka.proceed() to just commit the offset of the last message in the array
|
|
145
|
+
await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit })
|
|
146
|
+
|
|
147
|
+
// - 5.2. Commit DB transaction
|
|
148
|
+
await trx.commit()
|
|
149
|
+
|
|
150
|
+
// - 5.3. Loop through results and produce notification messages and audit messages
|
|
151
|
+
for (const item of result.notifyMessages) {
|
|
152
|
+
// 5.3.1. Produce notification message and audit message
|
|
153
|
+
const action = item.binItem.message?.value.metadata.event.action
|
|
154
|
+
await Kafka.produceGeneralMessage(Config.KAFKA_CONFIG, Producer, Enum.Events.Event.Type.NOTIFICATION, action, item.message, Enum.Events.EventStatus.SUCCESS, null, item.binItem.span)
|
|
155
|
+
}
|
|
156
|
+
histTimerEnd({ success: true })
|
|
157
|
+
} catch (err) {
|
|
158
|
+
// 6. If Bin Processor returns failure
|
|
159
|
+
// 6.1. Rollback DB transaction
|
|
160
|
+
await trx.rollback()
|
|
161
|
+
|
|
162
|
+
// 6.2. Audit Error for each message
|
|
163
|
+
const fspiopError = ErrorHandler.Factory.reformatFSPIOPError(err)
|
|
164
|
+
const state = new EventSdk.EventStateMetadata(EventSdk.EventStatusType.failed, fspiopError.apiErrorCode.code, fspiopError.apiErrorCode.message)
|
|
165
|
+
await BinProcessor.iterateThroughBins(bins, async (_accountID, _action, item) => {
|
|
166
|
+
const span = item.span
|
|
167
|
+
await span.error(fspiopError, state)
|
|
168
|
+
})
|
|
169
|
+
histTimerEnd({ success: false })
|
|
170
|
+
} finally {
|
|
171
|
+
// Finish span for each message
|
|
172
|
+
await BinProcessor.iterateThroughBins(bins, async (_accountID, action, item) => {
|
|
173
|
+
item.histTimerMsgEnd({ ...item.result, action })
|
|
174
|
+
const span = item.span
|
|
175
|
+
if (!span.isFinished) {
|
|
176
|
+
await span.finish()
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @function registerPositionHandler
|
|
184
|
+
*
|
|
185
|
+
* @async
|
|
186
|
+
* @description Registers the handler for position topic. Gets Kafka config from default.json
|
|
187
|
+
*
|
|
188
|
+
* @returns {boolean} - Returns a boolean: true if successful, or throws and error if failed
|
|
189
|
+
*/
|
|
190
|
+
const registerPositionHandler = async () => {
|
|
191
|
+
try {
|
|
192
|
+
await SettlementModelCached.initialize()
|
|
193
|
+
const positionHandler = {
|
|
194
|
+
command: positions,
|
|
195
|
+
// topicName: Kafka.transformGeneralTopicName(Config.KAFKA_CONFIG.TOPIC_TEMPLATES.GENERAL_TOPIC_TEMPLATE.TEMPLATE, Enum.Events.Event.Type.POSITION, Enum.Events.Event.Action.PREPARE),
|
|
196
|
+
// TODO: If there is no mapping, use default transformGeneralTopicName
|
|
197
|
+
topicName: Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.PREPARE,
|
|
198
|
+
// config: Kafka.getKafkaConfig(Config.KAFKA_CONFIG, Enum.Kafka.Config.CONSUMER, Enum.Events.Event.Type.TRANSFER.toUpperCase(), Enum.Events.Event.Action.POSITION.toUpperCase())
|
|
199
|
+
// There is no corresponding action for POSITION_BATCH, so using straight value
|
|
200
|
+
config: Kafka.getKafkaConfig(Config.KAFKA_CONFIG, Enum.Kafka.Config.CONSUMER, Enum.Events.Event.Type.TRANSFER.toUpperCase(), 'POSITION_BATCH')
|
|
201
|
+
}
|
|
202
|
+
positionHandler.config.rdkafkaConf['client.id'] = `${positionHandler.config.rdkafkaConf['client.id']}-${Uuid()}`
|
|
203
|
+
await Consumer.createHandler(positionHandler.topicName, positionHandler.config, positionHandler.command)
|
|
204
|
+
return true
|
|
205
|
+
} catch (err) {
|
|
206
|
+
Logger.isErrorEnabled && Logger.error(err)
|
|
207
|
+
throw ErrorHandler.Factory.reformatFSPIOPError(err)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @function RegisterAllHandlers
|
|
213
|
+
*
|
|
214
|
+
* @async
|
|
215
|
+
* @description Registers all handlers in positions
|
|
216
|
+
*
|
|
217
|
+
* @returns {boolean} - Returns a boolean: true if successful, or throws and error if failed
|
|
218
|
+
*/
|
|
219
|
+
const registerAllHandlers = async () => {
|
|
220
|
+
try {
|
|
221
|
+
return await registerPositionHandler()
|
|
222
|
+
} catch (err) {
|
|
223
|
+
Logger.isErrorEnabled && Logger.error(err)
|
|
224
|
+
throw ErrorHandler.Factory.reformatFSPIOPError(err)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = {
|
|
229
|
+
registerPositionHandler,
|
|
230
|
+
registerAllHandlers,
|
|
231
|
+
positions
|
|
232
|
+
}
|
package/src/handlers/register.js
CHANGED
|
@@ -50,6 +50,7 @@ const Logger = require('@mojaloop/central-services-logger')
|
|
|
50
50
|
const requireGlob = require('require-glob')
|
|
51
51
|
const TransferHandlers = require('./transfers/handler')
|
|
52
52
|
const PositionHandlers = require('./positions/handler')
|
|
53
|
+
const PositionHandlersBatch = require('./positions/handlerBatch')
|
|
53
54
|
const TimeoutHandlers = require('./timeouts/handler')
|
|
54
55
|
const AdminHandlers = require('./admin/handler')
|
|
55
56
|
const BulkHandlers = require('./bulk')
|
|
@@ -90,6 +91,10 @@ module.exports = {
|
|
|
90
91
|
registerAllHandlers: PositionHandlers.registerAllHandlers,
|
|
91
92
|
registerPositionHandler: PositionHandlers.registerPositionHandler
|
|
92
93
|
},
|
|
94
|
+
positionsBatch: {
|
|
95
|
+
registerAllHandlers: PositionHandlersBatch.registerAllHandlers,
|
|
96
|
+
registerPositionHandler: PositionHandlersBatch.registerPositionHandler
|
|
97
|
+
},
|
|
93
98
|
timeouts: {
|
|
94
99
|
registerAllHandlers: TimeoutHandlers.registerAllHandlers,
|
|
95
100
|
registerTimeoutHandler: TimeoutHandlers.registerTimeoutHandler
|
|
@@ -308,7 +308,7 @@ const getParticipantLimitByParticipantCurrencyLimit = async (participantId, curr
|
|
|
308
308
|
|
|
309
309
|
/* Checkpoint #1: participant found and is active */
|
|
310
310
|
if ((participant) && (participant.isActive)) {
|
|
311
|
-
/* use the
|
|
311
|
+
/* use the participant id and incoming params to prepare the filter */
|
|
312
312
|
const searchFilter = {
|
|
313
313
|
participantId: participant.participantId,
|
|
314
314
|
currencyId,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/*****
|
|
2
|
+
License
|
|
3
|
+
--------------
|
|
4
|
+
Copyright © 2017 Bill & Melinda Gates Foundation
|
|
5
|
+
The Mojaloop files are made available by the Bill & Melinda Gates 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
|
+
Gates Foundation organization 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
|
+
* Gates Foundation
|
|
23
|
+
- Name Surname <name.surname@gatesfoundation.com>
|
|
24
|
+
|
|
25
|
+
* INFITX
|
|
26
|
+
- Vijay Kumar Guthi <vijaya.guthi@infitx.com>
|
|
27
|
+
|
|
28
|
+
--------------
|
|
29
|
+
******/
|
|
30
|
+
|
|
31
|
+
'use strict'
|
|
32
|
+
|
|
33
|
+
const Db = require('../../lib/db')
|
|
34
|
+
const Logger = require('@mojaloop/central-services-logger')
|
|
35
|
+
|
|
36
|
+
const startDbTransaction = async () => {
|
|
37
|
+
const knex = await Db.getKnex()
|
|
38
|
+
const trx = await knex.transaction()
|
|
39
|
+
return trx
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const getLatestTransferStateChangesByTransferIdList = async (trx, transfersIdList) => {
|
|
43
|
+
const knex = await Db.getKnex()
|
|
44
|
+
try {
|
|
45
|
+
const latestTransferStateChanges = {}
|
|
46
|
+
const results = await knex('transferStateChange')
|
|
47
|
+
.transacting(trx)
|
|
48
|
+
.whereIn('transferStateChange.transferId', transfersIdList)
|
|
49
|
+
.orderBy('transferStateChangeId', 'desc')
|
|
50
|
+
.select('*')
|
|
51
|
+
|
|
52
|
+
for (const result of results) {
|
|
53
|
+
if (!latestTransferStateChanges[result.transferId]) {
|
|
54
|
+
latestTransferStateChanges[result.transferId] = result
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return latestTransferStateChanges
|
|
58
|
+
} catch (err) {
|
|
59
|
+
Logger.isErrorEnabled && Logger.error(err)
|
|
60
|
+
throw err
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const getAllParticipantCurrency = async (trx) => {
|
|
65
|
+
const knex = await Db.getKnex()
|
|
66
|
+
if (trx) {
|
|
67
|
+
const result = await knex('participantCurrency')
|
|
68
|
+
.transacting(trx)
|
|
69
|
+
.select('*')
|
|
70
|
+
return result
|
|
71
|
+
} else {
|
|
72
|
+
const result = await knex('participantCurrency')
|
|
73
|
+
.select('*')
|
|
74
|
+
return result
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const getPositionsByAccountIdsForUpdate = async (trx, accountIds) => {
|
|
79
|
+
const knex = await Db.getKnex()
|
|
80
|
+
const participantPositions = await knex('participantPosition')
|
|
81
|
+
.transacting(trx)
|
|
82
|
+
.whereIn('participantCurrencyId', accountIds)
|
|
83
|
+
.forUpdate()
|
|
84
|
+
.select('*')
|
|
85
|
+
const positions = {}
|
|
86
|
+
for (const position of participantPositions) {
|
|
87
|
+
positions[position.participantCurrencyId] = position
|
|
88
|
+
}
|
|
89
|
+
return positions
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const updateParticipantPosition = async (trx, participantPositionId, participantPositionValue, participantPositionReservedValue = null) => {
|
|
93
|
+
const knex = await Db.getKnex()
|
|
94
|
+
const optionalValues = {}
|
|
95
|
+
if (participantPositionReservedValue !== null) {
|
|
96
|
+
optionalValues.reservedValue = participantPositionReservedValue
|
|
97
|
+
}
|
|
98
|
+
return await knex('participantPosition').transacting(trx)
|
|
99
|
+
.where({ participantPositionId })
|
|
100
|
+
.update({
|
|
101
|
+
value: participantPositionValue,
|
|
102
|
+
...optionalValues,
|
|
103
|
+
changedDate: new Date()
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const bulkInsertTransferStateChanges = async (trx, transferStateChangeList) => {
|
|
108
|
+
const knex = await Db.getKnex()
|
|
109
|
+
return await knex.batchInsert('transferStateChange', transferStateChangeList).transacting(trx)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const bulkInsertParticipantPositionChanges = async (trx, participantPositionChangeList) => {
|
|
113
|
+
const knex = await Db.getKnex()
|
|
114
|
+
return await knex.batchInsert('participantPositionChange', participantPositionChangeList).transacting(trx)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
startDbTransaction,
|
|
119
|
+
getLatestTransferStateChangesByTransferIdList,
|
|
120
|
+
getPositionsByAccountIdsForUpdate,
|
|
121
|
+
updateParticipantPosition,
|
|
122
|
+
bulkInsertTransferStateChanges,
|
|
123
|
+
bulkInsertParticipantPositionChanges,
|
|
124
|
+
getAllParticipantCurrency
|
|
125
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/*****
|
|
2
|
+
License
|
|
3
|
+
--------------
|
|
4
|
+
Copyright © 2017 Bill & Melinda Gates Foundation
|
|
5
|
+
The Mojaloop files are made available by the Bill & Melinda Gates 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
|
+
Gates Foundation organization 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
|
+
* Gates Foundation
|
|
23
|
+
- Name Surname <name.surname@gatesfoundation.com>
|
|
24
|
+
|
|
25
|
+
* INFITX
|
|
26
|
+
- Kevin Leyow <kevin.leyow@infitx.com>
|
|
27
|
+
|
|
28
|
+
--------------
|
|
29
|
+
******/
|
|
30
|
+
|
|
31
|
+
'use strict'
|
|
32
|
+
|
|
33
|
+
const ErrorHandler = require('@mojaloop/central-services-error-handling')
|
|
34
|
+
const Cache = require('../../lib/cache')
|
|
35
|
+
const Metrics = require('@mojaloop/central-services-metrics')
|
|
36
|
+
const BatchPositionModel = require('./batch')
|
|
37
|
+
|
|
38
|
+
let cacheClient
|
|
39
|
+
let participantCurrencyAllCacheKey
|
|
40
|
+
|
|
41
|
+
/*
|
|
42
|
+
Private API
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const buildUnifiedParticipantCurrencyData = (allParticipantCurrency) => {
|
|
46
|
+
// build indexes (or indices?) - optimization for byId and byName access
|
|
47
|
+
const indexById = {}
|
|
48
|
+
const indexByParticipantId = {}
|
|
49
|
+
|
|
50
|
+
for (const oneParticipantCurrency of allParticipantCurrency) {
|
|
51
|
+
// Participant API returns Date type, but cache internals will serialize it to String
|
|
52
|
+
// by calling JSON.stringify(), which calls .toISOString() on a Date object.
|
|
53
|
+
// Let's ensure all places return same kind of String.
|
|
54
|
+
oneParticipantCurrency.createdDate = JSON.stringify(oneParticipantCurrency.createdDate)
|
|
55
|
+
|
|
56
|
+
// Add to indexes
|
|
57
|
+
if (!(oneParticipantCurrency.participantId in indexByParticipantId)) {
|
|
58
|
+
indexByParticipantId[oneParticipantCurrency.participantId] = []
|
|
59
|
+
}
|
|
60
|
+
indexByParticipantId[oneParticipantCurrency.participantId].push(oneParticipantCurrency)
|
|
61
|
+
indexById[oneParticipantCurrency.participantCurrencyId] = oneParticipantCurrency
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// build unified structure - indexes + data
|
|
65
|
+
const unifiedParticipantsCurrency = {
|
|
66
|
+
indexById,
|
|
67
|
+
indexByParticipantId,
|
|
68
|
+
allParticipantCurrency
|
|
69
|
+
}
|
|
70
|
+
return unifiedParticipantsCurrency
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const getParticipantCurrencyCached = async (trx) => {
|
|
74
|
+
const histTimer = Metrics.getHistogram(
|
|
75
|
+
'model_participant_batch',
|
|
76
|
+
'model_getParticipantsCached - Metrics for participant model',
|
|
77
|
+
['success', 'queryName', 'hit']
|
|
78
|
+
).startTimer()
|
|
79
|
+
// Do we have valid participants list in the cache ?
|
|
80
|
+
let cachedParticipantCurrency = cacheClient.get(participantCurrencyAllCacheKey)
|
|
81
|
+
if (!cachedParticipantCurrency) {
|
|
82
|
+
const allParticipantCurrency = await BatchPositionModel.getAllParticipantCurrency(trx)
|
|
83
|
+
cachedParticipantCurrency = buildUnifiedParticipantCurrencyData(allParticipantCurrency)
|
|
84
|
+
|
|
85
|
+
// store in cache
|
|
86
|
+
cacheClient.set(participantCurrencyAllCacheKey, cachedParticipantCurrency)
|
|
87
|
+
histTimer({ success: true, queryName: 'model_getParticipantCurrencyBatchCached', hit: false })
|
|
88
|
+
} else {
|
|
89
|
+
// unwrap participants list from catbox structure
|
|
90
|
+
cachedParticipantCurrency = cachedParticipantCurrency.item
|
|
91
|
+
histTimer({ success: true, queryName: 'model_getParticipantCurrencyBatchCached', hit: true })
|
|
92
|
+
}
|
|
93
|
+
return cachedParticipantCurrency
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/*
|
|
97
|
+
Public API
|
|
98
|
+
*/
|
|
99
|
+
exports.initialize = async () => {
|
|
100
|
+
/* Register as cache client */
|
|
101
|
+
const participantCurrencyCacheClientMeta = {
|
|
102
|
+
id: 'participantCurrency',
|
|
103
|
+
preloadCache: getParticipantCurrencyCached
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
cacheClient = Cache.registerCacheClient(participantCurrencyCacheClientMeta)
|
|
107
|
+
participantCurrencyAllCacheKey = cacheClient.createKey('participantCurrency')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
exports.getParticipantCurrencyByIds = async (trx, participantCurrencyIds) => {
|
|
111
|
+
try {
|
|
112
|
+
let participantCurrencies = []
|
|
113
|
+
const cachedParticipantCurrency = await getParticipantCurrencyCached(trx)
|
|
114
|
+
for (const participantCurrencyId of participantCurrencyIds) {
|
|
115
|
+
participantCurrencies = participantCurrencies.concat(cachedParticipantCurrency.indexById[participantCurrencyId])
|
|
116
|
+
}
|
|
117
|
+
return participantCurrencies
|
|
118
|
+
} catch (err) {
|
|
119
|
+
throw ErrorHandler.Factory.reformatFSPIOPError(err)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
exports.getParticipantCurrencyByParticipantIds = async (trx, participantIds) => {
|
|
124
|
+
try {
|
|
125
|
+
let participantCurrencies = []
|
|
126
|
+
const cachedParticipantCurrency = await getParticipantCurrencyCached(trx)
|
|
127
|
+
for (const participantId of participantIds) {
|
|
128
|
+
participantCurrencies = participantCurrencies.concat(cachedParticipantCurrency.indexByParticipantId[participantId])
|
|
129
|
+
}
|
|
130
|
+
return participantCurrencies
|
|
131
|
+
} catch (err) {
|
|
132
|
+
throw ErrorHandler.Factory.reformatFSPIOPError(err)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -42,12 +42,12 @@ const buildUnifiedSettlementModelsData = (allSettlementModels) => {
|
|
|
42
42
|
const indexByName = {}
|
|
43
43
|
const indexByLedgerAccountTypeId = {}
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
for (const oneSettlementModel of allSettlementModels) {
|
|
46
46
|
// Add to indexes
|
|
47
47
|
indexById[oneSettlementModel.settlementModelId] = oneSettlementModel
|
|
48
48
|
indexByName[oneSettlementModel.name] = oneSettlementModel
|
|
49
49
|
indexByLedgerAccountTypeId[oneSettlementModel.ledgerAccountTypeId] = oneSettlementModel
|
|
50
|
-
}
|
|
50
|
+
}
|
|
51
51
|
|
|
52
52
|
// build unified structure - indexes + data
|
|
53
53
|
const unifiedSettlementModels = {
|
package/src/shared/setup.js
CHANGED
|
@@ -51,6 +51,7 @@ const EnumCached = require('../lib/enumCached')
|
|
|
51
51
|
const ParticipantCached = require('../models/participant/participantCached')
|
|
52
52
|
const ParticipantCurrencyCached = require('../models/participant/participantCurrencyCached')
|
|
53
53
|
const ParticipantLimitCached = require('../models/participant/participantLimitCached')
|
|
54
|
+
const BatchPositionModelCached = require('../models/position/batchCached')
|
|
54
55
|
const MongoUriBuilder = require('mongo-uri-builder')
|
|
55
56
|
|
|
56
57
|
const migrate = (runMigrations) => {
|
|
@@ -175,6 +176,10 @@ const createHandlers = async (handlers) => {
|
|
|
175
176
|
// }
|
|
176
177
|
break
|
|
177
178
|
}
|
|
179
|
+
case 'positionbatch': {
|
|
180
|
+
await RegisterHandlers.positionsBatch.registerPositionHandler()
|
|
181
|
+
break
|
|
182
|
+
}
|
|
178
183
|
case 'fulfil': {
|
|
179
184
|
await RegisterHandlers.transfers.registerFulfilHandler()
|
|
180
185
|
break
|
|
@@ -230,6 +235,7 @@ const initializeCache = async () => {
|
|
|
230
235
|
await ParticipantCached.initialize()
|
|
231
236
|
await ParticipantCurrencyCached.initialize()
|
|
232
237
|
await ParticipantLimitCached.initialize()
|
|
238
|
+
await BatchPositionModelCached.initialize()
|
|
233
239
|
await Cache.initCache()
|
|
234
240
|
}
|
|
235
241
|
|