@mojaloop/central-ledger 17.7.8 → 17.8.0-orb.1
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/.ncurc.yaml +3 -1
- package/.nvmrc +1 -1
- package/.nycrc.yml +16 -1
- package/Dockerfile +9 -10
- package/README.md +39 -9
- package/audit-ci.jsonc +15 -2
- package/config/default.json +17 -1
- package/docker/central-ledger/default.json +9 -0
- package/docker/config-modifier/configs/central-ledger.js +25 -0
- package/docker/env.sh +15 -0
- package/docker/kafka/scripts/provision.sh +4 -1
- package/docker/ml-api-adapter/default.json +4 -1
- package/docker-compose.yml +90 -2
- package/documentation/db/erd-transfer-timeout.png +0 -0
- package/documentation/db/erd-transfer-timeout.txt +81 -0
- package/documentation/fx-implementation/README.md +48 -0
- package/documentation/fx-implementation/assets/fx-position-movements.drawio.svg +4 -0
- package/documentation/fx-implementation/assets/test-scenario.drawio.svg +4 -0
- package/documentation/sequence-diagrams/Handler - FX timeout.plantuml +123 -0
- package/documentation/sequence-diagrams/Handler - FX timeout.png +0 -0
- package/documentation/sequence-diagrams/Handler - timeout.plantuml +81 -0
- package/documentation/sequence-diagrams/Handler - timeout.png +0 -0
- package/documentation/state-diagrams/transfer-ML-spec-states-diagram.png +0 -0
- package/documentation/state-diagrams/transfer-internal-states-diagram.png +0 -0
- package/documentation/state-diagrams/transfer-internal-states.plantuml +75 -0
- package/documentation/state-diagrams/transfer-states.plantuml +13 -0
- package/migrations/310204_transferParticipant-participantId.js +52 -0
- package/migrations/310403_participantPositionChange-participantCurrencyId.js +47 -0
- package/migrations/310404_participantPositionChange-change.js +46 -0
- package/migrations/600010_fxTransferType.js +43 -0
- package/migrations/600011_fxTransferType-indexes.js +38 -0
- package/migrations/600012_fxParticipantCurrencyType.js +43 -0
- package/migrations/600013_fxParticipantCurrencyType-indexes.js +38 -0
- package/migrations/600100_fxTransferDuplicateCheck.js +42 -0
- package/migrations/600110_fxTransferErrorDuplicateCheck.js.js +17 -0
- package/migrations/600200_fxTransfer.js +51 -0
- package/migrations/600201_fxTransfer-indexes.js +40 -0
- package/migrations/600400_fxTransferStateChange.js +46 -0
- package/migrations/600401_fxTransferStateChange-indexes.js +40 -0
- package/migrations/600501_fxWatchList.js +46 -0
- package/migrations/600502_fxWatchList-indexes.js +40 -0
- package/migrations/600600_fxTransferFulfilmentDuplicateCheck.js +43 -0
- package/migrations/600601_fxTransferFulfilmentDuplicateCheck-indexes.js +38 -0
- package/migrations/600700_fxTransferFulfilment.js +47 -0
- package/migrations/600701_fxTransferFulfilment-indexes.js +43 -0
- package/migrations/600800_fxTransferExtension.js +47 -0
- package/migrations/601400_fxTransferTimeout.js +43 -0
- package/migrations/601401_fxTransferTimeout-indexes.js +37 -0
- package/migrations/601500_fxTransferError.js +44 -0
- package/migrations/601501_fxTransferError-indexes.js +37 -0
- package/migrations/610200_fxTransferParticipant.js +52 -0
- package/migrations/610201_fxTransferParticipant-indexes.js +44 -0
- package/migrations/610202_fxTransferParticipant-participantId.js +52 -0
- package/migrations/610403_participantPositionChange-fxTransfer.js +46 -0
- package/migrations/910101_feature904DataMigration.js +46 -52
- package/migrations/910102_feature949DataMigration.js +219 -225
- package/migrations/950104_settlementModel-settlementAccountTypeId.js +15 -20
- package/migrations/950108_participantProxy.js +18 -0
- package/migrations/950109_fxQuote.js +19 -0
- package/migrations/950110_fxQuoteResponse.js +25 -0
- package/migrations/950111_fxQuoteError.js +23 -0
- package/migrations/950113_fxQuoteDuplicateCheck.js +18 -0
- package/migrations/950114_fxQuoteResponseDuplicateCheck.js +21 -0
- package/migrations/950115_fxQuoteConversionTerms.js +36 -0
- package/migrations/950116_fxQuoteConversionTermsExtension.js +21 -0
- package/migrations/950117_fxQuoteResponseConversionTerms.js +39 -0
- package/migrations/950118_fxQuoteResponseConversionTermsExtension.js +21 -0
- package/migrations/950119_fxCharge.js +27 -0
- package/migrations/960100_create_externalParticipant.js +47 -0
- package/migrations/960110_alter_transferParticipant__addFiled_externalParticipantId.js +50 -0
- package/migrations/960111_alter_fxTransferParticipant__addFiled_externalParticipantId.js +50 -0
- package/package.json +27 -20
- package/seeds/endpointType.js +18 -0
- package/seeds/fxParticipantCurrencyType.js +45 -0
- package/seeds/fxTransferType.js +45 -0
- package/seeds/participant.js +2 -1
- package/seeds/transferParticipantRoleType.js +9 -0
- package/seeds/transferState.js +10 -0
- package/src/api/interface/swagger.json +23 -30
- package/src/api/participants/handler.js +6 -2
- package/src/api/participants/routes.js +8 -7
- package/src/api/root/handler.js +15 -5
- package/src/domain/fx/cyril.js +466 -0
- package/src/domain/fx/index.js +107 -0
- package/src/domain/participant/index.js +108 -1
- package/src/domain/position/abort.js +215 -0
- package/src/domain/position/binProcessor.js +361 -99
- package/src/domain/position/fulfil.js +252 -111
- package/src/domain/position/fx-fulfil.js +138 -0
- package/src/domain/position/fx-prepare.js +280 -0
- package/src/domain/position/fx-timeout-reserved.js +159 -0
- package/src/domain/position/index.js +1 -0
- package/src/domain/position/prepare.js +69 -49
- package/src/domain/position/timeout-reserved.js +162 -0
- package/src/domain/timeout/index.js +26 -2
- package/src/domain/transfer/index.js +22 -5
- package/src/domain/transfer/transform.js +19 -6
- package/src/handlers/admin/handler.js +0 -2
- package/src/handlers/bulk/fulfil/handler.js +5 -5
- package/src/handlers/bulk/get/handler.js +5 -5
- package/src/handlers/bulk/prepare/handler.js +9 -9
- package/src/handlers/bulk/processing/handler.js +8 -7
- package/src/handlers/bulk/shared/validator.js +1 -1
- package/src/handlers/positions/handler.js +23 -10
- package/src/handlers/positions/handlerBatch.js +54 -26
- package/src/handlers/register.js +2 -1
- package/src/handlers/timeouts/handler.js +212 -68
- package/src/handlers/transfers/FxFulfilService.js +387 -0
- package/src/handlers/transfers/createRemittanceEntity.js +106 -0
- package/src/handlers/transfers/dto.js +53 -0
- package/src/handlers/transfers/handler.js +609 -571
- package/src/handlers/transfers/prepare.js +572 -0
- package/src/handlers/transfers/validator.js +75 -14
- package/src/lib/cache.js +1 -1
- package/src/lib/config.js +6 -3
- package/src/lib/healthCheck/subServiceHealth.js +12 -2
- package/src/lib/proxyCache.js +131 -0
- package/src/models/bulkTransfer/facade.js +58 -82
- package/src/models/fxTransfer/duplicateCheck.js +153 -0
- package/src/models/fxTransfer/fxTransfer.js +578 -0
- package/src/models/fxTransfer/fxTransferError.js +53 -0
- package/src/models/fxTransfer/fxTransferExtension.js +41 -0
- package/src/models/fxTransfer/fxTransferTimeout.js +68 -0
- package/src/models/fxTransfer/index.js +15 -0
- package/src/models/fxTransfer/stateChange.js +47 -0
- package/src/models/fxTransfer/watchList.js +49 -0
- package/src/models/ledgerAccountType/ledgerAccountType.js +8 -32
- package/src/models/misc/segment.js +0 -1
- package/src/models/participant/externalParticipant.js +96 -0
- package/src/models/participant/externalParticipantCached.js +148 -0
- package/src/models/participant/facade.js +192 -123
- package/src/models/participant/participant.js +2 -1
- package/src/models/participant/participantCurrency.js +1 -1
- package/src/models/participant/participantPosition.js +2 -8
- package/src/models/position/batch.js +90 -1
- package/src/models/position/facade.js +5 -5
- package/src/models/position/participantPositionChanges.js +68 -0
- package/src/models/settlement/settlementModel.js +5 -23
- package/src/models/transfer/facade.js +806 -447
- package/src/shared/constants.js +52 -0
- package/src/shared/fspiopErrorFactory.js +131 -0
- package/src/shared/logger/index.js +8 -0
- package/src/shared/loggingPlugin.js +43 -0
- package/src/shared/plugins.js +6 -0
- package/src/shared/setup.js +10 -0
- package/test-integration.Dockerfile +1 -1
- package/test.Dockerfile +1 -1
|
@@ -0,0 +1,280 @@
|
|
|
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 processFxPositionPrepareBin
|
|
10
|
+
*
|
|
11
|
+
* @async
|
|
12
|
+
* @description This is the domain function to process a bin of position-prepare messages of a single participant account.
|
|
13
|
+
*
|
|
14
|
+
* @param {array} binItems - an array of objects that contain a position prepare message and its span. {message, decodedPayload, span}
|
|
15
|
+
* @param {object} options
|
|
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} accumulatedFxTransferStates - object with fx commit request id keys and fx transfer state id values. Used to check if fx 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} participantLimit - participant limit object for the currency
|
|
21
|
+
* @param {boolean} changePositions - whether to change positions or not
|
|
22
|
+
* @returns {object} - Returns an object containing accumulatedPositionValue, accumulatedPositionReservedValue, accumulatedFxTransferStateChanges, accumulatedTransferStates, resultMessages, limitAlarms or throws an error if failed
|
|
23
|
+
*/
|
|
24
|
+
const processFxPositionPrepareBin = async (
|
|
25
|
+
binItems,
|
|
26
|
+
{
|
|
27
|
+
accumulatedPositionValue,
|
|
28
|
+
accumulatedPositionReservedValue,
|
|
29
|
+
accumulatedFxTransferStates,
|
|
30
|
+
settlementParticipantPosition,
|
|
31
|
+
participantLimit,
|
|
32
|
+
changePositions = true
|
|
33
|
+
}
|
|
34
|
+
) => {
|
|
35
|
+
const fxTransferStateChanges = []
|
|
36
|
+
const participantPositionChanges = []
|
|
37
|
+
const resultMessages = []
|
|
38
|
+
const limitAlarms = []
|
|
39
|
+
const accumulatedFxTransferStatesCopy = Object.assign({}, accumulatedFxTransferStates)
|
|
40
|
+
|
|
41
|
+
let currentPosition = new MLNumber(accumulatedPositionValue)
|
|
42
|
+
let liquidityCover = 0
|
|
43
|
+
let availablePositionBasedOnLiquidityCover = 0
|
|
44
|
+
let availablePositionBasedOnPayerLimit = 0
|
|
45
|
+
|
|
46
|
+
if (changePositions) {
|
|
47
|
+
const reservedPosition = new MLNumber(accumulatedPositionReservedValue)
|
|
48
|
+
const effectivePosition = new MLNumber(currentPosition.add(reservedPosition).toFixed(Config.AMOUNT.SCALE))
|
|
49
|
+
const payerLimit = new MLNumber(participantLimit.value)
|
|
50
|
+
liquidityCover = new MLNumber(settlementParticipantPosition).multiply(-1)
|
|
51
|
+
availablePositionBasedOnLiquidityCover = new MLNumber(liquidityCover.subtract(effectivePosition).toFixed(Config.AMOUNT.SCALE))
|
|
52
|
+
Logger.isInfoEnabled && Logger.info(`processFxPositionPrepareBin::availablePositionBasedOnLiquidityCover: ${availablePositionBasedOnLiquidityCover}`)
|
|
53
|
+
availablePositionBasedOnPayerLimit = new MLNumber(payerLimit.subtract(effectivePosition).toFixed(Config.AMOUNT.SCALE))
|
|
54
|
+
Logger.isDebugEnabled && Logger.debug(`processFxPositionPrepareBin::availablePositionBasedOnPayerLimit: ${availablePositionBasedOnPayerLimit}`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (binItems && binItems.length > 0) {
|
|
58
|
+
for (const binItem of binItems) {
|
|
59
|
+
let transferStateId
|
|
60
|
+
let reason
|
|
61
|
+
let resultMessage
|
|
62
|
+
const fxTransfer = binItem.decodedPayload
|
|
63
|
+
const cyrilResult = binItem.message.value.content.context.cyrilResult
|
|
64
|
+
const transferAmount = fxTransfer.targetAmount.currency === cyrilResult.currencyId ? fxTransfer.targetAmount.amount : fxTransfer.sourceAmount.amount
|
|
65
|
+
|
|
66
|
+
Logger.isDebugEnabled && Logger.debug(`processFxPositionPrepareBin::transfer:processingMessage: ${JSON.stringify(fxTransfer)}`)
|
|
67
|
+
|
|
68
|
+
// Check if fxTransfer is in correct state for processing, produce an internal error message
|
|
69
|
+
if (accumulatedFxTransferStates[fxTransfer.commitRequestId] !== Enum.Transfers.TransferInternalState.RECEIVED_PREPARE) {
|
|
70
|
+
Logger.isDebugEnabled && Logger.debug(`processFxPositionPrepareBin::transferState: ${accumulatedFxTransferStates[fxTransfer.commitRequestId]} !== ${Enum.Transfers.TransferInternalState.RECEIVED_PREPARE}`)
|
|
71
|
+
|
|
72
|
+
transferStateId = Enum.Transfers.TransferInternalState.ABORTED_REJECTED
|
|
73
|
+
reason = 'FxTransfer in incorrect state'
|
|
74
|
+
|
|
75
|
+
// forward same headers from the prepare message, except the content-length header
|
|
76
|
+
// set destination to initiatingFsp and source to switch
|
|
77
|
+
const headers = { ...binItem.message.value.content.headers }
|
|
78
|
+
headers[Enum.Http.Headers.FSPIOP.DESTINATION] = fxTransfer.initiatingFsp
|
|
79
|
+
headers[Enum.Http.Headers.FSPIOP.SOURCE] = Config.HUB_NAME
|
|
80
|
+
delete headers['content-length']
|
|
81
|
+
|
|
82
|
+
const fspiopError = ErrorHandler.Factory.createFSPIOPError(
|
|
83
|
+
ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR
|
|
84
|
+
).toApiErrorObject(Config.ERROR_HANDLING)
|
|
85
|
+
|
|
86
|
+
const state = Utility.StreamingProtocol.createEventState(
|
|
87
|
+
Enum.Events.EventStatus.FAILURE.status,
|
|
88
|
+
fspiopError.errorInformation.errorCode,
|
|
89
|
+
fspiopError.errorInformation.errorDescription
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
|
|
93
|
+
fxTransfer.commitRequestId,
|
|
94
|
+
Enum.Kafka.Topics.NOTIFICATION,
|
|
95
|
+
Enum.Events.Event.Action.FX_PREPARE,
|
|
96
|
+
state
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
resultMessage = Utility.StreamingProtocol.createMessage(
|
|
100
|
+
fxTransfer.commitRequestId,
|
|
101
|
+
fxTransfer.initiatingFsp,
|
|
102
|
+
Config.HUB_NAME,
|
|
103
|
+
metadata,
|
|
104
|
+
headers,
|
|
105
|
+
fspiopError,
|
|
106
|
+
{ id: fxTransfer.commitRequestId },
|
|
107
|
+
'application/json'
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
binItem.result = { success: false }
|
|
111
|
+
|
|
112
|
+
// Check if payer has insufficient liquidity, produce an error message and abort transfer
|
|
113
|
+
} else if (changePositions && availablePositionBasedOnLiquidityCover.toNumber() < transferAmount) {
|
|
114
|
+
transferStateId = Enum.Transfers.TransferInternalState.ABORTED_REJECTED
|
|
115
|
+
reason = ErrorHandler.Enums.FSPIOPErrorCodes.PAYER_FSP_INSUFFICIENT_LIQUIDITY.message
|
|
116
|
+
|
|
117
|
+
// forward same headers from the prepare message, except the content-length header
|
|
118
|
+
// set destination to payerfsp and source to switch
|
|
119
|
+
const headers = { ...binItem.message.value.content.headers }
|
|
120
|
+
headers[Enum.Http.Headers.FSPIOP.DESTINATION] = fxTransfer.initiatingFsp
|
|
121
|
+
headers[Enum.Http.Headers.FSPIOP.SOURCE] = Config.HUB_NAME
|
|
122
|
+
delete headers['content-length']
|
|
123
|
+
|
|
124
|
+
const fspiopError = ErrorHandler.Factory.createFSPIOPError(
|
|
125
|
+
ErrorHandler.Enums.FSPIOPErrorCodes.PAYER_FSP_INSUFFICIENT_LIQUIDITY
|
|
126
|
+
).toApiErrorObject(Config.ERROR_HANDLING)
|
|
127
|
+
|
|
128
|
+
const state = Utility.StreamingProtocol.createEventState(
|
|
129
|
+
Enum.Events.EventStatus.FAILURE.status,
|
|
130
|
+
fspiopError.errorInformation.errorCode,
|
|
131
|
+
fspiopError.errorInformation.errorDescription
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
|
|
135
|
+
fxTransfer.commitRequestId,
|
|
136
|
+
Enum.Kafka.Topics.NOTIFICATION,
|
|
137
|
+
Enum.Events.Event.Action.FX_PREPARE,
|
|
138
|
+
state
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
resultMessage = Utility.StreamingProtocol.createMessage(
|
|
142
|
+
fxTransfer.commitRequestId,
|
|
143
|
+
fxTransfer.initiatingFsp,
|
|
144
|
+
Config.HUB_NAME,
|
|
145
|
+
metadata,
|
|
146
|
+
headers,
|
|
147
|
+
fspiopError,
|
|
148
|
+
{ id: fxTransfer.commitRequestId },
|
|
149
|
+
'application/json'
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
binItem.result = { success: false }
|
|
153
|
+
|
|
154
|
+
// Check if payer has surpassed their limit, produce an error message and abort transfer
|
|
155
|
+
} else if (changePositions && availablePositionBasedOnPayerLimit.toNumber() < transferAmount) {
|
|
156
|
+
transferStateId = Enum.Transfers.TransferInternalState.ABORTED_REJECTED
|
|
157
|
+
reason = ErrorHandler.Enums.FSPIOPErrorCodes.PAYER_LIMIT_ERROR.message
|
|
158
|
+
|
|
159
|
+
// forward same headers from the prepare message, except the content-length header
|
|
160
|
+
// set destination to payerfsp and source to switch
|
|
161
|
+
const headers = { ...binItem.message.value.content.headers }
|
|
162
|
+
headers[Enum.Http.Headers.FSPIOP.DESTINATION] = fxTransfer.initiatingFsp
|
|
163
|
+
headers[Enum.Http.Headers.FSPIOP.SOURCE] = Config.HUB_NAME
|
|
164
|
+
delete headers['content-length']
|
|
165
|
+
|
|
166
|
+
const fspiopError = ErrorHandler.Factory.createFSPIOPError(
|
|
167
|
+
ErrorHandler.Enums.FSPIOPErrorCodes.PAYER_LIMIT_ERROR
|
|
168
|
+
).toApiErrorObject(Config.ERROR_HANDLING)
|
|
169
|
+
|
|
170
|
+
const state = Utility.StreamingProtocol.createEventState(
|
|
171
|
+
Enum.Events.EventStatus.FAILURE.status,
|
|
172
|
+
fspiopError.errorInformation.errorCode,
|
|
173
|
+
fspiopError.errorInformation.errorDescription
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
|
|
177
|
+
fxTransfer.commitRequestId,
|
|
178
|
+
Enum.Kafka.Topics.NOTIFICATION,
|
|
179
|
+
Enum.Events.Event.Action.FX_PREPARE,
|
|
180
|
+
state
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
resultMessage = Utility.StreamingProtocol.createMessage(
|
|
184
|
+
fxTransfer.commitRequestId,
|
|
185
|
+
fxTransfer.initiatingFsp,
|
|
186
|
+
Config.HUB_NAME,
|
|
187
|
+
metadata,
|
|
188
|
+
headers,
|
|
189
|
+
fspiopError,
|
|
190
|
+
{ id: fxTransfer.commitRequestId },
|
|
191
|
+
'application/json'
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
binItem.result = { success: false }
|
|
195
|
+
|
|
196
|
+
// Payer has sufficient liquidity and limit
|
|
197
|
+
} else {
|
|
198
|
+
transferStateId = Enum.Transfers.TransferInternalState.RESERVED
|
|
199
|
+
|
|
200
|
+
if (changePositions) {
|
|
201
|
+
currentPosition = currentPosition.add(transferAmount)
|
|
202
|
+
availablePositionBasedOnLiquidityCover = availablePositionBasedOnLiquidityCover.add(transferAmount)
|
|
203
|
+
availablePositionBasedOnPayerLimit = availablePositionBasedOnPayerLimit.add(transferAmount)
|
|
204
|
+
const participantPositionChange = {
|
|
205
|
+
commitRequestId: fxTransfer.commitRequestId, // Need to delete this in bin processor while updating fxTransferStateChangeId
|
|
206
|
+
fxTransferStateChangeId: null, // Need to update this in bin processor while executing queries
|
|
207
|
+
value: currentPosition.toNumber(),
|
|
208
|
+
change: transferAmount,
|
|
209
|
+
reservedValue: accumulatedPositionReservedValue
|
|
210
|
+
}
|
|
211
|
+
participantPositionChanges.push(participantPositionChange)
|
|
212
|
+
Logger.isDebugEnabled && Logger.debug(`processFxPositionPrepareBin::participantPositionChange: ${JSON.stringify(participantPositionChange)}`)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// forward same headers from the prepare message, except the content-length header
|
|
216
|
+
const headers = { ...binItem.message.value.content.headers }
|
|
217
|
+
delete headers['content-length']
|
|
218
|
+
|
|
219
|
+
const state = Utility.StreamingProtocol.createEventState(
|
|
220
|
+
Enum.Events.EventStatus.SUCCESS.status,
|
|
221
|
+
null,
|
|
222
|
+
null
|
|
223
|
+
)
|
|
224
|
+
const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
|
|
225
|
+
fxTransfer.commitRequestId,
|
|
226
|
+
Enum.Kafka.Topics.TRANSFER,
|
|
227
|
+
Enum.Events.Event.Action.FX_PREPARE,
|
|
228
|
+
state
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
resultMessage = Utility.StreamingProtocol.createMessage(
|
|
232
|
+
fxTransfer.commitRequestId,
|
|
233
|
+
fxTransfer.counterPartyFsp,
|
|
234
|
+
fxTransfer.initiatingFsp,
|
|
235
|
+
metadata,
|
|
236
|
+
headers,
|
|
237
|
+
fxTransfer,
|
|
238
|
+
{},
|
|
239
|
+
'application/json'
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
binItem.result = { success: true }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
resultMessages.push({ binItem, message: resultMessage })
|
|
246
|
+
|
|
247
|
+
if (changePositions) {
|
|
248
|
+
Logger.isDebugEnabled && Logger.debug(`processFxPositionPrepareBin::limitAlarm: ${currentPosition.toNumber()} > ${liquidityCover.multiply(participantLimit.thresholdAlarmPercentage)}`)
|
|
249
|
+
if (currentPosition.toNumber() > liquidityCover.multiply(participantLimit.thresholdAlarmPercentage).toNumber()) {
|
|
250
|
+
limitAlarms.push(participantLimit)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const fxTransferStateChange = {
|
|
255
|
+
commitRequestId: fxTransfer.commitRequestId,
|
|
256
|
+
transferStateId,
|
|
257
|
+
reason
|
|
258
|
+
}
|
|
259
|
+
fxTransferStateChanges.push(fxTransferStateChange)
|
|
260
|
+
Logger.isDebugEnabled && Logger.debug(`processFxPositionPrepareBin::fxTransferStateChange: ${JSON.stringify(fxTransferStateChange)}`)
|
|
261
|
+
|
|
262
|
+
accumulatedFxTransferStatesCopy[fxTransfer.commitRequestId] = transferStateId
|
|
263
|
+
Logger.isDebugEnabled && Logger.debug(`processFxPositionPrepareBin::accumulatedTransferStatesCopy:finalizedTransferState ${JSON.stringify(transferStateId)}`)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
accumulatedPositionValue: changePositions ? currentPosition.toNumber() : accumulatedPositionValue,
|
|
269
|
+
accumulatedFxTransferStates: accumulatedFxTransferStatesCopy, // finalized transfer state after prepare processing
|
|
270
|
+
accumulatedPositionReservedValue, // not used but kept for consistency
|
|
271
|
+
accumulatedFxTransferStateChanges: fxTransferStateChanges, // fx-transfer state changes to be persisted in order
|
|
272
|
+
limitAlarms, // array of participant limits that have been breached
|
|
273
|
+
accumulatedPositionChanges: changePositions ? participantPositionChanges : [], // participant position changes to be persisted in order
|
|
274
|
+
notifyMessages: resultMessages // array of objects containing bin item and result message. {binItem, message}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
processFxPositionPrepareBin
|
|
280
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
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 processPositionFxTimeoutReservedBin
|
|
10
|
+
*
|
|
11
|
+
* @async
|
|
12
|
+
* @description This is the domain function to process a bin of timeout-reserved messages of a single participant account.
|
|
13
|
+
*
|
|
14
|
+
* @param {array} fxTimeoutReservedBins - an array containing timeout-reserved action bins
|
|
15
|
+
* @param {object} options
|
|
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} accumulatedFxTransferStates - object with commitRequest id keys and fxTransfer state id values. Used to check if fxTransfer is in correct state for processing. Clone and update states for output.
|
|
19
|
+
* @param {object} transferInfoList - object with transfer id keys and transfer info values. Used to pass transfer info to domain function.
|
|
20
|
+
* @param {boolean} changePositions - whether to change positions or not
|
|
21
|
+
* @returns {object} - Returns an object containing accumulatedPositionValue, accumulatedPositionReservedValue, accumulatedTransferStateChanges, accumulatedFxTransferStates, resultMessages, limitAlarms or throws an error if failed
|
|
22
|
+
*/
|
|
23
|
+
const processPositionFxTimeoutReservedBin = async (
|
|
24
|
+
fxTimeoutReservedBins,
|
|
25
|
+
{
|
|
26
|
+
accumulatedPositionValue,
|
|
27
|
+
accumulatedPositionReservedValue,
|
|
28
|
+
accumulatedFxTransferStates,
|
|
29
|
+
fetchedReservedPositionChangesByCommitRequestIds,
|
|
30
|
+
changePositions = true
|
|
31
|
+
}
|
|
32
|
+
) => {
|
|
33
|
+
const fxTransferStateChanges = []
|
|
34
|
+
const participantPositionChanges = []
|
|
35
|
+
const resultMessages = []
|
|
36
|
+
const accumulatedFxTransferStatesCopy = Object.assign({}, accumulatedFxTransferStates)
|
|
37
|
+
let runningPosition = new MLNumber(accumulatedPositionValue)
|
|
38
|
+
// Position action FX_RESERVED_TIMEOUT event messages are keyed with payer account id.
|
|
39
|
+
// We need to revert the payer's position for the source currency amount of the fxTransfer.
|
|
40
|
+
// We need to notify the payee of the timeout.
|
|
41
|
+
if (fxTimeoutReservedBins && fxTimeoutReservedBins.length > 0) {
|
|
42
|
+
for (const binItem of fxTimeoutReservedBins) {
|
|
43
|
+
Logger.isDebugEnabled && Logger.debug(`processPositionFxTimeoutReservedBin::binItem: ${JSON.stringify(binItem.message.value)}`)
|
|
44
|
+
const participantAccountId = binItem.message.key.toString()
|
|
45
|
+
const commitRequestId = binItem.message.value.content.uriParams.id
|
|
46
|
+
const counterPartyFsp = binItem.message.value.to
|
|
47
|
+
const initiatingFsp = binItem.message.value.from
|
|
48
|
+
|
|
49
|
+
// If the transfer is not in `RESERVED_TIMEOUT`, a position fx-timeout-reserved message was incorrectly published.
|
|
50
|
+
// i.e Something has gone extremely wrong.
|
|
51
|
+
if (accumulatedFxTransferStates[commitRequestId] !== Enum.Transfers.TransferInternalState.RESERVED_TIMEOUT) {
|
|
52
|
+
throw ErrorHandler.Factory.createInternalServerFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR.message)
|
|
53
|
+
} else {
|
|
54
|
+
Logger.isDebugEnabled && Logger.debug(`accumulatedFxTransferStates: ${JSON.stringify(accumulatedFxTransferStates)}`)
|
|
55
|
+
|
|
56
|
+
const transferAmount = fetchedReservedPositionChangesByCommitRequestIds[commitRequestId][participantAccountId].change
|
|
57
|
+
|
|
58
|
+
// Construct payee notification message
|
|
59
|
+
const resultMessage = _constructFxTimeoutReservedResultMessage(
|
|
60
|
+
binItem,
|
|
61
|
+
commitRequestId,
|
|
62
|
+
counterPartyFsp,
|
|
63
|
+
initiatingFsp
|
|
64
|
+
)
|
|
65
|
+
Logger.isDebugEnabled && Logger.debug(`processPositionFxTimeoutReservedBin::resultMessage: ${JSON.stringify(resultMessage)}`)
|
|
66
|
+
|
|
67
|
+
// Revert payer's position for the amount of the transfer
|
|
68
|
+
const { participantPositionChange, fxTransferStateChange, transferStateId, updatedRunningPosition } =
|
|
69
|
+
_handleParticipantPositionChange(runningPosition, transferAmount, commitRequestId, accumulatedPositionReservedValue)
|
|
70
|
+
Logger.isDebugEnabled && Logger.debug(`processPositionFxTimeoutReservedBin::participantPositionChange: ${JSON.stringify(participantPositionChange)}`)
|
|
71
|
+
runningPosition = updatedRunningPosition
|
|
72
|
+
binItem.result = { success: true }
|
|
73
|
+
participantPositionChanges.push(participantPositionChange)
|
|
74
|
+
fxTransferStateChanges.push(fxTransferStateChange)
|
|
75
|
+
accumulatedFxTransferStatesCopy[commitRequestId] = transferStateId
|
|
76
|
+
resultMessages.push({ binItem, message: resultMessage })
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
accumulatedPositionValue: changePositions ? runningPosition.toNumber() : accumulatedPositionValue,
|
|
83
|
+
accumulatedFxTransferStates: accumulatedFxTransferStatesCopy, // finalized transfer state after fx fulfil processing
|
|
84
|
+
accumulatedPositionReservedValue, // not used but kept for consistency
|
|
85
|
+
accumulatedFxTransferStateChanges: fxTransferStateChanges, // fx-transfer state changes to be persisted in order
|
|
86
|
+
accumulatedPositionChanges: changePositions ? participantPositionChanges : [], // participant position changes to be persisted in order
|
|
87
|
+
notifyMessages: resultMessages // array of objects containing bin item and result message. {binItem, message}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const _constructFxTimeoutReservedResultMessage = (binItem, commitRequestId, counterPartyFsp, initiatingFsp) => {
|
|
92
|
+
// IMPORTANT: This singular message is taken by the ml-api-adapter and used to
|
|
93
|
+
// notify the payer and payee of the timeout.
|
|
94
|
+
// As long as the `to` and `from` message values are the payer and payee,
|
|
95
|
+
// and the action is `timeout-reserved`, the ml-api-adapter will notify both.
|
|
96
|
+
// Create a FSPIOPError object for timeout payee notification
|
|
97
|
+
const fspiopError = ErrorHandler.Factory.createFSPIOPError(
|
|
98
|
+
ErrorHandler.Enums.FSPIOPErrorCodes.TRANSFER_EXPIRED,
|
|
99
|
+
null,
|
|
100
|
+
null,
|
|
101
|
+
null,
|
|
102
|
+
null
|
|
103
|
+
).toApiErrorObject(Config.ERROR_HANDLING)
|
|
104
|
+
|
|
105
|
+
const state = Utility.StreamingProtocol.createEventState(
|
|
106
|
+
Enum.Events.EventStatus.FAILURE.status,
|
|
107
|
+
fspiopError.errorInformation.errorCode,
|
|
108
|
+
fspiopError.errorInformation.errorDescription
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// Create metadata for the message, associating the payee notification
|
|
112
|
+
// with the position event fx-timeout-reserved action
|
|
113
|
+
const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
|
|
114
|
+
commitRequestId,
|
|
115
|
+
Enum.Kafka.Topics.POSITION,
|
|
116
|
+
Enum.Events.Event.Action.FX_TIMEOUT_RESERVED,
|
|
117
|
+
state
|
|
118
|
+
)
|
|
119
|
+
const resultMessage = Utility.StreamingProtocol.createMessage(
|
|
120
|
+
commitRequestId,
|
|
121
|
+
counterPartyFsp,
|
|
122
|
+
initiatingFsp,
|
|
123
|
+
metadata,
|
|
124
|
+
binItem.message.value.content.headers, // Headers don't really matter here. ml-api-adapter will ignore them and create their own.
|
|
125
|
+
fspiopError,
|
|
126
|
+
{ id: commitRequestId },
|
|
127
|
+
'application/json'
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return resultMessage
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const _handleParticipantPositionChange = (runningPosition, transferAmount, commitRequestId, accumulatedPositionReservedValue) => {
|
|
134
|
+
const transferStateId = Enum.Transfers.TransferInternalState.EXPIRED_RESERVED
|
|
135
|
+
// Revert payer's position for the amount of the transfer
|
|
136
|
+
const updatedRunningPosition = new MLNumber(runningPosition.subtract(transferAmount).toFixed(Config.AMOUNT.SCALE))
|
|
137
|
+
Logger.isDebugEnabled && Logger.debug(`processPositionFxTimeoutReservedBin::_handleParticipantPositionChange::updatedRunningPosition: ${updatedRunningPosition.toString()}`)
|
|
138
|
+
Logger.isDebugEnabled && Logger.debug(`processPositionFxTimeoutReservedBin::_handleParticipantPositionChange::transferAmount: ${transferAmount}`)
|
|
139
|
+
// Construct participant position change object
|
|
140
|
+
const participantPositionChange = {
|
|
141
|
+
commitRequestId, // Need to delete this in bin processor while updating transferStateChangeId
|
|
142
|
+
transferStateChangeId: null, // Need to update this in bin processor while executing queries
|
|
143
|
+
value: updatedRunningPosition.toNumber(),
|
|
144
|
+
change: transferAmount,
|
|
145
|
+
reservedValue: accumulatedPositionReservedValue
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Construct transfer state change object
|
|
149
|
+
const fxTransferStateChange = {
|
|
150
|
+
commitRequestId,
|
|
151
|
+
transferStateId,
|
|
152
|
+
reason: ErrorHandler.Enums.FSPIOPErrorCodes.TRANSFER_EXPIRED.message
|
|
153
|
+
}
|
|
154
|
+
return { participantPositionChange, fxTransferStateChange, transferStateId, updatedRunningPosition }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
processPositionFxTimeoutReservedBin
|
|
159
|
+
}
|