@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 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
 
@@ -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.2",
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.1.0",
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
+ }
@@ -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
+ }
@@ -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 paricipant id and incoming params to prepare the filter */
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
- allSettlementModels.forEach((oneSettlementModel) => {
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 = {
@@ -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