@mojaloop/central-ledger 17.2.0 → 17.3.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 +19 -0
- package/config/default.json +29 -1
- package/package.json +6 -6
- package/src/domain/position/binProcessor.js +235 -0
- package/src/domain/position/prepare.js +251 -0
- package/src/handlers/index.js +9 -0
- package/src/handlers/positions/handlerBatch.js +231 -0
- package/src/handlers/register.js +5 -0
- package/src/models/position/batch.js +134 -0
- package/src/models/position/batchCached.js +134 -0
- package/src/shared/setup.js +6 -0
package/README.md
CHANGED
|
@@ -187,6 +187,25 @@ If you want to run integration tests in a repetitive manner, you can startup the
|
|
|
187
187
|
|
|
188
188
|
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
189
|
|
|
190
|
+
#### For running integration tests for batch processing interactively
|
|
191
|
+
- Run dependecies
|
|
192
|
+
```
|
|
193
|
+
docker-compose up -d mysql kafka init-kafka kafka-debug-console
|
|
194
|
+
npm run wait-4-docker
|
|
195
|
+
```
|
|
196
|
+
- Run central-ledger services
|
|
197
|
+
```
|
|
198
|
+
nvm use
|
|
199
|
+
npm run migrate
|
|
200
|
+
env "CLEDG_KAFKA__EVENT_TYPE_ACTION_TOPIC_MAP__POSITION__PREPARE=topic-transfer-position-batch" npm start
|
|
201
|
+
```
|
|
202
|
+
- Additionally, run position batch handler in a new terminal
|
|
203
|
+
```
|
|
204
|
+
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
|
|
205
|
+
```
|
|
206
|
+
- Run tests using `npx tape 'test/integration-override/**/handlerBatch.test.js'`
|
|
207
|
+
|
|
208
|
+
|
|
190
209
|
If you want to just run all of the integration suite non-interactively then use npm run `test:integration`.
|
|
191
210
|
It will handle docker start up, migration, service starting and testing. Be sure to exit any previously ran handlers or docker commands.
|
|
192
211
|
|
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": 1000
|
|
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.3.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",
|
|
@@ -90,9 +90,9 @@
|
|
|
90
90
|
"@mojaloop/central-services-health": "14.0.2",
|
|
91
91
|
"@mojaloop/central-services-logger": "11.2.2",
|
|
92
92
|
"@mojaloop/central-services-metrics": "12.0.8",
|
|
93
|
-
"@mojaloop/central-services-shared": "18.1.
|
|
93
|
+
"@mojaloop/central-services-shared": "18.1.2",
|
|
94
94
|
"@mojaloop/central-services-stream": "11.1.1",
|
|
95
|
-
"@mojaloop/event-sdk": "
|
|
95
|
+
"@mojaloop/event-sdk": "13.0.0",
|
|
96
96
|
"@mojaloop/ml-number": "11.2.3",
|
|
97
97
|
"@mojaloop/object-store-lib": "12.0.1",
|
|
98
98
|
"@now-ims/hapi-now-auth": "2.1.0",
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"docdash": "2.0.2",
|
|
107
107
|
"event-stream": "4.0.1",
|
|
108
108
|
"five-bells-condition": "5.0.1",
|
|
109
|
-
"glob": "10.3.
|
|
109
|
+
"glob": "10.3.5",
|
|
110
110
|
"hapi-auth-basic": "5.0.0",
|
|
111
111
|
"hapi-auth-bearer-token": "8.0.0",
|
|
112
112
|
"hapi-swagger": "17.1.0",
|
|
@@ -129,12 +129,12 @@
|
|
|
129
129
|
"jsdoc": "4.0.2",
|
|
130
130
|
"jsonpath": "1.1.1",
|
|
131
131
|
"nodemon": "3.0.1",
|
|
132
|
-
"npm-check-updates": "16.
|
|
132
|
+
"npm-check-updates": "16.14.4",
|
|
133
133
|
"nyc": "15.1.0",
|
|
134
134
|
"pre-commit": "1.2.2",
|
|
135
135
|
"proxyquire": "2.1.3",
|
|
136
136
|
"replace": "^1.2.2",
|
|
137
|
-
"sinon": "
|
|
137
|
+
"sinon": "16.0.0",
|
|
138
138
|
"standard": "17.1.0",
|
|
139
139
|
"standard-version": "^9.5.0",
|
|
140
140
|
"tap-spec": "^5.0.0",
|
|
@@ -0,0 +1,235 @@
|
|
|
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
|
+
const Logger = require('@mojaloop/central-services-logger')
|
|
33
|
+
const BatchPositionModel = require('../../models/position/batch')
|
|
34
|
+
const BatchPositionModelCached = require('../../models/position/batchCached')
|
|
35
|
+
const PositionPrepareDomain = require('./prepare')
|
|
36
|
+
const SettlementModelCached = require('../../models/settlement/settlementModelCached')
|
|
37
|
+
const Enum = require('@mojaloop/central-services-shared').Enum
|
|
38
|
+
const ErrorHandler = require('@mojaloop/central-services-error-handling')
|
|
39
|
+
// TODO: We may not need this if we optimize the participantLimit query
|
|
40
|
+
const participantFacade = require('../../models/participant/facade')
|
|
41
|
+
const decodePayload = require('@mojaloop/central-services-shared').Util.StreamingProtocol.decodePayload
|
|
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
|
+
await iterateThroughBins(bins, async (_accountID, _action, item) => {
|
|
57
|
+
Logger.info(decodePayload(item.message.value.content.payload))
|
|
58
|
+
if (item.decodedPayload?.transferId) {
|
|
59
|
+
transferIdList.push(item.decodedPayload.transferId)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
// Pre fetch latest transferStates for all the transferIds in the account-bin
|
|
63
|
+
const latestTransferStateChanges = await BatchPositionModel.getLatestTransferStateChangesByTransferIdList(trx, transferIdList)
|
|
64
|
+
const latestTransferStates = {}
|
|
65
|
+
for (const key in latestTransferStateChanges) {
|
|
66
|
+
latestTransferStates[key] = latestTransferStateChanges[key].transferStateId
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const accountIds = Object.keys(bins)
|
|
70
|
+
|
|
71
|
+
// Pre fetch all settlement accounts corresponding to the position accounts
|
|
72
|
+
// Get all participantIdMap for the accountIds
|
|
73
|
+
const participantCurrencyIds = await BatchPositionModelCached.getParticipantCurrencyByIds(trx, accountIds)
|
|
74
|
+
|
|
75
|
+
// TODO: Validate if all the participantCurrencyIds exist for all the accountIds
|
|
76
|
+
|
|
77
|
+
const allSettlementModels = await SettlementModelCached.getAll()
|
|
78
|
+
|
|
79
|
+
// Construct an object participantIdMap, accountIdMap amd currencyIdMap
|
|
80
|
+
const participantIdMap = {}
|
|
81
|
+
const accountIdMap = {}
|
|
82
|
+
const currencyIdMap = {}
|
|
83
|
+
participantCurrencyIds.forEach(item => {
|
|
84
|
+
const { participantId, currencyId, participantCurrencyId } = item
|
|
85
|
+
if (!participantIdMap[participantId]) {
|
|
86
|
+
participantIdMap[participantId] = {}
|
|
87
|
+
}
|
|
88
|
+
if (!currencyIdMap[currencyId]) {
|
|
89
|
+
currencyIdMap[currencyId] = {
|
|
90
|
+
settlementModel: _getSettlementModelForCurrency(currencyId, allSettlementModels)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
participantIdMap[participantId][currencyId] = participantCurrencyId
|
|
94
|
+
accountIdMap[participantCurrencyId] = { participantId, currencyId }
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// TODO: Verify if all the accountIds have a corresponding participantCurrencyId
|
|
98
|
+
// TODO: Verify all maps are correctly constructed
|
|
99
|
+
|
|
100
|
+
// Get all participantCurrencyIds for the participantIdMap
|
|
101
|
+
const allParticipantCurrencyIds = await BatchPositionModelCached.getParticipantCurrencyByParticipantIds(trx, Object.keys(participantIdMap))
|
|
102
|
+
const settlementCurrencyIds = []
|
|
103
|
+
allParticipantCurrencyIds.forEach(pc => {
|
|
104
|
+
const correspondingParticipantCurrencyId = participantIdMap[pc.participantId][pc.currencyId]
|
|
105
|
+
if (correspondingParticipantCurrencyId) {
|
|
106
|
+
const settlementModel = currencyIdMap[pc.currencyId].settlementModel
|
|
107
|
+
if (pc.ledgerAccountTypeId === settlementModel.settlementAccountTypeId) {
|
|
108
|
+
settlementCurrencyIds.push(pc)
|
|
109
|
+
accountIdMap[correspondingParticipantCurrencyId].settlementCurrencyId = pc.participantCurrencyId
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Pre fetch all position account balances for the account-bin and acquire lock on position
|
|
115
|
+
const positions = await BatchPositionModel.getPositionsByAccountIdsForUpdate(trx, [
|
|
116
|
+
...accountIds,
|
|
117
|
+
...settlementCurrencyIds.map(pc => pc.participantCurrencyId)
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
let notifyMessages = []
|
|
121
|
+
let limitAlarms = []
|
|
122
|
+
|
|
123
|
+
// For each account-bin in the list
|
|
124
|
+
for (const accountID in bins) {
|
|
125
|
+
const accountBin = bins[accountID]
|
|
126
|
+
const actions = Object.keys(accountBin)
|
|
127
|
+
|
|
128
|
+
const settlementParticipantPosition = positions[accountIdMap[accountID].settlementCurrencyId].value
|
|
129
|
+
const settlementModel = currencyIdMap[accountIdMap[accountID].currencyId].settlementModel
|
|
130
|
+
|
|
131
|
+
// TODO: Refactor the following SQL query to optimize the performance
|
|
132
|
+
const participantLimit = await participantFacade.getParticipantLimitByParticipantCurrencyLimit(accountIdMap[accountID].participantId, accountIdMap[accountID].currencyId, Enum.Accounts.LedgerAccountType.POSITION, Enum.Accounts.ParticipantLimitType.NET_DEBIT_CAP)
|
|
133
|
+
// Initialize accumulated values
|
|
134
|
+
// These values will be passed across various actions in the bin
|
|
135
|
+
let accumulatedPositionValue = positions[accountID].value
|
|
136
|
+
let accumulatedPositionReservedValue = positions[accountID].reservedValue
|
|
137
|
+
let accumulatedTransferStates = latestTransferStates
|
|
138
|
+
let accumulatedTransferStateChanges = []
|
|
139
|
+
let accumulatedPositionChanges = []
|
|
140
|
+
|
|
141
|
+
// If non-prepare action found, log error
|
|
142
|
+
// We need to remove this once we implement all the actions
|
|
143
|
+
if (actions.length > 1 || (actions.length === 1 && actions[0] !== 'prepare')) {
|
|
144
|
+
Logger.isErrorEnabled && Logger.error('Only prepare action is allowed in a batch')
|
|
145
|
+
// throw new Error('Only prepare action is allowed in a batch')
|
|
146
|
+
}
|
|
147
|
+
// If prepare action found then call processPositionPrepareBin function
|
|
148
|
+
const prepareActionResult = await PositionPrepareDomain.processPositionPrepareBin(
|
|
149
|
+
accountBin.prepare,
|
|
150
|
+
accumulatedPositionValue,
|
|
151
|
+
accumulatedPositionReservedValue,
|
|
152
|
+
accumulatedTransferStates,
|
|
153
|
+
settlementParticipantPosition,
|
|
154
|
+
settlementModel,
|
|
155
|
+
participantLimit
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
// Update accumulated values
|
|
159
|
+
accumulatedPositionValue = prepareActionResult.accumulatedPositionValue
|
|
160
|
+
accumulatedPositionReservedValue = prepareActionResult.accumulatedPositionReservedValue
|
|
161
|
+
accumulatedTransferStates = prepareActionResult.accumulatedTransferStates
|
|
162
|
+
// Append accumulated arrays
|
|
163
|
+
accumulatedTransferStateChanges = accumulatedTransferStateChanges.concat(prepareActionResult.accumulatedTransferStateChanges)
|
|
164
|
+
accumulatedPositionChanges = accumulatedPositionChanges.concat(prepareActionResult.accumulatedPositionChanges)
|
|
165
|
+
notifyMessages = notifyMessages.concat(prepareActionResult.notifyMessages)
|
|
166
|
+
|
|
167
|
+
// Update accumulated position values by calling a facade function
|
|
168
|
+
await BatchPositionModel.updateParticipantPosition(trx, positions[accountID].participantPositionId, accumulatedPositionValue, accumulatedPositionReservedValue)
|
|
169
|
+
|
|
170
|
+
// Bulk insert accumulated transferStateChanges by calling a facade function
|
|
171
|
+
await BatchPositionModel.bulkInsertTransferStateChanges(trx, accumulatedTransferStateChanges)
|
|
172
|
+
|
|
173
|
+
// Bulk get the transferStateChangeIds for transferids using select whereIn
|
|
174
|
+
const fetchedTransferStateChanges = await BatchPositionModel.getLatestTransferStateChangesByTransferIdList(trx, accumulatedTransferStateChanges.map(item => item.transferId))
|
|
175
|
+
// Mutate accumulated positionChanges with transferStateChangeIds
|
|
176
|
+
accumulatedPositionChanges.forEach(positionChange => {
|
|
177
|
+
positionChange.transferStateChangeId = fetchedTransferStateChanges[positionChange.transferId].transferStateChangeId
|
|
178
|
+
positionChange.participantPositionId = positions[accountID].participantPositionId
|
|
179
|
+
delete positionChange.transferId
|
|
180
|
+
})
|
|
181
|
+
// Bulk insert accumulated positionChanges by calling a facade function
|
|
182
|
+
await BatchPositionModel.bulkInsertParticipantPositionChanges(trx, accumulatedPositionChanges)
|
|
183
|
+
|
|
184
|
+
// testing: await trx.rollback()
|
|
185
|
+
limitAlarms = limitAlarms.concat(prepareActionResult.limitAlarms)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Return results
|
|
189
|
+
return {
|
|
190
|
+
notifyMessages,
|
|
191
|
+
limitAlarms
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @function iterateThroughBins
|
|
197
|
+
*
|
|
198
|
+
* @async
|
|
199
|
+
* @description Helper function to iterate though all messages in bins.
|
|
200
|
+
*
|
|
201
|
+
* @param {array} bins - a list of account-bins to iterate
|
|
202
|
+
* @param {async function} cb - callback function to call for each item
|
|
203
|
+
*
|
|
204
|
+
* @returns {void} - Doesn't return anything
|
|
205
|
+
*/
|
|
206
|
+
|
|
207
|
+
const iterateThroughBins = async (bins, cb) => {
|
|
208
|
+
for (const accountID in bins) {
|
|
209
|
+
const accountBin = bins[accountID]
|
|
210
|
+
for (const action in accountBin) {
|
|
211
|
+
const actionBin = accountBin[action]
|
|
212
|
+
for (const item of actionBin) {
|
|
213
|
+
try {
|
|
214
|
+
await cb(accountID, action, item)
|
|
215
|
+
} catch (err) {}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const _getSettlementModelForCurrency = (currencyId, allSettlementModels) => {
|
|
222
|
+
let settlementModels = allSettlementModels.filter(model => model.currencyId === currencyId)
|
|
223
|
+
if (settlementModels.length === 0) {
|
|
224
|
+
settlementModels = allSettlementModels.filter(model => model.currencyId === null) // Default settlement model
|
|
225
|
+
if (settlementModels.length === 0) {
|
|
226
|
+
throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.GENERIC_SETTLEMENT_ERROR, 'Unable to find a matching or default, Settlement Model')
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return settlementModels.find(sm => sm.ledgerAccountTypeId === Enum.Accounts.LedgerAccountType.POSITION)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
processBins,
|
|
234
|
+
iterateThroughBins
|
|
235
|
+
}
|
|
@@ -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 Handler V2')
|
|
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,231 @@
|
|
|
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 messagses 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
|
+
result.notifyMessages.forEach(async (item) => {
|
|
152
|
+
// 5.3.1. Produce notification message and audit message
|
|
153
|
+
Kafka.produceGeneralMessage(Config.KAFKA_CONFIG, Producer, Enum.Events.Event.Type.NOTIFICATION, Enum.Events.Event.Action.EVENT, item.message, Enum.Events.EventStatus.SUCCESS, null, item.binItem.span)
|
|
154
|
+
})
|
|
155
|
+
histTimerEnd({ success: true })
|
|
156
|
+
} catch (err) {
|
|
157
|
+
// 6. If Bin Processor returns failure
|
|
158
|
+
// 6.1. Rollback DB transaction
|
|
159
|
+
await trx.rollback()
|
|
160
|
+
|
|
161
|
+
// 6.2. Audit Error for each message
|
|
162
|
+
const fspiopError = ErrorHandler.Factory.reformatFSPIOPError(err)
|
|
163
|
+
const state = new EventSdk.EventStateMetadata(EventSdk.EventStatusType.failed, fspiopError.apiErrorCode.code, fspiopError.apiErrorCode.message)
|
|
164
|
+
await BinProcessor.iterateThroughBins(bins, async (_accountID, _action, item) => {
|
|
165
|
+
const span = item.span
|
|
166
|
+
await span.error(fspiopError, state)
|
|
167
|
+
})
|
|
168
|
+
histTimerEnd({ success: false })
|
|
169
|
+
} finally {
|
|
170
|
+
// Finish span for each message
|
|
171
|
+
await BinProcessor.iterateThroughBins(bins, async (_accountID, action, item) => {
|
|
172
|
+
item.histTimerMsgEnd({ ...item.result, action })
|
|
173
|
+
const span = item.span
|
|
174
|
+
if (!span.isFinished) {
|
|
175
|
+
await span.finish()
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @function registerPositionHandler
|
|
183
|
+
*
|
|
184
|
+
* @async
|
|
185
|
+
* @description Registers the handler for position topic. Gets Kafka config from default.json
|
|
186
|
+
*
|
|
187
|
+
* @returns {boolean} - Returns a boolean: true if successful, or throws and error if failed
|
|
188
|
+
*/
|
|
189
|
+
const registerPositionHandler = async () => {
|
|
190
|
+
try {
|
|
191
|
+
await SettlementModelCached.initialize()
|
|
192
|
+
const positionHandler = {
|
|
193
|
+
command: positions,
|
|
194
|
+
// topicName: Kafka.transformGeneralTopicName(Config.KAFKA_CONFIG.TOPIC_TEMPLATES.GENERAL_TOPIC_TEMPLATE.TEMPLATE, Enum.Events.Event.Type.POSITION, Enum.Events.Event.Action.PREPARE),
|
|
195
|
+
// TODO: If there is no mapping, use default transformGeneralTopicName
|
|
196
|
+
topicName: Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.PREPARE,
|
|
197
|
+
// config: Kafka.getKafkaConfig(Config.KAFKA_CONFIG, Enum.Kafka.Config.CONSUMER, Enum.Events.Event.Type.TRANSFER.toUpperCase(), Enum.Events.Event.Action.POSITION.toUpperCase())
|
|
198
|
+
// There is no corresponding action for POSITION_BATCH, so using straight value
|
|
199
|
+
config: Kafka.getKafkaConfig(Config.KAFKA_CONFIG, Enum.Kafka.Config.CONSUMER, Enum.Events.Event.Type.TRANSFER.toUpperCase(), 'POSITION_BATCH')
|
|
200
|
+
}
|
|
201
|
+
positionHandler.config.rdkafkaConf['client.id'] = `${positionHandler.config.rdkafkaConf['client.id']}-${Uuid()}`
|
|
202
|
+
await Consumer.createHandler(positionHandler.topicName, positionHandler.config, positionHandler.command)
|
|
203
|
+
return true
|
|
204
|
+
} catch (err) {
|
|
205
|
+
Logger.isErrorEnabled && Logger.error(err)
|
|
206
|
+
throw ErrorHandler.Factory.reformatFSPIOPError(err)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @function RegisterAllHandlers
|
|
212
|
+
*
|
|
213
|
+
* @async
|
|
214
|
+
* @description Registers all handlers in positions
|
|
215
|
+
*
|
|
216
|
+
* @returns {boolean} - Returns a boolean: true if successful, or throws and error if failed
|
|
217
|
+
*/
|
|
218
|
+
const registerAllHandlers = async () => {
|
|
219
|
+
try {
|
|
220
|
+
return await registerPositionHandler()
|
|
221
|
+
} catch (err) {
|
|
222
|
+
Logger.isErrorEnabled && Logger.error(err)
|
|
223
|
+
throw ErrorHandler.Factory.reformatFSPIOPError(err)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
registerPositionHandler,
|
|
229
|
+
registerAllHandlers,
|
|
230
|
+
positions
|
|
231
|
+
}
|
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
|
|
@@ -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
|
+
- 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
|
+
let knex
|
|
37
|
+
|
|
38
|
+
const _initKnex = async () => {
|
|
39
|
+
if (!knex) {
|
|
40
|
+
knex = await Db.getKnex()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const _unsetKnex = async () => {
|
|
45
|
+
knex = null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const startDbTransaction = async () => {
|
|
49
|
+
await _initKnex()
|
|
50
|
+
const trx = await knex.transaction()
|
|
51
|
+
return trx
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const getLatestTransferStateChangesByTransferIdList = async (trx, transfersIdList) => {
|
|
55
|
+
try {
|
|
56
|
+
const latestTransferStateChanges = {}
|
|
57
|
+
const results = await knex('transferStateChange')
|
|
58
|
+
.transacting(trx)
|
|
59
|
+
.whereIn('transferStateChange.transferId', transfersIdList)
|
|
60
|
+
.orderBy('transferStateChangeId', 'desc')
|
|
61
|
+
.select('*')
|
|
62
|
+
|
|
63
|
+
results.forEach((result) => {
|
|
64
|
+
if (!latestTransferStateChanges[result.transferId]) {
|
|
65
|
+
latestTransferStateChanges[result.transferId] = result
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
return latestTransferStateChanges
|
|
69
|
+
} catch (err) {
|
|
70
|
+
Logger.isErrorEnabled && Logger.error(err)
|
|
71
|
+
throw err
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const getAllParticipantCurrency = async (trx) => {
|
|
76
|
+
const knex = Db.getKnex()
|
|
77
|
+
if (trx) {
|
|
78
|
+
const result = await knex('participantCurrency')
|
|
79
|
+
.transacting(trx)
|
|
80
|
+
.select('*')
|
|
81
|
+
return result
|
|
82
|
+
} else {
|
|
83
|
+
const result = await knex('participantCurrency')
|
|
84
|
+
.select('*')
|
|
85
|
+
return result
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const getPositionsByAccountIdsForUpdate = async (trx, accountIds) => {
|
|
90
|
+
const participantPositions = await knex('participantPosition')
|
|
91
|
+
.transacting(trx)
|
|
92
|
+
.whereIn('participantCurrencyId', accountIds)
|
|
93
|
+
.forUpdate()
|
|
94
|
+
.select('*')
|
|
95
|
+
const positions = {}
|
|
96
|
+
participantPositions.forEach((position) => {
|
|
97
|
+
positions[position.participantCurrencyId] = position
|
|
98
|
+
})
|
|
99
|
+
return positions
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const updateParticipantPosition = async (trx, participantPositionId, participantPositionValue, participantPositionReservedValue = null) => {
|
|
103
|
+
const optionalValues = {}
|
|
104
|
+
if (participantPositionReservedValue !== null) {
|
|
105
|
+
optionalValues.reservedValue = participantPositionReservedValue
|
|
106
|
+
}
|
|
107
|
+
return await knex('participantPosition').transacting(trx)
|
|
108
|
+
.where({ participantPositionId })
|
|
109
|
+
.update({
|
|
110
|
+
value: participantPositionValue,
|
|
111
|
+
...optionalValues,
|
|
112
|
+
changedDate: new Date()
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const bulkInsertTransferStateChanges = async (trx, transferStateChangeList) => {
|
|
117
|
+
return await knex.batchInsert('transferStateChange', transferStateChangeList).transacting(trx)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const bulkInsertParticipantPositionChanges = async (trx, participantPositionChangeList) => {
|
|
121
|
+
return await knex.batchInsert('participantPositionChange', participantPositionChangeList).transacting(trx)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
_initKnex, // for testing
|
|
126
|
+
_unsetKnex,
|
|
127
|
+
startDbTransaction,
|
|
128
|
+
getLatestTransferStateChangesByTransferIdList,
|
|
129
|
+
getPositionsByAccountIdsForUpdate,
|
|
130
|
+
updateParticipantPosition,
|
|
131
|
+
bulkInsertTransferStateChanges,
|
|
132
|
+
bulkInsertParticipantPositionChanges,
|
|
133
|
+
getAllParticipantCurrency
|
|
134
|
+
}
|
|
@@ -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
|
+
allParticipantCurrency.forEach((oneParticipantCurrency) => {
|
|
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
|
+
}
|
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
|
|