@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,466 @@
|
|
|
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
|
+
* Vijay Kumar Guthi <vijaya.guthi@infitx.com>
|
|
21
|
+
--------------
|
|
22
|
+
******/
|
|
23
|
+
|
|
24
|
+
'use strict'
|
|
25
|
+
|
|
26
|
+
const Metrics = require('@mojaloop/central-services-metrics')
|
|
27
|
+
const { Enum } = require('@mojaloop/central-services-shared')
|
|
28
|
+
const TransferModel = require('../../models/transfer/transfer')
|
|
29
|
+
const TransferFacade = require('../../models/transfer/facade')
|
|
30
|
+
const ParticipantPositionChangesModel = require('../../models/position/participantPositionChanges')
|
|
31
|
+
const { fxTransfer, watchList } = require('../../models/fxTransfer')
|
|
32
|
+
const Config = require('../../lib/config')
|
|
33
|
+
const ProxyCache = require('../../lib/proxyCache')
|
|
34
|
+
|
|
35
|
+
const checkIfDeterminingTransferExistsForTransferMessage = async (payload, proxyObligation) => {
|
|
36
|
+
// Does this determining transfer ID appear on the watch list?
|
|
37
|
+
const watchListRecords = await watchList.getItemsInWatchListByDeterminingTransferId(payload.transferId)
|
|
38
|
+
const determiningTransferExistsInWatchList = (watchListRecords !== null && watchListRecords.length > 0)
|
|
39
|
+
// Create a list of participants and currencies to validate against
|
|
40
|
+
const participantCurrencyValidationList = []
|
|
41
|
+
if (determiningTransferExistsInWatchList) {
|
|
42
|
+
// If there's a currency conversion before the transfer is requested, it must be the debtor who did it.
|
|
43
|
+
if (!proxyObligation.isCounterPartyFspProxy) {
|
|
44
|
+
participantCurrencyValidationList.push({
|
|
45
|
+
participantName: payload.payeeFsp,
|
|
46
|
+
currencyId: payload.amount.currency
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
// Normal transfer request or payee side currency conversion
|
|
51
|
+
if (!proxyObligation.isInitiatingFspProxy) {
|
|
52
|
+
participantCurrencyValidationList.push({
|
|
53
|
+
participantName: payload.payerFsp,
|
|
54
|
+
currencyId: payload.amount.currency
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
// If it is a normal transfer, we need to validate payeeFsp against the currency of the transfer.
|
|
58
|
+
// But its tricky to differentiate between normal transfer and payee side currency conversion.
|
|
59
|
+
if (Config.PAYEE_PARTICIPANT_CURRENCY_VALIDATION_ENABLED) {
|
|
60
|
+
if (!proxyObligation.isCounterPartyFspProxy) {
|
|
61
|
+
participantCurrencyValidationList.push({
|
|
62
|
+
participantName: payload.payeeFsp,
|
|
63
|
+
currencyId: payload.amount.currency
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
determiningTransferExistsInWatchList,
|
|
70
|
+
watchListRecords,
|
|
71
|
+
participantCurrencyValidationList
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const checkIfDeterminingTransferExistsForFxTransferMessage = async (payload, proxyObligation) => {
|
|
76
|
+
// Does this determining transfer ID appear on the transfer list?
|
|
77
|
+
const transferRecord = await TransferModel.getById(payload.determiningTransferId)
|
|
78
|
+
const determiningTransferExistsInTransferList = (transferRecord !== null)
|
|
79
|
+
// We need to validate counterPartyFsp (FXP) against both source and target currencies anyway
|
|
80
|
+
const participantCurrencyValidationList = [
|
|
81
|
+
{
|
|
82
|
+
participantName: payload.counterPartyFsp,
|
|
83
|
+
currencyId: payload.sourceAmount.currency
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
// If a proxy is representing a FXP in a jurisdictional scenario,
|
|
87
|
+
// they would not hold a position account for the `targetAmount` currency
|
|
88
|
+
// for a /fxTransfer. So we skip adding this to accounts to be validated.
|
|
89
|
+
if (!proxyObligation.isCounterPartyFspProxy) {
|
|
90
|
+
participantCurrencyValidationList.push({
|
|
91
|
+
participantName: payload.counterPartyFsp,
|
|
92
|
+
currencyId: payload.targetAmount.currency
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
if (determiningTransferExistsInTransferList) {
|
|
96
|
+
// If there's a currency conversion which is not the first message, then it must be issued by the creditor party
|
|
97
|
+
participantCurrencyValidationList.push({
|
|
98
|
+
participantName: payload.initiatingFsp,
|
|
99
|
+
currencyId: payload.targetAmount.currency
|
|
100
|
+
})
|
|
101
|
+
} else {
|
|
102
|
+
// If there's a currency conversion before the transfer is requested, then it must be issued by the debtor party
|
|
103
|
+
participantCurrencyValidationList.push({
|
|
104
|
+
participantName: payload.initiatingFsp,
|
|
105
|
+
currencyId: payload.sourceAmount.currency
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
determiningTransferExistsInTransferList,
|
|
110
|
+
transferRecord,
|
|
111
|
+
participantCurrencyValidationList
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const getParticipantAndCurrencyForTransferMessage = async (payload, determiningTransferCheckResult, proxyObligation) => {
|
|
116
|
+
const histTimer = Metrics.getHistogram(
|
|
117
|
+
'fx_domain_cyril_getParticipantAndCurrencyForTransferMessage',
|
|
118
|
+
'fx_domain_cyril_getParticipantAndCurrencyForTransferMessage - Metrics for fx cyril',
|
|
119
|
+
['success', 'determiningTransferExists']
|
|
120
|
+
).startTimer()
|
|
121
|
+
|
|
122
|
+
let participantName, currencyId, amount
|
|
123
|
+
|
|
124
|
+
if (determiningTransferCheckResult.determiningTransferExistsInWatchList) {
|
|
125
|
+
// If there's a currency conversion before the transfer is requested, it must be the debtor who did it.
|
|
126
|
+
// Get the FX request corresponding to this transaction ID
|
|
127
|
+
let fxTransferRecord
|
|
128
|
+
if (proxyObligation.isCounterPartyFspProxy) {
|
|
129
|
+
// If a proxy is representing a FXP in a jurisdictional scenario,
|
|
130
|
+
// they would not hold a position account for the `targetAmount` currency
|
|
131
|
+
// for a /fxTransfer. So we skip adding this to accounts to be validated.
|
|
132
|
+
fxTransferRecord = await fxTransfer.getAllDetailsByCommitRequestIdForProxiedFxTransfer(determiningTransferCheckResult.watchListRecords[0].commitRequestId)
|
|
133
|
+
} else {
|
|
134
|
+
fxTransferRecord = await fxTransfer.getAllDetailsByCommitRequestId(determiningTransferCheckResult.watchListRecords[0].commitRequestId)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Liquidity check and reserve funds against FXP in FX target currency
|
|
138
|
+
participantName = fxTransferRecord.counterPartyFspName
|
|
139
|
+
currencyId = fxTransferRecord.targetCurrency
|
|
140
|
+
amount = fxTransferRecord.targetAmount
|
|
141
|
+
} else {
|
|
142
|
+
// Normal transfer request or payee side currency conversion
|
|
143
|
+
// Liquidity check and reserve against payer
|
|
144
|
+
participantName = payload.payerFsp
|
|
145
|
+
currencyId = payload.amount.currency
|
|
146
|
+
amount = payload.amount.amount
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
histTimer({ success: true, determiningTransferExists: determiningTransferCheckResult.determiningTransferExistsInWatchList })
|
|
150
|
+
return {
|
|
151
|
+
participantName,
|
|
152
|
+
currencyId,
|
|
153
|
+
amount
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const getParticipantAndCurrencyForFxTransferMessage = async (payload, determiningTransferCheckResult) => {
|
|
158
|
+
const histTimer = Metrics.getHistogram(
|
|
159
|
+
'fx_domain_cyril_getParticipantAndCurrencyForFxTransferMessage',
|
|
160
|
+
'fx_domain_cyril_getParticipantAndCurrencyForFxTransferMessage - Metrics for fx cyril',
|
|
161
|
+
['success', 'determiningTransferExists']
|
|
162
|
+
).startTimer()
|
|
163
|
+
|
|
164
|
+
let participantName, currencyId, amount
|
|
165
|
+
|
|
166
|
+
if (determiningTransferCheckResult.determiningTransferExistsInTransferList) {
|
|
167
|
+
// If there's a currency conversion which is not the first message, then it must be issued by the creditor party
|
|
168
|
+
// Liquidity check and reserve funds against FXP in FX target currency
|
|
169
|
+
participantName = payload.counterPartyFsp
|
|
170
|
+
currencyId = payload.targetAmount.currency
|
|
171
|
+
amount = payload.targetAmount.amount
|
|
172
|
+
await watchList.addToWatchList({
|
|
173
|
+
commitRequestId: payload.commitRequestId,
|
|
174
|
+
determiningTransferId: payload.determiningTransferId,
|
|
175
|
+
fxTransferTypeId: Enum.Fx.FxTransferType.PAYEE_CONVERSION
|
|
176
|
+
})
|
|
177
|
+
} else {
|
|
178
|
+
// If there's a currency conversion before the transfer is requested, then it must be issued by the debtor party
|
|
179
|
+
// Liquidity check and reserve funds against requester in FX source currency
|
|
180
|
+
participantName = payload.initiatingFsp
|
|
181
|
+
currencyId = payload.sourceAmount.currency
|
|
182
|
+
amount = payload.sourceAmount.amount
|
|
183
|
+
await watchList.addToWatchList({
|
|
184
|
+
commitRequestId: payload.commitRequestId,
|
|
185
|
+
determiningTransferId: payload.determiningTransferId,
|
|
186
|
+
fxTransferTypeId: Enum.Fx.FxTransferType.PAYER_CONVERSION
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
histTimer({ success: true, determiningTransferExists: determiningTransferCheckResult.determiningTransferExistsInTransferList })
|
|
191
|
+
return {
|
|
192
|
+
participantName,
|
|
193
|
+
currencyId,
|
|
194
|
+
amount
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const processFxFulfilMessage = async (commitRequestId) => {
|
|
199
|
+
const histTimer = Metrics.getHistogram(
|
|
200
|
+
'fx_domain_cyril_processFxFulfilMessage',
|
|
201
|
+
'fx_domain_cyril_processFxFulfilMessage - Metrics for fx cyril',
|
|
202
|
+
['success']
|
|
203
|
+
).startTimer()
|
|
204
|
+
// Does this commitRequestId appear on the watch list?
|
|
205
|
+
const watchListRecord = await watchList.getItemInWatchListByCommitRequestId(commitRequestId)
|
|
206
|
+
if (!watchListRecord) {
|
|
207
|
+
throw new Error(`Commit request ID ${commitRequestId} not found in watch list`)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// TODO: May need to update the watchList record to indicate that the fxTransfer has been fulfilled
|
|
211
|
+
|
|
212
|
+
histTimer({ success: true })
|
|
213
|
+
return true
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @typedef {Object} PositionChangeItem
|
|
218
|
+
*
|
|
219
|
+
* @property {boolean} isFxTransferStateChange - Indicates whether the position change is related to an FX transfer.
|
|
220
|
+
* @property {string} [commitRequestId] - commitRequestId for the position change (only for FX transfers).
|
|
221
|
+
* @property {string} [transferId] - transferId for the position change (only for normal transfers).
|
|
222
|
+
* @property {string} notifyTo - The FSP to notify about the position change.
|
|
223
|
+
* @property {number} participantCurrencyId - The ID of the participant's currency involved in the position change.
|
|
224
|
+
* @property {number} amount - The amount of the position change, represented as a negative value.
|
|
225
|
+
*/
|
|
226
|
+
/**
|
|
227
|
+
* Retrieves position changes based on a list of commitRequestIds and transferIds.
|
|
228
|
+
*
|
|
229
|
+
* @param {Array<string>} commitRequestIdList - List of commit request IDs to retrieve FX-related position changes.
|
|
230
|
+
* @param {Array<string>} transferIdList - List of transfer IDs to retrieve regular transfer-related position changes.
|
|
231
|
+
* @returns {Promise<PositionChangeItem[]>} - A promise that resolves to an array of position change objects.
|
|
232
|
+
*/
|
|
233
|
+
const _getPositionChanges = async (commitRequestIdList, transferIdList) => {
|
|
234
|
+
const positionChanges = []
|
|
235
|
+
for (const commitRequestId of commitRequestIdList) {
|
|
236
|
+
const fxRecord = await fxTransfer.getAllDetailsByCommitRequestIdForProxiedFxTransfer(commitRequestId)
|
|
237
|
+
const fxPositionChanges = await ParticipantPositionChangesModel.getReservedPositionChangesByCommitRequestId(commitRequestId)
|
|
238
|
+
fxPositionChanges.forEach((fxPositionChange) => {
|
|
239
|
+
positionChanges.push({
|
|
240
|
+
isFxTransferStateChange: true,
|
|
241
|
+
commitRequestId,
|
|
242
|
+
notifyTo: fxRecord.externalInitiatingFspName || fxRecord.initiatingFspName,
|
|
243
|
+
participantCurrencyId: fxPositionChange.participantCurrencyId,
|
|
244
|
+
amount: -fxPositionChange.change
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (const transferId of transferIdList) {
|
|
250
|
+
const transferRecord = await TransferFacade.getById(transferId)
|
|
251
|
+
const transferPositionChanges = await ParticipantPositionChangesModel.getReservedPositionChangesByTransferId(transferId)
|
|
252
|
+
transferPositionChanges.forEach((transferPositionChange) => {
|
|
253
|
+
positionChanges.push({
|
|
254
|
+
isFxTransferStateChange: false,
|
|
255
|
+
transferId,
|
|
256
|
+
notifyTo: transferRecord.externalPayerName || transferRecord.payerFsp,
|
|
257
|
+
participantCurrencyId: transferPositionChange.participantCurrencyId,
|
|
258
|
+
amount: -transferPositionChange.change
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return positionChanges
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @returns {Promise<{positionChanges: PositionChangeItem[]}>}
|
|
268
|
+
*/
|
|
269
|
+
const processFxAbortMessage = async (commitRequestId) => {
|
|
270
|
+
const histTimer = Metrics.getHistogram(
|
|
271
|
+
'fx_domain_cyril_processFxAbortMessage',
|
|
272
|
+
'fx_domain_cyril_processFxAbortMessage - Metrics for fx cyril',
|
|
273
|
+
['success']
|
|
274
|
+
).startTimer()
|
|
275
|
+
|
|
276
|
+
// Get the fxTransfer record
|
|
277
|
+
const fxTransferRecord = await fxTransfer.getByCommitRequestId(commitRequestId)
|
|
278
|
+
// const fxTransferRecord = await fxTransfer.getAllDetailsByCommitRequestId(commitRequestId)
|
|
279
|
+
// In case of reference currency, there might be multiple fxTransfers associated with a transfer.
|
|
280
|
+
const relatedFxTransferRecords = await fxTransfer.getByDeterminingTransferId(fxTransferRecord.determiningTransferId)
|
|
281
|
+
|
|
282
|
+
// Get position changes
|
|
283
|
+
const positionChanges = await _getPositionChanges(relatedFxTransferRecords.map(item => item.commitRequestId), [fxTransferRecord.determiningTransferId])
|
|
284
|
+
|
|
285
|
+
histTimer({ success: true })
|
|
286
|
+
return {
|
|
287
|
+
positionChanges
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const processAbortMessage = async (transferId) => {
|
|
292
|
+
const histTimer = Metrics.getHistogram(
|
|
293
|
+
'fx_domain_cyril_processAbortMessage',
|
|
294
|
+
'fx_domain_cyril_processAbortMessage - Metrics for fx cyril',
|
|
295
|
+
['success']
|
|
296
|
+
).startTimer()
|
|
297
|
+
|
|
298
|
+
// Get all related fxTransfers
|
|
299
|
+
const relatedFxTransferRecords = await fxTransfer.getByDeterminingTransferId(transferId)
|
|
300
|
+
|
|
301
|
+
// Get position changes
|
|
302
|
+
const positionChanges = await _getPositionChanges(relatedFxTransferRecords.map(item => item.commitRequestId), [transferId])
|
|
303
|
+
|
|
304
|
+
histTimer({ success: true })
|
|
305
|
+
return {
|
|
306
|
+
positionChanges
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const processFulfilMessage = async (transferId, payload, transfer) => {
|
|
311
|
+
const histTimer = Metrics.getHistogram(
|
|
312
|
+
'fx_domain_cyril_processFulfilMessage',
|
|
313
|
+
'fx_domain_cyril_processFulfilMessage - Metrics for fx cyril',
|
|
314
|
+
['success']
|
|
315
|
+
).startTimer()
|
|
316
|
+
// Let's define a format for the function result
|
|
317
|
+
const result = {
|
|
318
|
+
isFx: false,
|
|
319
|
+
positionChanges: [],
|
|
320
|
+
patchNotifications: []
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Does this transferId appear on the watch list?
|
|
324
|
+
const watchListRecords = await watchList.getItemsInWatchListByDeterminingTransferId(transferId)
|
|
325
|
+
if (watchListRecords && watchListRecords.length > 0) {
|
|
326
|
+
result.isFx = true
|
|
327
|
+
|
|
328
|
+
// TODO: Sense check: Are all entries on the watchlist marked as RESERVED?
|
|
329
|
+
|
|
330
|
+
// Loop around watch list
|
|
331
|
+
let sendingFxpExists = false
|
|
332
|
+
let receivingFxpExists = false
|
|
333
|
+
let sendingFxpRecord = null
|
|
334
|
+
let receivingFxpRecord = null
|
|
335
|
+
for (const watchListRecord of watchListRecords) {
|
|
336
|
+
const fxTransferRecord = await fxTransfer.getAllDetailsByCommitRequestIdForProxiedFxTransfer(watchListRecord.commitRequestId)
|
|
337
|
+
// Original Plan: If the reservation is against the FXP, then this is a conversion at the creditor. Mark FXP as receiving FXP
|
|
338
|
+
// The above condition is not required as we are setting the fxTransferType in the watchList beforehand
|
|
339
|
+
if (watchListRecord.fxTransferTypeId === Enum.Fx.FxTransferType.PAYEE_CONVERSION) {
|
|
340
|
+
receivingFxpExists = true
|
|
341
|
+
receivingFxpRecord = fxTransferRecord
|
|
342
|
+
// Create obligation between FXP and FX requesting party in currency of reservation
|
|
343
|
+
// Find out the participantCurrencyId of the initiatingFsp
|
|
344
|
+
// The following is hardcoded for Payer side conversion with SEND amountType.
|
|
345
|
+
const proxyParticipantAccountDetails = await ProxyCache.getProxyParticipantAccountDetails(fxTransferRecord.initiatingFspName, fxTransferRecord.targetCurrency)
|
|
346
|
+
if (proxyParticipantAccountDetails.participantCurrencyId) {
|
|
347
|
+
result.positionChanges.push({
|
|
348
|
+
isFxTransferStateChange: false,
|
|
349
|
+
transferId,
|
|
350
|
+
participantCurrencyId: proxyParticipantAccountDetails.participantCurrencyId,
|
|
351
|
+
amount: -fxTransferRecord.targetAmount
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
// TODO: Send PATCH notification to FXP
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Original Plan: If the reservation is against the DFSP, then this is a conversion at the debtor. Mark FXP as sending FXP
|
|
358
|
+
// The above condition is not required as we are setting the fxTransferType in the watchList beforehand
|
|
359
|
+
if (watchListRecord.fxTransferTypeId === Enum.Fx.FxTransferType.PAYER_CONVERSION) {
|
|
360
|
+
sendingFxpExists = true
|
|
361
|
+
sendingFxpRecord = fxTransferRecord
|
|
362
|
+
// Create obligation between FX requesting party and FXP in currency of reservation
|
|
363
|
+
const proxyParticipantAccountDetails = await ProxyCache.getProxyParticipantAccountDetails(fxTransferRecord.counterPartyFspName, fxTransferRecord.sourceCurrency)
|
|
364
|
+
if (proxyParticipantAccountDetails.participantCurrencyId) {
|
|
365
|
+
result.positionChanges.push({
|
|
366
|
+
isFxTransferStateChange: true,
|
|
367
|
+
commitRequestId: fxTransferRecord.commitRequestId,
|
|
368
|
+
participantCurrencyId: proxyParticipantAccountDetails.participantCurrencyId,
|
|
369
|
+
amount: -fxTransferRecord.sourceAmount
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
result.patchNotifications.push({
|
|
373
|
+
commitRequestId: watchListRecord.commitRequestId,
|
|
374
|
+
fxpName: fxTransferRecord.counterPartyFspName,
|
|
375
|
+
fulfilment: fxTransferRecord.fulfilment,
|
|
376
|
+
completedTimestamp: fxTransferRecord.completedTimestamp
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!sendingFxpExists && !receivingFxpExists) {
|
|
382
|
+
// If there are no sending and receiving fxp, throw an error
|
|
383
|
+
throw new Error(`Required records not found in watch list for transfer ID ${transferId}`)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (sendingFxpExists && receivingFxpExists) {
|
|
387
|
+
// If we have both a sending and a receiving FXP, Create obligation between sending and receiving FXP in currency of transfer.
|
|
388
|
+
const proxyParticipantAccountDetails = await ProxyCache.getProxyParticipantAccountDetails(receivingFxpRecord.counterPartyFspName, receivingFxpRecord.sourceCurrency)
|
|
389
|
+
if (proxyParticipantAccountDetails.participantCurrencyId) {
|
|
390
|
+
result.positionChanges.push({
|
|
391
|
+
isFxTransferStateChange: true,
|
|
392
|
+
commitRequestId: receivingFxpRecord.commitRequestId,
|
|
393
|
+
participantCurrencyId: proxyParticipantAccountDetails.participantCurrencyId,
|
|
394
|
+
amount: -receivingFxpRecord.sourceAmount
|
|
395
|
+
})
|
|
396
|
+
}
|
|
397
|
+
} else if (sendingFxpExists) {
|
|
398
|
+
// If we have a sending FXP, Create obligation between FXP and creditor party to the transfer in currency of FX transfer
|
|
399
|
+
// Get participantCurrencyId for transfer.payeeParticipantId/transfer.payeeFsp and sendingFxpRecord.targetCurrency
|
|
400
|
+
const proxyParticipantAccountDetails = await ProxyCache.getProxyParticipantAccountDetails(transfer.payeeFsp, sendingFxpRecord.targetCurrency)
|
|
401
|
+
if (proxyParticipantAccountDetails.participantCurrencyId) {
|
|
402
|
+
let isPositionChange = false
|
|
403
|
+
if (proxyParticipantAccountDetails.inScheme) {
|
|
404
|
+
isPositionChange = true
|
|
405
|
+
} else {
|
|
406
|
+
// We are not expecting this. Payee participant is a proxy and have an account in the targetCurrency.
|
|
407
|
+
// In this case we need to check if FXP is also a proxy and have the same account as payee.
|
|
408
|
+
const proxyParticipantAccountDetails2 = await ProxyCache.getProxyParticipantAccountDetails(sendingFxpRecord.counterPartyFspName, sendingFxpRecord.targetCurrency)
|
|
409
|
+
if (!proxyParticipantAccountDetails2.inScheme && (proxyParticipantAccountDetails.participantCurrencyId !== proxyParticipantAccountDetails2.participantCurrencyId)) {
|
|
410
|
+
isPositionChange = true
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (isPositionChange) {
|
|
414
|
+
result.positionChanges.push({
|
|
415
|
+
isFxTransferStateChange: false,
|
|
416
|
+
transferId,
|
|
417
|
+
participantCurrencyId: proxyParticipantAccountDetails.participantCurrencyId,
|
|
418
|
+
amount: -sendingFxpRecord.targetAmount
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} else if (receivingFxpExists) {
|
|
423
|
+
// If we have a receiving FXP, Create obligation between debtor party to the transfer and FXP in currency of transfer
|
|
424
|
+
const proxyParticipantAccountDetails = await ProxyCache.getProxyParticipantAccountDetails(receivingFxpRecord.counterPartyFspName, receivingFxpRecord.sourceCurrency)
|
|
425
|
+
if (proxyParticipantAccountDetails.participantCurrencyId) {
|
|
426
|
+
let isPositionChange = false
|
|
427
|
+
if (proxyParticipantAccountDetails.inScheme) {
|
|
428
|
+
isPositionChange = true
|
|
429
|
+
} else {
|
|
430
|
+
// We are not expecting this. FXP participant is a proxy and have an account in the sourceCurrency.
|
|
431
|
+
// In this case we need to check if Payer is also a proxy and have the same account as FXP.
|
|
432
|
+
const proxyParticipantAccountDetails2 = await ProxyCache.getProxyParticipantAccountDetails(transfer.payerFsp, receivingFxpRecord.sourceCurrency)
|
|
433
|
+
if (!proxyParticipantAccountDetails2.inScheme && (proxyParticipantAccountDetails.participantCurrencyId !== proxyParticipantAccountDetails2.participantCurrencyId)) {
|
|
434
|
+
isPositionChange = true
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (isPositionChange) {
|
|
438
|
+
result.positionChanges.push({
|
|
439
|
+
isFxTransferStateChange: true,
|
|
440
|
+
commitRequestId: receivingFxpRecord.commitRequestId,
|
|
441
|
+
participantCurrencyId: proxyParticipantAccountDetails.participantCurrencyId,
|
|
442
|
+
amount: -receivingFxpRecord.sourceAmount
|
|
443
|
+
})
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// TODO: Remove entries from watchlist
|
|
449
|
+
} else {
|
|
450
|
+
// Normal transfer request, just return isFx = false
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
histTimer({ success: true })
|
|
454
|
+
return result
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
module.exports = {
|
|
458
|
+
getParticipantAndCurrencyForTransferMessage,
|
|
459
|
+
getParticipantAndCurrencyForFxTransferMessage,
|
|
460
|
+
processFxFulfilMessage,
|
|
461
|
+
processFxAbortMessage,
|
|
462
|
+
processFulfilMessage,
|
|
463
|
+
processAbortMessage,
|
|
464
|
+
checkIfDeterminingTransferExistsForTransferMessage,
|
|
465
|
+
checkIfDeterminingTransferExistsForFxTransferMessage
|
|
466
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
* Vijay Kumar Guthi <vijaya.guthi@infitx.com>
|
|
22
|
+
--------------
|
|
23
|
+
******/
|
|
24
|
+
|
|
25
|
+
'use strict'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @module src/domain/transfer/
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const ErrorHandler = require('@mojaloop/central-services-error-handling')
|
|
32
|
+
const Metrics = require('@mojaloop/central-services-metrics')
|
|
33
|
+
const FxTransferModel = require('../../models/fxTransfer')
|
|
34
|
+
// const TransferObjectTransform = require('./transform')
|
|
35
|
+
const Cyril = require('./cyril')
|
|
36
|
+
|
|
37
|
+
const handleFulfilResponse = async (transferId, payload, action, fspiopError) => {
|
|
38
|
+
const histTimerTransferServiceHandlePayeeResponseEnd = Metrics.getHistogram(
|
|
39
|
+
'fx_domain_transfer',
|
|
40
|
+
'prepare - Metrics for fx transfer domain',
|
|
41
|
+
['success', 'funcName']
|
|
42
|
+
).startTimer()
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await FxTransferModel.fxTransfer.saveFxFulfilResponse(transferId, payload, action, fspiopError)
|
|
46
|
+
// TODO: Need to return a result if we need
|
|
47
|
+
// const result = TransferObjectTransform.toTransfer(fxTransfer)
|
|
48
|
+
const result = {}
|
|
49
|
+
histTimerTransferServiceHandlePayeeResponseEnd({ success: true, funcName: 'handleFulfilResponse' })
|
|
50
|
+
return result
|
|
51
|
+
} catch (err) {
|
|
52
|
+
histTimerTransferServiceHandlePayeeResponseEnd({ success: false, funcName: 'handleFulfilResponse' })
|
|
53
|
+
throw ErrorHandler.Factory.reformatFSPIOPError(err)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const forwardedFxPrepare = async (commitRequestId) => {
|
|
58
|
+
const histTimerTransferServicePrepareEnd = Metrics.getHistogram(
|
|
59
|
+
'fx_domain_transfer',
|
|
60
|
+
'prepare - Metrics for fx transfer domain',
|
|
61
|
+
['success', 'funcName']
|
|
62
|
+
).startTimer()
|
|
63
|
+
try {
|
|
64
|
+
const result = await FxTransferModel.fxTransfer.updateFxPrepareReservedForwarded(commitRequestId)
|
|
65
|
+
histTimerTransferServicePrepareEnd({ success: true, funcName: 'forwardedFxPrepare' })
|
|
66
|
+
return result
|
|
67
|
+
} catch (err) {
|
|
68
|
+
histTimerTransferServicePrepareEnd({ success: false, funcName: 'forwardedFxPrepare' })
|
|
69
|
+
throw ErrorHandler.Factory.reformatFSPIOPError(err)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// TODO: Need to implement this for fxTransferError
|
|
74
|
+
// /**
|
|
75
|
+
// * @function LogFxTransferError
|
|
76
|
+
// *
|
|
77
|
+
// * @async
|
|
78
|
+
// * @description This will insert a record into the fxTransferError table for the latest fxTransfer stage change id.
|
|
79
|
+
// *
|
|
80
|
+
// * FxTransferModel.stateChange.getByCommitRequestId called to get the latest fx transfer state change id
|
|
81
|
+
// * FxTransferModel.error.insert called to insert the record into the fxTransferError table
|
|
82
|
+
// *
|
|
83
|
+
// * @param {string} commitRequestId - the transfer id
|
|
84
|
+
// * @param {integer} errorCode - the error code
|
|
85
|
+
// * @param {string} errorDescription - the description error
|
|
86
|
+
// *
|
|
87
|
+
// * @returns {integer} - Returns the id of the transferError record if successful, or throws an error if failed
|
|
88
|
+
// */
|
|
89
|
+
|
|
90
|
+
// const logFxTransferError = async (commitRequestId, errorCode, errorDescription) => {
|
|
91
|
+
// try {
|
|
92
|
+
// const transferStateChange = await FxTransferModel.stateChange.getByCommitRequestId(commitRequestId)
|
|
93
|
+
// return FxTransferModel.error.insert(commitRequestId, transferStateChange.fxTransferStateChangeId, errorCode, errorDescription)
|
|
94
|
+
// } catch (err) {
|
|
95
|
+
// throw ErrorHandler.Factory.reformatFSPIOPError(err)
|
|
96
|
+
// }
|
|
97
|
+
// }
|
|
98
|
+
|
|
99
|
+
const TransferService = {
|
|
100
|
+
handleFulfilResponse,
|
|
101
|
+
forwardedFxPrepare,
|
|
102
|
+
getByIdLight: FxTransferModel.fxTransfer.getByIdLight,
|
|
103
|
+
// logFxTransferError,
|
|
104
|
+
Cyril
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = TransferService
|