@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.
Files changed (147) hide show
  1. package/.ncurc.yaml +3 -1
  2. package/.nvmrc +1 -1
  3. package/.nycrc.yml +16 -1
  4. package/Dockerfile +9 -10
  5. package/README.md +39 -9
  6. package/audit-ci.jsonc +15 -2
  7. package/config/default.json +17 -1
  8. package/docker/central-ledger/default.json +9 -0
  9. package/docker/config-modifier/configs/central-ledger.js +25 -0
  10. package/docker/env.sh +15 -0
  11. package/docker/kafka/scripts/provision.sh +4 -1
  12. package/docker/ml-api-adapter/default.json +4 -1
  13. package/docker-compose.yml +90 -2
  14. package/documentation/db/erd-transfer-timeout.png +0 -0
  15. package/documentation/db/erd-transfer-timeout.txt +81 -0
  16. package/documentation/fx-implementation/README.md +48 -0
  17. package/documentation/fx-implementation/assets/fx-position-movements.drawio.svg +4 -0
  18. package/documentation/fx-implementation/assets/test-scenario.drawio.svg +4 -0
  19. package/documentation/sequence-diagrams/Handler - FX timeout.plantuml +123 -0
  20. package/documentation/sequence-diagrams/Handler - FX timeout.png +0 -0
  21. package/documentation/sequence-diagrams/Handler - timeout.plantuml +81 -0
  22. package/documentation/sequence-diagrams/Handler - timeout.png +0 -0
  23. package/documentation/state-diagrams/transfer-ML-spec-states-diagram.png +0 -0
  24. package/documentation/state-diagrams/transfer-internal-states-diagram.png +0 -0
  25. package/documentation/state-diagrams/transfer-internal-states.plantuml +75 -0
  26. package/documentation/state-diagrams/transfer-states.plantuml +13 -0
  27. package/migrations/310204_transferParticipant-participantId.js +52 -0
  28. package/migrations/310403_participantPositionChange-participantCurrencyId.js +47 -0
  29. package/migrations/310404_participantPositionChange-change.js +46 -0
  30. package/migrations/600010_fxTransferType.js +43 -0
  31. package/migrations/600011_fxTransferType-indexes.js +38 -0
  32. package/migrations/600012_fxParticipantCurrencyType.js +43 -0
  33. package/migrations/600013_fxParticipantCurrencyType-indexes.js +38 -0
  34. package/migrations/600100_fxTransferDuplicateCheck.js +42 -0
  35. package/migrations/600110_fxTransferErrorDuplicateCheck.js.js +17 -0
  36. package/migrations/600200_fxTransfer.js +51 -0
  37. package/migrations/600201_fxTransfer-indexes.js +40 -0
  38. package/migrations/600400_fxTransferStateChange.js +46 -0
  39. package/migrations/600401_fxTransferStateChange-indexes.js +40 -0
  40. package/migrations/600501_fxWatchList.js +46 -0
  41. package/migrations/600502_fxWatchList-indexes.js +40 -0
  42. package/migrations/600600_fxTransferFulfilmentDuplicateCheck.js +43 -0
  43. package/migrations/600601_fxTransferFulfilmentDuplicateCheck-indexes.js +38 -0
  44. package/migrations/600700_fxTransferFulfilment.js +47 -0
  45. package/migrations/600701_fxTransferFulfilment-indexes.js +43 -0
  46. package/migrations/600800_fxTransferExtension.js +47 -0
  47. package/migrations/601400_fxTransferTimeout.js +43 -0
  48. package/migrations/601401_fxTransferTimeout-indexes.js +37 -0
  49. package/migrations/601500_fxTransferError.js +44 -0
  50. package/migrations/601501_fxTransferError-indexes.js +37 -0
  51. package/migrations/610200_fxTransferParticipant.js +52 -0
  52. package/migrations/610201_fxTransferParticipant-indexes.js +44 -0
  53. package/migrations/610202_fxTransferParticipant-participantId.js +52 -0
  54. package/migrations/610403_participantPositionChange-fxTransfer.js +46 -0
  55. package/migrations/910101_feature904DataMigration.js +46 -52
  56. package/migrations/910102_feature949DataMigration.js +219 -225
  57. package/migrations/950104_settlementModel-settlementAccountTypeId.js +15 -20
  58. package/migrations/950108_participantProxy.js +18 -0
  59. package/migrations/950109_fxQuote.js +19 -0
  60. package/migrations/950110_fxQuoteResponse.js +25 -0
  61. package/migrations/950111_fxQuoteError.js +23 -0
  62. package/migrations/950113_fxQuoteDuplicateCheck.js +18 -0
  63. package/migrations/950114_fxQuoteResponseDuplicateCheck.js +21 -0
  64. package/migrations/950115_fxQuoteConversionTerms.js +36 -0
  65. package/migrations/950116_fxQuoteConversionTermsExtension.js +21 -0
  66. package/migrations/950117_fxQuoteResponseConversionTerms.js +39 -0
  67. package/migrations/950118_fxQuoteResponseConversionTermsExtension.js +21 -0
  68. package/migrations/950119_fxCharge.js +27 -0
  69. package/migrations/960100_create_externalParticipant.js +47 -0
  70. package/migrations/960110_alter_transferParticipant__addFiled_externalParticipantId.js +50 -0
  71. package/migrations/960111_alter_fxTransferParticipant__addFiled_externalParticipantId.js +50 -0
  72. package/package.json +27 -20
  73. package/seeds/endpointType.js +18 -0
  74. package/seeds/fxParticipantCurrencyType.js +45 -0
  75. package/seeds/fxTransferType.js +45 -0
  76. package/seeds/participant.js +2 -1
  77. package/seeds/transferParticipantRoleType.js +9 -0
  78. package/seeds/transferState.js +10 -0
  79. package/src/api/interface/swagger.json +23 -30
  80. package/src/api/participants/handler.js +6 -2
  81. package/src/api/participants/routes.js +8 -7
  82. package/src/api/root/handler.js +15 -5
  83. package/src/domain/fx/cyril.js +466 -0
  84. package/src/domain/fx/index.js +107 -0
  85. package/src/domain/participant/index.js +108 -1
  86. package/src/domain/position/abort.js +215 -0
  87. package/src/domain/position/binProcessor.js +361 -99
  88. package/src/domain/position/fulfil.js +252 -111
  89. package/src/domain/position/fx-fulfil.js +138 -0
  90. package/src/domain/position/fx-prepare.js +280 -0
  91. package/src/domain/position/fx-timeout-reserved.js +159 -0
  92. package/src/domain/position/index.js +1 -0
  93. package/src/domain/position/prepare.js +69 -49
  94. package/src/domain/position/timeout-reserved.js +162 -0
  95. package/src/domain/timeout/index.js +26 -2
  96. package/src/domain/transfer/index.js +22 -5
  97. package/src/domain/transfer/transform.js +19 -6
  98. package/src/handlers/admin/handler.js +0 -2
  99. package/src/handlers/bulk/fulfil/handler.js +5 -5
  100. package/src/handlers/bulk/get/handler.js +5 -5
  101. package/src/handlers/bulk/prepare/handler.js +9 -9
  102. package/src/handlers/bulk/processing/handler.js +8 -7
  103. package/src/handlers/bulk/shared/validator.js +1 -1
  104. package/src/handlers/positions/handler.js +23 -10
  105. package/src/handlers/positions/handlerBatch.js +54 -26
  106. package/src/handlers/register.js +2 -1
  107. package/src/handlers/timeouts/handler.js +212 -68
  108. package/src/handlers/transfers/FxFulfilService.js +387 -0
  109. package/src/handlers/transfers/createRemittanceEntity.js +106 -0
  110. package/src/handlers/transfers/dto.js +53 -0
  111. package/src/handlers/transfers/handler.js +609 -571
  112. package/src/handlers/transfers/prepare.js +572 -0
  113. package/src/handlers/transfers/validator.js +75 -14
  114. package/src/lib/cache.js +1 -1
  115. package/src/lib/config.js +6 -3
  116. package/src/lib/healthCheck/subServiceHealth.js +12 -2
  117. package/src/lib/proxyCache.js +131 -0
  118. package/src/models/bulkTransfer/facade.js +58 -82
  119. package/src/models/fxTransfer/duplicateCheck.js +153 -0
  120. package/src/models/fxTransfer/fxTransfer.js +578 -0
  121. package/src/models/fxTransfer/fxTransferError.js +53 -0
  122. package/src/models/fxTransfer/fxTransferExtension.js +41 -0
  123. package/src/models/fxTransfer/fxTransferTimeout.js +68 -0
  124. package/src/models/fxTransfer/index.js +15 -0
  125. package/src/models/fxTransfer/stateChange.js +47 -0
  126. package/src/models/fxTransfer/watchList.js +49 -0
  127. package/src/models/ledgerAccountType/ledgerAccountType.js +8 -32
  128. package/src/models/misc/segment.js +0 -1
  129. package/src/models/participant/externalParticipant.js +96 -0
  130. package/src/models/participant/externalParticipantCached.js +148 -0
  131. package/src/models/participant/facade.js +192 -123
  132. package/src/models/participant/participant.js +2 -1
  133. package/src/models/participant/participantCurrency.js +1 -1
  134. package/src/models/participant/participantPosition.js +2 -8
  135. package/src/models/position/batch.js +90 -1
  136. package/src/models/position/facade.js +5 -5
  137. package/src/models/position/participantPositionChanges.js +68 -0
  138. package/src/models/settlement/settlementModel.js +5 -23
  139. package/src/models/transfer/facade.js +806 -447
  140. package/src/shared/constants.js +52 -0
  141. package/src/shared/fspiopErrorFactory.js +131 -0
  142. package/src/shared/logger/index.js +8 -0
  143. package/src/shared/loggingPlugin.js +43 -0
  144. package/src/shared/plugins.js +6 -0
  145. package/src/shared/setup.js +10 -0
  146. package/test-integration.Dockerfile +1 -1
  147. 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
+ }
@@ -23,6 +23,7 @@
23
23
  - Name Surname <name.surname@gatesfoundation.com>
24
24
 
25
25
  * Shashikant Hirugade <shashikant.hirugade@modusbox.com>
26
+ * Vijay Kumar Guthi <vijaya.guthi@infitx.com>
26
27
 
27
28
  --------------
28
29
  ******/