@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
@@ -13,149 +13,290 @@ const TransferObjectTransform = require('../../domain/transfer/transform')
13
13
  * @description This is the domain function to process a bin of position-fulfil messages of a single participant account.
14
14
  *
15
15
  * @param {array} commitReserveFulfilBins - an array containing commit and reserve action bins
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.
16
+ * @param {object} options
17
+ * @param {number} accumulatedPositionValue - value of position accumulated so far from previous bin processing
18
+ * @param {number} accumulatedPositionReservedValue - value of position reserved accumulated so far, not used but kept for consistency
19
+ * @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.
20
+ * @param {object} transferInfoList - object with transfer id keys and transfer info values. Used to pass transfer info to domain function.
21
+ * @param {boolean} changePositions - whether to change positions or not
20
22
  * @returns {object} - Returns an object containing accumulatedPositionValue, accumulatedPositionReservedValue, accumulatedTransferStateChanges, accumulatedTransferStates, resultMessages, limitAlarms or throws an error if failed
21
23
  */
22
24
  const processPositionFulfilBin = async (
23
25
  commitReserveFulfilBins,
24
- accumulatedPositionValue,
25
- accumulatedPositionReservedValue,
26
- accumulatedTransferStates,
27
- transferInfoList,
28
- reservedActionTransfers
26
+ {
27
+ accumulatedPositionValue,
28
+ accumulatedPositionReservedValue,
29
+ accumulatedTransferStates,
30
+ accumulatedFxTransferStates,
31
+ transferInfoList,
32
+ reservedActionTransfers,
33
+ changePositions = true
34
+ }
29
35
  ) => {
30
36
  const transferStateChanges = []
37
+ const fxTransferStateChanges = []
31
38
  const participantPositionChanges = []
32
39
  const resultMessages = []
40
+ const followupMessages = []
33
41
  const accumulatedTransferStatesCopy = Object.assign({}, accumulatedTransferStates)
42
+ const accumulatedFxTransferStatesCopy = Object.assign({}, accumulatedFxTransferStates)
34
43
  let runningPosition = new MLNumber(accumulatedPositionValue)
35
44
 
36
45
  for (const binItems of commitReserveFulfilBins) {
37
46
  if (binItems && binItems.length > 0) {
38
47
  for (const binItem of binItems) {
39
- let transferStateId
40
- let reason
41
- let resultMessage
42
48
  const transferId = binItem.message.value.content.uriParams.id
43
49
  const payeeFsp = binItem.message.value.from
44
50
  const payerFsp = binItem.message.value.to
45
51
  const transfer = binItem.decodedPayload
46
- Logger.isDebugEnabled && Logger.debug(`processPositionFulfilBin::transfer:processingMessage: ${JSON.stringify(transfer)}`)
47
- Logger.isDebugEnabled && Logger.debug(`accumulatedTransferStates: ${JSON.stringify(accumulatedTransferStates)}`)
52
+
48
53
  // Inform payee dfsp if transfer is not in RECEIVED_FULFIL state, skip making any transfer state changes
49
54
  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
- )
55
+ const resultMessage = _handleIncorrectTransferState(binItem, payeeFsp, transferId, accumulatedTransferStates)
56
+ resultMessages.push({ binItem, message: resultMessage })
83
57
  } 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']
89
-
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
- )
101
-
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
58
+ Logger.isDebugEnabled && Logger.debug(`processPositionFulfilBin::transfer:processingMessage: ${JSON.stringify(transfer)}`)
59
+ Logger.isDebugEnabled && Logger.debug(`accumulatedTransferStates: ${JSON.stringify(accumulatedTransferStates)}`)
60
+ const cyrilResult = binItem.message.value.content.context?.cyrilResult
61
+ if (cyrilResult && cyrilResult.isFx) {
62
+ // This is FX transfer
63
+ // Handle position movements
64
+ // Iterate through positionChanges and handle each position movement, mark as done and publish a position-commit kafka message again for the next item
65
+ // Find out the first item to be processed
66
+ const positionChangeIndex = cyrilResult.positionChanges.findIndex(positionChange => !positionChange.isDone)
67
+ const positionChangeToBeProcessed = cyrilResult.positionChanges[positionChangeIndex]
68
+ let transferStateIdCopy
69
+ if (positionChangeToBeProcessed.isFxTransferStateChange) {
70
+ const { participantPositionChange, fxTransferStateChange, transferStateId, updatedRunningPosition } =
71
+ _handleParticipantPositionChangeFx(runningPosition, positionChangeToBeProcessed.amount, positionChangeToBeProcessed.commitRequestId, accumulatedPositionReservedValue)
72
+ transferStateIdCopy = transferStateId
73
+ runningPosition = updatedRunningPosition
74
+ participantPositionChanges.push(participantPositionChange)
75
+ fxTransferStateChanges.push(fxTransferStateChange)
76
+ accumulatedFxTransferStatesCopy[positionChangeToBeProcessed.commitRequestId] = transferStateId
77
+ const patchMessages = _constructPatchNotificationResultMessage(
78
+ binItem,
79
+ cyrilResult
80
+ )
81
+ for (const patchMessage of patchMessages) {
82
+ resultMessages.push({ binItem, message: patchMessage })
83
+ }
84
+ } else {
85
+ const { participantPositionChange, transferStateChange, transferStateId, updatedRunningPosition } =
86
+ _handleParticipantPositionChange(runningPosition, positionChangeToBeProcessed.amount, positionChangeToBeProcessed.transferId, accumulatedPositionReservedValue)
87
+ transferStateIdCopy = transferStateId
88
+ runningPosition = updatedRunningPosition
89
+ participantPositionChanges.push(participantPositionChange)
90
+ transferStateChanges.push(transferStateChange)
91
+ accumulatedTransferStatesCopy[positionChangeToBeProcessed.transferId] = transferStateId
92
+ }
93
+ binItem.result = { success: true }
94
+ cyrilResult.positionChanges[positionChangeIndex].isDone = true
95
+ const nextIndex = cyrilResult.positionChanges.findIndex(positionChange => !positionChange.isDone)
96
+ if (nextIndex === -1) {
97
+ // All position changes are done
98
+ const resultMessage = _constructTransferFulfilResultMessage(binItem, transferId, payerFsp, payeeFsp, transfer, reservedActionTransfers, transferStateIdCopy)
99
+ resultMessages.push({ binItem, message: resultMessage })
100
+ } else {
101
+ // There are still position changes to be processed
102
+ // Send position-commit kafka message again for the next item
103
+ const participantCurrencyId = cyrilResult.positionChanges[nextIndex].participantCurrencyId
104
+ const followupMessage = _constructTransferFulfilResultMessage(binItem, transferId, payerFsp, payeeFsp, transfer, reservedActionTransfers, transferStateIdCopy)
105
+ // Pass down the context to the followup message with mutated cyrilResult
106
+ followupMessage.content.context = binItem.message.value.content.context
107
+ followupMessages.push({ binItem, messageKey: participantCurrencyId.toString(), message: followupMessage })
108
+ }
109
+ } else {
110
+ const transferAmount = transferInfoList[transferId].amount
111
+ const { participantPositionChange, transferStateChange, transferStateId, updatedRunningPosition } =
112
+ _handleParticipantPositionChange(runningPosition, transferAmount, transferId, accumulatedPositionReservedValue)
113
+ runningPosition = updatedRunningPosition
114
+ binItem.result = { success: true }
115
+ participantPositionChanges.push(participantPositionChange)
116
+ transferStateChanges.push(transferStateChange)
117
+ accumulatedTransferStatesCopy[transferId] = transferStateId
118
+ const resultMessage = _constructTransferFulfilResultMessage(binItem, transferId, payerFsp, payeeFsp, transfer, reservedActionTransfers, transferStateId)
119
+ resultMessages.push({ binItem, message: resultMessage })
128
120
  }
129
- participantPositionChanges.push(participantPositionChange)
130
- binItem.result = { success: true }
131
- }
132
-
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
121
  }
