@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
@@ -40,214 +40,86 @@
40
40
 
41
41
  const Logger = require('@mojaloop/central-services-logger')
42
42
  const EventSdk = require('@mojaloop/event-sdk')
43
+ const ErrorHandler = require('@mojaloop/central-services-error-handling')
44
+ const Metrics = require('@mojaloop/central-services-metrics')
45
+ const { Enum, Util } = require('@mojaloop/central-services-shared')
46
+ const { Consumer, Producer } = require('@mojaloop/central-services-stream').Util
47
+
48
+ const { logger } = require('../../shared/logger')
49
+ const { ERROR_MESSAGES } = require('../../shared/constants')
50
+ const Config = require('../../lib/config')
43
51
  const TransferService = require('../../domain/transfer')
44
- const Util = require('@mojaloop/central-services-shared').Util
45
- const Kafka = require('@mojaloop/central-services-shared').Util.Kafka
46
- const Producer = require('@mojaloop/central-services-stream').Util.Producer
47
- const Consumer = require('@mojaloop/central-services-stream').Util.Consumer
52
+ const FxService = require('../../domain/fx')
53
+ // TODO: Can define domain functions instead of accessing model directly from handler
54
+ const FxTransferModel = require('../../models/fxTransfer')
55
+ const TransferObjectTransform = require('../../domain/transfer/transform')
56
+ const Participant = require('../../domain/participant')
48
57
  const Validator = require('./validator')
49
- const Enum = require('@mojaloop/central-services-shared').Enum
58
+ const FxFulfilService = require('./FxFulfilService')
59
+
60
+ // particular handlers
61
+ const { prepare } = require('./prepare')
62
+
63
+ const { Kafka, Comparators } = Util
50
64
  const TransferState = Enum.Transfers.TransferState
51
65
  const TransferEventType = Enum.Events.Event.Type
52
66
  const TransferEventAction = Enum.Events.Event.Action
53
- const TransferObjectTransform = require('../../domain/transfer/transform')
54
- const Metrics = require('@mojaloop/central-services-metrics')
55
- const Config = require('../../lib/config')
56
67
  const decodePayload = Util.StreamingProtocol.decodePayload
57
- const Comparators = require('@mojaloop/central-services-shared').Util.Comparators
58
- const ErrorHandler = require('@mojaloop/central-services-error-handling')
59
- const Participant = require('../../domain/participant')
60
68
 
61
69
  const consumerCommit = true
62
70
  const fromSwitch = true
63
71
 
64
- /**
65
- * @function TransferPrepareHandler
66
- *
67
- * @async
68
- * @description This is the consumer callback function that gets registered to a topic. This then gets a list of messages,
69
- * we will only ever use the first message in non batch processing. We then break down the message into its payload and
70
- * begin validating the payload. Once the payload is validated successfully it will be written to the database to
71
- * the relevant tables. If the validation fails it is still written to the database for auditing purposes but with an
72
- * INVALID status. For any duplicate requests we will send appropriate callback based on the transfer state and the hash validation
73
- *
74
- * Validator.validatePrepare called to validate the payload of the message
75
- * TransferService.getById called to get the details of the existing transfer
76
- * TransferObjectTransform.toTransfer called to transform the transfer object
77
- * TransferService.prepare called and creates new entries in transfer tables for successful prepare transfer
78
- * TransferService.logTransferError called to log the invalid request
79
- *
80
- * @param {error} error - error thrown if something fails within Kafka
81
- * @param {array} messages - a list of messages to consume for the relevant topic
82
- *
83
- * @returns {object} - Returns a boolean: true if successful, or throws and error if failed
84
- */
85
- const prepare = async (error, messages) => {
86
- const location = { module: 'PrepareHandler', method: '', path: '' }
87
- const histTimerEnd = Metrics.getHistogram(
88
- 'transfer_prepare',
89
- 'Consume a prepare transfer message from the kafka topic and process it accordingly',
90
- ['success', 'fspId']
91
- ).startTimer()
72
+ const fulfil = async (error, messages) => {
92
73
  if (error) {
93
- histTimerEnd({ success: false, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
94
74
  throw ErrorHandler.Factory.reformatFSPIOPError(error)
95
75
  }
96
- let message = {}
76
+ let message
97
77
  if (Array.isArray(messages)) {
98
78
  message = messages[0]
99
79
  } else {
100
80
  message = messages
101
81
  }
102
- const parentSpanService = 'cl_transfer_prepare'
103
82
  const contextFromMessage = EventSdk.Tracer.extractContextFromMessage(message.value)
104
- const span = EventSdk.Tracer.createChildSpanFromContext(parentSpanService, contextFromMessage)
83
+ const span = EventSdk.Tracer.createChildSpanFromContext('cl_transfer_fulfil', contextFromMessage)
105
84
  try {
106
- const payload = decodePayload(message.value.content.payload)
107
- const headers = message.value.content.headers
108
- const action = message.value.metadata.event.action
109
- const transferId = payload.transferId
110
- span.setTags({ transactionId: transferId })
111
85
  await span.audit(message, EventSdk.AuditEventAction.start)
112
- const kafkaTopic = message.topic
113
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, { method: 'prepare' }))
114
-
115
- const actionLetter = action === TransferEventAction.PREPARE
116
- ? Enum.Events.ActionLetter.prepare
117
- : (action === TransferEventAction.BULK_PREPARE
118
- ? Enum.Events.ActionLetter.bulkPrepare
119
- : Enum.Events.ActionLetter.unknown)
120
-
121
- let functionality = action === TransferEventAction.PREPARE
122
- ? TransferEventType.NOTIFICATION
123
- : (action === TransferEventAction.BULK_PREPARE
124
- ? TransferEventType.BULK_PROCESSING
125
- : Enum.Events.ActionLetter.unknown)
126
- const params = { message, kafkaTopic, decodedPayload: payload, span, consumer: Consumer, producer: Producer }
127
-
128
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, { path: 'dupCheck' }))
129
- const histTimerDuplicateCheckEnd = Metrics.getHistogram(
130
- 'handler_transfers',
131
- 'prepare_duplicateCheckComparator - Metrics for transfer handler',
132
- ['success', 'funcName']
133
- ).startTimer()
134
-
135
- const { hasDuplicateId, hasDuplicateHash } = await Comparators.duplicateCheckComparator(transferId, payload, TransferService.getTransferDuplicateCheck, TransferService.saveTransferDuplicateCheck)
136
- histTimerDuplicateCheckEnd({ success: true, funcName: 'prepare_duplicateCheckComparator' })
137
- if (hasDuplicateId && hasDuplicateHash) {
138
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, 'handleResend'))
139
- const transfer = await TransferService.getByIdLight(transferId)
140
- const transferStateEnum = transfer && transfer.transferStateEnumeration
141
- const eventDetail = { functionality, action: TransferEventAction.PREPARE_DUPLICATE }
142
- if ([TransferState.COMMITTED, TransferState.ABORTED].includes(transferStateEnum)) {
143
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, 'finalized'))
144
- if (action === TransferEventAction.PREPARE) {
145
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callback--${actionLetter}1`))
146
- message.value.content.payload = TransferObjectTransform.toFulfil(transfer)
147
- message.value.content.uriParams = { id: transferId }
148
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail, fromSwitch })
149
- histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
150
- return true
151
- } else if (action === TransferEventAction.BULK_PREPARE) {
152
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `validationError1--${actionLetter}2`))
153
- const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.MODIFIED_REQUEST, 'Individual transfer prepare duplicate')
154
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
155
- throw fspiopError
156
- }
157
- } else {
158
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, 'inProgress'))
159
- if (action === TransferEventAction.BULK_PREPARE) {
160
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `validationError2--${actionLetter}4`))
161
- const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.MODIFIED_REQUEST, 'Individual transfer prepare duplicate')
162
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
163
- throw fspiopError
164
- } else { // action === TransferEventAction.PREPARE
165
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `ignore--${actionLetter}3`))
166
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit })
167
- histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
168
- return true
169
- }
170
- }
171
- } else if (hasDuplicateId && !hasDuplicateHash) {
172
- Logger.isErrorEnabled && Logger.error(Util.breadcrumb(location, `callbackErrorModified1--${actionLetter}5`))
173
- const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.MODIFIED_REQUEST)
174
- const eventDetail = { functionality, action }
175
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
176
- throw fspiopError
177
- } else { // !hasDuplicateId
178
- const { validationPassed, reasons } = await Validator.validatePrepare(payload, headers)
179
- if (validationPassed) {
180
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, { path: 'validationPassed' }))
181
- try {
182
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, 'saveTransfer'))
183
- await TransferService.prepare(payload)
184
- } catch (err) {
185
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorInternal1--${actionLetter}6`))
186
- Logger.isErrorEnabled && Logger.error(`${Util.breadcrumb(location)}::${err.message}`)
187
- const fspiopError = ErrorHandler.Factory.reformatFSPIOPError(err, ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR)
188
- const eventDetail = { functionality, action: TransferEventAction.PREPARE }
189
- /**
190
- * TODO: BULK-Handle at BulkProcessingHandler (not in scope of #967)
191
- * HOWTO: Stop execution at the `TransferService.prepare`, stop mysql,
192
- * continue execution to catch block, start mysql
193
- */
194
- Logger.isErrorEnabled && Logger.error(`${Util.breadcrumb(location)}::${err.message}`)
195
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
196
- throw fspiopError
197
- }
198
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `positionTopic1--${actionLetter}7`))
199
- functionality = TransferEventType.POSITION
200
- const eventDetail = { functionality, action }
201
- // Key position prepare message with payer account id
202
- const payerAccount = await Participant.getAccountByNameAndCurrency(payload.payerFsp, payload.amount.currency, Enum.Accounts.LedgerAccountType.POSITION)
203
- // We route bulk-prepare and prepare messages differently based on the topic configured for it.
204
- // Note: The batch handler does not currently support bulk-prepare messages, only prepare messages are supported.
205
- // Therefore, it is necessary to check the action to determine the topic to route to.
206
- const topicNameOverride =
207
- action === TransferEventAction.BULK_PREPARE
208
- ? Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.BULK_PREPARE
209
- : Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.PREPARE
210
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail, messageKey: payerAccount.participantCurrencyId.toString(), topicNameOverride })
211
- histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
212
- return true
213
- } else {
214
- Logger.isErrorEnabled && Logger.error(Util.breadcrumb(location, { path: 'validationFailed' }))
215
- try {
216
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, 'saveInvalidRequest'))
217
- await TransferService.prepare(payload, reasons.toString(), false)
218
- } catch (err) {
219
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorInternal2--${actionLetter}8`))
220
- Logger.isErrorEnabled && Logger.error(`${Util.breadcrumb(location)}::${err.message}`)
221
- const fspiopError = ErrorHandler.Factory.reformatFSPIOPError(err, ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR)
222
- const eventDetail = { functionality, action: TransferEventAction.PREPARE }
223
- /**
224
- * TODO: BULK-Handle at BulkProcessingHandler (not in scope of #967)
225
- * HOWTO: For regular transfers this branch may be triggered by sending
226
- * a transfer in a currency not supported by either dfsp and also stopping
227
- * mysql at `TransferService.prepare` and starting it after entring catch.
228
- * Not sure if it will work for bulk, because of the BulkPrepareHandler.
229
- */
230
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
231
- throw fspiopError
232
- }
233
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorGeneric--${actionLetter}9`))
234
- const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR, reasons.toString())
235
- await TransferService.logTransferError(transferId, ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR.code, reasons.toString())
236
- const eventDetail = { functionality, action }
237
- /**
238
- * TODO: BULK-Handle at BulkProcessingHandler (not in scope of #967)
239
- * HOWTO: For regular transfers this branch may be triggered by sending
240
- * a tansfer in a currency not supported by either dfsp. Not sure if it
241
- * will be triggered for bulk, because of the BulkPrepareHandler.
242
- */
243
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
244
- throw fspiopError
86
+ const action = message.value.metadata.event.action
87
+
88
+ const functionality = (() => {
89
+ switch (action) {
90
+ case TransferEventAction.COMMIT:
91
+ case TransferEventAction.FX_COMMIT:
92
+ case TransferEventAction.RESERVE:
93
+ case TransferEventAction.FX_RESERVE:
94
+ case TransferEventAction.REJECT:
95
+ case TransferEventAction.FX_REJECT:
96
+ case TransferEventAction.ABORT:
97
+ case TransferEventAction.FX_ABORT:
98
+ return TransferEventType.NOTIFICATION
99
+ case TransferEventAction.BULK_COMMIT:
100
+ case TransferEventAction.BULK_ABORT:
101
+ return TransferEventType.BULK_PROCESSING
102
+ default: return Enum.Events.ActionLetter.unknown
245
103
  }
104
+ })()
105
+ logger.info('FulfilHandler start:', { action, functionality })
106
+
107
+ const fxActions = [
108
+ TransferEventAction.FX_COMMIT,
109
+ TransferEventAction.FX_RESERVE,
110
+ TransferEventAction.FX_REJECT,
111
+ TransferEventAction.FX_ABORT,
112
+ TransferEventAction.FX_FORWARDED
113
+ ]
114
+
115
+ if (fxActions.includes(action)) {
116
+ return await processFxFulfilMessage(message, functionality, span)
117
+ } else {
118
+ return await processFulfilMessage(message, functionality, span)
246
119
  }
