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