@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 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
 
@@ -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.2.0",
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.0",
93
+ "@mojaloop/central-services-shared": "18.1.2",
94
94
  "@mojaloop/central-services-stream": "11.1.1",
95
- "@mojaloop/event-sdk": "12.0.2",
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.4",
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.13.3",
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": "15.2.0",
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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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