247
120
  } catch (err) {
248
- histTimerEnd({ success: false, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
121
+ logger.error(`error in FulfilHandler: ${err?.message}`, { err })
249
122
  const fspiopError = ErrorHandler.Factory.reformatFSPIOPError(err)
250
- Logger.isErrorEnabled && Logger.error(`${Util.breadcrumb(location)}::${err.message}--P0`)
251
123
  const state = new EventSdk.EventStateMetadata(EventSdk.EventStatusType.failed, fspiopError.apiErrorCode.code, fspiopError.apiErrorCode.message)
252
124
  await span.error(fspiopError, state)
253
125
  await span.finish(fspiopError.message, state)
@@ -259,107 +131,82 @@ const prepare = async (error, messages) => {
259
131
  }
260
132
  }
261
133
 
262
- const fulfil = async (error, messages) => {
134
+ const processFulfilMessage = async (message, functionality, span) => {
263
135
  const location = { module: 'FulfilHandler', method: '', path: '' }
264
136
  const histTimerEnd = Metrics.getHistogram(
265
137
  'transfer_fulfil',
266
138
  'Consume a fulfil transfer message from the kafka topic and process it accordingly',
267
139
  ['success', 'fspId']
268
140
  ).startTimer()
269
- if (error) {
270
- throw ErrorHandler.Factory.reformatFSPIOPError(error)
271
- }
272
- let message = {}
273
- if (Array.isArray(messages)) {
274
- message = messages[0]
275
- } else {
276
- message = messages
277
- }
278
- const contextFromMessage = EventSdk.Tracer.extractContextFromMessage(message.value)
279
- const span = EventSdk.Tracer.createChildSpanFromContext('cl_transfer_fulfil', contextFromMessage)
280
- try {
281
- await span.audit(message, EventSdk.AuditEventAction.start)
282
- const payload = decodePayload(message.value.content.payload)
283
- const headers = message.value.content.headers
284
- const type = message.value.metadata.event.type
285
- const action = message.value.metadata.event.action
286
- const transferId = message.value.content.uriParams.id
287
- const kafkaTopic = message.topic
288
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, { method: `fulfil:${action}` }))
289
141
 
290
- const actionLetter = (() => {
291
- switch (action) {
292
- case TransferEventAction.COMMIT: return Enum.Events.ActionLetter.commit
293
- case TransferEventAction.RESERVE: return Enum.Events.ActionLetter.reserve
294
- case TransferEventAction.REJECT: return Enum.Events.ActionLetter.reject
295
- case TransferEventAction.ABORT: return Enum.Events.ActionLetter.abort
296
- case TransferEventAction.BULK_COMMIT: return Enum.Events.ActionLetter.bulkCommit
297
- case TransferEventAction.BULK_ABORT: return Enum.Events.ActionLetter.bulkAbort
298
- default: return Enum.Events.ActionLetter.unknown
299
- }
300
- })()
142
+ const payload = decodePayload(message.value.content.payload)
143
+ const headers = message.value.content.headers
144
+ const type = message.value.metadata.event.type
145
+ const action = message.value.metadata.event.action
146
+ const transferId = message.value.content.uriParams.id
147
+ const kafkaTopic = message.topic
148
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, { method: `fulfil:${action}` }))
301
149
 
302
- const functionality = (() => {
303
- switch (action) {
304
- case TransferEventAction.COMMIT:
305
- case TransferEventAction.RESERVE:
306
- case TransferEventAction.REJECT:
307
- case TransferEventAction.ABORT:
308
- return TransferEventType.NOTIFICATION
309
- case TransferEventAction.BULK_COMMIT:
310
- case TransferEventAction.BULK_ABORT:
311
- return TransferEventType.BULK_PROCESSING
312
- default: return Enum.Events.ActionLetter.unknown
313
- }
314
- })()
315
-
316
- // fulfil-specific declarations
317
- const isTransferError = action === TransferEventAction.ABORT
318
- const params = { message, kafkaTopic, decodedPayload: payload, span, consumer: Consumer, producer: Producer }
319
-
320
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, { path: 'getById' }))
321
-
322
- // We fail early and silently to allow timeout handler abort transfer
323
- // if 'RESERVED' transfer state is sent in with v1.0 content-type
324
- if (headers['content-type'].split('=')[1] === '1.0' && payload.transferState === TransferState.RESERVED) {
325
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `failSilentlyforReservedStateWith1.0ContentType--${actionLetter}0`))
326
- const errorMessage = 'action "RESERVE" is not allowed in fulfil handler for v1.0 clients.'
327
- Logger.isErrorEnabled && Logger.error(errorMessage)
328
- !!span && span.error(errorMessage)
329
- histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
330
- return true
150
+ const actionLetter = (() => {
151
+ switch (action) {
152
+ case TransferEventAction.COMMIT: return Enum.Events.ActionLetter.commit
153
+ case TransferEventAction.RESERVE: return Enum.Events.ActionLetter.reserve
154
+ case TransferEventAction.REJECT: return Enum.Events.ActionLetter.reject
155
+ case TransferEventAction.ABORT: return Enum.Events.ActionLetter.abort
156
+ case TransferEventAction.BULK_COMMIT: return Enum.Events.ActionLetter.bulkCommit
157
+ case TransferEventAction.BULK_ABORT: return Enum.Events.ActionLetter.bulkAbort
158
+ default: return Enum.Events.ActionLetter.unknown
331
159
  }
160
+ })()
161
+
162
+ // We fail early and silently to allow timeout handler abort transfer
163
+ // if 'RESERVED' transfer state is sent in with v1.0 content-type
164
+ if (headers['content-type'].split('=')[1] === '1.0' && payload.transferState === TransferState.RESERVED) {
165
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `failSilentlyforReservedStateWith1.0ContentType--${actionLetter}0`))
166
+ const errorMessage = 'action "RESERVE" is not allowed in fulfil handler for v1.0 clients.'
167
+ Logger.isErrorEnabled && Logger.error(errorMessage)
168
+ !!span && span.error(errorMessage)
169
+ return true
170
+ }
332
171
 
