@mojaloop/central-ledger 17.5.0 → 17.6.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.6.0](https://github.com/mojaloop/central-ledger/compare/v17.5.0...v17.6.0) (2023-12-20)
6
+
7
+
8
+ ### Features
9
+
10
+ * **mojaloop/#3524:** add reserve action to fulfil logic ([#992](https://github.com/mojaloop/central-ledger/issues/992)) ([eb82c33](https://github.com/mojaloop/central-ledger/commit/eb82c33c0572214a73930d8357102928c59e016f)), closes [mojaloop/#3524](https://github.com/mojaloop/project/issues/3524)
11
+
5
12
  ## [17.5.0](https://github.com/mojaloop/central-ledger/compare/v17.4.1...v17.5.0) (2023-12-13)
6
13
 
7
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mojaloop/central-ledger",
3
- "version": "17.5.0",
3
+ "version": "17.6.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",
@@ -128,7 +128,7 @@
128
128
  "jsdoc": "4.0.2",
129
129
  "jsonpath": "1.1.1",
130
130
  "nodemon": "3.0.2",
131
- "npm-check-updates": "16.14.11",
131
+ "npm-check-updates": "16.14.12",
132
132
  "nyc": "15.1.0",
133
133
  "pre-commit": "1.2.2",
134
134
  "proxyquire": "2.1.3",
@@ -53,12 +53,16 @@ const participantFacade = require('../../models/participant/facade')
53
53
  */
54
54
  const processBins = async (bins, trx) => {
55
55
  const transferIdList = []
56
- await iterateThroughBins(bins, (_accountID, _action, item) => {
56
+ const reservedActionTransferIdList = []
57
+ await iterateThroughBins(bins, (_accountID, action, item) => {
57
58
  if (item.decodedPayload?.transferId) {
58
59
  transferIdList.push(item.decodedPayload.transferId)
59
60
  // get transferId from uriParams for fulfil messages
60
61
  } else if (item.message?.value?.content?.uriParams?.id) {
61
62
  transferIdList.push(item.message.value.content.uriParams.id)
63
+ if (action === Enum.Events.Event.Action.RESERVE) {
64
+ reservedActionTransferIdList.push(item.message.value.content.uriParams.id)
65
+ }
62
66
  }
63
67
  })
64
68
  // Pre fetch latest transferStates for all the transferIds in the account-bin
@@ -131,6 +135,12 @@ const processBins = async (bins, trx) => {
131
135
  Enum.Accounts.LedgerEntryType.PRINCIPLE_VALUE
132
136
  )
133
137
 
138
+ // Pre fetch transfers for all reserve action fulfils
139
+ const reservedActionTransfers = await BatchPositionModel.getTransferByIdsForReserve(
140
+ trx,
141
+ reservedActionTransferIdList
142
+ )
143
+
134
144
  let notifyMessages = []
135
145
  let limitAlarms = []
136
146
 
@@ -142,7 +152,7 @@ const processBins = async (bins, trx) => {
142
152
  array2.every((element) => array1.includes(element))
143
153
  // If non-prepare/non-commit action found, log error
144
154
  // We need to remove this once we implement all the actions
145
- if (!isSubset(['prepare', 'commit'], actions)) {
155
+ if (!isSubset(['prepare', 'commit', 'reserve'], actions)) {
146
156
  Logger.isErrorEnabled && Logger.error('Only prepare/commit actions are allowed in a batch')
147
157
  // throw new Error('Only prepare action is allowed in a batch')
148
158
  }
@@ -167,11 +177,12 @@ const processBins = async (bins, trx) => {
167
177
 
168
178
  // If fulfil action found then call processPositionPrepareBin function
169
179
  const fulfilActionResult = await PositionFulfilDomain.processPositionFulfilBin(
170
- accountBin.commit,
180
+ [accountBin.commit, accountBin.reserve],
171
181
  accumulatedPositionValue,
172
182
  accumulatedPositionReservedValue,
173
183
  accumulatedTransferStates,
174
- latestTransferInfoByTransferId
184
+ latestTransferInfoByTransferId,
185
+ reservedActionTransfers
175
186
  )
176
187
 
177
188
  // Update accumulated values
@@ -4,6 +4,7 @@ const Config = require('../../lib/config')
4
4
  const Utility = require('@mojaloop/central-services-shared').Util
5
5
  const MLNumber = require('@mojaloop/ml-number')
6
6
  const Logger = require('@mojaloop/central-services-logger')
7
+ const TransferObjectTransform = require('../../domain/transfer/transform')
7
8
 
8
9
  /**
9
10
  * @function processPositionFulfilBin
@@ -11,7 +12,7 @@ const Logger = require('@mojaloop/central-services-logger')
11
12
  * @async
12
13
  * @description This is the domain function to process a bin of position-fulfil messages of a single participant account.
13
14
  *
14
- * @param {array} binItems - an array of objects that contain a position fulfil message and its span. {message, span}
15
+ * @param {array} commitReserveFulfilBins - an array containing commit and reserve action bins
15
16
  * @param {number} accumulatedPositionValue - value of position accumulated so far from previous bin processing
16
17
  * @param {number} accumulatedPositionReservedValue - value of position reserved accumulated so far, not used but kept for consistency
17
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,11 +20,12 @@ const Logger = require('@mojaloop/central-services-logger')
19
20
  * @returns {object} - Returns an object containing accumulatedPositionValue, accumulatedPositionReservedValue, accumulatedTransferStateChanges, accumulatedTransferStates, resultMessages, limitAlarms or throws an error if failed
20
21
  */
21
22
  const processPositionFulfilBin = async (
22
- binItems,
23
+ commitReserveFulfilBins,
23
24
  accumulatedPositionValue,
24
25
  accumulatedPositionReservedValue,
25
26
  accumulatedTransferStates,
26
- transferInfoList
27
+ transferInfoList,
28
+ reservedActionTransfers
27
29
  ) => {
28
30
  const transferStateChanges = []
29
31
  const participantPositionChanges = []
@@ -31,109 +33,117 @@ const processPositionFulfilBin = async (
31
33
  const accumulatedTransferStatesCopy = Object.assign({}, accumulatedTransferStates)
32
34
  let runningPosition = new MLNumber(accumulatedPositionValue)
33
35
 
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
- }
36
+ for (const binItems of commitReserveFulfilBins) {
37
+ if (binItems && binItems.length > 0) {
38
+ for (const binItem of binItems) {
39
+ let transferStateId
40
+ let reason
41
+ let resultMessage
42
+ const transferId = binItem.message.value.content.uriParams.id
43
+ const payeeFsp = binItem.message.value.from
44
+ const payerFsp = binItem.message.value.to
45
+ const transfer = binItem.decodedPayload
46
+ Logger.isDebugEnabled && Logger.debug(`processPositionFulfilBin::transfer:processingMessage: ${JSON.stringify(transfer)}`)
47
+ Logger.isDebugEnabled && Logger.debug(`accumulatedTransferStates: ${JSON.stringify(accumulatedTransferStates)}`)
48
+ // Inform payee dfsp if transfer is not in RECEIVED_FULFIL state, skip making any transfer state changes
49
+ if (accumulatedTransferStates[transferId] !== Enum.Transfers.TransferInternalState.RECEIVED_FULFIL) {
50
+ // forward same headers from the prepare message, except the content-length header
51
+ // set destination to payeefsp and source to switch
52
+ const headers = { ...binItem.message.value.content.headers }
53
+ headers[Enum.Http.Headers.FSPIOP.DESTINATION] = payeeFsp
54
+ headers[Enum.Http.Headers.FSPIOP.SOURCE] = Enum.Http.Headers.FSPIOP.SWITCH.value
55
+ delete headers['content-length']
56
+
57
+ const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError(
58
+ `Invalid State: ${accumulatedTransferStates[transferId]} - expected: ${Enum.Transfers.TransferInternalState.RECEIVED_FULFIL}`
59
+ ).toApiErrorObject(Config.ERROR_HANDLING)
60
+ const state = Utility.StreamingProtocol.createEventState(
61
+ Enum.Events.EventStatus.FAILURE.status,
62
+ fspiopError.errorInformation.errorCode,
63
+ fspiopError.errorInformation.errorDescription
64
+ )
65
+
66
+ const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
67
+ transferId,
68
+ Enum.Kafka.Topics.NOTIFICATION,
69
+ Enum.Events.Event.Action.FULFIL,
70
+ state
71
+ )
72
+
73
+ resultMessage = Utility.StreamingProtocol.createMessage(
74
+ transferId,
75
+ payeeFsp,
76
+ Enum.Http.Headers.FSPIOP.SWITCH.value,
77
+ metadata,
78
+ headers,
79
+ fspiopError,
80
+ { id: transferId },
81
+ 'application/json'
82
+ )
83
+ } else {
84
+ const transferInfo = transferInfoList[transferId]
85
+
86
+ // forward same headers from the prepare message, except the content-length header
87
+ const headers = { ...binItem.message.value.content.headers }
88
+ delete headers['content-length']
123
89
 
124
- resultMessages.push({ binItem, message: resultMessage })
90
+ const state = Utility.StreamingProtocol.createEventState(
91
+ Enum.Events.EventStatus.SUCCESS.status,
92
+ null,
93
+ null
94
+ )
95
+ const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
96
+ transferId,
97
+ Enum.Kafka.Topics.TRANSFER,
98
+ Enum.Events.Event.Action.COMMIT,
99
+ state
100
+ )
125
101
 
126
- if (transferStateId) {
127
- const transferStateChange = {
128
- transferId,
129
- transferStateId,
130
- reason
102
+ resultMessage = Utility.StreamingProtocol.createMessage(
103
+ transferId,
104
+ payerFsp,
105
+ payeeFsp,
106
+ metadata,
107
+ headers,
108
+ transfer,
109
+ { id: transferId },
110
+ 'application/json'
111
+ )
112
+
113
+ if (binItem.message.value.metadata.event.action === Enum.Events.Event.Action.RESERVE) {
114
+ resultMessage.content.payload = TransferObjectTransform.toFulfil(
115
+ reservedActionTransfers[transferId]
116
+ )
117
+ }
118
+
119
+ transferStateId = Enum.Transfers.TransferState.COMMITTED
120
+ // Amounts in `transferParticipant` for the payee are stored as negative values
121
+ runningPosition = new MLNumber(runningPosition.add(transferInfo.amount).toFixed(Config.AMOUNT.SCALE))
122
+
123
+ const participantPositionChange = {
124
+ transferId, // Need to delete this in bin processor while updating transferStateChangeId
125
+ transferStateChangeId: null, // Need to update this in bin processor while executing queries
126
+ value: runningPosition.toNumber(),
127
+ reservedValue: accumulatedPositionReservedValue
128
+ }
129
+ participantPositionChanges.push(participantPositionChange)
130
+ binItem.result = { success: true }
131
131
  }
132
- transferStateChanges.push(transferStateChange)
133
- Logger.isDebugEnabled && Logger.debug(`processPositionFulfilBin::transferStateChange: ${JSON.stringify(transferStateChange)}`)
134
132
 
135
- accumulatedTransferStatesCopy[transferId] = transferStateId
136
- Logger.isDebugEnabled && Logger.debug(`processPositionFulfilBin::accumulatedTransferStatesCopy:finalizedTransferState ${JSON.stringify(transferStateId)}`)
133
+ resultMessages.push({ binItem, message: resultMessage })
134
+
135
+ if (transferStateId) {
136
+ const transferStateChange = {
137
+ transferId,
138
+ transferStateId,
139
+ reason
140
+ }
141
+ transferStateChanges.push(transferStateChange)
142
+ Logger.isDebugEnabled && Logger.debug(`processPositionFulfilBin::transferStateChange: ${JSON.stringify(transferStateChange)}`)
143
+
144
+ accumulatedTransferStatesCopy[transferId] = transferStateId
145
+ Logger.isDebugEnabled && Logger.debug(`processPositionFulfilBin::accumulatedTransferStatesCopy:finalizedTransferState ${JSON.stringify(transferStateId)}`)
146
+ }
137
147
  }
138
148
  }
139
149
  }
@@ -32,6 +32,8 @@
32
32
 
33
33
  const Db = require('../../lib/db')
34
34
  const Logger = require('@mojaloop/central-services-logger')
35
+ const TransferExtensionModel = require('../transfer/transferExtension')
36
+ const { Enum } = require('@mojaloop/central-services-shared')
35
37
 
36
38
  const startDbTransaction = async () => {
37
39
  const knex = await Db.getKnex()
@@ -141,6 +143,47 @@ const bulkInsertParticipantPositionChanges = async (trx, participantPositionChan
141
143
  return await knex.batchInsert('participantPositionChange', participantPositionChangeList).transacting(trx)
142
144
  }
143
145
 
146
+ const getTransferByIdsForReserve = async (trx, transferIds) => {
147
+ if (transferIds && transferIds.length > 0) {
148
+ try {
149
+ const knex = await Db.getKnex()
150
+ const query = await knex('transfer')
151
+ .transacting(trx)
152
+ .leftJoin('transferStateChange AS tsc', 'tsc.transferId', 'transfer.transferId')
153
+ .leftJoin('transferState AS ts', 'ts.transferStateId', 'tsc.transferStateId')
154
+ .leftJoin('transferFulfilment AS tf', 'tf.transferId', 'transfer.transferId')
155
+ .leftJoin('transferError as te', 'te.transferId', 'transfer.transferId') // currently transferError.transferId is PK ensuring one error per transferId
156
+ .whereIn('transfer.transferId', transferIds)
157
+ .select(
158
+ 'transfer.*',
159
+ 'tsc.createdDate AS completedTimestamp',
160
+ 'ts.enumeration as transferStateEnumeration',
161
+ 'tf.ilpFulfilment AS fulfilment',
162
+ 'te.errorCode',
163
+ 'te.errorDescription'
164
+ )
165
+ const transfers = {}
166
+ for (const transfer of query) {
167
+ transfer.extensionList = await TransferExtensionModel.getByTransferId(transfer.transferId)
168
+ if (transfer.errorCode && transfer.transferStateEnumeration === Enum.Transfers.TransferState.ABORTED) {
169
+ if (!transfer.extensionList) transfer.extensionList = []
170
+ transfer.extensionList.push({
171
+ key: 'cause',
172
+ value: `${transfer.errorCode}: ${transfer.errorDescription}`.substr(0, 128)
173
+ })
174
+ }
175
+ transfer.isTransferReadModel = true
176
+ transfers[transfer.transferId] = transfer
177
+ }
178
+ return transfers
179
+ } catch (err) {
180
+ Logger.isErrorEnabled && Logger.error(err)
181
+ throw err
182
+ }
183
+ }
184
+ return {}
185
+ }
186
+
144
187
  module.exports = {
145
188
  startDbTransaction,
146
189
  getLatestTransferStateChangesByTransferIdList,
@@ -149,5 +192,6 @@ module.exports = {
149
192
  bulkInsertTransferStateChanges,
150
193
  bulkInsertParticipantPositionChanges,
151
194
  getAllParticipantCurrency,
152
- getTransferInfoList
195
+ getTransferInfoList,
196
+ getTransferByIdsForReserve
153
197
  }