@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,387 @@
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2017 Bill & Melinda Gates Foundation
5
+ The Mojaloop files are made available by the Bill & Melinda Gates Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
6
+ http://www.apache.org/licenses/LICENSE-2.0
7
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
8
+ Contributors
9
+ --------------
10
+ This is the official list of the Mojaloop project contributors for this file.
11
+ Names of the original copyright holders (individuals or organizations)
12
+ should be listed with a '*' in the first column. People who have
13
+ contributed from an organization can be listed under the organization
14
+ that actually holds the copyright for their contributions (see the
15
+ Gates Foundation organization for an example). Those individuals should have
16
+ their names indented and be marked with a '-'. Email address can be added
17
+ optionally within square brackets <email>.
18
+ * Gates Foundation
19
+ - Name Surname <name.surname@gatesfoundation.com>
20
+
21
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
22
+ --------------
23
+ **********/
24
+
25
+ /* eslint-disable space-before-function-paren */
26
+ const { Enum, Util } = require('@mojaloop/central-services-shared')
27
+ const cyril = require('../../domain/fx/cyril')
28
+ const TransferObjectTransform = require('../../domain/transfer/transform')
29
+ const fspiopErrorFactory = require('../../shared/fspiopErrorFactory')
30
+ const ErrorHandler = require('@mojaloop/central-services-error-handling')
31
+
32
+ const { Type, Action } = Enum.Events.Event
33
+ const { SOURCE, DESTINATION } = Enum.Http.Headers.FSPIOP
34
+ const { TransferState, TransferInternalState } = Enum.Transfers
35
+
36
+ const consumerCommit = true
37
+ const fromSwitch = true
38
+
39
+ class FxFulfilService {
40
+ // #state = null
41
+
42
+ constructor(deps) {
43
+ this.log = deps.log
44
+ this.Config = deps.Config
45
+ this.Comparators = deps.Comparators
46
+ this.Validator = deps.Validator
47
+ this.FxTransferModel = deps.FxTransferModel
48
+ this.Kafka = deps.Kafka
49
+ this.params = deps.params // todo: rename to kafkaParams
50
+ this.cyril = deps.cyril || cyril
51
+ this.transform = deps.transform || TransferObjectTransform
52
+ }
53
+
54
+ async getFxTransferDetails(commitRequestId, functionality) {
55
+ const fxTransfer = await this.FxTransferModel.fxTransfer.getAllDetailsByCommitRequestIdForProxiedFxTransfer(commitRequestId)
56
+
57
+ if (!fxTransfer) {
58
+ const fspiopError = fspiopErrorFactory.fxTransferNotFound()
59
+ const apiFSPIOPError = fspiopError.toApiErrorObject(this.Config.ERROR_HANDLING)
60
+ const eventDetail = {
61
+ functionality,
62
+ action: Action.FX_RESERVE
63
+ }
64
+ this.log.warn('fxTransfer not found', { commitRequestId, eventDetail, apiFSPIOPError })
65
+
66
+ await this.kafkaProceed({
67
+ consumerCommit,
68
+ fspiopError: apiFSPIOPError,
69
+ eventDetail,
70
+ fromSwitch
71
+ })
72
+ throw fspiopError
73
+ }
74
+
75
+ this.log.debug('fxTransfer is found', { fxTransfer })
76
+ return fxTransfer
77
+ }
78
+
79
+ async validateHeaders({ transfer, headers, payload }) {
80
+ let fspiopError = null
81
+
82
+ if (!transfer.counterPartyFspIsProxy && (headers[SOURCE]?.toLowerCase() !== transfer.counterPartyFspName.toLowerCase())) {
83
+ fspiopError = fspiopErrorFactory.fxHeaderSourceValidationError()
84
+ }
85
+ if (!transfer.initiatingFspIsProxy && (headers[DESTINATION]?.toLowerCase() !== transfer.initiatingFspName.toLowerCase())) {
86
+ fspiopError = fspiopErrorFactory.fxHeaderDestinationValidationError()
87
+ }
88
+
89
+ if (fspiopError) {
90
+ const apiFSPIOPError = fspiopError.toApiErrorObject(this.Config.ERROR_HANDLING)
91
+ const eventDetail = {
92
+ functionality: Type.POSITION,
93
+ action: Action.FX_ABORT_VALIDATION
94
+ }
95
+ this.log.warn('headers validation error', { eventDetail, apiFSPIOPError })
96
+
97
+ // Lets handle the abort validation and change the fxTransfer state to reflect this
98
+ await this.FxTransferModel.fxTransfer.saveFxFulfilResponse(transfer.commitRequestId, payload, eventDetail.action, apiFSPIOPError)
99
+
100
+ await this._handleAbortValidation(transfer, apiFSPIOPError, eventDetail)
101
+ throw fspiopError
102
+ }
103
+ }
104
+
105
+ async _handleAbortValidation(fxTransfer, apiFSPIOPError, eventDetail) {
106
+ const cyrilResult = await this.cyril.processFxAbortMessage(fxTransfer.commitRequestId)
107
+
108
+ this.params.message.value.content.context = {
109
+ ...this.params.message.value.content.context,
110
+ cyrilResult
111
+ }
112
+ if (cyrilResult.positionChanges.length > 0) {
113
+ const participantCurrencyId = cyrilResult.positionChanges[0].participantCurrencyId
114
+ await this.kafkaProceed({
115
+ consumerCommit,
116
+ fspiopError: apiFSPIOPError,
117
+ eventDetail,
118
+ fromSwitch,
119
+ toDestination: fxTransfer.externalInitiatingFspName || fxTransfer.initiatingFspName,
120
+ messageKey: participantCurrencyId.toString(),
121
+ topicNameOverride: this.Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.FX_ABORT
122
+ })
123
+ } else {
124
+ const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError('Invalid cyril result')
125
+ throw fspiopError
126
+ }
127
+ }
128
+
129
+ async getDuplicateCheckResult({ commitRequestId, payload, action }) {
130
+ const { duplicateCheck } = this.FxTransferModel
131
+ const isFxTransferError = action === Action.FX_ABORT
132
+
133
+ const getDuplicateFn = isFxTransferError
134
+ ? duplicateCheck.getFxTransferErrorDuplicateCheck
135
+ : duplicateCheck.getFxTransferFulfilmentDuplicateCheck
136
+ const saveHashFn = isFxTransferError
137
+ ? duplicateCheck.saveFxTransferErrorDuplicateCheck
138
+ : duplicateCheck.saveFxTransferFulfilmentDuplicateCheck
139
+
140
+ return this.Comparators.duplicateCheckComparator(
141
+ commitRequestId,
142
+ payload,
143
+ getDuplicateFn,
144
+ saveHashFn
145
+ )
146
+ }
147
+
148
+ async checkDuplication({ dupCheckResult, transfer, functionality, action, type }) {
149
+ const transferStateEnum = transfer?.transferStateEnumeration
150
+ this.log.info('fxTransfer checkDuplication...', { dupCheckResult, action, transferStateEnum })
151
+
152
+ if (!dupCheckResult.hasDuplicateId) {
153
+ this.log.debug('No duplication found')
154
+ return false
155
+ }
156
+
157
+ if (!dupCheckResult.hasDuplicateHash) {
158
+ // ERROR: We've seen fxTransfer of this ID before, but it's message hash doesn't match the previous message hash.
159
+ const fspiopError = fspiopErrorFactory.noFxDuplicateHash()
160
+ const apiFSPIOPError = fspiopError.toApiErrorObject(this.Config.ERROR_HANDLING)
161
+ const eventDetail = {
162
+ functionality,
163
+ action: action === Action.FX_ABORT ? Action.FX_ABORT_DUPLICATE : Action.FX_FULFIL_DUPLICATE
164
+ }
165
+ this.log.warn('callbackErrorModified - no hasDuplicateHash', { eventDetail, apiFSPIOPError })
166
+
167
+ await this.kafkaProceed({
168
+ consumerCommit,
169
+ fspiopError: apiFSPIOPError,
170
+ eventDetail,
171
+ fromSwitch
172
+ })
173
+ throw fspiopError
174
+ }
175
+
176
+ // This is a duplicate message for a fxTransfer that is already in a finalized state
177
+ // respond as if we received a GET /fxTransfers/{ID} from the client
178
+ if ([TransferState.COMMITTED, TransferState.ABORTED].includes(transferStateEnum)) {
179
+ this.params.message.value.content.payload = this.transform.toFulfil(transfer)
180
+ const eventDetail = {
181
+ functionality,
182
+ action: action === Action.FX_ABORT ? Action.FX_ABORT_DUPLICATE : Action.FX_FULFIL_DUPLICATE
183
+ }
184
+ this.log.info('eventDetail:', { eventDetail })
185
+ await this.kafkaProceed({ consumerCommit, eventDetail, fromSwitch })
186
+ return true
187
+ }
188
+
189
+ if ([TransferState.RECEIVED, TransferState.RESERVED].includes(transferStateEnum)) {
190
+ this.log.info('state: RECEIVED or RESERVED')
191
+ await this.kafkaProceed({ consumerCommit })
192
+ // this code doesn't publish any message to kafka, coz we don't provide eventDetail:
193
+ // https://github.com/mojaloop/central-services-shared/blob/main/src/util/kafka/index.js#L315
194
+ return true
195
+ }
196
+
197
+ // Error scenario - fxTransfer.transferStateEnumeration is in some invalid state
198
+ const fspiopError = fspiopErrorFactory.invalidFxTransferState({ transferStateEnum, action, type })
199
+ const apiFSPIOPError = fspiopError.toApiErrorObject(this.Config.ERROR_HANDLING)
200
+ const eventDetail = {
201
+ functionality,
202
+ action: Action.FX_RESERVE
203
+ }
204
+ this.log.warn('callbackErrorInvalidTransferStateEnum', { eventDetail, apiFSPIOPError })
205
+ await this.kafkaProceed({
206
+ consumerCommit,
207
+ fspiopError: apiFSPIOPError,
208
+ eventDetail,
209
+ fromSwitch
210
+ })
211
+
212
+ return true
213
+ }
214
+
215
+ async validateEventType(type, functionality) {
216
+ if (type !== Type.FULFIL) {
217
+ const fspiopError = fspiopErrorFactory.invalidEventType(type)
218
+ const apiFSPIOPError = fspiopError.toApiErrorObject(this.Config.ERROR_HANDLING)
219
+ const eventDetail = {
220
+ functionality,
221
+ action: Action.FX_RESERVE
222
+ }
223
+ this.log.warn('callbackErrorInvalidEventType', { type, eventDetail, apiFSPIOPError })
224
+
225
+ await this.kafkaProceed({
226
+ consumerCommit,
227
+ fspiopError: apiFSPIOPError,
228
+ eventDetail,
229
+ fromSwitch
230
+ })
231
+ throw fspiopError
232
+ }
233
+ this.log.debug('validateEventType is passed', { type, functionality })
234
+ }
235
+
236
+ async validateFulfilment(fxTransfer, payload) {
237
+ const isValid = this.validateFulfilCondition(payload.fulfilment, fxTransfer.ilpCondition)
238
+
239
+ if (!isValid) {
240
+ const fspiopError = fspiopErrorFactory.fxInvalidFulfilment()
241
+ const apiFSPIOPError = fspiopError.toApiErrorObject(this.Config.ERROR_HANDLING)
242
+ const eventDetail = {
243
+ functionality: Type.POSITION,
244
+ action: Action.FX_ABORT_VALIDATION
245
+ }
246
+ this.log.warn('callbackErrorInvalidFulfilment', { eventDetail, apiFSPIOPError, fxTransfer, payload })
247
+ await this.FxTransferModel.fxTransfer.saveFxFulfilResponse(fxTransfer.commitRequestId, payload, eventDetail.action, apiFSPIOPError)
248
+
249
+ await this._handleAbortValidation(fxTransfer, apiFSPIOPError, eventDetail)
250
+ throw fspiopError
251
+ }
252
+
253
+ this.log.info('fulfilmentCheck passed successfully', { isValid })
254
+ return isValid
255
+ }
256
+
257
+ async validateTransferState(transfer, functionality) {
258
+ if (transfer.transferState !== TransferInternalState.RESERVED &&
259
+ transfer.transferState !== TransferInternalState.RESERVED_FORWARDED) {
260
+ const fspiopError = fspiopErrorFactory.fxTransferNonReservedState()
261
+ const apiFSPIOPError = fspiopError.toApiErrorObject(this.Config.ERROR_HANDLING)
262
+ const eventDetail = {
263
+ functionality,
264
+ action: Action.FX_RESERVE
265
+ }
266
+ this.log.warn('callbackErrorNonReservedState', { eventDetail, apiFSPIOPError, transfer })
267
+
268
+ await this.kafkaProceed({
269
+ consumerCommit,
270
+ fspiopError: apiFSPIOPError,
271
+ eventDetail,
272
+ fromSwitch
273
+ })
274
+ throw fspiopError
275
+ }
276
+ this.log.debug('validateTransferState is passed')
277
+ return true
278
+ }
279
+
280
+ async validateExpirationDate(transfer, functionality) {
281
+ if (transfer.expirationDate <= new Date(Util.Time.getUTCString(new Date()))) {
282
+ const fspiopError = fspiopErrorFactory.fxTransferExpired()
283
+ const apiFSPIOPError = fspiopError.toApiErrorObject(this.Config.ERROR_HANDLING)
284
+ const eventDetail = {
285
+ functionality,
286
+ action: Action.FX_RESERVE
287
+ }
288
+ this.log.warn('callbackErrorTransferExpired', { eventDetail, apiFSPIOPError })
289
+
290
+ await this.kafkaProceed({
291
+ consumerCommit,
292
+ fspiopError: apiFSPIOPError,
293
+ eventDetail,
294
+ fromSwitch
295
+ })
296
+ throw fspiopError
297
+ }
298
+ }
299
+
300
+ async processFxAbort({ transfer, payload, action }) {
301
+ const fspiopError = fspiopErrorFactory.fromErrorInformation(payload.errorInformation)
302
+ const apiFSPIOPError = fspiopError.toApiErrorObject(this.Config.ERROR_HANDLING)
303
+ const eventDetail = {
304
+ functionality: Type.POSITION,
305
+ action // FX_ABORT
306
+ }
307
+ this.log.warn('FX_ABORT case', { eventDetail, apiFSPIOPError })
308
+
309
+ await this.FxTransferModel.fxTransfer.saveFxFulfilResponse(transfer.commitRequestId, payload, action, apiFSPIOPError)
310
+ const cyrilResult = await this.cyril.processFxAbortMessage(transfer.commitRequestId)
311
+
312
+ this.params.message.value.content.context = {
313
+ ...this.params.message.value.content.context,
314
+ cyrilResult
315
+ }
316
+ if (cyrilResult.positionChanges.length > 0) {
317
+ const participantCurrencyId = cyrilResult.positionChanges[0].participantCurrencyId
318
+ await this.kafkaProceed({
319
+ consumerCommit,
320
+ eventDetail,
321
+ messageKey: participantCurrencyId.toString(),
322
+ topicNameOverride: this.Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.FX_ABORT
323
+ })
324
+ } else {
325
+ const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError('Invalid cyril result')
326
+ throw fspiopError
327
+ }
328
+ return true
329
+ }
330
+
331
+ async processFxFulfil({ transfer, payload, action }) {
332
+ await this.FxTransferModel.fxTransfer.saveFxFulfilResponse(transfer.commitRequestId, payload, action)
333
+ await this.cyril.processFxFulfilMessage(transfer.commitRequestId)
334
+ const eventDetail = {
335
+ functionality: Type.POSITION,
336
+ action
337
+ }
338
+ this.log.info('handle fxFulfilResponse', { eventDetail })
339
+
340
+ await this.kafkaProceed({
341
+ consumerCommit,
342
+ eventDetail,
343
+ messageKey: transfer.counterPartyFspSourceParticipantCurrencyId.toString(),
344
+ topicNameOverride: this.Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.COMMIT
345
+ })
346
+ return true
347
+ }
348
+
349
+ async kafkaProceed(kafkaOpts) {
350
+ return this.Kafka.proceed(this.Config.KAFKA_CONFIG, this.params, {
351
+ ...kafkaOpts,
352
+ hubName: this.Config.HUB_NAME
353
+ })
354
+ }
355
+
356
+ validateFulfilCondition(fulfilment, condition) {
357
+ try {
358
+ const isValid = fulfilment && this.Validator.validateFulfilCondition(fulfilment, condition)
359
+ this.log.debug('validateFulfilCondition result:', { isValid, fulfilment, condition })
360
+ return isValid
361
+ } catch (err) {
362
+ this.log.warn(`validateFulfilCondition error: ${err?.message}`, { fulfilment, condition })
363
+ return false
364
+ }
365
+ }
366
+
367
+ static decodeKafkaMessage(message) {
368
+ if (!message?.value) {
369
+ throw TypeError('Invalid message format!')
370
+ }
371
+ const payload = Util.StreamingProtocol.decodePayload(message.value.content.payload)
372
+ const { headers } = message.value.content
373
+ const { type, action } = message.value.metadata.event
374
+ const commitRequestId = message.value.content.uriParams.id
375
+
376
+ return Object.freeze({
377
+ payload,
378
+ headers,
379
+ type,
380
+ action,
381
+ commitRequestId,
382
+ kafkaTopic: message.topic
383
+ })
384
+ }
385
+ }
386
+
387
+ module.exports = FxFulfilService
@@ -0,0 +1,106 @@
1
+ const fxTransferModel = require('../../models/fxTransfer')
2
+ const TransferService = require('../../domain/transfer')
3
+ const cyril = require('../../domain/fx/cyril')
4
+ const { logger } = require('../../shared/logger')
5
+
6
+ /** @import { ProxyObligation } from './prepare.js' */
7
+
8
+ // abstraction on transfer and fxTransfer
9
+ const createRemittanceEntity = (isFx) => {
10
+ return {
11
+ isFx,
12
+
13
+ async getDuplicate (id) {
14
+ return isFx
15
+ ? fxTransferModel.duplicateCheck.getFxTransferDuplicateCheck(id)
16
+ : TransferService.getTransferDuplicateCheck(id)
17
+ },
18
+ async saveDuplicateHash (id, hash) {
19
+ return isFx
20
+ ? fxTransferModel.duplicateCheck.saveFxTransferDuplicateCheck(id, hash)
21
+ : TransferService.saveTransferDuplicateCheck(id, hash)
22
+ },
23
+
24
+ /**
25
+ * Saves prepare transfer/fxTransfer details to DB.
26
+ *
27
+ * @param {Object} payload - Message payload.
28
+ * @param {string | null} reason - Validation failure reasons.
29
+ * @param {Boolean} isValid - isValid.
30
+ * @param {DeterminingTransferCheckResult} determiningTransferCheckResult - The determining transfer check result.
31
+ * @param {ProxyObligation} proxyObligation - The proxy obligation
32
+ * @returns {Promise<void>}
33
+ */
34
+ async savePreparedRequest (
35
+ payload,
36
+ reason,
37
+ isValid,
38
+ determiningTransferCheckResult,
39
+ proxyObligation
40
+ ) {
41
+ return isFx
42
+ ? fxTransferModel.fxTransfer.savePreparedRequest(
43
+ payload,
44
+ reason,
45
+ isValid,
46
+ determiningTransferCheckResult,
47
+ proxyObligation
48
+ )
49
+ : TransferService.prepare(
50
+ payload,
51
+ reason,
52
+ isValid,
53
+ determiningTransferCheckResult,
54
+ proxyObligation
55
+ )
56
+ },
57
+
58
+ async getByIdLight (id) {
59
+ return isFx
60
+ ? fxTransferModel.fxTransfer.getByIdLight(id)
61
+ : TransferService.getByIdLight(id)
62
+ },
63
+
64
+ /**
65
+ * @typedef {Object} DeterminingTransferCheckResult
66
+ *
67
+ * @property {boolean} determiningTransferExists - Indicates if the determining transfer exists.
68
+ * @property {Array<{participantName, currencyId}>} participantCurrencyValidationList - List of validations for participant currencies.
69
+ * @property {Object} [transferRecord] - Determining transfer for the FX transfer (optional).
70
+ * @property {Array} [watchListRecords] - Records from fxWatchList-table for the transfer (optional).
71
+ */
72
+ /**
73
+ * Checks if a determining transfer exists based on the payload and proxy obligation.
74
+ * The function determines which method to use based on whether it is an FX transfer.
75
+ *
76
+ * @param {Object} payload - The payload data required for the transfer check.
77
+ * @param {ProxyObligation} proxyObligation - The proxy obligation details.
78
+ * @returns {DeterminingTransferCheckResult} determiningTransferCheckResult
79
+ */
80
+ async checkIfDeterminingTransferExists (payload, proxyObligation) {
81
+ const result = isFx
82
+ ? await cyril.checkIfDeterminingTransferExistsForFxTransferMessage(payload, proxyObligation)
83
+ : await cyril.checkIfDeterminingTransferExistsForTransferMessage(payload, proxyObligation)
84
+
85
+ logger.debug('cyril determiningTransferCheckResult:', { result })
86
+ return result
87
+ },
88
+
89
+ async getPositionParticipant (payload, determiningTransferCheckResult, proxyObligation) {
90
+ const result = isFx
91
+ ? await cyril.getParticipantAndCurrencyForFxTransferMessage(payload, determiningTransferCheckResult)
92
+ : await cyril.getParticipantAndCurrencyForTransferMessage(payload, determiningTransferCheckResult, proxyObligation)
93
+
94
+ logger.debug('cyril getPositionParticipant result:', { result })
95
+ return result
96
+ },
97
+
98
+ async logTransferError (id, errorCode, errorDescription) {
99
+ return isFx
100
+ ? fxTransferModel.stateChange.logTransferError(id, errorCode, errorDescription)
101
+ : TransferService.logTransferError(id, errorCode, errorDescription)
102
+ }
103
+ }
104
+ }
105
+
106
+ module.exports = createRemittanceEntity
@@ -0,0 +1,53 @@
1
+ const { Util, Enum } = require('@mojaloop/central-services-shared')
2
+ const { PROM_METRICS } = require('../../shared/constants')
3
+
4
+ const { decodePayload } = Util.StreamingProtocol
5
+ const { Action, Type } = Enum.Events.Event
6
+
7
+ const prepareInputDto = (error, messages) => {
8
+ if (error || !messages) {
9
+ return {
10
+ error,
11
+ metric: PROM_METRICS.transferPrepare()
12
+ }
13
+ }
14
+
15
+ const message = Array.isArray(messages) ? messages[0] : messages
16
+ if (!message) throw new Error('No input kafka message')
17
+
18
+ const payload = decodePayload(message.value.content.payload)
19
+ const isFx = !payload.transferId
20
+
21
+ const { action } = message.value.metadata.event
22
+ const isForwarded = [Action.FORWARDED, Action.FX_FORWARDED].includes(action)
23
+ const isPrepare = [Action.PREPARE, Action.FX_PREPARE, Action.FORWARDED, Action.FX_FORWARDED].includes(action)
24
+
25
+ const actionLetter = isPrepare
26
+ ? Enum.Events.ActionLetter.prepare
27
+ : (action === Action.BULK_PREPARE
28
+ ? Enum.Events.ActionLetter.bulkPrepare
29
+ : Enum.Events.ActionLetter.unknown)
30
+
31
+ const functionality = isPrepare
32
+ ? Type.NOTIFICATION
33
+ : (action === Action.BULK_PREPARE
34
+ ? Type.BULK_PROCESSING
35
+ : Enum.Events.ActionLetter.unknown)
36
+
37
+ return {
38
+ message,
39
+ payload,
40
+ action,
41
+ functionality,
42
+ isFx,
43
+ isForwarded,
44
+ ID: payload.transferId || payload.commitRequestId || message.value.id,
45
+ headers: message.value.content.headers,
46
+ metric: PROM_METRICS.transferPrepare(isFx, isForwarded),
47
+ actionLetter // just for logging
48
+ }
49
+ }
50
+
51
+ module.exports = {
52
+ prepareInputDto
53
+ }