333
- const transfer = await TransferService.getById(transferId)
334
- const transferStateEnum = transfer && transfer.transferStateEnumeration
335
-
336
- // List of valid actions that Source & Destination headers should be checked
337
- const validActionsForRouteValidations = [
338
- TransferEventAction.COMMIT,
339
- TransferEventAction.RESERVE,
340
- TransferEventAction.REJECT,
341
- TransferEventAction.ABORT
342
- ]
343
-
344
- if (!transfer) {
345
- Logger.isErrorEnabled && Logger.error(Util.breadcrumb(location, `callbackInternalServerErrorNotFound--${actionLetter}1`))
346
- const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError('transfer not found')
347
- const eventDetail = { functionality, action: TransferEventAction.COMMIT }
348
- /**
349
- * TODO: BULK-Handle at BulkProcessingHandler (not in scope of #967)
350
- * HOWTO: The list of individual transfers being committed should contain
351
- * non-existing transferId
352
- */
353
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
354
- throw fspiopError
355
-
356
- // Lets validate FSPIOP Source & Destination Headers
357
- } else if (
358
- validActionsForRouteValidations.includes(action) && // Lets only check headers for specific actions that need checking (i.e. bulk should not since its already done elsewhere)
359
- (
360
- (headers[Enum.Http.Headers.FSPIOP.SOURCE] && (headers[Enum.Http.Headers.FSPIOP.SOURCE].toLowerCase() !== transfer.payeeFsp.toLowerCase())) ||
361
- (headers[Enum.Http.Headers.FSPIOP.DESTINATION] && (headers[Enum.Http.Headers.FSPIOP.DESTINATION].toLowerCase() !== transfer.payerFsp.toLowerCase()))
362
- )
172
+ // fulfil-specific declarations
173
+ const isTransferError = action === TransferEventAction.ABORT
174
+ const params = { message, kafkaTopic, decodedPayload: payload, span, consumer: Consumer, producer: Producer }
175
+
176
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, { path: 'getById' }))
177
+
178
+ const transfer = await TransferService.getById(transferId)
179
+ const transferStateEnum = transfer && transfer.transferStateEnumeration
180
+
181
+ // List of valid actions that Source & Destination headers should be checked
182
+ const validActionsForRouteValidations = [
183
+ TransferEventAction.COMMIT,
184
+ TransferEventAction.RESERVE,
185
+ TransferEventAction.REJECT,
186
+ TransferEventAction.ABORT
187
+ ]
188
+
189
+ if (!transfer) {
190
+ Logger.isErrorEnabled && Logger.error(Util.breadcrumb(location, `callbackInternalServerErrorNotFound--${actionLetter}1`))
191
+ const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError('transfer not found')
192
+ const eventDetail = { functionality, action: TransferEventAction.COMMIT }
193
+ /**
194
+ * TODO: BULK-Handle at BulkProcessingHandler (not in scope of #967)
195
+ * HOWTO: The list of individual transfers being committed should contain
196
+ * non-existing transferId
197
+ */
198
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch, hubName: Config.HUB_NAME })
199
+ throw fspiopError
200
+
201
+ // Lets validate FSPIOP Source & Destination Headers
202
+ // In interscheme scenario, we store proxy fsp id in transferParticipant table and hence we can't compare that data with fspiop headers in fulfil
203
+ } else if (
204
+ validActionsForRouteValidations.includes(action) // Lets only check headers for specific actions that need checking (i.e. bulk should not since its already done elsewhere)
205
+ ) {
206
+ // Check if the payerFsp and payeeFsp are proxies and if they are, skip validating headers
207
+ if (
208
+ (headers[Enum.Http.Headers.FSPIOP.SOURCE] && !transfer.payeeIsProxy && (headers[Enum.Http.Headers.FSPIOP.SOURCE].toLowerCase() !== transfer.payeeFsp.toLowerCase())) ||
209
+ (headers[Enum.Http.Headers.FSPIOP.DESTINATION] && !transfer.payerIsProxy && (headers[Enum.Http.Headers.FSPIOP.DESTINATION].toLowerCase() !== transfer.payerFsp.toLowerCase()))
363
210
  ) {
364
211
  /**
365
212
  * If fulfilment request is coming from a source not matching transfer payee fsp or destination not matching transfer payer fsp,
@@ -370,19 +217,22 @@ const fulfil = async (error, messages) => {
370
217
  let fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR, 'FSP does not match one of the fsp-id\'s associated with a transfer on the Fulfil callback response')
371
218
 
372
219
  // Lets make the error specific if the PayeeFSP IDs do not match
373
- if (headers[Enum.Http.Headers.FSPIOP.SOURCE].toLowerCase() !== transfer.payeeFsp.toLowerCase()) {
220
+ if (!transfer.payeeIsProxy && (headers[Enum.Http.Headers.FSPIOP.SOURCE].toLowerCase() !== transfer.payeeFsp.toLowerCase())) {
374
221
  fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR, `${Enum.Http.Headers.FSPIOP.SOURCE} does not match payee fsp on the Fulfil callback response`)
375
222
  }
376
223
 
377
224
  // Lets make the error specific if the PayerFSP IDs do not match
378
- if (headers[Enum.Http.Headers.FSPIOP.DESTINATION].toLowerCase() !== transfer.payerFsp.toLowerCase()) {
225
+ if (!transfer.payerIsProxy && (headers[Enum.Http.Headers.FSPIOP.DESTINATION].toLowerCase() !== transfer.payerFsp.toLowerCase())) {
379
226
  fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR, `${Enum.Http.Headers.FSPIOP.DESTINATION} does not match payer fsp on the Fulfil callback response`)
380
227
  }
381
228
 
382
229
  const apiFSPIOPError = fspiopError.toApiErrorObject(Config.ERROR_HANDLING)
383
230
 
384
231
  // Set the event details to map to an ABORT_VALIDATION event targeted to the Position Handler
385
- const eventDetail = { functionality: TransferEventType.POSITION, action: TransferEventAction.ABORT_VALIDATION }
232
+ const eventDetail = {
233
+ functionality: TransferEventType.POSITION,
234
+ action: TransferEventAction.ABORT_VALIDATION
235
+ }
386
236
 
387
237
  // Lets handle the abort validation and change the transfer state to reflect this
388
238
  const transferAbortResult = await TransferService.handlePayeeResponse(transferId, payload, TransferEventAction.ABORT_VALIDATION, apiFSPIOPError)
@@ -397,7 +247,7 @@ const fulfil = async (error, messages) => {
397
247
  // Publish message to Position Handler
398
248
  // Key position abort with payer account id
399
249
  const payerAccount = await Participant.getAccountByNameAndCurrency(transfer.payerFsp, transfer.currency, Enum.Accounts.LedgerAccountType.POSITION)
400
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: apiFSPIOPError, eventDetail, fromSwitch, toDestination: transfer.payerFsp, messageKey: payerAccount.participantCurrencyId.toString() })
250
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: apiFSPIOPError, eventDetail, fromSwitch, toDestination: transfer.payerFsp, messageKey: payerAccount.participantCurrencyId.toString(), hubName: Config.HUB_NAME })
401
251
 
402
252
  /**
403
253
  * Send patch notification callback to original payee fsp if they asked for a a patch response.
@@ -427,319 +277,486 @@ const fulfil = async (error, messages) => {
427
277
  }
428
278
  }
429
279
  message.value.content.payload = reservedAbortedPayload
430
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail: reserveAbortedEventDetail, fromSwitch: true, toDestination: transfer.payeeFsp })
280
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail: reserveAbortedEventDetail, fromSwitch: true, toDestination: transfer.payeeFsp, hubName: Config.HUB_NAME })
431
281
  }
432
282
 
433
283
  throw apiFSPIOPError
434
284
  }
435
- // If execution continues after this point we are sure transfer exists and source matches payee fsp
436
-
437
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, { path: 'dupCheck' }))
438
- const histTimerDuplicateCheckEnd = Metrics.getHistogram(
439
- 'handler_transfers',
440
- 'fulfil_duplicateCheckComparator - Metrics for transfer handler',
441
- ['success', 'funcName']
442
- ).startTimer()
443
-
444
- let dupCheckResult
445
- if (!isTransferError) {
446
- dupCheckResult = await Comparators.duplicateCheckComparator(transferId, payload, TransferService.getTransferFulfilmentDuplicateCheck, TransferService.saveTransferFulfilmentDuplicateCheck)
447
- } else {
448
- dupCheckResult = await Comparators.duplicateCheckComparator(transferId, payload, TransferService.getTransferErrorDuplicateCheck, TransferService.saveTransferErrorDuplicateCheck)
449
- }
450
- const { hasDuplicateId, hasDuplicateHash } = dupCheckResult
451
- histTimerDuplicateCheckEnd({ success: true, funcName: 'fulfil_duplicateCheckComparator' })
452
- if (hasDuplicateId && hasDuplicateHash) {
453
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, 'handleResend'))
454
-
455
- // This is a duplicate message for a transfer that is already in a finalized state
456
- // respond as if we received a GET /transfers/{ID} from the client
457
- if (transferStateEnum === TransferState.COMMITTED || transferStateEnum === TransferState.ABORTED) {
458
- message.value.content.payload = TransferObjectTransform.toFulfil(transfer)
459
- const eventDetail = { functionality, action }
460
- if (action !== TransferEventAction.RESERVE) {
461
- if (!isTransferError) {
462
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackFinalized2--${actionLetter}3`))
463
- eventDetail.action = TransferEventAction.FULFIL_DUPLICATE
464
- /**
465
- * HOWTO: During bulk fulfil use an individualTransfer from a previous bulk fulfil
466
- */
467
- } else {
468
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackFinalized3--${actionLetter}4`))
469
- eventDetail.action = TransferEventAction.ABORT_DUPLICATE
470
- }
471
- }
472
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail, fromSwitch })
473
- histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
474
- return true
475
- }
285
+ }
286
+ // If execution continues after this point we are sure transfer exists and source matches payee fsp
476
287
 
477
- if (transferStateEnum === TransferState.RECEIVED || transferStateEnum === TransferState.RESERVED) {
478
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `inProgress2--${actionLetter}5`))
479
- /**
480
- * HOWTO: Nearly impossible to trigger for bulk - an individual transfer from a bulk needs to be triggered
481
- * for processing in order to have the fulfil duplicate hash recorded. While it is still in RESERVED state
482
- * the individual transfer needs to be requested by another bulk fulfil request!
483
- *
484
- * TODO: find a way to trigger this code branch and handle it at BulkProcessingHandler (not in scope of #967)
485
- */
486
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, histTimerEnd })
487
- histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
488
- return true
489
- }
288
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, { path: 'dupCheck' }))
289
+ const histTimerDuplicateCheckEnd = Metrics.getHistogram(
290
+ 'handler_transfers',
291
+ 'fulfil_duplicateCheckComparator - Metrics for transfer handler',
292
+ ['success', 'funcName']
293
+ ).startTimer()
490
294
 
491
- // Error scenario - transfer.transferStateEnumeration is in some invalid state
492
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorInvalidTransferStateEnum--${actionLetter}6`))
493
- const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError(
494
- `Invalid transferStateEnumeration:(${transferStateEnum}) for event action:(${action}) and type:(${type})`).toApiErrorObject(Config.ERROR_HANDLING)
495
- const eventDetail = { functionality, action: TransferEventAction.COMMIT }
496
- /**
497
- * HOWTO: Impossible to trigger for individual transfer in a bulk? (not in scope of #967)
498
- */
499
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError, eventDetail, fromSwitch })
295
+ let dupCheckResult
296
+ if (!isTransferError) {
297
+ dupCheckResult = await Comparators.duplicateCheckComparator(transferId, payload, TransferService.getTransferFulfilmentDuplicateCheck, TransferService.saveTransferFulfilmentDuplicateCheck)
298
+ } else {
299
+ dupCheckResult = await Comparators.duplicateCheckComparator(transferId, payload, TransferService.getTransferErrorDuplicateCheck, TransferService.saveTransferErrorDuplicateCheck)
300
+ }
301
+ const { hasDuplicateId, hasDuplicateHash } = dupCheckResult
302
+ histTimerDuplicateCheckEnd({ success: true, funcName: 'fulfil_duplicateCheckComparator' })
303
+ if (hasDuplicateId && hasDuplicateHash) {
304
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, 'handleResend'))
305
+
306
+ // This is a duplicate message for a transfer that is already in a finalized state
307
+ // respond as if we received a GET /transfers/{ID} from the client
308
+ if (transferStateEnum === TransferState.COMMITTED || transferStateEnum === TransferState.ABORTED) {
309
+ message.value.content.payload = TransferObjectTransform.toFulfil(transfer)
310
+ const eventDetail = { functionality, action }
311
+ if (action !== TransferEventAction.RESERVE) {
312
+ if (!isTransferError) {
313
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackFinalized2--${actionLetter}3`))
314
+ eventDetail.action = TransferEventAction.FULFIL_DUPLICATE
315
+ /**
316
+ * HOWTO: During bulk fulfil use an individualTransfer from a previous bulk fulfil
317
+ */
318
+ } else {
319
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackFinalized3--${actionLetter}4`))
320
+ eventDetail.action = TransferEventAction.ABORT_DUPLICATE
321
+ }
322
+ }
323
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail, fromSwitch, hubName: Config.HUB_NAME })
500
324
  histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
