@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,578 @@
1
+ const Metrics = require('@mojaloop/central-services-metrics')
2
+ const ErrorHandler = require('@mojaloop/central-services-error-handling')
3
+ const { Enum, Util } = require('@mojaloop/central-services-shared')
4
+ const Time = require('@mojaloop/central-services-shared').Util.Time
5
+ const TransferEventAction = Enum.Events.Event.Action
6
+
7
+ const { logger } = require('../../shared/logger')
8
+ const { TABLE_NAMES } = require('../../shared/constants')
9
+ const Db = require('../../lib/db')
10
+ const participant = require('../participant/facade')
11
+ const ParticipantCachedModel = require('../participant/participantCached')
12
+ const TransferExtensionModel = require('./fxTransferExtension')
13
+
14
+ const { TransferInternalState } = Enum.Transfers
15
+
16
+ const UnsupportedActionText = 'Unsupported action'
17
+
18
+ const getByCommitRequestId = async (commitRequestId) => {
19
+ logger.debug('get fxTransfer by commitRequestId:', { commitRequestId })
20
+ return Db.from(TABLE_NAMES.fxTransfer).findOne({ commitRequestId })
21
+ }
22
+
23
+ const getByDeterminingTransferId = async (determiningTransferId) => {
24
+ logger.debug('get fxTransfers by determiningTransferId:', { determiningTransferId })
25
+ return Db.from(TABLE_NAMES.fxTransfer).find({ determiningTransferId })
26
+ }
27
+
28
+ const saveFxTransfer = async (record) => {
29
+ logger.debug('save fxTransfer record:', { record })
30
+ return Db.from(TABLE_NAMES.fxTransfer).insert(record)
31
+ }
32
+
33
+ const getByIdLight = async (id) => {
34
+ try {
35
+ /** @namespace Db.fxTransfer **/
36
+ return await Db.from(TABLE_NAMES.fxTransfer).query(async (builder) => {
37
+ return builder
38
+ .where({ 'fxTransfer.commitRequestId': id })
39
+ .leftJoin('fxTransferStateChange AS tsc', 'tsc.commitRequestId', 'fxTransfer.commitRequestId')
40
+ .leftJoin('transferState AS ts', 'ts.transferStateId', 'tsc.transferStateId')
41
+ .leftJoin('fxTransferFulfilment AS tf', 'tf.commitRequestId', 'fxTransfer.commitRequestId')
42
+ .select(
43
+ 'fxTransfer.*',
44
+ 'tsc.fxTransferStateChangeId',
45
+ 'tsc.transferStateId AS fxTransferState',
46
+ 'ts.enumeration AS fxTransferStateEnumeration',
47
+ 'ts.description as fxTransferStateDescription',
48
+ 'tsc.reason AS reason',
49
+ 'tsc.createdDate AS completedTimestamp',
50
+ 'fxTransfer.ilpCondition AS condition',
51
+ 'tf.ilpFulfilment AS fulfilment'
52
+ )
53
+ .orderBy('tsc.fxTransferStateChangeId', 'desc')
54
+ .first()
55
+ })
56
+ } catch (err) {
57
+ throw ErrorHandler.Factory.reformatFSPIOPError(err)
58
+ }
59
+ }
60
+
61
+ const getAllDetailsByCommitRequestId = async (commitRequestId) => {
62
+ try {
63
+ /** @namespace Db.fxTransfer **/
64
+ return await Db.from('fxTransfer').query(async (builder) => {
65
+ const transferResult = await builder
66
+ .where({
67
+ 'fxTransfer.commitRequestId': commitRequestId,
68
+ 'tprt1.name': 'INITIATING_FSP',
69
+ 'tprt2.name': 'COUNTER_PARTY_FSP',
70
+ 'tprt3.name': 'COUNTER_PARTY_FSP',
71
+ 'fpct1.name': 'SOURCE',
72
+ 'fpct2.name': 'TARGET'
73
+ })
74
+ // INITIATING_FSP
75
+ .innerJoin('fxTransferParticipant AS tp1', 'tp1.commitRequestId', 'fxTransfer.commitRequestId')
76
+ .innerJoin('transferParticipantRoleType AS tprt1', 'tprt1.transferParticipantRoleTypeId', 'tp1.transferParticipantRoleTypeId')
77
+ .innerJoin('participant AS da', 'da.participantId', 'tp1.participantId')
78
+ // COUNTER_PARTY_FSP SOURCE currency
79
+ .innerJoin('fxTransferParticipant AS tp21', 'tp21.commitRequestId', 'fxTransfer.commitRequestId')
80
+ .innerJoin('transferParticipantRoleType AS tprt2', 'tprt2.transferParticipantRoleTypeId', 'tp21.transferParticipantRoleTypeId')
81
+ .innerJoin('fxParticipantCurrencyType AS fpct1', 'fpct1.fxParticipantCurrencyTypeId', 'tp21.fxParticipantCurrencyTypeId')
82
+ .innerJoin('participant AS ca', 'ca.participantId', 'tp21.participantId')
83
+ .leftJoin('participantCurrency AS pc21', 'pc21.participantCurrencyId', 'tp21.participantCurrencyId')
84
+ // COUNTER_PARTY_FSP TARGET currency
85
+ .innerJoin('fxTransferParticipant AS tp22', 'tp22.commitRequestId', 'fxTransfer.commitRequestId')
86
+ .innerJoin('transferParticipantRoleType AS tprt3', 'tprt3.transferParticipantRoleTypeId', 'tp22.transferParticipantRoleTypeId')
87
+ .innerJoin('fxParticipantCurrencyType AS fpct2', 'fpct2.fxParticipantCurrencyTypeId', 'tp22.fxParticipantCurrencyTypeId')
88
+ // .innerJoin('participantCurrency AS pc22', 'pc22.participantCurrencyId', 'tp22.participantCurrencyId')
89
+ // OTHER JOINS
90
+ .leftJoin('fxTransferStateChange AS tsc', 'tsc.commitRequestId', 'fxTransfer.commitRequestId')
91
+ .leftJoin('transferState AS ts', 'ts.transferStateId', 'tsc.transferStateId')
92
+ .leftJoin('fxTransferFulfilment AS tf', 'tf.commitRequestId', 'fxTransfer.commitRequestId')
93
+ // .leftJoin('transferError as te', 'te.commitRequestId', 'transfer.commitRequestId') // currently transferError.transferId is PK ensuring one error per transferId
94
+ .select(
95
+ 'fxTransfer.*',
96
+ 'da.participantId AS initiatingFspParticipantId',
97
+ 'da.name AS initiatingFspName',
98
+ 'da.isProxy AS initiatingFspIsProxy',
99
+ // 'pc21.participantCurrencyId AS counterPartyFspSourceParticipantCurrencyId',
100
+ // 'pc22.participantCurrencyId AS counterPartyFspTargetParticipantCurrencyId',
101
+ 'tp21.participantCurrencyId AS counterPartyFspSourceParticipantCurrencyId',
102
+ 'tp22.participantCurrencyId AS counterPartyFspTargetParticipantCurrencyId',
103
+ 'ca.participantId AS counterPartyFspParticipantId',
104
+ 'ca.name AS counterPartyFspName',
105
+ 'ca.isProxy AS counterPartyFspIsProxy',
106
+ 'tsc.fxTransferStateChangeId',
107
+ 'tsc.transferStateId AS transferState',
108
+ 'tsc.reason AS reason',
109
+ 'tsc.createdDate AS completedTimestamp',
110
+ 'ts.enumeration as transferStateEnumeration',
111
+ 'ts.description as transferStateDescription',
112
+ 'tf.ilpFulfilment AS fulfilment'
113
+ )
114
+ .orderBy('tsc.fxTransferStateChangeId', 'desc')
115
+ .first()
116
+ if (transferResult) {
117
+ transferResult.extensionList = await TransferExtensionModel.getByCommitRequestId(commitRequestId)
118
+ if (transferResult.errorCode && transferResult.transferStateEnumeration === Enum.Transfers.TransferState.ABORTED) {
119
+ if (!transferResult.extensionList) transferResult.extensionList = []
120
+ transferResult.extensionList.push({
121
+ key: 'cause',
122
+ value: `${transferResult.errorCode}: ${transferResult.errorDescription}`.substr(0, 128)
123
+ })
124
+ }
125
+ transferResult.isTransferReadModel = true
126
+ }
127
+ return transferResult
128
+ })
129
+ } catch (err) {
130
+ logger.warn('error in getAllDetailsByCommitRequestId', err)
131
+ throw ErrorHandler.Factory.reformatFSPIOPError(err)
132
+ }
133
+ }
134
+
135
+ // For proxied fxTransfers and transfers in a regional and jurisdictional scenario, proxy participants
136
+ // are not expected to have a target currency account, so we need a slightly altered version of the above function.
137
+ const getAllDetailsByCommitRequestIdForProxiedFxTransfer = async (commitRequestId) => {
138
+ try {
139
+ /** @namespace Db.fxTransfer **/
140
+ return await Db.from('fxTransfer').query(async (builder) => {
141
+ const transferResult = await builder
142
+ .where({
143
+ 'fxTransfer.commitRequestId': commitRequestId,
144
+ 'tprt1.name': 'INITIATING_FSP',
145
+ 'tprt2.name': 'COUNTER_PARTY_FSP',
146
+ 'fpct1.name': 'SOURCE'
147
+ })
148
+ // INITIATING_FSP
149
+ .innerJoin('fxTransferParticipant AS tp1', 'tp1.commitRequestId', 'fxTransfer.commitRequestId')
150
+ .leftJoin('externalParticipant AS ep1', 'ep1.externalParticipantId', 'tp1.externalParticipantId')
151
+ .innerJoin('transferParticipantRoleType AS tprt1', 'tprt1.transferParticipantRoleTypeId', 'tp1.transferParticipantRoleTypeId')
152
+ .innerJoin('participant AS da', 'da.participantId', 'tp1.participantId')
153
+ // COUNTER_PARTY_FSP SOURCE currency
154
+ .innerJoin('fxTransferParticipant AS tp21', 'tp21.commitRequestId', 'fxTransfer.commitRequestId')
155
+ .leftJoin('externalParticipant AS ep2', 'ep2.externalParticipantId', 'tp21.externalParticipantId')
156
+ .innerJoin('transferParticipantRoleType AS tprt2', 'tprt2.transferParticipantRoleTypeId', 'tp21.transferParticipantRoleTypeId')
157
+ .innerJoin('fxParticipantCurrencyType AS fpct1', 'fpct1.fxParticipantCurrencyTypeId', 'tp21.fxParticipantCurrencyTypeId')
158
+ .innerJoin('participant AS ca', 'ca.participantId', 'tp21.participantId')
159
+ .leftJoin('participantCurrency AS pc21', 'pc21.participantCurrencyId', 'tp21.participantCurrencyId')
160
+ // .innerJoin('participantCurrency AS pc22', 'pc22.participantCurrencyId', 'tp22.participantCurrencyId')
161
+ // OTHER JOINS
162
+ .leftJoin('fxTransferStateChange AS tsc', 'tsc.commitRequestId', 'fxTransfer.commitRequestId')
163
+ .leftJoin('transferState AS ts', 'ts.transferStateId', 'tsc.transferStateId')
164
+ .leftJoin('fxTransferFulfilment AS tf', 'tf.commitRequestId', 'fxTransfer.commitRequestId')
165
+ // .leftJoin('transferError as te', 'te.commitRequestId', 'transfer.commitRequestId') // currently transferError.transferId is PK ensuring one error per transferId
166
+ .select(
167
+ 'fxTransfer.*',
168
+ 'da.participantId AS initiatingFspParticipantId',
169
+ 'da.name AS initiatingFspName',
170
+ 'da.isProxy AS initiatingFspIsProxy',
171
+ // 'pc21.participantCurrencyId AS counterPartyFspSourceParticipantCurrencyId',
172
+ // 'pc22.participantCurrencyId AS counterPartyFspTargetParticipantCurrencyId',
173
+ 'tp21.participantCurrencyId AS counterPartyFspSourceParticipantCurrencyId',
174
+ 'ca.participantId AS counterPartyFspParticipantId',
175
+ 'ca.name AS counterPartyFspName',
176
+ 'ca.isProxy AS counterPartyFspIsProxy',
177
+ 'tsc.fxTransferStateChangeId',
178
+ 'tsc.transferStateId AS transferState',
179
+ 'tsc.reason AS reason',
180
+ 'tsc.createdDate AS completedTimestamp',
181
+ 'ts.enumeration as transferStateEnumeration',
182
+ 'ts.description as transferStateDescription',
183
+ 'tf.ilpFulfilment AS fulfilment',
184
+ 'ep1.name AS externalInitiatingFspName',
185
+ 'ep2.name AS externalCounterPartyFspName'
186
+ )
187
+ .orderBy('tsc.fxTransferStateChangeId', 'desc')
188
+ .first()
189
+
190
+ if (transferResult) {
191
+ transferResult.extensionList = await TransferExtensionModel.getByCommitRequestId(commitRequestId)
192
+ if (transferResult.errorCode && transferResult.transferStateEnumeration === Enum.Transfers.TransferState.ABORTED) {
193
+ if (!transferResult.extensionList) transferResult.extensionList = []
194
+ transferResult.extensionList.push({
195
+ key: 'cause',
196
+ value: `${transferResult.errorCode}: ${transferResult.errorDescription}`.substr(0, 128)
197
+ })
198
+ }
199
+ transferResult.isTransferReadModel = true
200
+ }
201
+ return transferResult
202
+ })
203
+ } catch (err) {
204
+ logger.warn('error in getAllDetailsByCommitRequestIdForProxiedFxTransfer', err)
205
+ throw ErrorHandler.Factory.reformatFSPIOPError(err)
206
+ }
207
+ }
208
+
209
+ const getParticipant = async (name, currency) =>
210
+ participant.getByNameAndCurrency(name, currency, Enum.Accounts.LedgerAccountType.POSITION)
211
+
212
+ /**
213
+ * Saves prepare fxTransfer details to DB.
214
+ *
215
+ * @param {Object} payload - Message payload.
216
+ * @param {string | null} stateReason - Validation failure reasons.
217
+ * @param {Boolean} hasPassedValidation - Is fxTransfer prepare validation passed.
218
+ * @param {DeterminingTransferCheckResult} determiningTransferCheckResult - Determining transfer check result.
219
+ * @param {ProxyObligation} proxyObligation - The proxy obligation
220
+ * @returns {Promise<void>}
221
+ */
222
+ const savePreparedRequest = async (
223
+ payload,
224
+ stateReason,
225
+ hasPassedValidation,
226
+ determiningTransferCheckResult,
227
+ proxyObligation
228
+ ) => {
229
+ const histTimerSaveFxTransferEnd = Metrics.getHistogram(
230
+ 'model_fx_transfer',
231
+ 'facade_saveFxTransferPrepared - Metrics for transfer model',
232
+ ['success', 'queryName']
233
+ ).startTimer()
234
+
235
+ // Substitute out of scheme participants with their proxy representatives
236
+ const initiatingFsp = proxyObligation.isInitiatingFspProxy
237
+ ? proxyObligation.initiatingFspProxyOrParticipantId.proxyId
238
+ : payload.initiatingFsp
239
+ const counterPartyFsp = proxyObligation.isCounterPartyFspProxy
240
+ ? proxyObligation.counterPartyFspProxyOrParticipantId.proxyId
241
+ : payload.counterPartyFsp
242
+
243
+ // If creditor(counterPartyFsp) is a proxy in a jurisdictional scenario,
244
+ // they would not hold a position account for the target currency,
245
+ // so we skip adding records of the target currency for the creditor.
246
+ try {
247
+ const [initiatingParticipant, counterParticipant1, counterParticipant2] = await Promise.all([
248
+ ParticipantCachedModel.getByName(initiatingFsp),
249
+ getParticipant(counterPartyFsp, payload.sourceAmount.currency),
250
+ !proxyObligation.isCounterPartyFspProxy ? getParticipant(counterPartyFsp, payload.targetAmount.currency) : null
251
+ ])
252
+ // todo: clarify, what we should do if no initiatingParticipant or counterParticipant found?
253
+
254
+ const fxTransferRecord = {
255
+ commitRequestId: payload.commitRequestId,
256
+ determiningTransferId: payload.determiningTransferId,
257
+ sourceAmount: payload.sourceAmount.amount,
258
+ sourceCurrency: payload.sourceAmount.currency,
259
+ targetAmount: payload.targetAmount.amount,
260
+ targetCurrency: payload.targetAmount.currency,
261
+ ilpCondition: payload.condition,
262
+ expirationDate: Util.Time.getUTCString(new Date(payload.expiration))
263
+ }
264
+
265
+ const fxTransferStateChangeRecord = {
266
+ commitRequestId: payload.commitRequestId,
267
+ transferStateId: hasPassedValidation ? TransferInternalState.RECEIVED_PREPARE : TransferInternalState.INVALID,
268
+ reason: stateReason,
269
+ createdDate: Util.Time.getUTCString(new Date())
270
+ }
271
+
272
+ const initiatingParticipantRecord = {
273
+ commitRequestId: payload.commitRequestId,
274
+ participantId: initiatingParticipant.participantId,
275
+ participantCurrencyId: null,
276
+ amount: payload.sourceAmount.amount,
277
+ transferParticipantRoleTypeId: Enum.Accounts.TransferParticipantRoleType.INITIATING_FSP,
278
+ ledgerEntryTypeId: Enum.Accounts.LedgerEntryType.PRINCIPLE_VALUE
279
+ }
280
+ if (proxyObligation.isInitiatingFspProxy) {
281
+ initiatingParticipantRecord.externalParticipantId = await participant
282
+ .getExternalParticipantIdByNameOrCreate(proxyObligation.initiatingFspProxyOrParticipantId)
283
+ }
284
+
285
+ const counterPartyParticipantRecord1 = {
286
+ commitRequestId: payload.commitRequestId,
287
+ participantId: counterParticipant1.participantId,
288
+ participantCurrencyId: counterParticipant1.participantCurrencyId,
289
+ amount: -payload.sourceAmount.amount,
290
+ transferParticipantRoleTypeId: Enum.Accounts.TransferParticipantRoleType.COUNTER_PARTY_FSP,
291
+ fxParticipantCurrencyTypeId: Enum.Fx.FxParticipantCurrencyType.SOURCE,
292
+ ledgerEntryTypeId: Enum.Accounts.LedgerEntryType.PRINCIPLE_VALUE
293
+ }
294
+ if (proxyObligation.isCounterPartyFspProxy) {
295
+ counterPartyParticipantRecord1.externalParticipantId = await participant
296
+ .getExternalParticipantIdByNameOrCreate(proxyObligation.counterPartyFspProxyOrParticipantId)
297
+ }
298
+
299
+ let counterPartyParticipantRecord2 = null
300
+ if (!proxyObligation.isCounterPartyFspProxy) {
301
+ counterPartyParticipantRecord2 = {
302
+ commitRequestId: payload.commitRequestId,
303
+ participantId: counterParticipant2.participantId,
304
+ participantCurrencyId: counterParticipant2.participantCurrencyId,
305
+ amount: -payload.targetAmount.amount,
306
+ transferParticipantRoleTypeId: Enum.Accounts.TransferParticipantRoleType.COUNTER_PARTY_FSP,
307
+ fxParticipantCurrencyTypeId: Enum.Fx.FxParticipantCurrencyType.TARGET,
308
+ ledgerEntryTypeId: Enum.Accounts.LedgerEntryType.PRINCIPLE_VALUE
309
+ }
310
+ }
311
+
312
+ const knex = await Db.getKnex()
313
+ if (hasPassedValidation) {
314
+ const histTimerSaveTranferTransactionValidationPassedEnd = Metrics.getHistogram(
315
+ 'model_fx_transfer',
316
+ 'facade_saveFxTransferPrepared_transaction - Metrics for transfer model',
317
+ ['success', 'queryName']
318
+ ).startTimer()
319
+ return await knex.transaction(async (trx) => {
320
+ try {
321
+ await knex(TABLE_NAMES.fxTransfer).transacting(trx).insert(fxTransferRecord)
322
+ await knex(TABLE_NAMES.fxTransferParticipant).transacting(trx).insert(initiatingParticipantRecord)
323
+ await knex(TABLE_NAMES.fxTransferParticipant).transacting(trx).insert(counterPartyParticipantRecord1)
324
+ if (!proxyObligation.isCounterPartyFspProxy) {
325
+ await knex(TABLE_NAMES.fxTransferParticipant).transacting(trx).insert(counterPartyParticipantRecord2)
326
+ }
327
+ initiatingParticipantRecord.name = payload.initiatingFsp
328
+ counterPartyParticipantRecord1.name = payload.counterPartyFsp
329
+ if (!proxyObligation.isCounterPartyFspProxy) {
330
+ counterPartyParticipantRecord2.name = payload.counterPartyFsp
331
+ }
332
+
333
+ await knex(TABLE_NAMES.fxTransferStateChange).transacting(trx).insert(fxTransferStateChangeRecord)
334
+ histTimerSaveTranferTransactionValidationPassedEnd({ success: true, queryName: 'facade_saveFxTransferPrepared_transaction' })
335
+ } catch (err) {
336
+ histTimerSaveTranferTransactionValidationPassedEnd({ success: false, queryName: 'facade_saveFxTransferPrepared_transaction' })
337
+ throw err
338
+ }
339
+ })
340
+ } else {
341
+ const queryName = 'facade_saveFxTransferPrepared_no_validation'
342
+ const histTimerNoValidationEnd = Metrics.getHistogram(
343
+ 'model_fx_transfer',
344
+ `${queryName} - Metrics for fxTransfer model`,
345
+ ['success', 'queryName']
346
+ ).startTimer()
347
+ await knex(TABLE_NAMES.fxTransfer).insert(fxTransferRecord)
348
+
349
+ try {
350
+ await knex(TABLE_NAMES.fxTransferParticipant).insert(initiatingParticipantRecord)
351
+ } catch (err) {
352
+ logger.warn(`Payer fxTransferParticipant insert error: ${err.message}`)
353
+ histTimerNoValidationEnd({ success: false, queryName })
354
+ }
355
+
356
+ try {
357
+ await knex(TABLE_NAMES.fxTransferParticipant).insert(counterPartyParticipantRecord1)
358
+ if (!proxyObligation.isCounterPartyFspProxy) {
359
+ await knex(TABLE_NAMES.fxTransferParticipant).insert(counterPartyParticipantRecord2)
360
+ }
361
+ } catch (err) {
362
+ histTimerNoValidationEnd({ success: false, queryName })
363
+ logger.warn(`Payee fxTransferParticipant insert error: ${err.message}`)
364
+ }
365
+ initiatingParticipantRecord.name = payload.initiatingFsp
366
+ counterPartyParticipantRecord1.name = payload.counterPartyFsp
367
+ if (!proxyObligation.isCounterPartyFspProxy) {
368
+ counterPartyParticipantRecord2.name = payload.counterPartyFsp
369
+ }
370
+
371
+ try {
372
+ await knex(TABLE_NAMES.fxTransferStateChange).insert(fxTransferStateChangeRecord)
373
+ histTimerNoValidationEnd({ success: true, queryName })
374
+ } catch (err) {
375
+ logger.warn(`fxTransferStateChange insert error: ${err.message}`)
376
+ histTimerNoValidationEnd({ success: false, queryName })
377
+ }
378
+ }
379
+ histTimerSaveFxTransferEnd({ success: true, queryName: 'transfer_model_facade_saveTransferPrepared' })
380
+ } catch (err) {
381
+ logger.warn('error in savePreparedRequest', err)
382
+ histTimerSaveFxTransferEnd({ success: false, queryName: 'transfer_model_facade_saveTransferPrepared' })
383
+ throw ErrorHandler.Factory.reformatFSPIOPError(err)
384
+ }
385
+ }
386
+
387
+ const saveFxFulfilResponse = async (commitRequestId, payload, action, fspiopError) => {
388
+ const histTimerSaveFulfilResponseEnd = Metrics.getHistogram(
389
+ 'fx_model_transfer',
390
+ 'facade_saveFxFulfilResponse - Metrics for fxTransfer model',
391
+ ['success', 'queryName']
392
+ ).startTimer()
393
+
394
+ let state
395
+ let isFulfilment = false
396
+ let isError = false
397
+ // const errorCode = fspiopError && fspiopError.errorInformation && fspiopError.errorInformation.errorCode
398
+ const errorDescription = fspiopError && fspiopError.errorInformation && fspiopError.errorInformation.errorDescription
399
+ let extensionList
400
+ switch (action) {
401
+ case TransferEventAction.FX_COMMIT:
402
+ case TransferEventAction.FX_RESERVE:
403
+ case TransferEventAction.FX_FORWARDED:
404
+ state = TransferInternalState.RECEIVED_FULFIL_DEPENDENT
405
+ extensionList = payload && payload.extensionList
406
+ isFulfilment = true
407
+ break
408
+ case TransferEventAction.FX_REJECT:
409
+ state = TransferInternalState.RECEIVED_REJECT
410
+ extensionList = payload && payload.extensionList
411
+ isFulfilment = true
412
+ break
413
+
414
+ case TransferEventAction.FX_ABORT_VALIDATION:
415
+ case TransferEventAction.FX_ABORT:
416
+ state = TransferInternalState.RECEIVED_ERROR
417
+ extensionList = payload && payload.errorInformation && payload.errorInformation.extensionList
418
+ isError = true
419
+ break
420
+ default:
421
+ throw ErrorHandler.Factory.createInternalServerFSPIOPError(UnsupportedActionText)
422
+ }
423
+ const completedTimestamp = Time.getUTCString((payload.completedTimestamp && new Date(payload.completedTimestamp)) || new Date())
424
+ const transactionTimestamp = Time.getUTCString(new Date())
425
+ const result = {
426
+ savePayeeTransferResponseExecuted: false
427
+ }
428
+
429
+ const fxTransferFulfilmentRecord = {
430
+ commitRequestId,
431
+ ilpFulfilment: payload.fulfilment || null,
432
+ completedDate: completedTimestamp,
433
+ isValid: !fspiopError,
434
+ settlementWindowId: null,
435
+ createdDate: transactionTimestamp
436
+ }
437
+ let fxTransferExtensionRecordsList = []
438
+ if (extensionList && extensionList.extension) {
439
+ fxTransferExtensionRecordsList = extensionList.extension.map(ext => {
440
+ return {
441
+ commitRequestId,
442
+ key: ext.key,
443
+ value: ext.value,
444
+ isFulfilment,
445
+ isError
446
+ }
447
+ })
448
+ }
449
+ const fxTransferStateChangeRecord = {
450
+ commitRequestId,
451
+ transferStateId: state,
452
+ reason: errorDescription,
453
+ createdDate: transactionTimestamp
454
+ }
455
+ // const fxTransferErrorRecord = {
456
+ // commitRequestId,
457
+ // fxTransferStateChangeId: null,
458
+ // errorCode,
459
+ // errorDescription,
460
+ // createdDate: transactionTimestamp
461
+ // }
462
+
463
+ try {
464
+ /** @namespace Db.getKnex **/
465
+ const knex = await Db.getKnex()
466
+ const histTFxFulfilResponseValidationPassedEnd = Metrics.getHistogram(
467
+ 'model_transfer',
468
+ 'facade_saveTransferPrepared_transaction - Metrics for transfer model',
469
+ ['success', 'queryName']
470
+ ).startTimer()
471
+
472
+ await knex.transaction(async (trx) => {
473
+ try {
474
+ if (!fspiopError && [TransferEventAction.FX_COMMIT, TransferEventAction.FX_RESERVE].includes(action)) {
475
+ const res = await Db.from('settlementWindow').query(builder => {
476
+ return builder
477
+ .leftJoin('settlementWindowStateChange AS swsc', 'swsc.settlementWindowStateChangeId', 'settlementWindow.currentStateChangeId')
478
+ .select(
479
+ 'settlementWindow.settlementWindowId',
480
+ 'swsc.settlementWindowStateId as state',
481
+ 'swsc.reason as reason',
482
+ 'settlementWindow.createdDate as createdDate',
483
+ 'swsc.createdDate as changedDate'
484
+ )
485
+ .where('swsc.settlementWindowStateId', 'OPEN')
486
+ .orderBy('changedDate', 'desc')
487
+ })
488
+ fxTransferFulfilmentRecord.settlementWindowId = res[0].settlementWindowId
489
+ logger.debug('saveFxFulfilResponse::settlementWindowId')
490
+ }
491
+ if (isFulfilment) {
492
+ await knex('fxTransferFulfilment').transacting(trx).insert(fxTransferFulfilmentRecord)
493
+ result.fxTransferFulfilmentRecord = fxTransferFulfilmentRecord
494
+ logger.debug('saveFxFulfilResponse::fxTransferFulfilment')
495
+ }
496
+ if (fxTransferExtensionRecordsList.length > 0) {
497
+ await knex('fxTransferExtension').transacting(trx).insert(fxTransferExtensionRecordsList)
498
+ result.fxTransferExtensionRecordsList = fxTransferExtensionRecordsList
499
+ logger.debug('saveFxFulfilResponse::transferExtensionRecordsList')
500
+ }
501
+ await knex('fxTransferStateChange').transacting(trx).insert(fxTransferStateChangeRecord)
502
+ result.fxTransferStateChangeRecord = fxTransferStateChangeRecord
503
+ logger.debug('saveFxFulfilResponse::fxTransferStateChange')
504
+ // TODO: Need to handle the following incase of error
505
+ // if (fspiopError) {
506
+ // const insertedTransferStateChange = await knex('fxTransferStateChange').transacting(trx)
507
+ // .where({ commitRequestId })
508
+ // .forUpdate().first().orderBy('fxTransferStateChangeId', 'desc')
509
+ // fxTransferStateChangeRecord.fxTransferStateChangeId = insertedTransferStateChange.fxTransferStateChangeId
510
+ // fxTransferErrorRecord.fxTransferStateChangeId = insertedTransferStateChange.fxTransferStateChangeId
511
+ // await knex('transferError').transacting(trx).insert(fxTransferErrorRecord)
512
+ // result.fxTransferErrorRecord = fxTransferErrorRecord
513
+ // logger.debug('saveFxFulfilResponse::transferError')
514
+ // }
515
+ histTFxFulfilResponseValidationPassedEnd({ success: true, queryName: 'facade_saveFxFulfilResponse_transaction' })
516
+ result.savePayeeTransferResponseExecuted = true
517
+ logger.debug('saveFxFulfilResponse::success')
518
+ } catch (err) {
519
+ histTFxFulfilResponseValidationPassedEnd({ success: false, queryName: 'facade_saveFxFulfilResponse_transaction' })
520
+ logger.error('saveFxFulfilResponse::failure')
521
+ throw err
522
+ }
523
+ })
524
+ histTimerSaveFulfilResponseEnd({ success: true, queryName: 'facade_saveFulfilResponse' })
525
+ return result
526
+ } catch (err) {
527
+ logger.warn('error in saveFxFulfilResponse', err)
528
+ histTimerSaveFulfilResponseEnd({ success: false, queryName: 'facade_saveFulfilResponse' })
529
+ throw ErrorHandler.Factory.reformatFSPIOPError(err)
530
+ }
531
+ }
532
+
533
+ const updateFxPrepareReservedForwarded = async function (commitRequestId) {
534
+ try {
535
+ const knex = await Db.getKnex()
536
+ return await knex('fxTransferStateChange')
537
+ .insert({
538
+ commitRequestId,
539
+ transferStateId: TransferInternalState.RESERVED_FORWARDED,
540
+ reason: null,
541
+ createdDate: Time.getUTCString(new Date())
542
+ })
543
+ } catch (err) {
544
+ throw ErrorHandler.Factory.reformatFSPIOPError(err)
545
+ }
546
+ }
547
+
548
+ const getFxTransferParticipant = async (participantName, commitRequestId) => {
549
+ try {
550
+ return Db.from('participant').query(async (builder) => {
551
+ return builder
552
+ .where({
553
+ 'ftp.commitRequestId': commitRequestId,
554
+ 'participant.name': participantName,
555
+ 'participant.isActive': 1
556
+ })
557
+ .innerJoin('fxTransferParticipant AS ftp', 'ftp.participantId', 'participant.participantId')
558
+ .select(
559
+ 'ftp.*'
560
+ )
561
+ })
562
+ } catch (err) {
563
+ throw ErrorHandler.Factory.reformatFSPIOPError(err)
564
+ }
565
+ }
566
+
567
+ module.exports = {
568
+ getByCommitRequestId,
569
+ getByDeterminingTransferId,
570
+ getByIdLight,
571
+ getAllDetailsByCommitRequestId,
572
+ getAllDetailsByCommitRequestIdForProxiedFxTransfer,
573
+ getFxTransferParticipant,
574
+ savePreparedRequest,
575
+ saveFxFulfilResponse,
576
+ saveFxTransfer,
577
+ updateFxPrepareReservedForwarded
578
+ }
@@ -0,0 +1,53 @@
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
+ * Vijaya Kumar Guthi <vijaya.guthi@infitx.com>
22
+ --------------
23
+ ******/
24
+
25
+ 'use strict'
26
+
27
+ /**
28
+ * @module src/models/transfer/transferError/
29
+ */
30
+
31
+ const Db = require('../../lib/db')
32
+ const Logger = require('@mojaloop/central-services-logger')
33
+
34
+ const getByCommitRequestId = async (id) => {
35
+ try {
36
+ const fxTransferError = await Db.from('fxTransferError').query(async (builder) => {
37
+ const result = builder
38
+ .where({ commitRequestId: id })
39
+ .select('*')
40
+ .first()
41
+ return result
42
+ })
43
+ fxTransferError.errorCode = fxTransferError.errorCode.toString()
44
+ return fxTransferError
45
+ } catch (err) {
46
+ Logger.isErrorEnabled && Logger.error(err)
47
+ throw err
48
+ }
49
+ }
50
+
51
+ module.exports = {
52
+ getByCommitRequestId
53
+ }
@@ -0,0 +1,41 @@
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
+ * Infitx
22
+ - Kalin Krustev <kalin.krustev@infitx.com>
23
+ --------------
24
+ ******/
25
+
26
+ 'use strict'
27
+
28
+ const Db = require('../../lib/db')
29
+ const ErrorHandler = require('@mojaloop/central-services-error-handling')
30
+
31
+ const getByCommitRequestId = async (commitRequestId, isFulfilment = false, isError = false) => {
32
+ try {
33
+ return await Db.from('fxTransferExtension').find({ commitRequestId, isFulfilment, isError })
34
+ } catch (err) {
35
+ throw ErrorHandler.Factory.reformatFSPIOPError(err)
36
+ }
37
+ }
38
+
39
+ module.exports = {
40
+ getByCommitRequestId
41
+ }