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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/.ncurc.yaml +3 -1
  2. package/.nvmrc +1 -1
  3. package/.nycrc.yml +16 -1
  4. package/Dockerfile +9 -10
  5. package/README.md +39 -9
  6. package/audit-ci.jsonc +15 -2
  7. package/config/default.json +17 -1
  8. package/docker/central-ledger/default.json +9 -0
  9. package/docker/config-modifier/configs/central-ledger.js +25 -0
  10. package/docker/env.sh +15 -0
  11. package/docker/kafka/scripts/provision.sh +4 -1
  12. package/docker/ml-api-adapter/default.json +4 -1
  13. package/docker-compose.yml +90 -2
  14. package/documentation/db/erd-transfer-timeout.png +0 -0
  15. package/documentation/db/erd-transfer-timeout.txt +81 -0
  16. package/documentation/fx-implementation/README.md +48 -0
  17. package/documentation/fx-implementation/assets/fx-position-movements.drawio.svg +4 -0
  18. package/documentation/fx-implementation/assets/test-scenario.drawio.svg +4 -0
  19. package/documentation/sequence-diagrams/Handler - FX timeout.plantuml +123 -0
  20. package/documentation/sequence-diagrams/Handler - FX timeout.png +0 -0
  21. package/documentation/sequence-diagrams/Handler - timeout.plantuml +81 -0
  22. package/documentation/sequence-diagrams/Handler - timeout.png +0 -0
  23. package/documentation/state-diagrams/transfer-ML-spec-states-diagram.png +0 -0
  24. package/documentation/state-diagrams/transfer-internal-states-diagram.png +0 -0
  25. package/documentation/state-diagrams/transfer-internal-states.plantuml +75 -0
  26. package/documentation/state-diagrams/transfer-states.plantuml +13 -0
  27. package/migrations/310204_transferParticipant-participantId.js +52 -0
  28. package/migrations/310403_participantPositionChange-participantCurrencyId.js +47 -0
  29. package/migrations/310404_participantPositionChange-change.js +46 -0
  30. package/migrations/600010_fxTransferType.js +43 -0
  31. package/migrations/600011_fxTransferType-indexes.js +38 -0
  32. package/migrations/600012_fxParticipantCurrencyType.js +43 -0
  33. package/migrations/600013_fxParticipantCurrencyType-indexes.js +38 -0
  34. package/migrations/600100_fxTransferDuplicateCheck.js +42 -0
  35. package/migrations/600110_fxTransferErrorDuplicateCheck.js.js +17 -0
  36. package/migrations/600200_fxTransfer.js +51 -0
  37. package/migrations/600201_fxTransfer-indexes.js +40 -0
  38. package/migrations/600400_fxTransferStateChange.js +46 -0
  39. package/migrations/600401_fxTransferStateChange-indexes.js +40 -0
  40. package/migrations/600501_fxWatchList.js +46 -0
  41. package/migrations/600502_fxWatchList-indexes.js +40 -0
  42. package/migrations/600600_fxTransferFulfilmentDuplicateCheck.js +43 -0
  43. package/migrations/600601_fxTransferFulfilmentDuplicateCheck-indexes.js +38 -0
  44. package/migrations/600700_fxTransferFulfilment.js +47 -0
  45. package/migrations/600701_fxTransferFulfilment-indexes.js +43 -0
  46. package/migrations/600800_fxTransferExtension.js +47 -0
  47. package/migrations/601400_fxTransferTimeout.js +43 -0
  48. package/migrations/601401_fxTransferTimeout-indexes.js +37 -0
  49. package/migrations/601500_fxTransferError.js +44 -0
  50. package/migrations/601501_fxTransferError-indexes.js +37 -0
  51. package/migrations/610200_fxTransferParticipant.js +52 -0
  52. package/migrations/610201_fxTransferParticipant-indexes.js +44 -0
  53. package/migrations/610202_fxTransferParticipant-participantId.js +52 -0
  54. package/migrations/610403_participantPositionChange-fxTransfer.js +46 -0
  55. package/migrations/910101_feature904DataMigration.js +46 -52
  56. package/migrations/910102_feature949DataMigration.js +219 -225
  57. package/migrations/950104_settlementModel-settlementAccountTypeId.js +15 -20
  58. package/migrations/950108_participantProxy.js +18 -0
  59. package/migrations/950109_fxQuote.js +19 -0
  60. package/migrations/950110_fxQuoteResponse.js +25 -0
  61. package/migrations/950111_fxQuoteError.js +23 -0
  62. package/migrations/950113_fxQuoteDuplicateCheck.js +18 -0
  63. package/migrations/950114_fxQuoteResponseDuplicateCheck.js +21 -0
  64. package/migrations/950115_fxQuoteConversionTerms.js +36 -0
  65. package/migrations/950116_fxQuoteConversionTermsExtension.js +21 -0
  66. package/migrations/950117_fxQuoteResponseConversionTerms.js +39 -0
  67. package/migrations/950118_fxQuoteResponseConversionTermsExtension.js +21 -0
  68. package/migrations/950119_fxCharge.js +27 -0
  69. package/migrations/960100_create_externalParticipant.js +47 -0
  70. package/migrations/960110_alter_transferParticipant__addFiled_externalParticipantId.js +50 -0
  71. package/migrations/960111_alter_fxTransferParticipant__addFiled_externalParticipantId.js +50 -0
  72. package/package.json +27 -20
  73. package/seeds/endpointType.js +18 -0
  74. package/seeds/fxParticipantCurrencyType.js +45 -0
  75. package/seeds/fxTransferType.js +45 -0
  76. package/seeds/participant.js +2 -1
  77. package/seeds/transferParticipantRoleType.js +9 -0
  78. package/seeds/transferState.js +10 -0
  79. package/src/api/interface/swagger.json +23 -30
  80. package/src/api/participants/handler.js +6 -2
  81. package/src/api/participants/routes.js +8 -7
  82. package/src/api/root/handler.js +15 -5
  83. package/src/domain/fx/cyril.js +466 -0
  84. package/src/domain/fx/index.js +107 -0
  85. package/src/domain/participant/index.js +108 -1
  86. package/src/domain/position/abort.js +215 -0
  87. package/src/domain/position/binProcessor.js +361 -99
  88. package/src/domain/position/fulfil.js +252 -111
  89. package/src/domain/position/fx-fulfil.js +138 -0
  90. package/src/domain/position/fx-prepare.js +280 -0
  91. package/src/domain/position/fx-timeout-reserved.js +159 -0
  92. package/src/domain/position/index.js +1 -0
  93. package/src/domain/position/prepare.js +69 -49
  94. package/src/domain/position/timeout-reserved.js +162 -0
  95. package/src/domain/timeout/index.js +26 -2
  96. package/src/domain/transfer/index.js +22 -5
  97. package/src/domain/transfer/transform.js +19 -6
  98. package/src/handlers/admin/handler.js +0 -2
  99. package/src/handlers/bulk/fulfil/handler.js +5 -5
  100. package/src/handlers/bulk/get/handler.js +5 -5
  101. package/src/handlers/bulk/prepare/handler.js +9 -9
  102. package/src/handlers/bulk/processing/handler.js +8 -7
  103. package/src/handlers/bulk/shared/validator.js +1 -1
  104. package/src/handlers/positions/handler.js +23 -10
  105. package/src/handlers/positions/handlerBatch.js +54 -26
  106. package/src/handlers/register.js +2 -1
  107. package/src/handlers/timeouts/handler.js +212 -68
  108. package/src/handlers/transfers/FxFulfilService.js +387 -0
  109. package/src/handlers/transfers/createRemittanceEntity.js +106 -0
  110. package/src/handlers/transfers/dto.js +53 -0
  111. package/src/handlers/transfers/handler.js +609 -571
  112. package/src/handlers/transfers/prepare.js +572 -0
  113. package/src/handlers/transfers/validator.js +75 -14
  114. package/src/lib/cache.js +1 -1
  115. package/src/lib/config.js +6 -3
  116. package/src/lib/healthCheck/subServiceHealth.js +12 -2
  117. package/src/lib/proxyCache.js +131 -0
  118. package/src/models/bulkTransfer/facade.js +58 -82
  119. package/src/models/fxTransfer/duplicateCheck.js +153 -0
  120. package/src/models/fxTransfer/fxTransfer.js +578 -0
  121. package/src/models/fxTransfer/fxTransferError.js +53 -0
  122. package/src/models/fxTransfer/fxTransferExtension.js +41 -0
  123. package/src/models/fxTransfer/fxTransferTimeout.js +68 -0
  124. package/src/models/fxTransfer/index.js +15 -0
  125. package/src/models/fxTransfer/stateChange.js +47 -0
  126. package/src/models/fxTransfer/watchList.js +49 -0
  127. package/src/models/ledgerAccountType/ledgerAccountType.js +8 -32
  128. package/src/models/misc/segment.js +0 -1
  129. package/src/models/participant/externalParticipant.js +96 -0
  130. package/src/models/participant/externalParticipantCached.js +148 -0
  131. package/src/models/participant/facade.js +192 -123
  132. package/src/models/participant/participant.js +2 -1
  133. package/src/models/participant/participantCurrency.js +1 -1
  134. package/src/models/participant/participantPosition.js +2 -8
  135. package/src/models/position/batch.js +90 -1
  136. package/src/models/position/facade.js +5 -5
  137. package/src/models/position/participantPositionChanges.js +68 -0
  138. package/src/models/settlement/settlementModel.js +5 -23
  139. package/src/models/transfer/facade.js +806 -447
  140. package/src/shared/constants.js +52 -0
  141. package/src/shared/fspiopErrorFactory.js +131 -0
  142. package/src/shared/logger/index.js +8 -0
  143. package/src/shared/loggingPlugin.js +43 -0
  144. package/src/shared/plugins.js +6 -0
  145. package/src/shared/setup.js +10 -0
  146. package/test-integration.Dockerfile +1 -1
  147. package/test.Dockerfile +1 -1
@@ -0,0 +1,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