501
325
  return true
502
326
  }
503
327
 
504
- // ERROR: We have seen a transfer of this ID before, but it's message hash doesn't match
505
- // the previous message hash.
506
- if (hasDuplicateId && !hasDuplicateHash) {
507
- const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.MODIFIED_REQUEST)
508
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorModified2--${actionLetter}7`))
509
- let action = TransferEventAction.FULFIL_DUPLICATE
510
- if (isTransferError) {
511
- action = TransferEventAction.ABORT_DUPLICATE
512
- }
513
-
328
+ if (transferStateEnum === TransferState.RECEIVED || transferStateEnum === TransferState.RESERVED) {
329
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `inProgress2--${actionLetter}5`))
514
330
  /**
515
- * HOWTO: During bulk fulfil use an individualTransfer from a previous bulk fulfil,
516
- * but use different fulfilment value.
331
+ * HOWTO: Nearly impossible to trigger for bulk - an individual transfer from a bulk needs to be triggered
332
+ * for processing in order to have the fulfil duplicate hash recorded. While it is still in RESERVED state
333
+ * the individual transfer needs to be requested by another bulk fulfil request!
334
+ *
335
+ * TODO: find a way to trigger this code branch and handle it at BulkProcessingHandler (not in scope of #967)
517
336
  */
518
- const eventDetail = { functionality, action }
519
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
520
- throw fspiopError
337
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, histTimerEnd, hubName: Config.HUB_NAME })
338
+ histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
339
+ return true
521
340
  }
522
341
 
523
- // Transfer is not a duplicate, or message hasn't been changed.
342
+ // Error scenario - transfer.transferStateEnumeration is in some invalid state
343
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorInvalidTransferStateEnum--${actionLetter}6`))
344
+ const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError(
345
+ `Invalid transferStateEnumeration:(${transferStateEnum}) for event action:(${action}) and type:(${type})`).toApiErrorObject(Config.ERROR_HANDLING)
346
+ const eventDetail = { functionality, action: TransferEventAction.COMMIT }
347
+ /**
348
+ * HOWTO: Impossible to trigger for individual transfer in a bulk? (not in scope of #967)
349
+ */
350
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError, eventDetail, fromSwitch, hubName: Config.HUB_NAME })
351
+ histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
352
+ return true
353
+ }
524
354
 
525
- if (type !== TransferEventType.FULFIL) {
526
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorInvalidEventType--${actionLetter}15`))
527
- const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError(`Invalid event type:(${type})`)
528
- const eventDetail = { functionality, action: TransferEventAction.COMMIT }
529
- /**
530
- * TODO: BulkProcessingHandler (not in scope of #967)
531
- */
532
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
533
- throw fspiopError
355
+ // ERROR: We have seen a transfer of this ID before, but it's message hash doesn't match
356
+ // the previous message hash.
357
+ if (hasDuplicateId && !hasDuplicateHash) {
358
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.MODIFIED_REQUEST)
359
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorModified2--${actionLetter}7`))
360
+ let action = TransferEventAction.FULFIL_DUPLICATE
361
+ if (isTransferError) {
362
+ action = TransferEventAction.ABORT_DUPLICATE
534
363
  }
535
364
 