147
122
  }
148
123
  }
149
124
  }
150
125
 
151
126
  return {
152
- accumulatedPositionValue: runningPosition.toNumber(),
127
+ accumulatedPositionValue: changePositions ? runningPosition.toNumber() : accumulatedPositionValue,
153
128
  accumulatedTransferStates: accumulatedTransferStatesCopy, // finalized transfer state after fulfil processing
129
+ accumulatedFxTransferStates: accumulatedFxTransferStatesCopy, // finalized transfer state after fx fulfil processing
154
130
  accumulatedPositionReservedValue, // not used but kept for consistency
155
131
  accumulatedTransferStateChanges: transferStateChanges, // transfer state changes to be persisted in order
156
- accumulatedPositionChanges: participantPositionChanges, // participant position changes to be persisted in order
157
- notifyMessages: resultMessages // array of objects containing bin item and result message. {binItem, message}
132
+ accumulatedFxTransferStateChanges: fxTransferStateChanges, // fx-transfer state changes to be persisted in order
133
+ accumulatedPositionChanges: changePositions ? participantPositionChanges : [], // participant position changes to be persisted in order
134
+ notifyMessages: resultMessages, // array of objects containing bin item and result message. {binItem, message}
135
+ followupMessages // array of objects containing bin item, message key and followup message. {binItem, messageKey, message}
136
+ }
137
+ }
138
+
139
+ const _handleIncorrectTransferState = (binItem, payeeFsp, transferId, accumulatedTransferStates) => {
140
+ // forward same headers from the prepare message, except the content-length header
141
+ // set destination to payeefsp and source to switch
142
+ const headers = { ...binItem.message.value.content.headers }
143
+ headers[Enum.Http.Headers.FSPIOP.DESTINATION] = payeeFsp
144
+ headers[Enum.Http.Headers.FSPIOP.SOURCE] = Config.HUB_NAME
145
+ delete headers['content-length']
146
+
147
+ const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError(
148
+ `Invalid State: ${accumulatedTransferStates[transferId]} - expected: ${Enum.Transfers.TransferInternalState.RECEIVED_FULFIL}`
149
+ ).toApiErrorObject(Config.ERROR_HANDLING)
150
+ const state = Utility.StreamingProtocol.createEventState(
151
+ Enum.Events.EventStatus.FAILURE.status,
152
+ fspiopError.errorInformation.errorCode,
153
+ fspiopError.errorInformation.errorDescription
154
+ )
155
+
156
+ const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
157
+ transferId,
158
+ Enum.Kafka.Topics.NOTIFICATION,
159
+ Enum.Events.Event.Action.FULFIL,
160
+ state
161
+ )
162
+
163
+ return Utility.StreamingProtocol.createMessage(
164
+ transferId,
165
+ payeeFsp,
166
+ Config.HUB_NAME,
167
+ metadata,
168
+ headers,
169
+ fspiopError,
170
+ { id: transferId },
171
+ 'application/json'
172
+ )
173
+ }
174
+
175
+ const _constructTransferFulfilResultMessage = (binItem, transferId, payerFsp, payeeFsp, transfer, reservedActionTransfers, transferStateId) => {
176
+ // forward same headers from the prepare message, except the content-length header
177
+ const headers = { ...binItem.message.value.content.headers }
178
+ delete headers['content-length']
179
+
180
+ const state = Utility.StreamingProtocol.createEventState(
181
+ Enum.Events.EventStatus.SUCCESS.status,
182
+ null,
183
+ null
184
+ )
185
+ const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
186
+ transferId,
187
+ Enum.Kafka.Topics.TRANSFER,
188
+ Enum.Events.Event.Action.COMMIT,
189
+ state
190
+ )
191
+
192
+ const resultMessage = Utility.StreamingProtocol.createMessage(
193
+ transferId,
194
+ payerFsp,
195
+ payeeFsp,
196
+ metadata,
197
+ headers,
198
+ transfer,
199
+ { id: transferId },
200
+ 'application/json'
201
+ )
202
+
203
+ if (binItem.message.value.metadata.event.action === Enum.Events.Event.Action.RESERVE) {
204
+ resultMessage.content.payload = TransferObjectTransform.toFulfil(
205
+ reservedActionTransfers[transferId]
206
+ )
207
+ resultMessage.content.payload.transferState = transferStateId
208
+ }
209
+ return resultMessage
210
+ }
211
+
212
+ const _constructPatchNotificationResultMessage = (binItem, cyrilResult) => {
213
+ const messages = []
214
+ const patchNotifications = cyrilResult.patchNotifications
215
+ for (const patchNotification of patchNotifications) {
216
+ const commitRequestId = patchNotification.commitRequestId
217
+ const fxpName = patchNotification.fxpName
218
+ const fulfilment = patchNotification.fulfilment
219
+ const completedTimestamp = patchNotification.completedTimestamp
220
+ const headers = {
221
+ ...binItem.message.value.content.headers,
222
+ 'fspiop-source': Config.HUB_NAME,
223
+ 'fspiop-destination': fxpName
224
+ }
225
+
226
+ const fulfil = {
227
+ conversionState: Enum.Transfers.TransferState.COMMITTED,
228
+ fulfilment,
229
+ completedTimestamp
230
+ }
231
+
232
+ const state = Utility.StreamingProtocol.createEventState(
233
+ Enum.Events.EventStatus.SUCCESS.status,
234
+ null,
235
+ null
236
+ )
237
+ const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
238
+ commitRequestId,
239
+ Enum.Kafka.Topics.TRANSFER,
240
+ Enum.Events.Event.Action.FX_NOTIFY,
241
+ state
242
+ )
243
+
244
+ const resultMessage = Utility.StreamingProtocol.createMessage(
245
+ commitRequestId,
246
+ fxpName,
247
+ Config.HUB_NAME,
248
+ metadata,
249
+ headers,
250
+ fulfil,
251
+ { id: commitRequestId },
252
+ 'application/json'
253
+ )
254
+
255
+ messages.push(resultMessage)
256
+ }
257
+ return messages
258
+ }
259
+
260
+ const _handleParticipantPositionChange = (runningPosition, transferAmount, transferId, accumulatedPositionReservedValue) => {
261
+ const transferStateId = Enum.Transfers.TransferState.COMMITTED
262
+ // Amounts in `transferParticipant` for the payee are stored as negative values
263
+ const updatedRunningPosition = new MLNumber(runningPosition.add(transferAmount).toFixed(Config.AMOUNT.SCALE))
264
+
265
+ const participantPositionChange = {
266
+ transferId, // Need to delete this in bin processor while updating transferStateChangeId
267
+ transferStateChangeId: null, // Need to update this in bin processor while executing queries
268
+ value: updatedRunningPosition.toNumber(),
269
+ change: transferAmount,
270
+ reservedValue: accumulatedPositionReservedValue
271
+ }
272
+
273
+ const transferStateChange = {
274
+ transferId,
275
+ transferStateId,
276
+ reason: undefined
277
+ }
278
+ return { participantPositionChange, transferStateChange, transferStateId, updatedRunningPosition }
279
+ }
280
+
281
+ const _handleParticipantPositionChangeFx = (runningPosition, transferAmount, commitRequestId, accumulatedPositionReservedValue) => {
282
+ const transferStateId = Enum.Transfers.TransferState.COMMITTED
283
+ // Amounts in `transferParticipant` for the payee are stored as negative values
284
+ const updatedRunningPosition = new MLNumber(runningPosition.add(transferAmount).toFixed(Config.AMOUNT.SCALE))
285
+
286
+ const participantPositionChange = {
287
+ commitRequestId, // Need to delete this in bin processor while updating fxTransferStateChangeId
288
+ fxTransferStateChangeId: null, // Need to update this in bin processor while executing queries
289
+ value: updatedRunningPosition.toNumber(),
290
+ change: transferAmount,
291
+ reservedValue: accumulatedPositionReservedValue
292
+ }
293
+
294
+ const fxTransferStateChange = {
295
+ commitRequestId,
296
+ transferStateId,
297
+ reason: null
158
298
  }
299
+ return { participantPositionChange, fxTransferStateChange, transferStateId, updatedRunningPosition }
159
300
  }
