@mojaloop/central-ledger 17.7.8 → 17.8.0-orb.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/.ncurc.yaml +3 -1
  2. package/.nvmrc +1 -1
  3. package/.nycrc.yml +16 -1
  4. package/Dockerfile +9 -10
  5. package/README.md +39 -9
  6. package/audit-ci.jsonc +15 -2
  7. package/config/default.json +17 -1
  8. package/docker/central-ledger/default.json +9 -0
  9. package/docker/config-modifier/configs/central-ledger.js +25 -0
  10. package/docker/env.sh +15 -0
  11. package/docker/kafka/scripts/provision.sh +4 -1
  12. package/docker/ml-api-adapter/default.json +4 -1
  13. package/docker-compose.yml +90 -2
  14. package/documentation/db/erd-transfer-timeout.png +0 -0
  15. package/documentation/db/erd-transfer-timeout.txt +81 -0
  16. package/documentation/fx-implementation/README.md +48 -0
  17. package/documentation/fx-implementation/assets/fx-position-movements.drawio.svg +4 -0
  18. package/documentation/fx-implementation/assets/test-scenario.drawio.svg +4 -0
  19. package/documentation/sequence-diagrams/Handler - FX timeout.plantuml +123 -0
  20. package/documentation/sequence-diagrams/Handler - FX timeout.png +0 -0
  21. package/documentation/sequence-diagrams/Handler - timeout.plantuml +81 -0
  22. package/documentation/sequence-diagrams/Handler - timeout.png +0 -0
  23. package/documentation/state-diagrams/transfer-ML-spec-states-diagram.png +0 -0
  24. package/documentation/state-diagrams/transfer-internal-states-diagram.png +0 -0
  25. package/documentation/state-diagrams/transfer-internal-states.plantuml +75 -0
  26. package/documentation/state-diagrams/transfer-states.plantuml +13 -0
  27. package/migrations/310204_transferParticipant-participantId.js +52 -0
  28. package/migrations/310403_participantPositionChange-participantCurrencyId.js +47 -0
  29. package/migrations/310404_participantPositionChange-change.js +46 -0
  30. package/migrations/600010_fxTransferType.js +43 -0
  31. package/migrations/600011_fxTransferType-indexes.js +38 -0
  32. package/migrations/600012_fxParticipantCurrencyType.js +43 -0
  33. package/migrations/600013_fxParticipantCurrencyType-indexes.js +38 -0
  34. package/migrations/600100_fxTransferDuplicateCheck.js +42 -0
  35. package/migrations/600110_fxTransferErrorDuplicateCheck.js.js +17 -0
  36. package/migrations/600200_fxTransfer.js +51 -0
  37. package/migrations/600201_fxTransfer-indexes.js +40 -0
  38. package/migrations/600400_fxTransferStateChange.js +46 -0
  39. package/migrations/600401_fxTransferStateChange-indexes.js +40 -0
  40. package/migrations/600501_fxWatchList.js +46 -0
  41. package/migrations/600502_fxWatchList-indexes.js +40 -0
  42. package/migrations/600600_fxTransferFulfilmentDuplicateCheck.js +43 -0
  43. package/migrations/600601_fxTransferFulfilmentDuplicateCheck-indexes.js +38 -0
  44. package/migrations/600700_fxTransferFulfilment.js +47 -0
  45. package/migrations/600701_fxTransferFulfilment-indexes.js +43 -0
  46. package/migrations/600800_fxTransferExtension.js +47 -0
  47. package/migrations/601400_fxTransferTimeout.js +43 -0
  48. package/migrations/601401_fxTransferTimeout-indexes.js +37 -0
  49. package/migrations/601500_fxTransferError.js +44 -0
  50. package/migrations/601501_fxTransferError-indexes.js +37 -0
  51. package/migrations/610200_fxTransferParticipant.js +52 -0
  52. package/migrations/610201_fxTransferParticipant-indexes.js +44 -0
  53. package/migrations/610202_fxTransferParticipant-participantId.js +52 -0
  54. package/migrations/610403_participantPositionChange-fxTransfer.js +46 -0
  55. package/migrations/910101_feature904DataMigration.js +46 -52
  56. package/migrations/910102_feature949DataMigration.js +219 -225
  57. package/migrations/950104_settlementModel-settlementAccountTypeId.js +15 -20
  58. package/migrations/950108_participantProxy.js +18 -0
  59. package/migrations/950109_fxQuote.js +19 -0
  60. package/migrations/950110_fxQuoteResponse.js +25 -0
  61. package/migrations/950111_fxQuoteError.js +23 -0
  62. package/migrations/950113_fxQuoteDuplicateCheck.js +18 -0
  63. package/migrations/950114_fxQuoteResponseDuplicateCheck.js +21 -0
  64. package/migrations/950115_fxQuoteConversionTerms.js +36 -0
  65. package/migrations/950116_fxQuoteConversionTermsExtension.js +21 -0
  66. package/migrations/950117_fxQuoteResponseConversionTerms.js +39 -0
  67. package/migrations/950118_fxQuoteResponseConversionTermsExtension.js +21 -0
  68. package/migrations/950119_fxCharge.js +27 -0
  69. package/migrations/960100_create_externalParticipant.js +47 -0
  70. package/migrations/960110_alter_transferParticipant__addFiled_externalParticipantId.js +50 -0
  71. package/migrations/960111_alter_fxTransferParticipant__addFiled_externalParticipantId.js +50 -0
  72. package/package.json +27 -20
  73. package/seeds/endpointType.js +18 -0
  74. package/seeds/fxParticipantCurrencyType.js +45 -0
  75. package/seeds/fxTransferType.js +45 -0
  76. package/seeds/participant.js +2 -1
  77. package/seeds/transferParticipantRoleType.js +9 -0
  78. package/seeds/transferState.js +10 -0
  79. package/src/api/interface/swagger.json +23 -30
  80. package/src/api/participants/handler.js +6 -2
  81. package/src/api/participants/routes.js +8 -7
  82. package/src/api/root/handler.js +15 -5
  83. package/src/domain/fx/cyril.js +466 -0
  84. package/src/domain/fx/index.js +107 -0
  85. package/src/domain/participant/index.js +108 -1
  86. package/src/domain/position/abort.js +215 -0
  87. package/src/domain/position/binProcessor.js +361 -99
  88. package/src/domain/position/fulfil.js +252 -111
  89. package/src/domain/position/fx-fulfil.js +138 -0
  90. package/src/domain/position/fx-prepare.js +280 -0
  91. package/src/domain/position/fx-timeout-reserved.js +159 -0
  92. package/src/domain/position/index.js +1 -0
  93. package/src/domain/position/prepare.js +69 -49
  94. package/src/domain/position/timeout-reserved.js +162 -0
  95. package/src/domain/timeout/index.js +26 -2
  96. package/src/domain/transfer/index.js +22 -5
  97. package/src/domain/transfer/transform.js +19 -6
  98. package/src/handlers/admin/handler.js +0 -2
  99. package/src/handlers/bulk/fulfil/handler.js +5 -5
  100. package/src/handlers/bulk/get/handler.js +5 -5
  101. package/src/handlers/bulk/prepare/handler.js +9 -9
  102. package/src/handlers/bulk/processing/handler.js +8 -7
  103. package/src/handlers/bulk/shared/validator.js +1 -1
  104. package/src/handlers/positions/handler.js +23 -10
  105. package/src/handlers/positions/handlerBatch.js +54 -26
  106. package/src/handlers/register.js +2 -1
  107. package/src/handlers/timeouts/handler.js +212 -68
  108. package/src/handlers/transfers/FxFulfilService.js +387 -0
  109. package/src/handlers/transfers/createRemittanceEntity.js +106 -0
  110. package/src/handlers/transfers/dto.js +53 -0
  111. package/src/handlers/transfers/handler.js +609 -571
  112. package/src/handlers/transfers/prepare.js +572 -0
  113. package/src/handlers/transfers/validator.js +75 -14
  114. package/src/lib/cache.js +1 -1
  115. package/src/lib/config.js +6 -3
  116. package/src/lib/healthCheck/subServiceHealth.js +12 -2
  117. package/src/lib/proxyCache.js +131 -0
  118. package/src/models/bulkTransfer/facade.js +58 -82
  119. package/src/models/fxTransfer/duplicateCheck.js +153 -0
  120. package/src/models/fxTransfer/fxTransfer.js +578 -0
  121. package/src/models/fxTransfer/fxTransferError.js +53 -0
  122. package/src/models/fxTransfer/fxTransferExtension.js +41 -0
  123. package/src/models/fxTransfer/fxTransferTimeout.js +68 -0
  124. package/src/models/fxTransfer/index.js +15 -0
  125. package/src/models/fxTransfer/stateChange.js +47 -0
  126. package/src/models/fxTransfer/watchList.js +49 -0
  127. package/src/models/ledgerAccountType/ledgerAccountType.js +8 -32
  128. package/src/models/misc/segment.js +0 -1
  129. package/src/models/participant/externalParticipant.js +96 -0
  130. package/src/models/participant/externalParticipantCached.js +148 -0
  131. package/src/models/participant/facade.js +192 -123
  132. package/src/models/participant/participant.js +2 -1
  133. package/src/models/participant/participantCurrency.js +1 -1
  134. package/src/models/participant/participantPosition.js +2 -8
  135. package/src/models/position/batch.js +90 -1
  136. package/src/models/position/facade.js +5 -5
  137. package/src/models/position/participantPositionChanges.js +68 -0
  138. package/src/models/settlement/settlementModel.js +5 -23
  139. package/src/models/transfer/facade.js +806 -447
  140. package/src/shared/constants.js +52 -0
  141. package/src/shared/fspiopErrorFactory.js +131 -0
  142. package/src/shared/logger/index.js +8 -0
  143. package/src/shared/loggingPlugin.js +43 -0
  144. package/src/shared/plugins.js +6 -0
  145. package/src/shared/setup.js +10 -0
  146. package/test-integration.Dockerfile +1 -1
  147. package/test.Dockerfile +1 -1