536
- const validActions = [
537
- TransferEventAction.COMMIT,
538
- TransferEventAction.RESERVE,
539
- TransferEventAction.REJECT,
540
- TransferEventAction.ABORT,
541
- TransferEventAction.BULK_COMMIT,
542
- TransferEventAction.BULK_ABORT
543
- ]
544
- if (!validActions.includes(action)) {
545
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorInvalidEventAction--${actionLetter}15`))
546
- const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError(`Invalid event action:(${action}) and/or type:(${type})`)
547
- const eventDetail = { functionality, action: TransferEventAction.COMMIT }
548
- /**
549
- * TODO: BulkProcessingHandler (not in scope of #967)
550
- */
551
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
552
- throw fspiopError
553
- }
365
+ /**
366
+ * HOWTO: During bulk fulfil use an individualTransfer from a previous bulk fulfil,
367
+ * but use different fulfilment value.
368
+ */
369
+ const eventDetail = { functionality, action }
370
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch, hubName: Config.HUB_NAME })
371
+ throw fspiopError
372
+ }
554
373
 
555
- Util.breadcrumb(location, { path: 'validationCheck' })
556
- if (payload.fulfilment && !Validator.validateFulfilCondition(payload.fulfilment, transfer.condition)) {
557
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorInvalidFulfilment--${actionLetter}9`))
558
- const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR, 'invalid fulfilment')
559
- const apiFSPIOPError = fspiopError.toApiErrorObject(Config.ERROR_HANDLING)
560
- await TransferService.handlePayeeResponse(transferId, payload, action, apiFSPIOPError)
561
- const eventDetail = { functionality: TransferEventType.POSITION, action: TransferEventAction.ABORT_VALIDATION }
562
- /**
563
- * TODO: BulkProcessingHandler (not in scope of #967) The individual transfer is ABORTED by notification is never sent.
564
- */
565
- // Key position validation abort with payer account id
566
- const payerAccount = await Participant.getAccountByNameAndCurrency(transfer.payerFsp, transfer.currency, Enum.Accounts.LedgerAccountType.POSITION)
567
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: apiFSPIOPError, eventDetail, messageKey: payerAccount.participantCurrencyId.toString() })
374
+ // Transfer is not a duplicate, or message hasn't been changed.
375
+
376
+ if (type !== TransferEventType.FULFIL) {
377
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorInvalidEventType--${actionLetter}15`))
378
+ const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError(`Invalid event type:(${type})`)
379
+ const eventDetail = { functionality, action: TransferEventAction.COMMIT }
380
+ /**
381
+ * TODO: BulkProcessingHandler (not in scope of #967)
382
+ */
383
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch, hubName: Config.HUB_NAME })
384
+ throw fspiopError
385
+ }
568
386
 
569
- // emit an extra message - RESERVED_ABORTED if action === TransferEventAction.RESERVE
570
- if (action === TransferEventAction.RESERVE) {
571
- // Get the updated transfer now that completedTimestamp will be different
572
- // TODO: should we just modify TransferService.handlePayeeResponse to
573
- // return the completed timestamp? Or is it safer to go back to the DB here?
574
- const transferAbortResult = await TransferService.getById(transferId)
575
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackReservedAborted--${actionLetter}1`))
576
- const eventDetail = { functionality: TransferEventType.NOTIFICATION, action: TransferEventAction.RESERVED_ABORTED }
387
+ const validActions = [
388
+ TransferEventAction.COMMIT,
389
+ TransferEventAction.RESERVE,
390
+ TransferEventAction.REJECT,
391
+ TransferEventAction.ABORT,
392
+ TransferEventAction.BULK_COMMIT,
393
+ TransferEventAction.BULK_ABORT
394
+ ]
395
+ if (!validActions.includes(action)) {
396
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorInvalidEventAction--${actionLetter}15`))
397
+ const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError(`Invalid event action:(${action}) and/or type:(${type})`)
398
+ const eventDetail = { functionality, action: TransferEventAction.COMMIT }
399
+ /**
400
+ * TODO: BulkProcessingHandler (not in scope of #967)
401
+ */
402
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch, hubName: Config.HUB_NAME })
403
+ throw fspiopError
404
+ }
577
405
 
578
- // Extract error information
579
- const errorCode = apiFSPIOPError && apiFSPIOPError.errorInformation && apiFSPIOPError.errorInformation.errorCode
580
- const errorDescription = apiFSPIOPError && apiFSPIOPError.errorInformation && apiFSPIOPError.errorInformation.errorDescription
406
+ Util.breadcrumb(location, { path: 'validationCheck' })
407
+ if (payload.fulfilment && !Validator.validateFulfilCondition(payload.fulfilment, transfer.condition)) {
408
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorInvalidFulfilment--${actionLetter}9`))
409
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR, 'invalid fulfilment')
410
+ const apiFSPIOPError = fspiopError.toApiErrorObject(Config.ERROR_HANDLING)
411
+ await TransferService.handlePayeeResponse(transferId, payload, TransferEventAction.ABORT_VALIDATION, apiFSPIOPError)
412
+ const eventDetail = { functionality: TransferEventType.POSITION, action: TransferEventAction.ABORT_VALIDATION }
413
+ /**
414
+ * TODO: BulkProcessingHandler (not in scope of #967) The individual transfer is ABORTED by notification is never sent.
415
+ */
416
+ // Key position validation abort with payer account id
417
+
418
+ const cyrilResult = await FxService.Cyril.processAbortMessage(transferId)
419
+
420
+ params.message.value.content.context = {
421
+ ...params.message.value.content.context,
422
+ cyrilResult
423
+ }
424
+ if (cyrilResult.positionChanges.length > 0) {
425
+ const participantCurrencyId = cyrilResult.positionChanges[0].participantCurrencyId
426
+ await Kafka.proceed(
427
+ Config.KAFKA_CONFIG,
428
+ params,
429
+ {
430
+ consumerCommit,
431
+ fspiopError: apiFSPIOPError,
432
+ eventDetail,
433
+ messageKey: participantCurrencyId.toString(),
434
+ topicNameOverride: Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.ABORT,
435
+ hubName: Config.HUB_NAME
436
+ }
437
+ )
438
+ } else {
439
+ const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError('Invalid cyril result')
440
+ throw fspiopError
441
+ }
581
442
 
582
- // TODO: This should be handled by a PATCH /transfers/{id}/error callback in the future FSPIOP v1.2 specification, and instead we should just send the FSPIOP-Error instead! Ref: https://github.com/mojaloop/mojaloop-specification/issues/106.
583
- const reservedAbortedPayload = {
584
- transferId: transferAbortResult && transferAbortResult.id,
585
- completedTimestamp: transferAbortResult && transferAbortResult.completedTimestamp && (new Date(Date.parse(transferAbortResult.completedTimestamp))).toISOString(),
586
- transferState: TransferState.ABORTED,
587
- extensionList: { // lets add the extension list to handle the limitation of the FSPIOP v1.1 specification by adding the error cause...
588
- extension: [
589
- {
590
- key: 'cause',
591
- value: `${errorCode}: ${errorDescription}`
592
- }
593
- ]
594
- }
443
+ // const payerAccount = await Participant.getAccountByNameAndCurrency(transfer.payerFsp, transfer.currency, Enum.Accounts.LedgerAccountType.POSITION)
444
+ // await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: apiFSPIOPError, eventDetail, messageKey: payerAccount.participantCurrencyId.toString(), hubName: Config.HUB_NAME })
445
+
446
+ // emit an extra message - RESERVED_ABORTED if action === TransferEventAction.RESERVE
447
+ if (action === TransferEventAction.RESERVE) {
448
+ // Get the updated transfer now that completedTimestamp will be different
449
+ // TODO: should we just modify TransferService.handlePayeeResponse to
450
+ // return the completed timestamp? Or is it safer to go back to the DB here?
451
+ const transferAbortResult = await TransferService.getById(transferId)
452
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackReservedAborted--${actionLetter}1`))
453
+ const eventDetail = { functionality: TransferEventType.NOTIFICATION, action: TransferEventAction.RESERVED_ABORTED }
454
+
455
+ // Extract error information
456
+ const errorCode = apiFSPIOPError && apiFSPIOPError.errorInformation && apiFSPIOPError.errorInformation.errorCode
457
+ const errorDescription = apiFSPIOPError && apiFSPIOPError.errorInformation && apiFSPIOPError.errorInformation.errorDescription
458
+
459
+ // TODO: This should be handled by a PATCH /transfers/{id}/error callback in the future FSPIOP v1.2 specification, and instead we should just send the FSPIOP-Error instead! Ref: https://github.com/mojaloop/mojaloop-specification/issues/106.
460
+ const reservedAbortedPayload = {
461
+ transferId: transferAbortResult && transferAbortResult.id,
462
+ completedTimestamp: transferAbortResult && transferAbortResult.completedTimestamp && (new Date(Date.parse(transferAbortResult.completedTimestamp))).toISOString(),
463
+ transferState: TransferState.ABORTED,
464
+ extensionList: { // lets add the extension list to handle the limitation of the FSPIOP v1.1 specification by adding the error cause...
465
+ extension: [
466
+ {
467
+ key: 'cause',
468
+ value: `${errorCode}: ${errorDescription}`
469
+ }
470
+ ]
595
471
  }
596
- message.value.content.payload = reservedAbortedPayload
597
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail, fromSwitch: true, toDestination: transfer.payeeFsp })
598
472
  }
599
- throw fspiopError
473
+ message.value.content.payload = reservedAbortedPayload
474
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail, fromSwitch: true, toDestination: transfer.payeeFsp, hubName: Config.HUB_NAME })
600
475
  }
476
+ throw fspiopError
477
+ }
601
478
 
602
- if (transfer.transferState !== TransferState.RESERVED) {
603
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorNonReservedState--${actionLetter}10`))
604
- const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR, 'non-RESERVED transfer state')
605
- const eventDetail = { functionality, action: TransferEventAction.COMMIT }
606
- /**
607
- * TODO: BulkProcessingHandler (not in scope of #967)
608
- */
609
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
479
+ if (transfer.transferState !== Enum.Transfers.TransferInternalState.RESERVED &&
480
+ transfer.transferState !== Enum.Transfers.TransferInternalState.RESERVED_FORWARDED
481
+ ) {
482
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorNonReservedState--${actionLetter}10`))
483
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR, 'non-RESERVED transfer state')
484
+ const eventDetail = { functionality, action: TransferEventAction.COMMIT }
485
+ /**
486
+ * TODO: BulkProcessingHandler (not in scope of #967)
487
+ */
488
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch, hubName: Config.HUB_NAME })
489
+
490
+ // emit an extra message - RESERVED_ABORTED if action === TransferEventAction.RESERVE
491
+ if (action === TransferEventAction.RESERVE) {
492
+ // Get the updated transfer now that completedTimestamp will be different
493
+ // TODO: should we just modify TransferService.handlePayeeResponse to
494
+ // return the completed timestamp? Or is it safer to go back to the DB here?
495
+ const transferAborted = await TransferService.getById(transferId) // TODO: remove this once it can be tested
496
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackReservedAborted--${actionLetter}2`))
497
+ const eventDetail = { functionality: TransferEventType.NOTIFICATION, action: TransferEventAction.RESERVED_ABORTED }
498
+ const reservedAbortedPayload = {
499
+ transferId: transferAborted.id,
500
+ completedTimestamp: Util.Time.getUTCString(new Date(transferAborted.completedTimestamp)), // TODO: remove this once it can be tested
501
+ transferState: TransferState.ABORTED
502
+ }
503
+ message.value.content.payload = reservedAbortedPayload
504
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail, fromSwitch: true, toDestination: transfer.payeeFsp, hubName: Config.HUB_NAME })
505
+ }
506
+ throw fspiopError
507
+ }
610
508
 
