@mojaloop/central-ledger 17.4.1 → 17.5.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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [17.5.0](https://github.com/mojaloop/central-ledger/compare/v17.4.1...v17.5.0) (2023-12-13)
6
+
7
+
8
+ ### Features
9
+
10
+ * **mojaloop/#3524:** add position fulfil to binprocessor ([#990](https://github.com/mojaloop/central-ledger/issues/990)) ([dddedf6](https://github.com/mojaloop/central-ledger/commit/dddedf6adc03b441dcdc177f51d3850b79cfb144)), closes [mojaloop/#3524](https://github.com/mojaloop/project/issues/3524)
11
+
5
12
  ### [17.4.1](https://github.com/mojaloop/central-ledger/compare/v17.4.0...v17.4.1) (2023-12-05)
6
13
 
7
14
 
package/README.md CHANGED
@@ -96,13 +96,14 @@ diverges from the defaults.
96
96
  You can configure the customized topic names in the config. Each position action key
97
97
  refers to position messages with associated actions.
98
98
 
99
- NOTE: Only POSITION.PREPARE is supported at this time, with additional event-type-actions being added later when required.
99
+ NOTE: Only POSITION.PREPARE and POSITION.COMMIT is supported at this time, with additional event-type-actions being added later when required.
100
100
 
101
101
  ```
102
102
  "KAFKA": {
103
103
  "EVENT_TYPE_ACTION_TOPIC_MAP" : {
104
104
  "POSITION":{
105
- "PREPARE": "topic-transfer-position-batch"
105
+ "PREPARE": "topic-transfer-position-batch",
106
+ "COMMIT": "topic-transfer-position-batch"
106
107
  }
107
108
  }
108
109
  }
@@ -123,7 +124,10 @@ Batch processing can be enabled in the transfer execution flow. Follow the steps
123
124
  "EVENT_TYPE_ACTION_TOPIC_MAP" : {
124
125
  "POSITION":{
125
126
  "PREPARE": "topic-transfer-position-batch",
126
- "BULK_PREPARE": "topic-transfer-position"
127
+ "BULK_PREPARE": "topic-transfer-position",
128
+ "COMMIT": "topic-transfer-position-batch",
129
+ "BULK_COMMIT": "topic-transfer-position",
130
+ "RESERVE": "topic-transfer-position",
127
131
  }
128
132
  }
129
133
  }
@@ -243,7 +247,10 @@ env "CLEDG_KAFKA__EVENT_TYPE_ACTION_TOPIC_MAP__POSITION__PREPARE=topic-transfer-
243
247
  ```
244
248
  - Additionally, run position batch handler in a new terminal
245
249
  ```
246
- 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
250
+ export CLEDG_KAFKA__EVENT_TYPE_ACTION_TOPIC_MAP__POSITION__PREPARE=topic-transfer-position-batch
251
+ export CLEDG_KAFKA__EVENT_TYPE_ACTION_TOPIC_MAP__POSITION__COMMIT=topic-transfer-position-batch
252
+ export CLEDG_HANDLERS__API__DISABLED=true
253
+ node src/handlers/index.js handler --positionbatch
247
254
  ```
248
255
  - Run tests using `npx tape 'test/integration-override/**/handlerBatch.test.js'`
249
256
 
@@ -88,7 +88,10 @@
88
88
  "EVENT_TYPE_ACTION_TOPIC_MAP" : {
89
89
  "POSITION":{
90
90
  "PREPARE": null,
91
- "BULK_PREPARE": null
91
+ "BULK_PREPARE": null,
92
+ "COMMIT": null,
93
+ "BULK_COMMIT": null,
94
+ "RESERVE": null
92
95
  }
93
96
  },
94
97
  "TOPIC_TEMPLATES": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mojaloop/central-ledger",
3
- "version": "17.4.1",
3
+ "version": "17.5.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",
@@ -111,7 +111,7 @@
111
111
  "hapi-auth-bearer-token": "8.0.0",
112
112
  "hapi-swagger": "17.2.0",
113
113
  "ilp-packet": "2.2.0",
114
- "knex": "3.0.1",
114
+ "knex": "3.1.0",
115
115
  "lodash": "4.17.21",
116
116
  "moment": "2.29.4",
117
117
  "mongo-uri-builder": "^4.0.0",
@@ -21,7 +21,7 @@ const expectedContainers = [
21
21
  ]
22
22
 
23
23
  let retries = 40
24
- const waitTimeMs = 60000
24
+ const waitTimeMs = 30000
25
25
 
26
26
  async function main () {
27
27
  const waitingMap = {}
@@ -34,6 +34,7 @@ const Logger = require('@mojaloop/central-services-logger')
34
34
  const BatchPositionModel = require('../../models/position/batch')
35
35
  const BatchPositionModelCached = require('../../models/position/batchCached')
36
36
  const PositionPrepareDomain = require('./prepare')
37
+ const PositionFulfilDomain = require('./fulfil')
37
38
  const SettlementModelCached = require('../../models/settlement/settlementModelCached')
38
39
  const Enum = require('@mojaloop/central-services-shared').Enum
39
40
  const ErrorHandler = require('@mojaloop/central-services-error-handling')
@@ -52,9 +53,12 @@ const participantFacade = require('../../models/participant/facade')
52
53
  */
53
54
  const processBins = async (bins, trx) => {
54
55
  const transferIdList = []
55
- iterateThroughBins(bins, (_accountID, _action, item) => {
56
+ await iterateThroughBins(bins, (_accountID, _action, item) => {
56
57
  if (item.decodedPayload?.transferId) {
57
58
  transferIdList.push(item.decodedPayload.transferId)
59
+ // get transferId from uriParams for fulfil messages
60
+ } else if (item.message?.value?.content?.uriParams?.id) {
61
+ transferIdList.push(item.message.value.content.uriParams.id)
58
62
  }
59
63
  })
60
64
  // Pre fetch latest transferStates for all the transferIds in the account-bin
@@ -120,6 +124,13 @@ const processBins = async (bins, trx) => {
120
124
  ...settlementCurrencyIds.map(pc => pc.participantCurrencyId)
121
125
  ])
122
126
 
127
+ const latestTransferInfoByTransferId = await BatchPositionModel.getTransferInfoList(
128
+ trx,
129
+ transferIdList,
130
+ Enum.Accounts.TransferParticipantRoleType.PAYEE_DFSP,
131
+ Enum.Accounts.LedgerEntryType.PRINCIPLE_VALUE
132
+ )
133
+
123
134
  let notifyMessages = []
124
135
  let limitAlarms = []
125
136
 
@@ -127,6 +138,14 @@ const processBins = async (bins, trx) => {
127
138
  for (const accountID in bins) {
128
139
  const accountBin = bins[accountID]
129
140
  const actions = Object.keys(accountBin)
141
+ const isSubset = (array1, array2) =>
142
+ array2.every((element) => array1.includes(element))
143
+ // If non-prepare/non-commit action found, log error
144
+ // We need to remove this once we implement all the actions
145
+ if (!isSubset(['prepare', 'commit'], actions)) {
146
+ Logger.isErrorEnabled && Logger.error('Only prepare/commit actions are allowed in a batch')
147
+ // throw new Error('Only prepare action is allowed in a batch')
148
+ }
130
149
 
131
150
  const settlementParticipantPosition = positions[accountIdMap[accountID].settlementCurrencyId].value
132
151
  const settlementModel = currencyIdMap[accountIdMap[accountID].currencyId].settlementModel
@@ -146,12 +165,24 @@ const processBins = async (bins, trx) => {
146
165
  let accumulatedTransferStateChanges = []
147
166
  let accumulatedPositionChanges = []
148
167
 
149
- // If non-prepare action found, log error
150
- // We need to remove this once we implement all the actions
151
- if (actions.length > 1 || (actions.length === 1 && actions[0] !== 'prepare')) {
152
- Logger.isErrorEnabled && Logger.error('Only prepare action is allowed in a batch')
153
- // throw new Error('Only prepare action is allowed in a batch')
154
- }
168
+ // If fulfil action found then call processPositionPrepareBin function
169
+ const fulfilActionResult = await PositionFulfilDomain.processPositionFulfilBin(
170
+ accountBin.commit,
171
+ accumulatedPositionValue,
172
+ accumulatedPositionReservedValue,
173
+ accumulatedTransferStates,
174
+ latestTransferInfoByTransferId
175
+ )
176
+
177
+ // Update accumulated values
178
+ accumulatedPositionValue = fulfilActionResult.accumulatedPositionValue
179
+ accumulatedPositionReservedValue = fulfilActionResult.accumulatedPositionReservedValue
180
+ accumulatedTransferStates = fulfilActionResult.accumulatedTransferStates
181
+ // Append accumulated arrays
182
+ accumulatedTransferStateChanges = accumulatedTransferStateChanges.concat(fulfilActionResult.accumulatedTransferStateChanges)
183
+ accumulatedPositionChanges = accumulatedPositionChanges.concat(fulfilActionResult.accumulatedPositionChanges)
184
+ notifyMessages = notifyMessages.concat(fulfilActionResult.notifyMessages)
185
+
155
186
  // If prepare action found then call processPositionPrepareBin function
156
187
  const prepareActionResult = await PositionPrepareDomain.processPositionPrepareBin(
157
188
  accountBin.prepare,
@@ -0,0 +1,153 @@
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 MLNumber = require('@mojaloop/ml-number')
6
+ const Logger = require('@mojaloop/central-services-logger')
7
+
8
+ /**
9
+ * @function processPositionFulfilBin
10
+ *
11
+ * @async
12
+ * @description This is the domain function to process a bin of position-fulfil messages of a single participant account.
13
+ *
14
+ * @param {array} binItems - an array of objects that contain a position fulfil message and its span. {message, span}
15
+ * @param {number} accumulatedPositionValue - value of position accumulated so far from previous bin processing
16
+ * @param {number} accumulatedPositionReservedValue - value of position reserved accumulated so far, not used but kept for consistency
17
+ * @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.
18
+ * @param {object} transferInfoList - object with transfer id keys and transfer info values. Used to pass transfer info to domain function.
19
+ * @returns {object} - Returns an object containing accumulatedPositionValue, accumulatedPositionReservedValue, accumulatedTransferStateChanges, accumulatedTransferStates, resultMessages, limitAlarms or throws an error if failed
20
+ */
21
+ const processPositionFulfilBin = async (
22
+ binItems,
23
+ accumulatedPositionValue,
24
+ accumulatedPositionReservedValue,
25
+ accumulatedTransferStates,
26
+ transferInfoList
27
+ ) => {
28
+ const transferStateChanges = []
29
+ const participantPositionChanges = []
30
+ const resultMessages = []
31
+ const accumulatedTransferStatesCopy = Object.assign({}, accumulatedTransferStates)
32
+ let runningPosition = new MLNumber(accumulatedPositionValue)
33
+
34
+ if (binItems && binItems.length > 0) {
35
+ for (const binItem of binItems) {
36
+ let transferStateId
37
+ let reason
38
+ let resultMessage
39
+ const transferId = binItem.message.value.content.uriParams.id
40
+ const payeeFsp = binItem.message.value.from
41
+ const payerFsp = binItem.message.value.to
42
+ const transfer = binItem.decodedPayload
43
+ Logger.isDebugEnabled && Logger.debug(`processPositionFulfilBin::transfer:processingMessage: ${JSON.stringify(transfer)}`)
44
+ Logger.isDebugEnabled && Logger.debug(`accumulatedTransferStates: ${JSON.stringify(accumulatedTransferStates)}`)
45
+ // Inform payee dfsp if transfer is not in RECEIVED_FULFIL state, skip making any transfer state changes
46
+ if (accumulatedTransferStates[transferId] !== Enum.Transfers.TransferInternalState.RECEIVED_FULFIL) {
47
+ // forward same headers from the prepare message, except the content-length header
48
+ // set destination to payeefsp and source to switch
49
+ const headers = { ...binItem.message.value.content.headers }
50
+ headers[Enum.Http.Headers.FSPIOP.DESTINATION] = payeeFsp
51
+ headers[Enum.Http.Headers.FSPIOP.SOURCE] = Enum.Http.Headers.FSPIOP.SWITCH.value
52
+ delete headers['content-length']
53
+
54
+ const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError(
55
+ `Invalid State: ${accumulatedTransferStates[transferId]} - expected: ${Enum.Transfers.TransferInternalState.RECEIVED_FULFIL}`
56
+ ).toApiErrorObject(Config.ERROR_HANDLING)
57
+ const state = Utility.StreamingProtocol.createEventState(
58
+ Enum.Events.EventStatus.FAILURE.status,
59
+ fspiopError.errorInformation.errorCode,
60
+ fspiopError.errorInformation.errorDescription
61
+ )
62
+
63
+ const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
64
+ transferId,
65
+ Enum.Kafka.Topics.NOTIFICATION,
66
+ Enum.Events.Event.Action.FULFIL,
67
+ state
68
+ )
69
+
70
+ resultMessage = Utility.StreamingProtocol.createMessage(
71
+ transferId,
72
+ payeeFsp,
73
+ Enum.Http.Headers.FSPIOP.SWITCH.value,
74
+ metadata,
75
+ headers,
76
+ fspiopError,
77
+ { id: transferId },
78
+ 'application/json'
79
+ )
80
+ } else {
81
+ const transferInfo = transferInfoList[transferId]
82
+
83
+ // forward same headers from the prepare message, except the content-length header
84
+ const headers = { ...binItem.message.value.content.headers }
85
+ delete headers['content-length']
86
+
87
+ const state = Utility.StreamingProtocol.createEventState(
88
+ Enum.Events.EventStatus.SUCCESS.status,
89
+ null,
90
+ null
91
+ )
92
+ const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
93
+ transferId,
94
+ Enum.Kafka.Topics.TRANSFER,
95
+ Enum.Events.Event.Action.COMMIT,
96
+ state
97
+ )
98
+
99
+ resultMessage = Utility.StreamingProtocol.createMessage(
100
+ transferId,
101
+ payerFsp,
102
+ payeeFsp,
103
+ metadata,
104
+ headers,
105
+ transfer,
106
+ { id: transferId },
107
+ 'application/json'
108
+ )
109
+
110
+ transferStateId = Enum.Transfers.TransferState.COMMITTED
111
+ // Amounts in `transferParticipant` for the payee are stored as negative values
112
+ runningPosition = new MLNumber(runningPosition.add(transferInfo.amount).toFixed(Config.AMOUNT.SCALE))
113
+
114
+ const participantPositionChange = {
115
+ transferId, // Need to delete this in bin processor while updating transferStateChangeId
116
+ transferStateChangeId: null, // Need to update this in bin processor while executing queries
117
+ value: runningPosition.toNumber(),
118
+ reservedValue: accumulatedPositionReservedValue
119
+ }
120
+ participantPositionChanges.push(participantPositionChange)
121
+ binItem.result = { success: true }
122
+ }
123
+
124
+ resultMessages.push({ binItem, message: resultMessage })
125
+
126
+ if (transferStateId) {
127
+ const transferStateChange = {
128
+ transferId,
129
+ transferStateId,
130
+ reason
131
+ }
132
+ transferStateChanges.push(transferStateChange)
133
+ Logger.isDebugEnabled && Logger.debug(`processPositionFulfilBin::transferStateChange: ${JSON.stringify(transferStateChange)}`)
134
+
135
+ accumulatedTransferStatesCopy[transferId] = transferStateId
136
+ Logger.isDebugEnabled && Logger.debug(`processPositionFulfilBin::accumulatedTransferStatesCopy:finalizedTransferState ${JSON.stringify(transferStateId)}`)
137
+ }
138
+ }
139
+ }
140
+
141
+ return {
142
+ accumulatedPositionValue: runningPosition.toNumber(),
143
+ accumulatedTransferStates: accumulatedTransferStatesCopy, // finalized transfer state after fulfil processing
144
+ accumulatedPositionReservedValue, // not used but kept for consistency
145
+ accumulatedTransferStateChanges: transferStateChanges, // transfer state changes to be persisted in order
146
+ accumulatedPositionChanges: participantPositionChanges, // participant position changes to be persisted in order
147
+ notifyMessages: resultMessages // array of objects containing bin item and result message. {binItem, message}
148
+ }
149
+ }
150
+
151
+ module.exports = {
152
+ processPositionFulfilBin
153
+ }
@@ -665,8 +665,25 @@ const fulfil = async (error, messages) => {
665
665
  await TransferService.handlePayeeResponse(transferId, payload, action)
666
666
  const eventDetail = { functionality: TransferEventType.POSITION, action }
667
667
  // Key position fulfil message with payee account id
668
+ let topicNameOverride
669
+ if (action === TransferEventAction.COMMIT) {
670
+ topicNameOverride = Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.COMMIT
671
+ } else if (action === TransferEventAction.RESERVE) {
672
+ topicNameOverride = Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.RESERVE
673
+ } else if (action === TransferEventAction.BULK_COMMIT) {
674
+ topicNameOverride = Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.BULK_COMMIT
675
+ }
668
676
  const payeeAccount = await Participant.getAccountByNameAndCurrency(transfer.payeeFsp, transfer.currency, Enum.Accounts.LedgerAccountType.POSITION)
669
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail, messageKey: payeeAccount.participantCurrencyId.toString() })
677
+ await Kafka.proceed(
678
+ Config.KAFKA_CONFIG,
679
+ params,
680
+ {
681
+ consumerCommit,
682
+ eventDetail,
683
+ messageKey: payeeAccount.participantCurrencyId.toString(),
684
+ topicNameOverride
685
+ }
686
+ )
670
687
  histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
671
688
  return true
672
689
  }
@@ -104,6 +104,33 @@ const updateParticipantPosition = async (trx, participantPositionId, participant
104
104
  })
105
105
  }
106
106
 
107
+ const getTransferInfoList = async (trx, transferIds, transferParticipantRoleTypeId, ledgerEntryTypeId) => {
108
+ try {
109
+ const knex = await Db.getKnex()
110
+ const transferInfos = await knex('transferParticipant')
111
+ .transacting(trx)
112
+ .where({
113
+ 'transferParticipant.transferParticipantRoleTypeId': transferParticipantRoleTypeId,
114
+ 'transferParticipant.ledgerEntryTypeId': ledgerEntryTypeId
115
+ })
116
+ .whereIn('transferParticipant.transferId', transferIds)
117
+ .select(
118
+ 'transferParticipant.*'
119
+ )
120
+ const info = {}
121
+ // This should key the transfer info with the latest transferStateChangeId
122
+ for (const transferInfo of transferInfos) {
123
+ if (!(transferInfo.transferId in info)) {
124
+ info[transferInfo.transferId] = transferInfo
125
+ }
126
+ }
127
+ return info
128
+ } catch (err) {
129
+ Logger.isErrorEnabled && Logger.error(err)
130
+ throw err
131
+ }
132
+ }
133
+
107
134
  const bulkInsertTransferStateChanges = async (trx, transferStateChangeList) => {
108
135
  const knex = await Db.getKnex()
109
136
  return await knex.batchInsert('transferStateChange', transferStateChangeList).transacting(trx)
@@ -121,5 +148,6 @@ module.exports = {
121
148
  updateParticipantPosition,
122
149
  bulkInsertTransferStateChanges,
123
150
  bulkInsertParticipantPositionChanges,
124
- getAllParticipantCurrency
151
+ getAllParticipantCurrency,
152
+ getTransferInfoList
125
153
  }