@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,215 @@
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 processPositionAbortBin
10
+ *
11
+ * @async
12
+ * @description This is the domain function to process a bin of abort / fx-abort messages of a single participant account.
13
+ *
14
+ * @param {array} abortBins - an array containing abort / fx-abort 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} 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
+ * @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, accumulatedTransferStates, resultMessages, limitAlarms or throws an error if failed
22
+ */
23
+ const processPositionAbortBin = async (
24
+ abortBins,
25
+ {
26
+ accumulatedPositionValue,
27
+ accumulatedPositionReservedValue,
28
+ accumulatedTransferStates,
29
+ accumulatedFxTransferStates,
30
+ isFx,
31
+ changePositions = true
32
+ }
33
+ ) => {
34
+ const transferStateChanges = []
35
+ const participantPositionChanges = []
36
+ const resultMessages = []
37
+ const followupMessages = []
38
+ const fxTransferStateChanges = []
39
+ const accumulatedTransferStatesCopy = Object.assign({}, accumulatedTransferStates)
40
+ const accumulatedFxTransferStatesCopy = Object.assign({}, accumulatedFxTransferStates)
41
+ let runningPosition = new MLNumber(accumulatedPositionValue)
42
+
43
+ if (abortBins && abortBins.length > 0) {
44
+ for (const binItem of abortBins) {
45
+ Logger.isDebugEnabled && Logger.debug(`processPositionAbortBin::binItem: ${JSON.stringify(binItem.message.value)}`)
46
+ if (isFx) {
47
+ // If the transfer is not in `RECEIVED_ERROR`, a position fx-abort message was incorrectly published.
48
+ // i.e Something has gone extremely wrong.
49
+ if (accumulatedFxTransferStates[binItem.message.value.content.uriParams.id] !== Enum.Transfers.TransferInternalState.RECEIVED_ERROR) {
50
+ throw ErrorHandler.Factory.createInternalServerFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR.message)
51
+ }
52
+ } else {
53
+ // If the transfer is not in `RECEIVED_ERROR`, a position abort message was incorrectly published.
54
+ // i.e Something has gone extremely wrong.
55
+ if (accumulatedTransferStates[binItem.message.value.content.uriParams.id] !== Enum.Transfers.TransferInternalState.RECEIVED_ERROR) {
56
+ throw ErrorHandler.Factory.createInternalServerFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR.message)
57
+ }
58
+ }
59
+
60
+ const cyrilResult = binItem.message.value.content.context?.cyrilResult
61
+ if (!cyrilResult || !cyrilResult.positionChanges || cyrilResult.positionChanges.length === 0) {
62
+ throw ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR)
63
+ }
64
+
65
+ // Handle position movements
66
+ // Iterate through positionChanges and handle each position movement, mark as done and publish a position-commit kafka message again for the next item
67
+ // Find out the first item to be processed
68
+ const positionChangeIndex = cyrilResult.positionChanges.findIndex(positionChange => !positionChange.isDone)
69
+ const positionChangeToBeProcessed = cyrilResult.positionChanges[positionChangeIndex]
70
+ if (positionChangeToBeProcessed.isFxTransferStateChange) {
71
+ const { participantPositionChange, fxTransferStateChange, transferStateId, updatedRunningPosition } =
72
+ _handleParticipantPositionChangeFx(runningPosition, positionChangeToBeProcessed.amount, positionChangeToBeProcessed.commitRequestId, accumulatedPositionReservedValue)
73
+ runningPosition = updatedRunningPosition
74
+ participantPositionChanges.push(participantPositionChange)
75
+ fxTransferStateChanges.push(fxTransferStateChange)
76
+ accumulatedFxTransferStatesCopy[positionChangeToBeProcessed.commitRequestId] = transferStateId
77
+ } else {
78
+ const { participantPositionChange, transferStateChange, transferStateId, updatedRunningPosition } =
79
+ _handleParticipantPositionChange(runningPosition, positionChangeToBeProcessed.amount, positionChangeToBeProcessed.transferId, accumulatedPositionReservedValue)
80
+ runningPosition = updatedRunningPosition
81
+ participantPositionChanges.push(participantPositionChange)
82
+ transferStateChanges.push(transferStateChange)
83
+ accumulatedTransferStatesCopy[positionChangeToBeProcessed.transferId] = transferStateId
84
+ }
85
+ binItem.result = { success: true }
86
+ const from = binItem.message.value.from
87
+ cyrilResult.positionChanges[positionChangeIndex].isDone = true
88
+ const nextIndex = cyrilResult.positionChanges.findIndex(positionChange => !positionChange.isDone)
89
+ if (nextIndex === -1) {
90
+ // All position changes are done, we need to inform all the participants about the abort
91
+ // Construct a list of messages excluding the original message as it will notified anyway
92
+ for (const positionChange of cyrilResult.positionChanges) {
93
+ if (positionChange.isFxTransferStateChange) {
94
+ // Construct notification message for fx transfer state change
95
+ const resultMessage = _constructAbortResultMessage(binItem, positionChange.commitRequestId, from, positionChange.notifyTo)
96
+ resultMessages.push({ binItem, message: resultMessage })
97
+ } else {
98
+ // Construct notification message for transfer state change
99
+ const resultMessage = _constructAbortResultMessage(binItem, positionChange.transferId, from, positionChange.notifyTo)
100
+ resultMessages.push({ binItem, message: resultMessage })
101
+ }
102
+ }
103
+ } else {
104
+ // There are still position changes to be processed
105
+ // Send position-commit kafka message again for the next item
106
+ const participantCurrencyId = cyrilResult.positionChanges[nextIndex].participantCurrencyId
107
+ // const followupMessage = _constructTransferAbortFollowupMessage(binItem, transferId, payerFsp, payeeFsp, transfer)
108
+ // Pass down the context to the followup message with mutated cyrilResult
109
+ const followupMessage = { ...binItem.message.value }
110
+ // followupMessage.content.context = binItem.message.value.content.context
111
+ followupMessages.push({ binItem, messageKey: participantCurrencyId.toString(), message: followupMessage })
112
+ }
113
+ }
114
+ }
115
+
116
+ return {
117
+ accumulatedPositionValue: changePositions ? runningPosition.toNumber() : accumulatedPositionValue,
118
+ accumulatedTransferStates: accumulatedTransferStatesCopy, // finalized transfer state after fulfil processing
119
+ accumulatedPositionReservedValue, // not used but kept for consistency
120
+ accumulatedTransferStateChanges: transferStateChanges, // transfer state changes to be persisted in order
121
+ accumulatedFxTransferStates: accumulatedFxTransferStatesCopy, // finalized fx transfer state after fulfil processing
122
+ accumulatedFxTransferStateChanges: fxTransferStateChanges, // fx transfer state changes to be persisted in order
123
+ accumulatedPositionChanges: changePositions ? participantPositionChanges : [], // participant position changes to be persisted in order
124
+ notifyMessages: resultMessages, // array of objects containing bin item and result message. {binItem, message}
125
+ followupMessages // array of objects containing bin item, message key and followup message. {binItem, messageKey, message}
126
+ }
127
+ }
128
+
129
+ const _constructAbortResultMessage = (binItem, id, from, notifyTo) => {
130
+ let apiErrorCode = ErrorHandler.Enums.FSPIOPErrorCodes.PAYEE_REJECTION
131
+ let fromCalculated = from
132
+ if (binItem.message?.value.metadata.event.action === Enum.Events.Event.Action.FX_ABORT_VALIDATION || binItem.message?.value.metadata.event.action === Enum.Events.Event.Action.ABORT_VALIDATION) {
133
+ fromCalculated = Config.HUB_NAME
134
+ apiErrorCode = ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR
135
+ }
136
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(
137
+ apiErrorCode,
138
+ null,
139
+ null,
140
+ null,
141
+ null
142
+ ).toApiErrorObject(Config.ERROR_HANDLING)
143
+
144
+ const state = Utility.StreamingProtocol.createEventState(
145
+ Enum.Events.EventStatus.FAILURE.status,
146
+ fspiopError.errorInformation.errorCode,
147
+ fspiopError.errorInformation.errorDescription
148
+ )
149
+
150
+ // Create metadata for the message
151
+ const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
152
+ id,
153
+ Enum.Kafka.Topics.POSITION,
154
+ binItem.message?.value.metadata.event.action, // This will be replaced anyway in Kafka.produceGeneralMessage function
155
+ state
156
+ )
157
+ const resultMessage = Utility.StreamingProtocol.createMessage(
158
+ id,
159
+ notifyTo,
160
+ fromCalculated,
161
+ metadata,
162
+ binItem.message.value.content.headers, // Headers don't really matter here. ml-api-adapter will ignore them and create their own.
163
+ fspiopError,
164
+ { id },
165
+ 'application/json'
166
+ )
167
+
168
+ return resultMessage
169
+ }
170
+
171
+ const _handleParticipantPositionChange = (runningPosition, transferAmount, transferId, accumulatedPositionReservedValue) => {
172
+ const transferStateId = Enum.Transfers.TransferInternalState.ABORTED_ERROR
173
+ const updatedRunningPosition = new MLNumber(runningPosition.add(transferAmount).toFixed(Config.AMOUNT.SCALE))
174
+
175
+ const participantPositionChange = {
176
+ transferId, // Need to delete this in bin processor while updating transferStateChangeId
177
+ transferStateChangeId: null, // Need to update this in bin processor while executing queries
178
+ value: updatedRunningPosition.toNumber(),
179
+ change: transferAmount,
180
+ reservedValue: accumulatedPositionReservedValue
181
+ }
182
+
183
+ // Construct transfer state change object
184
+ const transferStateChange = {
185
+ transferId,
186
+ transferStateId,
187
+ reason: null
188
+ }
189
+ return { participantPositionChange, transferStateChange, transferStateId, updatedRunningPosition }
190
+ }
191
+
192
+ const _handleParticipantPositionChangeFx = (runningPosition, transferAmount, commitRequestId, accumulatedPositionReservedValue) => {
193
+ const transferStateId = Enum.Transfers.TransferInternalState.ABORTED_ERROR
194
+ // Amounts in `transferParticipant` for the payee are stored as negative values
195
+ const updatedRunningPosition = new MLNumber(runningPosition.add(transferAmount).toFixed(Config.AMOUNT.SCALE))
196
+
197
+ const participantPositionChange = {
198
+ commitRequestId, // Need to delete this in bin processor while updating fxTransferStateChangeId
199
+ fxTransferStateChangeId: null, // Need to update this in bin processor while executing queries
200
+ value: updatedRunningPosition.toNumber(),
201
+ change: transferAmount,
202
+ reservedValue: accumulatedPositionReservedValue
203
+ }
204
+
205
+ const fxTransferStateChange = {
206
+ commitRequestId,
207
+ transferStateId,
208
+ reason: null
209
+ }
210
+ return { participantPositionChange, fxTransferStateChange, transferStateId, updatedRunningPosition }
211
+ }
212
+
213
+ module.exports = {
214
+ processPositionAbortBin
215
+ }