611
- // emit an extra message - RESERVED_ABORTED if action === TransferEventAction.RESERVE
612
- if (action === TransferEventAction.RESERVE) {
613
- // Get the updated transfer now that completedTimestamp will be different
614
- // TODO: should we just modify TransferService.handlePayeeResponse to
615
- // return the completed timestamp? Or is it safer to go back to the DB here?
616
- const transferAborted = await TransferService.getById(transferId) // TODO: remove this once it can be tested
617
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackReservedAborted--${actionLetter}2`))
618
- const eventDetail = { functionality: TransferEventType.NOTIFICATION, action: TransferEventAction.RESERVED_ABORTED }
619
- const reservedAbortedPayload = {
620
- transferId: transferAborted.id,
621
- completedTimestamp: Util.Time.getUTCString(new Date(transferAborted.completedTimestamp)), // TODO: remove this once it can be tested
622
- transferState: TransferState.ABORTED
623
- }
624
- message.value.content.payload = reservedAbortedPayload
625
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail, fromSwitch: true, toDestination: transfer.payeeFsp })
509
+ if (transfer.expirationDate <= new Date(Util.Time.getUTCString(new Date()))) {
510
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorTransferExpired--${actionLetter}11`))
511
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.TRANSFER_EXPIRED)
512
+ const eventDetail = { functionality, action: TransferEventAction.COMMIT }
513
+ /**
514
+ * TODO: BulkProcessingHandler (not in scope of #967)
515
+ */
516
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch, hubName: Config.HUB_NAME })
517
+
518
+ // emit an extra message - RESERVED_ABORTED if action === TransferEventAction.RESERVE
519
+ if (action === TransferEventAction.RESERVE) {
520
+ // Get the updated transfer now that completedTimestamp will be different
521
+ // TODO: should we just modify TransferService.handlePayeeResponse to
522
+ // return the completed timestamp? Or is it safer to go back to the DB here?
523
+ const transferAborted = await TransferService.getById(transferId)
524
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackReservedAborted--${actionLetter}3`))
525
+ const eventDetail = { functionality: TransferEventType.NOTIFICATION, action: TransferEventAction.RESERVED_ABORTED }
526
+ const reservedAbortedPayload = {
527
+ transferId: transferAborted.id,
528
+ completedTimestamp: Util.Time.getUTCString(new Date(transferAborted.completedTimestamp)),
529
+ transferState: TransferState.ABORTED
626
530
  }
627
- throw fspiopError
531
+ message.value.content.payload = reservedAbortedPayload
532
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail, fromSwitch: true, hubName: Config.HUB_NAME })
628
533
  }
534
+ throw fspiopError
535
+ }
629
536
 
630
- if (transfer.expirationDate <= new Date(Util.Time.getUTCString(new Date()))) {
631
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorTransferExpired--${actionLetter}11`))
632
- const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.TRANSFER_EXPIRED)
633
- const eventDetail = { functionality, action: TransferEventAction.COMMIT }
634
- /**
635
- * TODO: BulkProcessingHandler (not in scope of #967)
636
- */
637
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
537
+ // Validations Succeeded - process the fulfil
538
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, { path: 'validationPassed' }))
539
+ switch (action) {
540
+ case TransferEventAction.COMMIT:
541
+ case TransferEventAction.RESERVE:
542
+ case TransferEventAction.BULK_COMMIT: {
543
+ let topicNameOverride
544
+ if (action === TransferEventAction.COMMIT) {
545
+ topicNameOverride = Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.COMMIT
546
+ } else if (action === TransferEventAction.RESERVE) {
547
+ topicNameOverride = Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.RESERVE
548
+ } else if (action === TransferEventAction.BULK_COMMIT) {
549
+ topicNameOverride = Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.BULK_COMMIT
550
+ }
638
551
 
639
- // emit an extra message - RESERVED_ABORTED if action === TransferEventAction.RESERVE
640
- if (action === TransferEventAction.RESERVE) {
641
- // Get the updated transfer now that completedTimestamp will be different
642
- // TODO: should we just modify TransferService.handlePayeeResponse to
643
- // return the completed timestamp? Or is it safer to go back to the DB here?
644
- const transferAborted = await TransferService.getById(transferId)
645
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackReservedAborted--${actionLetter}3`))
646
- const eventDetail = { functionality: TransferEventType.NOTIFICATION, action: TransferEventAction.RESERVED_ABORTED }
647
- const reservedAbortedPayload = {
648
- transferId: transferAborted.id,
649
- completedTimestamp: Util.Time.getUTCString(new Date(transferAborted.completedTimestamp)),
650
- transferState: TransferState.ABORTED
552
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `positionTopic2--${actionLetter}12`))
553
+ await TransferService.handlePayeeResponse(transferId, payload, action)
554
+ const eventDetail = { functionality: TransferEventType.POSITION, action }
555
+ // Key position fulfil message with payee account id
556
+ const cyrilResult = await FxService.Cyril.processFulfilMessage(transferId, payload, transfer)
557
+ if (cyrilResult.isFx) {
558
+ // const payeeAccount = await Participant.getAccountByNameAndCurrency(transfer.payeeFsp, transfer.currency, Enum.Accounts.LedgerAccountType.POSITION)
559
+ params.message.value.content.context = {
560
+ ...params.message.value.content.context,
561
+ cyrilResult
651
562
  }
652
- message.value.content.payload = reservedAbortedPayload
653
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail, fromSwitch: true })
563
+ if (cyrilResult.positionChanges.length > 0) {
564
+ const participantCurrencyId = cyrilResult.positionChanges[0].participantCurrencyId
565
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail, messageKey: participantCurrencyId.toString(), topicNameOverride, hubName: Config.HUB_NAME })
566
+ histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
567
+ } else {
568
+ histTimerEnd({ success: false, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
569
+ const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError('Invalid cyril result')
570
+ throw fspiopError
571
+ }
572
+ } else {
573
+ const payeeAccount = await Participant.getAccountByNameAndCurrency(transfer.payeeFsp, transfer.currency, Enum.Accounts.LedgerAccountType.POSITION)
574
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail, messageKey: payeeAccount.participantCurrencyId.toString(), topicNameOverride, hubName: Config.HUB_NAME })
575
+ histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
576
+ }
577
+ return true
578
+ }
579
+ // TODO: why do we let this logic get this far? Why not remove it from validActions array above?
580
+ case TransferEventAction.REJECT: {
581
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `positionTopic3--${actionLetter}13`))
582
+ const errorMessage = 'action REJECT is not allowed into fulfil handler'
583
+ Logger.isErrorEnabled && Logger.error(errorMessage)
584
+ !!span && span.error(errorMessage)
585
+ histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
586
+ return true
587
+ }
588
+ case TransferEventAction.BULK_ABORT: {
589
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `positionTopic4--${actionLetter}14`))
590
+ let fspiopError
591
+ const eInfo = payload.errorInformation
592
+ try { // handle only valid errorCodes provided by the payee
593
+ fspiopError = ErrorHandler.Factory.createFSPIOPErrorFromErrorInformation(eInfo)
594
+ } catch (err) {
595
+ /**
596
+ * TODO: Handling of out-of-range errorCodes is to be introduced to the ml-api-adapter,
597
+ * so that such requests are rejected right away, instead of aborting the transfer here.
598
+ */
599
+ Logger.isErrorEnabled && Logger.error(`${Util.breadcrumb(location)}::${err.message}`)
600
+ fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR, 'API specification undefined errorCode')
601
+ await TransferService.handlePayeeResponse(transferId, payload, action, fspiopError.toApiErrorObject(Config.ERROR_HANDLING))
602
+ const eventDetail = { functionality: TransferEventType.POSITION, action }
603
+ // Key position abort with payer account id
604
+ const payerAccount = await Participant.getAccountByNameAndCurrency(transfer.payerFsp, transfer.currency, Enum.Accounts.LedgerAccountType.POSITION)
605
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, messageKey: payerAccount.participantCurrencyId.toString(), hubName: Config.HUB_NAME })
606
+ throw fspiopError
654
607
  }
608
+ await TransferService.handlePayeeResponse(transferId, payload, action, fspiopError.toApiErrorObject(Config.ERROR_HANDLING))
609
+ const eventDetail = { functionality: TransferEventType.POSITION, action }
610
+ // Key position abort with payer account id
611
+ const payerAccount = await Participant.getAccountByNameAndCurrency(transfer.payerFsp, transfer.currency, Enum.Accounts.LedgerAccountType.POSITION)
612
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, messageKey: payerAccount.participantCurrencyId.toString(), hubName: Config.HUB_NAME })
613
+ // TODO(2556): I don't think we should emit an extra notification here
614
+ // this is the case where the Payee sent an ABORT, so we don't need to tell them to abort
655
615
  throw fspiopError
656
616
  }
657
-
658
- // Validations Succeeded - process the fulfil
659
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, { path: 'validationPassed' }))
660
- switch (action) {
661
- case TransferEventAction.COMMIT:
662
- case TransferEventAction.RESERVE:
663
- case TransferEventAction.BULK_COMMIT: {
664
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `positionTopic2--${actionLetter}12`))
665
- await TransferService.handlePayeeResponse(transferId, payload, action)
617
+ case TransferEventAction.ABORT: {
618
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `positionTopic4--${actionLetter}14`))
619
+ let fspiopError
620
+ const eInfo = payload.errorInformation
621
+ try { // handle only valid errorCodes provided by the payee
622
+ fspiopError = ErrorHandler.Factory.createFSPIOPErrorFromErrorInformation(eInfo)
623
+ } catch (err) {
624
+ /**
625
+ * TODO: Handling of out-of-range errorCodes is to be introduced to the ml-api-adapter,
626
+ * so that such requests are rejected right away, instead of aborting the transfer here.
627
+ */
628
+ Logger.isErrorEnabled && Logger.error(`${Util.breadcrumb(location)}::${err.message}`)
629
+ fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR, 'API specification undefined errorCode')
630
+ await TransferService.handlePayeeResponse(transferId, payload, action, fspiopError.toApiErrorObject(Config.ERROR_HANDLING))
666
631
  const eventDetail = { functionality: TransferEventType.POSITION, action }
667
- // Key position fulfil message with payee account id
668
- let topicNameOverride
669
- if (action === TransferEventAction.COMMIT) {
670
- topicNameOverride = Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.COMMIT
671
- } else if (action === TransferEventAction.RESERVE) {
672
- topicNameOverride = Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.RESERVE
673
- } else if (action === TransferEventAction.BULK_COMMIT) {
674
- topicNameOverride = Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.BULK_COMMIT
675
- }
676
- const payeeAccount = await Participant.getAccountByNameAndCurrency(transfer.payeeFsp, transfer.currency, Enum.Accounts.LedgerAccountType.POSITION)
632
+ // Key position abort with payer account id
633
+ const payerAccount = await Participant.getAccountByNameAndCurrency(transfer.payerFsp, transfer.currency, Enum.Accounts.LedgerAccountType.POSITION)
634
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, messageKey: payerAccount.participantCurrencyId.toString(), hubName: Config.HUB_NAME })
635
+ throw fspiopError
636
+ }
637
+ await TransferService.handlePayeeResponse(transferId, payload, action, fspiopError.toApiErrorObject(Config.ERROR_HANDLING))
638
+ const eventDetail = { functionality: TransferEventType.POSITION, action }
639
+ const cyrilResult = await FxService.Cyril.processAbortMessage(transferId)
640
+
641
+ params.message.value.content.context = {
642
+ ...params.message.value.content.context,
643
+ cyrilResult
644
+ }
645
+ if (cyrilResult.positionChanges.length > 0) {
646
+ const participantCurrencyId = cyrilResult.positionChanges[0].participantCurrencyId
677
647
  await Kafka.proceed(
678
648
  Config.KAFKA_CONFIG,
679
649
  params,
680
650
  {
681
651
  consumerCommit,
652
+ fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING),
682
653
  eventDetail,
683
- messageKey: payeeAccount.participantCurrencyId.toString(),
684
- topicNameOverride
654
+ messageKey: participantCurrencyId.toString(),
655
+ topicNameOverride: Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.ABORT,
656
+ hubName: Config.HUB_NAME
685
657
  }
686
658
  )
687
- histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
688
- return true
689
- }
690
- // TODO: why do we let this logic get this far? Why not remove it from validActions array above?
691
- case TransferEventAction.REJECT: {
692
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `positionTopic3--${actionLetter}13`))
693
- const errorMessage = 'action REJECT is not allowed into fulfil handler'
694
- Logger.isErrorEnabled && Logger.error(errorMessage)
695
- !!span && span.error(errorMessage)
696
- histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
697
- return true
698
- }
699
- // TODO: why do we let this logic get this far? Why not remove it from validActions array above?
700
- case TransferEventAction.ABORT:
701
- case TransferEventAction.BULK_ABORT:
702
- default: { // action === TransferEventAction.ABORT || action === TransferEventAction.BULK_ABORT // error-callback request to be processed
703
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `positionTopic4--${actionLetter}14`))
704
- let fspiopError
705
- const eInfo = payload.errorInformation
706
- try { // handle only valid errorCodes provided by the payee
707
- fspiopError = ErrorHandler.Factory.createFSPIOPErrorFromErrorInformation(eInfo)
708
- } catch (err) {
709
- /**
710
- * TODO: Handling of out-of-range errorCodes is to be introduced to the ml-api-adapter,
711
- * so that such requests are rejected right away, instead of aborting the transfer here.
712
- */
713
- Logger.isErrorEnabled && Logger.error(`${Util.breadcrumb(location)}::${err.message}`)
714
- fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR, 'API specification undefined errorCode')
715
- await TransferService.handlePayeeResponse(transferId, payload, action, fspiopError.toApiErrorObject(Config.ERROR_HANDLING))
716
- const eventDetail = { functionality: TransferEventType.POSITION, action }
717
- // Key position abort with payer account id
718
- const payerAccount = await Participant.getAccountByNameAndCurrency(transfer.payerFsp, transfer.currency, Enum.Accounts.LedgerAccountType.POSITION)
719
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, messageKey: payerAccount.participantCurrencyId.toString() })
720
- throw fspiopError
721
- }
722
- await TransferService.handlePayeeResponse(transferId, payload, action, fspiopError.toApiErrorObject(Config.ERROR_HANDLING))
723
- const eventDetail = { functionality: TransferEventType.POSITION, action }
724
- // Key position abort with payer account id
725
- const payerAccount = await Participant.getAccountByNameAndCurrency(transfer.payerFsp, transfer.currency, Enum.Accounts.LedgerAccountType.POSITION)
726
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, messageKey: payerAccount.participantCurrencyId.toString() })
727
- // TODO(2556): I don't think we should emit an extra notification here
728
- // this is the case where the Payee sent an ABORT, so we don't need to tell them to abort
659
+ } else {
660
+ const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError('Invalid cyril result')
729
661
  throw fspiopError
730
662
  }
731
663
  }
732
- } catch (err) {
733
- histTimerEnd({ success: false, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
734
- const fspiopError = ErrorHandler.Factory.reformatFSPIOPError(err)
735
- Logger.isErrorEnabled && Logger.error(`${Util.breadcrumb(location)}::${err.message}--F0`)
736
- const state = new EventSdk.EventStateMetadata(EventSdk.EventStatusType.failed, fspiopError.apiErrorCode.code, fspiopError.apiErrorCode.message)
737
- await span.error(fspiopError, state)
738
- await span.finish(fspiopError.message, state)
664
+ }
665
+ }
666
+
667
+ const processFxFulfilMessage = async (message, functionality, span) => {
668
+ const histTimerEnd = Metrics.getHistogram(
669
+ 'fx_transfer_fulfil',
670
+ 'Consume a fx fulfil transfer message from the kafka topic and process it accordingly',
671
+ ['success', 'fspId']
672
+ ).startTimer()
673
+
674
+ const {
675
+ payload,
676
+ headers,
677
+ type,
678
+ action,
679
+ commitRequestId,
680
+ kafkaTopic
681
+ } = FxFulfilService.decodeKafkaMessage(message)
682
+
683
+ const log = logger.child({ commitRequestId, type, action })
684
+ log.info('processFxFulfilMessage start...', { payload })
685
+
686
+ const params = {
687
+ message,
688
+ kafkaTopic,
689
+ span,
690
+ decodedPayload: payload,
691
+ consumer: Consumer,
692
+ producer: Producer
693
+ }
694
+
695
+ const fxFulfilService = new FxFulfilService({
696
+ log, Config, Comparators, Validator, FxTransferModel, Kafka, params
697
+ })
698
+
699
+ // Validate event type
700
+ await fxFulfilService.validateEventType(type, functionality)
701
+
702
+ // Validate action
703
+ const validActions = [
704
+ TransferEventAction.FX_RESERVE,
705
+ TransferEventAction.FX_COMMIT,
706
+ // TransferEventAction.FX_REJECT,
707
+ TransferEventAction.FX_ABORT,
708
+ TransferEventAction.FX_FORWARDED
709
+ ]
710
+ if (!validActions.includes(action)) {
711
+ const errorMessage = ERROR_MESSAGES.fxActionIsNotAllowed(action)
712
+ log.error(errorMessage)
713
+ span?.error(errorMessage)
714
+ histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
739
715
  return true
740
- } finally {
741
- if (!span.isFinished) {
742
- await span.finish()
716
+ }
717
+
718
+ const transfer = await fxFulfilService.getFxTransferDetails(commitRequestId, functionality)
719
+ // todo: rename to fxTransfer
720
+ await fxFulfilService.validateHeaders({ transfer, headers, payload })
721
+
722
+ // If execution continues after this point we are sure fxTransfer exists and source matches payee fsp
723
+ const histTimerDuplicateCheckEnd = Metrics.getHistogram(
724
+ 'fx_handler_transfers',
725
+ 'fxFulfil_duplicateCheckComparator - Metrics for fxTransfer handler',
726
+ ['success', 'funcName']
727
+ ).startTimer()
728
+
729
+ const dupCheckResult = await fxFulfilService.getDuplicateCheckResult({ commitRequestId, payload })
730
+ histTimerDuplicateCheckEnd({ success: true, funcName: 'fxFulfil_duplicateCheckComparator' })
731
+
732
+ const isDuplicate = await fxFulfilService.checkDuplication({ dupCheckResult, transfer, functionality, action, type })
733
+ if (isDuplicate) {
734
+ log.info('fxTransfer duplication detected, skip further processing')
735
+ histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
736
+ return true
737
+ }
738
+
739
+ // Transfer is not a duplicate, or message hasn't been changed.
740
+
741
+ payload.fulfilment && await fxFulfilService.validateFulfilment(transfer, payload)
742
+ await fxFulfilService.validateTransferState(transfer, functionality)
743
+ await fxFulfilService.validateExpirationDate(transfer, functionality)
744
+
745
+ log.info('Validations Succeeded - process the fxFulfil...')
746
+
747
+ switch (action) {
748
+ case TransferEventAction.FX_RESERVE:
749
+ case TransferEventAction.FX_COMMIT: {
750
+ const success = await fxFulfilService.processFxFulfil({ transfer, payload, action })
751
+ log.info('fxFulfil handling is done', { success })
752
+ histTimerEnd({ success, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
753
+ return success
754
+ }
755
+ case TransferEventAction.FX_ABORT: {
756
+ const success = await fxFulfilService.processFxAbort({ transfer, payload, action })
757
+ log.info('fxAbort handling is done', { success })
758
+ histTimerEnd({ success, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
759
+ return true
743
760
  }
744
761
  }
745
762
  }
@@ -769,46 +786,66 @@ const getTransfer = async (error, messages) => {
769
786
  } else {
770
787
  message = messages
771
788
  }
789
+ const action = message.value.metadata.event.action
790
+ const isFx = action === TransferEventAction.FX_GET
772
791
  const contextFromMessage = EventSdk.Tracer.extractContextFromMessage(message.value)
773
792
  const span = EventSdk.Tracer.createChildSpanFromContext('cl_transfer_get', contextFromMessage)
774
793
  try {
775
794
  await span.audit(message, EventSdk.AuditEventAction.start)
776
795
  const metadata = message.value.metadata
777
796
  const action = metadata.event.action
778
- const transferId = message.value.content.uriParams.id
797
+ const transferIdOrCommitRequestId = message.value.content.uriParams.id
779
798
  const kafkaTopic = message.topic
780
799
  Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, { method: `getTransfer:${action}` }))
781
800
 
782
801
  const actionLetter = Enum.Events.ActionLetter.get
783
802
  const params = { message, kafkaTopic, span, consumer: Consumer, producer: Producer }
784
- const eventDetail = { functionality: TransferEventType.NOTIFICATION, action: TransferEventAction.GET }
803
+ const eventDetail = { functionality: TransferEventType.NOTIFICATION, action }
785
804
 
786
805
  Util.breadcrumb(location, { path: 'validationFailed' })
787
806
  if (!await Validator.validateParticipantByName(message.value.from)) {
788
807
  Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `breakParticipantDoesntExist--${actionLetter}1`))
789
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, histTimerEnd })
808
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, histTimerEnd, hubName: Config.HUB_NAME })
790
809
  histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
791
810
  return true
792
811
  }
793
- const transfer = await TransferService.getByIdLight(transferId)
794
- if (!transfer) {
795
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorTransferNotFound--${actionLetter}3`))
796
- const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.TRANSFER_ID_NOT_FOUND, 'Provided Transfer ID was not found on the server.')
797
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
798
- throw fspiopError
799
- }
800
- if (!await Validator.validateParticipantTransferId(message.value.from, transferId)) {
801
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorNotTransferParticipant--${actionLetter}2`))
802
- const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.CLIENT_ERROR)
803
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
804
- throw fspiopError
812
+ if (isFx) {
813
+ const fxTransfer = await FxTransferModel.fxTransfer.getByIdLight(transferIdOrCommitRequestId)
814
+ if (!fxTransfer) {
815
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorTransferNotFound--${actionLetter}3`))
816
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.TRANSFER_ID_NOT_FOUND, 'Provided commitRequest ID was not found on the server.')
817
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch, hubName: Config.HUB_NAME })
818
+ throw fspiopError
819
+ }
820
+ if (!await Validator.validateParticipantForCommitRequestId(message.value.from, transferIdOrCommitRequestId)) {
821
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorNotFxTransferParticipant--${actionLetter}2`))
822
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.CLIENT_ERROR)
823
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch, hubName: Config.HUB_NAME })
824
+ throw fspiopError
825
+ }
826
+ Util.breadcrumb(location, { path: 'validationPassed' })
827
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackMessage--${actionLetter}4`))
828
+ message.value.content.payload = TransferObjectTransform.toFulfil(fxTransfer, true)
829
+ } else {
830
+ const transfer = await TransferService.getByIdLight(transferIdOrCommitRequestId)
831
+ if (!transfer) {
832
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorTransferNotFound--${actionLetter}3`))
833
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.TRANSFER_ID_NOT_FOUND, 'Provided Transfer ID was not found on the server.')
834
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch, hubName: Config.HUB_NAME })
835
+ throw fspiopError
836
+ }
837
+ if (!await Validator.validateParticipantTransferId(message.value.from, transferIdOrCommitRequestId)) {
838
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackErrorNotTransferParticipant--${actionLetter}2`))
839
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.CLIENT_ERROR)
840
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch, hubName: Config.HUB_NAME })
841
+ throw fspiopError
842
+ }
843
+ Util.breadcrumb(location, { path: 'validationPassed' })
844
+ Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackMessage--${actionLetter}4`))
845
+ message.value.content.payload = TransferObjectTransform.toFulfil(transfer)
805
846
  }
806
847
 
807
- // ============================================================================================
808
- Util.breadcrumb(location, { path: 'validationPassed' })
809
- Logger.isInfoEnabled && Logger.info(Util.breadcrumb(location, `callbackMessage--${actionLetter}4`))
810
- message.value.content.payload = TransferObjectTransform.toFulfil(transfer)
811
- await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail, fromSwitch })
848
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail, fromSwitch, hubName: Config.HUB_NAME })
812
849
  histTimerEnd({ success: true, fspId: Config.INSTRUMENTATION_METRICS_LABELS.fspId })
813
850
  return true
814
851
  } catch (err) {
@@ -836,13 +873,14 @@ const getTransfer = async (error, messages) => {
836
873
  */
837
874
  const registerPrepareHandler = async () => {
838
875
  try {
839
- const prepareHandler = {
840
- command: prepare,
841
- topicName: Kafka.transformGeneralTopicName(Config.KAFKA_CONFIG.TOPIC_TEMPLATES.GENERAL_TOPIC_TEMPLATE.TEMPLATE, TransferEventType.TRANSFER, TransferEventAction.PREPARE),
842
- config: Kafka.getKafkaConfig(Config.KAFKA_CONFIG, Enum.Kafka.Config.CONSUMER, TransferEventType.TRANSFER.toUpperCase(), TransferEventAction.PREPARE.toUpperCase())
843
- }
844
- prepareHandler.config.rdkafkaConf['client.id'] = prepareHandler.topicName
845
- await Consumer.createHandler(prepareHandler.topicName, prepareHandler.config, prepareHandler.command)
876
+ const { TRANSFER } = TransferEventType
877
+ const { PREPARE } = TransferEventAction
878
+
879
+ const topicName = Kafka.transformGeneralTopicName(Config.KAFKA_CONFIG.TOPIC_TEMPLATES.GENERAL_TOPIC_TEMPLATE.TEMPLATE, TRANSFER, PREPARE)
880
+ const consumeConfig = Kafka.getKafkaConfig(Config.KAFKA_CONFIG, Enum.Kafka.Config.CONSUMER, TRANSFER.toUpperCase(), PREPARE.toUpperCase())
881
+ consumeConfig.rdkafkaConf['client.id'] = topicName
882
+
883
+ await Consumer.createHandler(topicName, consumeConfig, prepare)
846
884
  return true
847
885
  } catch (err) {
848
886
  Logger.isErrorEnabled && Logger.error(err)