160
301
 
161
302
  module.exports = {
@@ -0,0 +1,138 @@
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 Logger = require('@mojaloop/central-services-logger')
6
+
7
+ /**
8
+ * @function processPositionFxFulfilBin
9
+ *
10
+ * @async
11
+ * @description This is the domain function to process a bin of position-fx-fulfil messages of a single participant account.
12
+ *
13
+ * @param {array} binItems - an array of objects that contain a position fx reserve message and its span. {message, span}
14
+ * @param {object} options
15
+ * @param {object} accumulatedFxTransferStates - object with fx 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.
16
+ * @returns {object} - Returns an object containing accumulatedFxTransferStateChanges, accumulatedFxTransferStates, resultMessages, limitAlarms or throws an error if failed
17
+ */
18
+ const processPositionFxFulfilBin = async (
19
+ binItems,
20
+ {
21
+ accumulatedFxTransferStates
22
+ }
23
+ ) => {
24
+ const fxTransferStateChanges = []
25
+ const resultMessages = []
26
+ const accumulatedFxTransferStatesCopy = Object.assign({}, accumulatedFxTransferStates)
27
+
28
+ if (binItems && binItems.length > 0) {
29
+ for (const binItem of binItems) {
30
+ let transferStateId
31
+ let reason
32
+ let resultMessage
33
+ const commitRequestId = binItem.message.value.content.uriParams.id
34
+ const counterPartyFsp = binItem.message.value.from
35
+ const initiatingFsp = binItem.message.value.to
36
+ const fxTransfer = binItem.decodedPayload
37
+ Logger.isDebugEnabled && Logger.debug(`processPositionFxFulfilBin::fxTransfer:processingMessage: ${JSON.stringify(fxTransfer)}`)
38
+ Logger.isDebugEnabled && Logger.debug(`accumulatedFxTransferStates: ${JSON.stringify(accumulatedFxTransferStates)}`)
39
+ Logger.isDebugEnabled && Logger.debug(`accumulatedFxTransferStates[commitRequestId]: ${accumulatedFxTransferStates[commitRequestId]}`)
40
+ // Inform sender if transfer is not in RECEIVED_FULFIL_DEPENDENT state, skip making any transfer state changes
41
+ if (accumulatedFxTransferStates[commitRequestId] !== Enum.Transfers.TransferInternalState.RECEIVED_FULFIL_DEPENDENT) {
42
+ // forward same headers from the request, except the content-length header
43
+ // set destination to counterPartyFsp and source to switch
44
+ const headers = { ...binItem.message.value.content.headers }
45
+ headers[Enum.Http.Headers.FSPIOP.DESTINATION] = counterPartyFsp
46
+ headers[Enum.Http.Headers.FSPIOP.SOURCE] = Config.HUB_NAME
47
+ delete headers['content-length']
48
+
49
+ // TODO: Confirm if this setting transferStateId to ABORTED_REJECTED is correct. There is no such logic in the fulfil handler.
50
+ transferStateId = Enum.Transfers.TransferInternalState.ABORTED_REJECTED
51
+ reason = 'FxFulfil in incorrect state'
52
+
53
+ const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError(
54
+ `Invalid State: ${accumulatedFxTransferStates[commitRequestId]} - expected: ${Enum.Transfers.TransferInternalState.RECEIVED_FULFIL_DEPENDENT}`
55
+ ).toApiErrorObject(Config.ERROR_HANDLING)
56
+ const state = Utility.StreamingProtocol.createEventState(
57
+ Enum.Events.EventStatus.FAILURE.status,
58
+ fspiopError.errorInformation.errorCode,
59
+ fspiopError.errorInformation.errorDescription
60
+ )
61
+
62
+ const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
63
+ commitRequestId,
64
+ Enum.Kafka.Topics.NOTIFICATION,
65
+ Enum.Events.Event.Action.FX_FULFIL,
66
+ state
67
+ )
68
+
69
+ resultMessage = Utility.StreamingProtocol.createMessage(
70
+ commitRequestId,
71
+ counterPartyFsp,
72
+ Config.HUB_NAME,
73
+ metadata,
74
+ headers,
75
+ fspiopError,
76
+ { id: commitRequestId },
77
+ 'application/json'
78
+ )
79
+ } else {
80
+ // forward same headers from the prepare message, except the content-length header
81
+ const headers = { ...binItem.message.value.content.headers }
82
+ delete headers['content-length']
83
+
84
+ const state = Utility.StreamingProtocol.createEventState(
85
+ Enum.Events.EventStatus.SUCCESS.status,
86
+ null,
87
+ null
88
+ )
89
+ const metadata = Utility.StreamingProtocol.createMetadataWithCorrelatedEvent(
90
+ commitRequestId,
91
+ Enum.Kafka.Topics.TRANSFER,
92
+ Enum.Events.Event.Action.COMMIT,
93
+ state
94
+ )
95
+
96
+ resultMessage = Utility.StreamingProtocol.createMessage(
97
+ commitRequestId,
98
+ initiatingFsp,
99
+ counterPartyFsp,
100
+ metadata,
101
+ headers,
102
+ fxTransfer,
103
+ { id: commitRequestId },
104
+ 'application/json'
105
+ )
106
+
107
+ // No need to change the transfer state here for success case.
108
+
109
+ binItem.result = { success: true }
110
+ }
111
+
112
+ resultMessages.push({ binItem, message: resultMessage })
113
+
114
+ if (transferStateId) {
115
+ const fxTransferStateChange = {
116
+ commitRequestId,
117
+ transferStateId,
118
+ reason
119
+ }
120
+ fxTransferStateChanges.push(fxTransferStateChange)
121
+ Logger.isDebugEnabled && Logger.debug(`processPositionFxFulfilBin::fxTransferStateChange: ${JSON.stringify(fxTransferStateChange)}`)
122
+
123
+ accumulatedFxTransferStatesCopy[commitRequestId] = transferStateId
124
+ Logger.isDebugEnabled && Logger.debug(`processPositionFxFulfilBin::accumulatedTransferStatesCopy:finalizedFxTransferState ${JSON.stringify(transferStateId)}`)
125
+ }
126
+ }
127
+ }
128
+
129
+ return {
130
+ accumulatedFxTransferStates: accumulatedFxTransferStatesCopy, // finalized fx transfer state after fx-fulfil processing
131
+ accumulatedFxTransferStateChanges: fxTransferStateChanges, // fx transfer state changes to be persisted in order
132
+ notifyMessages: resultMessages // array of objects containing bin item and result message. {binItem, message}
133
+ }
134
+ }
135
+
136
+ module.exports = {
137
+ processPositionFxFulfilBin
138
+ }