@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.
- package/.ncurc.yaml +3 -1
- package/.nvmrc +1 -1
- package/.nycrc.yml +16 -1
- package/Dockerfile +9 -10
- package/README.md +39 -9
- package/audit-ci.jsonc +15 -2
- package/config/default.json +17 -1
- package/docker/central-ledger/default.json +9 -0
- package/docker/config-modifier/configs/central-ledger.js +25 -0
- package/docker/env.sh +15 -0
- package/docker/kafka/scripts/provision.sh +4 -1
- package/docker/ml-api-adapter/default.json +4 -1
- package/docker-compose.yml +90 -2
- package/documentation/db/erd-transfer-timeout.png +0 -0
- package/documentation/db/erd-transfer-timeout.txt +81 -0
- package/documentation/fx-implementation/README.md +48 -0
- package/documentation/fx-implementation/assets/fx-position-movements.drawio.svg +4 -0
- package/documentation/fx-implementation/assets/test-scenario.drawio.svg +4 -0
- package/documentation/sequence-diagrams/Handler - FX timeout.plantuml +123 -0
- package/documentation/sequence-diagrams/Handler - FX timeout.png +0 -0
- package/documentation/sequence-diagrams/Handler - timeout.plantuml +81 -0
- package/documentation/sequence-diagrams/Handler - timeout.png +0 -0
- package/documentation/state-diagrams/transfer-ML-spec-states-diagram.png +0 -0
- package/documentation/state-diagrams/transfer-internal-states-diagram.png +0 -0
- package/documentation/state-diagrams/transfer-internal-states.plantuml +75 -0
- package/documentation/state-diagrams/transfer-states.plantuml +13 -0
- package/migrations/310204_transferParticipant-participantId.js +52 -0
- package/migrations/310403_participantPositionChange-participantCurrencyId.js +47 -0
- package/migrations/310404_participantPositionChange-change.js +46 -0
- package/migrations/600010_fxTransferType.js +43 -0
- package/migrations/600011_fxTransferType-indexes.js +38 -0
- package/migrations/600012_fxParticipantCurrencyType.js +43 -0
- package/migrations/600013_fxParticipantCurrencyType-indexes.js +38 -0
- package/migrations/600100_fxTransferDuplicateCheck.js +42 -0
- package/migrations/600110_fxTransferErrorDuplicateCheck.js.js +17 -0
- package/migrations/600200_fxTransfer.js +51 -0
- package/migrations/600201_fxTransfer-indexes.js +40 -0
- package/migrations/600400_fxTransferStateChange.js +46 -0
- package/migrations/600401_fxTransferStateChange-indexes.js +40 -0
- package/migrations/600501_fxWatchList.js +46 -0
- package/migrations/600502_fxWatchList-indexes.js +40 -0
- package/migrations/600600_fxTransferFulfilmentDuplicateCheck.js +43 -0
- package/migrations/600601_fxTransferFulfilmentDuplicateCheck-indexes.js +38 -0
- package/migrations/600700_fxTransferFulfilment.js +47 -0
- package/migrations/600701_fxTransferFulfilment-indexes.js +43 -0
- package/migrations/600800_fxTransferExtension.js +47 -0
- package/migrations/601400_fxTransferTimeout.js +43 -0
- package/migrations/601401_fxTransferTimeout-indexes.js +37 -0
- package/migrations/601500_fxTransferError.js +44 -0
- package/migrations/601501_fxTransferError-indexes.js +37 -0
- package/migrations/610200_fxTransferParticipant.js +52 -0
- package/migrations/610201_fxTransferParticipant-indexes.js +44 -0
- package/migrations/610202_fxTransferParticipant-participantId.js +52 -0
- package/migrations/610403_participantPositionChange-fxTransfer.js +46 -0
- package/migrations/910101_feature904DataMigration.js +46 -52
- package/migrations/910102_feature949DataMigration.js +219 -225
- package/migrations/950104_settlementModel-settlementAccountTypeId.js +15 -20
- package/migrations/950108_participantProxy.js +18 -0
- package/migrations/950109_fxQuote.js +19 -0
- package/migrations/950110_fxQuoteResponse.js +25 -0
- package/migrations/950111_fxQuoteError.js +23 -0
- package/migrations/950113_fxQuoteDuplicateCheck.js +18 -0
- package/migrations/950114_fxQuoteResponseDuplicateCheck.js +21 -0
- package/migrations/950115_fxQuoteConversionTerms.js +36 -0
- package/migrations/950116_fxQuoteConversionTermsExtension.js +21 -0
- package/migrations/950117_fxQuoteResponseConversionTerms.js +39 -0
- package/migrations/950118_fxQuoteResponseConversionTermsExtension.js +21 -0
- package/migrations/950119_fxCharge.js +27 -0
- package/migrations/960100_create_externalParticipant.js +47 -0
- package/migrations/960110_alter_transferParticipant__addFiled_externalParticipantId.js +50 -0
- package/migrations/960111_alter_fxTransferParticipant__addFiled_externalParticipantId.js +50 -0
- package/package.json +27 -20
- package/seeds/endpointType.js +18 -0
- package/seeds/fxParticipantCurrencyType.js +45 -0
- package/seeds/fxTransferType.js +45 -0
- package/seeds/participant.js +2 -1
- package/seeds/transferParticipantRoleType.js +9 -0
- package/seeds/transferState.js +10 -0
- package/src/api/interface/swagger.json +23 -30
- package/src/api/participants/handler.js +6 -2
- package/src/api/participants/routes.js +8 -7
- package/src/api/root/handler.js +15 -5
- package/src/domain/fx/cyril.js +466 -0
- package/src/domain/fx/index.js +107 -0
- package/src/domain/participant/index.js +108 -1
- package/src/domain/position/abort.js +215 -0
- package/src/domain/position/binProcessor.js +361 -99
- package/src/domain/position/fulfil.js +252 -111
- package/src/domain/position/fx-fulfil.js +138 -0
- package/src/domain/position/fx-prepare.js +280 -0
- package/src/domain/position/fx-timeout-reserved.js +159 -0
- package/src/domain/position/index.js +1 -0
- package/src/domain/position/prepare.js +69 -49
- package/src/domain/position/timeout-reserved.js +162 -0
- package/src/domain/timeout/index.js +26 -2
- package/src/domain/transfer/index.js +22 -5
- package/src/domain/transfer/transform.js +19 -6
- package/src/handlers/admin/handler.js +0 -2
- package/src/handlers/bulk/fulfil/handler.js +5 -5
- package/src/handlers/bulk/get/handler.js +5 -5
- package/src/handlers/bulk/prepare/handler.js +9 -9
- package/src/handlers/bulk/processing/handler.js +8 -7
- package/src/handlers/bulk/shared/validator.js +1 -1
- package/src/handlers/positions/handler.js +23 -10
- package/src/handlers/positions/handlerBatch.js +54 -26
- package/src/handlers/register.js +2 -1
- package/src/handlers/timeouts/handler.js +212 -68
- package/src/handlers/transfers/FxFulfilService.js +387 -0
- package/src/handlers/transfers/createRemittanceEntity.js +106 -0
- package/src/handlers/transfers/dto.js +53 -0
- package/src/handlers/transfers/handler.js +609 -571
- package/src/handlers/transfers/prepare.js +572 -0
- package/src/handlers/transfers/validator.js +75 -14
- package/src/lib/cache.js +1 -1
- package/src/lib/config.js +6 -3
- package/src/lib/healthCheck/subServiceHealth.js +12 -2
- package/src/lib/proxyCache.js +131 -0
- package/src/models/bulkTransfer/facade.js +58 -82
- package/src/models/fxTransfer/duplicateCheck.js +153 -0
- package/src/models/fxTransfer/fxTransfer.js +578 -0
- package/src/models/fxTransfer/fxTransferError.js +53 -0
- package/src/models/fxTransfer/fxTransferExtension.js +41 -0
- package/src/models/fxTransfer/fxTransferTimeout.js +68 -0
- package/src/models/fxTransfer/index.js +15 -0
- package/src/models/fxTransfer/stateChange.js +47 -0
- package/src/models/fxTransfer/watchList.js +49 -0
- package/src/models/ledgerAccountType/ledgerAccountType.js +8 -32
- package/src/models/misc/segment.js +0 -1
- package/src/models/participant/externalParticipant.js +96 -0
- package/src/models/participant/externalParticipantCached.js +148 -0
- package/src/models/participant/facade.js +192 -123
- package/src/models/participant/participant.js +2 -1
- package/src/models/participant/participantCurrency.js +1 -1
- package/src/models/participant/participantPosition.js +2 -8
- package/src/models/position/batch.js +90 -1
- package/src/models/position/facade.js +5 -5
- package/src/models/position/participantPositionChanges.js +68 -0
- package/src/models/settlement/settlementModel.js +5 -23
- package/src/models/transfer/facade.js +806 -447
- package/src/shared/constants.js +52 -0
- package/src/shared/fspiopErrorFactory.js +131 -0
- package/src/shared/logger/index.js +8 -0
- package/src/shared/loggingPlugin.js +43 -0
- package/src/shared/plugins.js +6 -0
- package/src/shared/setup.js +10 -0
- package/test-integration.Dockerfile +1 -1
- 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
|
+
}
|