@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 +7 -0
- package/package.json +2 -2
- package/src/domain/position/binProcessor.js +15 -4
- package/src/domain/position/fulfil.js +112 -102
- package/src/models/position/batch.js +45 -1
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.
|
|
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.
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
}
|