@@ -0,0 +1,572 @@
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2017 Bill & Melinda Gates Foundation
5
+ The Mojaloop files are made available by the Bill & Melinda Gates Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
6
+ http://www.apache.org/licenses/LICENSE-2.0
7
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
8
+ Contributors
9
+ --------------
10
+ This is the official list of the Mojaloop project contributors for this file.
11
+ Names of the original copyright holders (individuals or organizations)
12
+ should be listed with a '*' in the first column. People who have
13
+ contributed from an organization can be listed under the organization
14
+ that actually holds the copyright for their contributions (see the
15
+ Gates Foundation organization for an example). Those individuals should have
16
+ their names indented and be marked with a '-'. Email address can be added
17
+ optionally within square brackets <email>.
18
+ * Gates Foundation
19
+ - Name Surname <name.surname@gatesfoundation.com>
20
+
21
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
22
+ --------------
23
+ **********/
24
+
25
+ const EventSdk = require('@mojaloop/event-sdk')
26
+ const ErrorHandler = require('@mojaloop/central-services-error-handling')
27
+ const Metrics = require('@mojaloop/central-services-metrics')
28
+ const { Enum, Util } = require('@mojaloop/central-services-shared')
29
+ const { Consumer, Producer } = require('@mojaloop/central-services-stream').Util
30
+
31
+ const { logger } = require('../../shared/logger')
32
+ const Config = require('../../lib/config')
33
+ const TransferObjectTransform = require('../../domain/transfer/transform')
34
+ const Participant = require('../../domain/participant')
35
+
36
+ const createRemittanceEntity = require('./createRemittanceEntity')
37
+ const Validator = require('./validator')
38
+ const dto = require('./dto')
39
+ const TransferService = require('../../domain/transfer/index')
40
+ const ProxyCache = require('../../lib/proxyCache')
41
+ const FxTransferService = require('../../domain/fx/index')
42
+
43
+ const { Kafka, Comparators } = Util
44
+ const { TransferState, TransferInternalState } = Enum.Transfers
45
+ const { Action, Type } = Enum.Events.Event
46
+ const { FSPIOPErrorCodes } = ErrorHandler.Enums
47
+ const { createFSPIOPError, reformatFSPIOPError } = ErrorHandler.Factory
48
+ const { fspId } = Config.INSTRUMENTATION_METRICS_LABELS
49
+
50
+ const consumerCommit = true
51
+ const fromSwitch = true
52
+ const proxyEnabled = Config.PROXY_CACHE_CONFIG.enabled
53
+
54
+ const proceedForwardErrorMessage = async ({ fspiopError, isFx, params }) => {
55
+ const eventDetail = {
56
+ functionality: Type.NOTIFICATION,
57
+ action: isFx ? Action.FX_FORWARDED : Action.FORWARDED
58
+ }
59
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, {
60
+ fspiopError,
61
+ eventDetail,
62
+ consumerCommit
63
+ })
64
+ logger.warn('proceedForwardErrorMessage is done', { fspiopError, eventDetail })
65
+ }
66
+
67
+ // think better name
68
+ const forwardPrepare = async ({ isFx, params, ID }) => {
69
+ if (isFx) {
70
+ const fxTransfer = await FxTransferService.getByIdLight(ID)
71
+ if (!fxTransfer) {
72
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(
73
+ FSPIOPErrorCodes.ID_NOT_FOUND,
74
+ 'Forwarded fxTransfer could not be found.'
75
+ ).toApiErrorObject(Config.ERROR_HANDLING)
76
+ // IMPORTANT: This singular message is taken by the ml-api-adapter and used to
77
+ // notify the payerFsp and proxy of the error.
78
+ // As long as the `to` and `from` message values are the fsp and fxp,
79
+ // and the action is `fx-forwarded`, the ml-api-adapter will notify both.
80
+ await proceedForwardErrorMessage({ fspiopError, isFx, params })
81
+ return true
82
+ }
83
+
84
+ if (fxTransfer.fxTransferState === TransferInternalState.RESERVED) {
85
+ await FxTransferService.forwardedFxPrepare(ID)
86
+ } else {
87
+ const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError(
88
+ `Invalid State: ${fxTransfer.fxTransferState} - expected: ${TransferInternalState.RESERVED}`
89
+ ).toApiErrorObject(Config.ERROR_HANDLING)
90
+ // IMPORTANT: This singular message is taken by the ml-api-adapter and used to
91
+ // notify the payerFsp and proxy of the error.
92
+ // As long as the `to` and `from` message values are the fsp and fxp,
93
+ // and the action is `fx-forwarded`, the ml-api-adapter will notify both.
94
+ await proceedForwardErrorMessage({ fspiopError, isFx, params })
95
+ }
96
+ } else {
97
+ const transfer = await TransferService.getById(ID)
98
+ if (!transfer) {
99
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(
100
+ FSPIOPErrorCodes.ID_NOT_FOUND,
101
+ 'Forwarded transfer could not be found.'
102
+ ).toApiErrorObject(Config.ERROR_HANDLING)
103
+ // IMPORTANT: This singular message is taken by the ml-api-adapter and used to
104
+ // notify the payerFsp and proxy of the error.
105
+ // As long as the `to` and `from` message values are the payer and payee,
106
+ // and the action is `forwarded`, the ml-api-adapter will notify both.
107
+ await proceedForwardErrorMessage({ fspiopError, isFx, params })
108
+ return true
109
+ }
110
+
111
+ if (transfer.transferState === TransferInternalState.RESERVED) {
112
+ await TransferService.forwardedPrepare(ID)
113
+ } else {
114
+ const fspiopError = ErrorHandler.Factory.createInternalServerFSPIOPError(
115
+ `Invalid State: ${transfer.transferState} - expected: ${TransferInternalState.RESERVED}`
116
+ ).toApiErrorObject(Config.ERROR_HANDLING)
117
+ // IMPORTANT: This singular message is taken by the ml-api-adapter and used to
118
+ // notify the payerFsp and proxy of the error.
119
+ // As long as the `to` and `from` message values are the payer and payee,
120
+ // and the action is `forwarded`, the ml-api-adapter will notify both.
121
+ await proceedForwardErrorMessage({ fspiopError, isFx, params })
122
+ }
123
+ }
124
+
125
+ return true
126
+ }
127
+
128
+ /** @import { ProxyOrParticipant } from '#src/lib/proxyCache.js' */
129
+ /**
130
+ * @typedef {Object} ProxyObligation
131
+ * @property {boolean} isFx - Is FX transfer.
132
+ * @property {Object} payloadClone - A clone of the original payload.
133
+ * @property {ProxyOrParticipant} initiatingFspProxyOrParticipantId - initiating FSP: proxy or participant.
134
+ * @property {ProxyOrParticipant} counterPartyFspProxyOrParticipantId - counterparty FSP: proxy or participant.
135
+ * @property {boolean} isInitiatingFspProxy - initiatingFsp.(!inScheme && proxyId !== null).
136
+ * @property {boolean} isCounterPartyFspProxy - counterPartyFsp.(!inScheme && proxyId !== null).
137
+ */
138
+
139
+ /**
140
+ * Calculates proxyObligation.
141
+ * @returns {ProxyObligation} proxyObligation
142
+ */
143
+ const calculateProxyObligation = async ({ payload, isFx, params, functionality, action }) => {
144
+ const proxyObligation = {
145
+ isFx,
146
+ payloadClone: { ...payload },
147
+ isInitiatingFspProxy: false,
148
+ isCounterPartyFspProxy: false,
149
+ initiatingFspProxyOrParticipantId: null,
150
+ counterPartyFspProxyOrParticipantId: null
151
+ }
152
+
153
+ if (proxyEnabled) {
154
+ const [initiatingFsp, counterPartyFsp] = isFx ? [payload.initiatingFsp, payload.counterPartyFsp] : [payload.payerFsp, payload.payeeFsp]
155
+
156
+ // TODO: We need to double check the following validation logic incase of payee side currency conversion
157
+ const payeeFspLookupOptions = isFx ? null : { validateCurrencyAccounts: true, accounts: [{ currency: payload.amount.currency, accountType: Enum.Accounts.LedgerAccountType.POSITION }] }
158
+
159
+ ;[proxyObligation.initiatingFspProxyOrParticipantId, proxyObligation.counterPartyFspProxyOrParticipantId] = await Promise.all([
160
+ ProxyCache.getFSPProxy(initiatingFsp),
161
+ ProxyCache.getFSPProxy(counterPartyFsp, payeeFspLookupOptions)
162
+ ])
163
+ logger.debug('Prepare proxy cache lookup results', {
164
+ initiatingFsp,
165
+ counterPartyFsp,
166
+ initiatingFspProxyOrParticipantId: proxyObligation.initiatingFspProxyOrParticipantId,
167
+ counterPartyFspProxyOrParticipantId: proxyObligation.counterPartyFspProxyOrParticipantId
168
+ })
169
+
170
+ proxyObligation.isInitiatingFspProxy = !proxyObligation.initiatingFspProxyOrParticipantId.inScheme &&
171
+ proxyObligation.initiatingFspProxyOrParticipantId.proxyId !== null
172
+ proxyObligation.isCounterPartyFspProxy = !proxyObligation.counterPartyFspProxyOrParticipantId.inScheme &&
173
+ proxyObligation.counterPartyFspProxyOrParticipantId.proxyId !== null
174
+
175
+ if (isFx) {
176
+ proxyObligation.payloadClone.initiatingFsp = !proxyObligation.initiatingFspProxyOrParticipantId?.inScheme &&
177
+ proxyObligation.initiatingFspProxyOrParticipantId?.proxyId
178
+ ? proxyObligation.initiatingFspProxyOrParticipantId.proxyId
179
+ : payload.initiatingFsp
180
+ proxyObligation.payloadClone.counterPartyFsp = !proxyObligation.counterPartyFspProxyOrParticipantId?.inScheme &&
181
+ proxyObligation.counterPartyFspProxyOrParticipantId?.proxyId
182
+ ? proxyObligation.counterPartyFspProxyOrParticipantId.proxyId
183
+ : payload.counterPartyFsp
184
+ } else {
185
+ proxyObligation.payloadClone.payerFsp = !proxyObligation.initiatingFspProxyOrParticipantId?.inScheme &&
186
+ proxyObligation.initiatingFspProxyOrParticipantId?.proxyId
187
+ ? proxyObligation.initiatingFspProxyOrParticipantId.proxyId
188
+ : payload.payerFsp
189
+ proxyObligation.payloadClone.payeeFsp = !proxyObligation.counterPartyFspProxyOrParticipantId?.inScheme &&
190
+ proxyObligation.counterPartyFspProxyOrParticipantId?.proxyId
191
+ ? proxyObligation.counterPartyFspProxyOrParticipantId.proxyId
192
+ : payload.payeeFsp
193
+ }
194
+
195
+ // If either debtor participant or creditor participant aren't in the scheme and have no proxy representative, then throw an error.
196
+ if ((proxyObligation.initiatingFspProxyOrParticipantId.inScheme === false && proxyObligation.initiatingFspProxyOrParticipantId.proxyId === null) ||
197
+ (proxyObligation.counterPartyFspProxyOrParticipantId.inScheme === false && proxyObligation.counterPartyFspProxyOrParticipantId.proxyId === null)) {
198
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(
199
+ ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND,
200
+ `Payer proxy or payee proxy not found: initiatingFsp: ${initiatingFsp} counterPartyFsp: ${counterPartyFsp}`
201
+ ).toApiErrorObject(Config.ERROR_HANDLING)
202
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, {
203
+ consumerCommit,
204
+ fspiopError,
205
+ eventDetail: { functionality, action },
206
+ fromSwitch,
207
+ hubName: Config.HUB_NAME
208
+ })
209
+ throw fspiopError
210
+ }
211
+ }
212
+
213
+ return proxyObligation
214
+ }
215
+
216
+ const checkDuplication = async ({ payload, isFx, ID, location }) => {
217
+ const funcName = 'prepare_duplicateCheckComparator'
218
+ const histTimerDuplicateCheckEnd = Metrics.getHistogram(
219
+ 'handler_transfers',
220
+ `${funcName} - Metrics for transfer handler`,
221
+ ['success', 'funcName']
222
+ ).startTimer()
223
+
224
+ const remittance = createRemittanceEntity(isFx)
225
+ const { hasDuplicateId, hasDuplicateHash } = await Comparators.duplicateCheckComparator(
226
+ ID,
227
+ payload,
228
+ remittance.getDuplicate,
229
+ remittance.saveDuplicateHash
230
+ )
231
+
232
+ logger.info(Util.breadcrumb(location, { path: funcName }), { hasDuplicateId, hasDuplicateHash, isFx, ID })
233
+ histTimerDuplicateCheckEnd({ success: true, funcName })
234
+
235
+ return { hasDuplicateId, hasDuplicateHash }
236
+ }
237
+
238
+ const processDuplication = async ({
239
+ duplication, isFx, ID, functionality, action, actionLetter, params, location
240
+ }) => {
241
+ if (!duplication.hasDuplicateId) return
242
+
243
+ let error
244
+ if (!duplication.hasDuplicateHash) {
245
+ logger.warn(Util.breadcrumb(location, `callbackErrorModified1--${actionLetter}5`))
246
+ error = createFSPIOPError(FSPIOPErrorCodes.MODIFIED_REQUEST)
247
+ } else if (action === Action.BULK_PREPARE) {
248
+ logger.info(Util.breadcrumb(location, `validationError1--${actionLetter}2`))
249
+ error = createFSPIOPError('Individual transfer prepare duplicate')
250
+ }
251
+
252
+ if (error) {
253
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, {
254
+ consumerCommit,
255
+ fspiopError: error.toApiErrorObject(Config.ERROR_HANDLING),
256
+ eventDetail: { functionality, action },
257
+ fromSwitch,
258
+ hubName: Config.HUB_NAME
259
+ })
260
+ throw error
261
+ }
262
+ logger.info(Util.breadcrumb(location, 'handleResend'))
263
+
264
+ const transfer = await createRemittanceEntity(isFx)
265
+ .getByIdLight(ID)
266
+
267
+ const finalizedState = [TransferState.COMMITTED, TransferState.ABORTED, TransferState.RESERVED]
268
+ const isFinalized =
269
+ finalizedState.includes(transfer?.transferStateEnumeration) ||
270
+ finalizedState.includes(transfer?.fxTransferStateEnumeration)
271
+ const isPrepare = [Action.PREPARE, Action.FX_PREPARE, Action.FORWARDED, Action.FX_FORWARDED].includes(action)
272
+
273
+ let eventDetail = { functionality, action: Action.PREPARE_DUPLICATE }
274
+ if (isFinalized) {
275
+ if (isPrepare) {
276
+ logger.info(Util.breadcrumb(location, `finalized callback--${actionLetter}1`))
277
+ params.message.value.content.payload = TransferObjectTransform.toFulfil(transfer, isFx)
278
+ params.message.value.content.uriParams = { id: ID }
279
+ const action = isFx ? Action.FX_PREPARE_DUPLICATE : Action.PREPARE_DUPLICATE
280
+ eventDetail = { functionality, action }
281
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, eventDetail, fromSwitch, hubName: Config.HUB_NAME })
282
+ } else if (action === Action.BULK_PREPARE) {
283
+ logger.info(Util.breadcrumb(location, `validationError1--${actionLetter}2`))
284
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.MODIFIED_REQUEST, 'Individual transfer prepare duplicate')
285
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
286
+ throw fspiopError
287
+ }
288
+ } else {
289
+ logger.info(Util.breadcrumb(location, 'inProgress'))
290
+ if (action === Action.BULK_PREPARE) {
291
+ logger.info(Util.breadcrumb(location, `validationError2--${actionLetter}4`))
292
+ const fspiopError = ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.MODIFIED_REQUEST, 'Individual transfer prepare duplicate')
293
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit, fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING), eventDetail, fromSwitch })
294
+ throw fspiopError
295
+ } else { // action === TransferEventAction.PREPARE
296
+ logger.info(Util.breadcrumb(location, `ignore--${actionLetter}3`))
297
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, { consumerCommit })
298
+ return true
299
+ }
300
+ }
301
+
302
+ return true
303
+ }
304
+
305
+ const savePreparedRequest = async ({
306
+ validationPassed,
307
+ reasons,
308
+ payload,
309
+ isFx,
310
+ functionality,
311
+ params,
312
+ location,
313
+ determiningTransferCheckResult,
314
+ proxyObligation
315
+ }) => {
316
+ const logMessage = Util.breadcrumb(location, 'savePreparedRequest')
317
+ try {
318
+ logger.info(logMessage, { validationPassed, reasons })
319
+ const reason = validationPassed ? null : reasons.toString()
320
+ await createRemittanceEntity(isFx)
321
+ .savePreparedRequest(
322
+ payload,
323
+ reason,
324
+ validationPassed,
325
+ determiningTransferCheckResult,
326
+ proxyObligation
327
+ )
328
+ } catch (err) {
329
+ logger.error(`${logMessage} error:`, err)
330
+ const fspiopError = reformatFSPIOPError(err, FSPIOPErrorCodes.INTERNAL_SERVER_ERROR)
331
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, {
332
+ consumerCommit,
333
+ fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING),
334
+ eventDetail: { functionality, action: Action.PREPARE },
335
+ fromSwitch,
336
+ hubName: Config.HUB_NAME
337
+ })
338
+ throw fspiopError
339
+ }
340
+ }
341
+
342
+ const definePositionParticipant = async ({ isFx, payload, determiningTransferCheckResult, proxyObligation }) => {
343
+ const cyrilResult = await createRemittanceEntity(isFx)
344
+ .getPositionParticipant(payload, determiningTransferCheckResult, proxyObligation)
345
+
346
+ let messageKey
347
+ // On a proxied transfer prepare if there is a corresponding fx transfer `getPositionParticipant`
348
+ // should return the fxp's proxy as the participantName since the fxp proxy would be saved as the counterPartyFsp
349
+ // in the prior fx transfer prepare.
350
+ // Following interscheme rules, if the debtor(fxTransfer FXP) and the creditor(transfer payee) are
351
+ // represented by the same proxy, no position adjustment is needed.
352
+ let isSameProxy = false
353
+ // Only check transfers that have a related fxTransfer
354
+ if (determiningTransferCheckResult?.watchListRecords?.length > 0) {
355
+ const counterPartyParticipantFXPProxy = cyrilResult.participantName
356
+ isSameProxy = counterPartyParticipantFXPProxy && proxyObligation?.counterPartyFspProxyOrParticipantId?.proxyId
357
+ ? counterPartyParticipantFXPProxy === proxyObligation.counterPartyFspProxyOrParticipantId.proxyId
358
+ : false
359
+ }
360
+ if (isSameProxy) {
361
+ messageKey = '0'
362
+ } else {
363
+ const account = await Participant.getAccountByNameAndCurrency(
364
+ cyrilResult.participantName,
365
+ cyrilResult.currencyId,
366
+ Enum.Accounts.LedgerAccountType.POSITION
367
+ )
368
+ messageKey = account.participantCurrencyId.toString()
369
+ }
370
+ logger.info('prepare positionParticipant details:', { messageKey, isSameProxy, cyrilResult })
371
+
372
+ return {
373
+ messageKey,
374
+ cyrilResult
375
+ }
376
+ }
377
+
378
+ const sendPositionPrepareMessage = async ({
379
+ isFx,
380
+ action,
381
+ params,
382
+ determiningTransferCheckResult,
383
+ proxyObligation
384
+ }) => {
385
+ const eventDetail = {
386
+ functionality: Type.POSITION,
387
+ action
388
+ }
389
+
390
+ const { messageKey, cyrilResult } = await definePositionParticipant({
391
+ payload: proxyObligation.payloadClone,
392
+ isFx,
393
+ determiningTransferCheckResult,
394
+ proxyObligation
395
+ })
396
+
397
+ params.message.value.content.context = {
398
+ ...params.message.value.content.context,
399
+ cyrilResult
400
+ }
401
+ // We route fx-prepare, bulk-prepare and prepare messages differently based on the topic configured for it.
402
+ // Note: The batch handler does not currently support bulk-prepare messages, only prepare messages are supported.
403
+ // And non batch processing is not supported for fx-prepare messages.
404
+ // Therefore, it is necessary to check the action to determine the topic to route to.
405
+ let topicNameOverride = Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.PREPARE
406
+ if (action === Action.BULK_PREPARE) {
407
+ topicNameOverride = Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.BULK_PREPARE
408
+ } else if (action === Action.FX_PREPARE) {
409
+ topicNameOverride = Config.KAFKA_CONFIG.EVENT_TYPE_ACTION_TOPIC_MAP?.POSITION?.FX_PREPARE
410
+ }
411
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, {
412
+ consumerCommit,
413
+ eventDetail,
414
+ messageKey,
415
+ topicNameOverride,
416
+ hubName: Config.HUB_NAME
417
+ })
418
+
419
+ return true
420
+ }
421
+
422
+ /**
423
+ * @function TransferPrepareHandler
424
+ *
425
+ * @async
426
+ * @description This is the consumer callback function that gets registered to a topic. This then gets a list of messages,
427
+ * we will only ever use the first message in non batch processing. We then break down the message into its payload and
428
+ * begin validating the payload. Once the payload is validated successfully it will be written to the database to
429
+ * the relevant tables. If the validation fails it is still written to the database for auditing purposes but with an
430
+ * INVALID status. For any duplicate requests we will send appropriate callback based on the transfer state and the hash validation
431
+ *
432
+ * Validator.validatePrepare called to validate the payload of the message
433
+ * TransferService.getById called to get the details of the existing transfer
434
+ * TransferObjectTransform.toTransfer called to transform the transfer object
435
+ * TransferService.prepare called and creates new entries in transfer tables for successful prepare transfer
436
+ * TransferService.logTransferError called to log the invalid request
437
+ *
438
+ * @param {error} error - error thrown if something fails within Kafka
439
+ * @param {array} messages - a list of messages to consume for the relevant topic
440
+ *
441
+ * @returns {object} - Returns a boolean: true if successful, or throws and error if failed
442
+ */
443
+ const prepare = async (error, messages) => {
444
+ const location = { module: 'PrepareHandler', method: '', path: '' }
445
+ const input = dto.prepareInputDto(error, messages)
446
+
447
+ const histTimerEnd = Metrics.getHistogram(
448
+ input.metric,
449
+ `Consume a ${input.metric} message from the kafka topic and process it accordingly`,
450
+ ['success', 'fspId']
451
+ ).startTimer()
452
+ if (error) {
453
+ histTimerEnd({ success: false, fspId })
454
+ throw reformatFSPIOPError(error)
455
+ }
456
+
457
+ const {
458
+ message, payload, isFx, ID, headers, action, actionLetter, functionality, isForwarded
459
+ } = input
460
+
461
+ const contextFromMessage = EventSdk.Tracer.extractContextFromMessage(message.value)
462
+ const span = EventSdk.Tracer.createChildSpanFromContext(`cl_${input.metric}`, contextFromMessage)
463
+
464
+ try {
465
+ span.setTags({ transactionId: ID })
466
+ await span.audit(message, EventSdk.AuditEventAction.start)
467
+ logger.info(Util.breadcrumb(location, { method: 'prepare' }))
468
+
469
+ const params = {
470
+ message,
471
+ kafkaTopic: message.topic,
472
+ decodedPayload: payload,
473
+ span,
474
+ consumer: Consumer,
475
+ producer: Producer
476
+ }
477
+
478
+ if (proxyEnabled && isForwarded) {
479
+ const isOk = await forwardPrepare({ isFx, params, ID })
480
+ logger.info('forwardPrepare message is processed', { isOk, isFx, ID })
481
+ return isOk
482
+ }
483
+
484
+ const proxyObligation = await calculateProxyObligation({
485
+ payload, isFx, params, functionality, action
486
+ })
487
+
488
+ const duplication = await checkDuplication({ payload, isFx, ID, location })
489
+ if (duplication.hasDuplicateId) {
490
+ const success = await processDuplication({
491
+ duplication, isFx, ID, functionality, action, actionLetter, params, location
492
+ })
493
+ histTimerEnd({ success, fspId })
494
+ return success
495
+ }
496
+
497
+ const determiningTransferCheckResult = await createRemittanceEntity(isFx)
498
+ .checkIfDeterminingTransferExists(proxyObligation.payloadClone, proxyObligation)
499
+
500
+ const { validationPassed, reasons } = await Validator.validatePrepare(
501
+ payload,
502
+ headers,
503
+ isFx,
504
+ determiningTransferCheckResult,
505
+ proxyObligation
506
+ )
507
+
508
+ await savePreparedRequest({
509
+ validationPassed,
510
+ reasons,
511
+ payload,
512
+ isFx,
513
+ functionality,
514
+ params,
515
+ location,
516
+ determiningTransferCheckResult,
517
+ proxyObligation
518
+ })
519
+
520
+ if (!validationPassed) {
521
+ logger.warn(Util.breadcrumb(location, { path: 'validationFailed' }))
522
+ const fspiopError = createFSPIOPError(FSPIOPErrorCodes.VALIDATION_ERROR, reasons.toString())
523
+ await createRemittanceEntity(isFx)
524
+ .logTransferError(ID, FSPIOPErrorCodes.VALIDATION_ERROR.code, reasons.toString())
525
+ /**
526
+ * TODO: BULK-Handle at BulkProcessingHandler (not in scope of #967)
527
+ * HOWTO: For regular transfers this branch may be triggered by sending
528
+ * a transfer in a currency not supported by either dfsp. Not sure if it
529
+ * will be triggered for bulk, because of the BulkPrepareHandler.
530
+ */
531
+ await Kafka.proceed(Config.KAFKA_CONFIG, params, {
532
+ consumerCommit,
533
+ fspiopError: fspiopError.toApiErrorObject(Config.ERROR_HANDLING),
534
+ eventDetail: { functionality, action },
535
+ fromSwitch,
536
+ hubName: Config.HUB_NAME
537
+ })
538
+ throw fspiopError
539
+ }
540
+
541
+ logger.info(Util.breadcrumb(location, `positionTopic1--${actionLetter}7`))
542
+ const success = await sendPositionPrepareMessage({
543
+ isFx, action, params, determiningTransferCheckResult, proxyObligation
544
+ })
545
+
546
+ histTimerEnd({ success, fspId })
547
+ return success
548
+ } catch (err) {
549
+ histTimerEnd({ success: false, fspId })
550
+ const fspiopError = reformatFSPIOPError(err)
551
+ logger.error(`${Util.breadcrumb(location)}::${err.message}`, err)
552
+ const state = new EventSdk.EventStateMetadata(EventSdk.EventStatusType.failed, fspiopError.apiErrorCode.code, fspiopError.apiErrorCode.message)
553
+ await span.error(fspiopError, state)
554
+ await span.finish(fspiopError.message, state)
555
+ return true
556
+ } finally {
557
+ if (!span.isFinished) {
558
+ await span.finish()
559
+ }
560
+ }
561
+ }
562
+
563
+ module.exports = {
564
+ prepare,
565
+ forwardPrepare,
566
+ calculateProxyObligation,
567
+ checkDuplication,
568
+ processDuplication,
569
+ savePreparedRequest,
570
+ definePositionParticipant,
571
+ sendPositionPrepareMessage
572
+ }