@mojaloop/central-ledger 19.14.0 → 20.1.0

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 (394) hide show
  1. package/.grype.yaml +3 -0
  2. package/.license-scanner.json +16 -0
  3. package/.ncurc.yaml +19 -13
  4. package/.nvmrc +1 -1
  5. package/.nycrc.yml +14 -2
  6. package/CHANGELOG.md +51 -0
  7. package/Dockerfile +13 -10
  8. package/README.md +39 -89
  9. package/audit-ci.jsonc +1 -1
  10. package/config/default.json +60 -3
  11. package/config/deprecated.default-settlement.json +160 -0
  12. package/docker/ml-api-adapter/default.json +28 -0
  13. package/docker-compose.yml +6 -0
  14. package/nodemon.json +6 -0
  15. package/package.json +128 -64
  16. package/{sbom-v19.13.2.csv → sbom-v20.0.0.csv} +886 -736
  17. package/scripts/_wait4_all.js +5 -5
  18. package/scripts/transferSettlementTemp/interchangeFeeCalculation.js +51 -0
  19. package/src/api/index.ts +30 -0
  20. package/src/api/interface/swagger.json +2249 -2054
  21. package/src/api/participants/routes.js +1 -1
  22. package/src/api/root/handler.int.ts +55 -0
  23. package/src/api/routes.js +7 -6
  24. package/src/api/server.ts +27 -0
  25. package/src/api/server.unit.ts +16 -0
  26. package/src/api/settlementModels/routes.js +4 -4
  27. package/src/domain/participant/index.d.ts +47 -0
  28. package/src/domain/participant/index.int.ts +724 -0
  29. package/src/domain/participant/index.js +4 -5
  30. package/src/domain/position/fx-prepare.js +1 -1
  31. package/src/domain/position/fx-timeout-reserved.js +6 -0
  32. package/src/domain/position/prepare.js +1 -1
  33. package/src/handlers/admin/handler.js +1 -1
  34. package/src/handlers/index.js +30 -0
  35. package/src/handlers/positions/handler.js +0 -2
  36. package/src/handlers/positions/handlerBatch.js +5 -5
  37. package/src/handlers/register.js +44 -29
  38. package/src/handlers/timeouts/handler.js +1 -4
  39. package/src/handlers/transfers/fx-abort.int.ts +209 -0
  40. package/src/handlers/transfers/fx-handlers.int.ts +275 -0
  41. package/src/handlers/transfers/fx-timeout.int.ts +284 -0
  42. package/src/handlers/transfers/handler.js +1 -3
  43. package/src/handlers/transfers/handlers.int.ts +538 -0
  44. package/src/handlers/transfers/prepare.int.ts +1283 -0
  45. package/src/handlers/transfers/prepare.js +2 -2
  46. package/src/knexfile.js +19 -0
  47. package/src/lib/cache.js +1 -1
  48. package/src/lib/config/index.ts +60 -0
  49. package/src/lib/config/resolver.ts +231 -0
  50. package/src/lib/config/resolver.unit.ts +72 -0
  51. package/src/lib/config/types.ts +372 -0
  52. package/src/lib/config/util.ts +523 -0
  53. package/src/lib/config/util.unit.ts +825 -0
  54. package/src/lib/migrator.js +3 -3
  55. package/src/lib/proxyCache.js +3 -3
  56. package/{migrations → src/migrations}/100100_event.js +1 -1
  57. package/{migrations → src/migrations}/110100_contactType.js +1 -1
  58. package/{migrations → src/migrations}/110200_currency.js +1 -1
  59. package/{migrations → src/migrations}/110201_currency-scale.js +1 -1
  60. package/{migrations → src/migrations}/110300_endpointType.js +1 -1
  61. package/{migrations → src/migrations}/110400_ledgerEntryType.js +1 -1
  62. package/{migrations → src/migrations}/110450_ledgerAccountType.js +1 -1
  63. package/{migrations → src/migrations}/110500_participantLimitType.js +1 -1
  64. package/{migrations → src/migrations}/110600_transferParticipantRoleType.js +1 -1
  65. package/{migrations → src/migrations}/110700_transferState.js +1 -1
  66. package/{migrations → src/migrations}/110900_settlementState.js +1 -1
  67. package/{migrations → src/migrations}/112000_bulkTransferState.js +1 -1
  68. package/{migrations → src/migrations}/112100_bulkProcessingState.js +1 -1
  69. package/{migrations → src/migrations}/200100_participant.js +1 -1
  70. package/{migrations → src/migrations}/200200_participantContact.js +1 -1
  71. package/{migrations → src/migrations}/200300_participantEndpoint.js +1 -1
  72. package/{migrations → src/migrations}/200400_participantParty.js +1 -1
  73. package/{migrations → src/migrations}/200600_token.js +1 -1
  74. package/{migrations → src/migrations}/200601_token-indexes.js +1 -1
  75. package/{migrations → src/migrations}/300100_transferDuplicateCheck.js +2 -2
  76. package/{migrations → src/migrations}/300150_bulkTransferDuplicateCheck.js +2 -2
  77. package/{migrations → src/migrations}/300200_transfer.js +2 -2
  78. package/{migrations → src/migrations}/300202_transfer-decimal184.js +1 -1
  79. package/{migrations → src/migrations}/300250_bulkTransfer.js +2 -3
  80. package/{migrations → src/migrations}/300275_bulkTransferAssociation.js +2 -3
  81. package/{migrations → src/migrations}/300300_ilpPacket.js +1 -1
  82. package/{migrations → src/migrations}/300400_transferStateChange.js +1 -1
  83. package/{migrations → src/migrations}/300450_bulkTransferStateChange.js +1 -1
  84. package/{migrations → src/migrations}/300500_segment.js +1 -1
  85. package/{migrations → src/migrations}/310100_participantCurrency.js +1 -1
  86. package/{migrations → src/migrations}/310200_transferParticipant.js +1 -1
  87. package/{migrations → src/migrations}/310202_transferParticipant-decimal184.js +1 -1
  88. package/{migrations → src/migrations}/310204_transferParticipant-participantId.js +2 -2
  89. package/{migrations → src/migrations}/310300_participantPosition.js +1 -1
  90. package/{migrations → src/migrations}/310302_participantPosition-decimal184.js +1 -1
  91. package/{migrations → src/migrations}/310400_participantPositionChange.js +1 -1
  92. package/{migrations → src/migrations}/310402_participantPositionChange-decimal184.js +1 -1
  93. package/{migrations → src/migrations}/310403_participantPositionChange-participantCurrencyId.js +2 -2
  94. package/{migrations → src/migrations}/310404_participantPositionChange-change.js +2 -2
  95. package/{migrations → src/migrations}/310500_participantLimit.js +1 -1
  96. package/{migrations → src/migrations}/310502_participantLimit-decimal184.js +1 -1
  97. package/{migrations → src/migrations}/400100_settlementWindow.js +1 -1
  98. package/{migrations → src/migrations}/400200_settlement.js +1 -1
  99. package/{migrations → src/migrations}/400300_settlementSettlementWindow.js +1 -1
  100. package/{migrations → src/migrations}/400301_settlementSettlementWindow-indexes.js +0 -1
  101. package/{migrations → src/migrations}/400400_settlementWindowStateChange.js +1 -1
  102. package/{migrations → src/migrations}/400500_settlementTransferParticipant.js +1 -1
  103. package/{migrations → src/migrations}/400502_settlementTransferParticipant-decimal184.js +1 -1
  104. package/{migrations → src/migrations}/400600_settlementParticipantCurrency.js +1 -1
  105. package/{migrations → src/migrations}/400602_settlementParticipantCurrency-decimal184.js +1 -1
  106. package/{migrations → src/migrations}/400700_settlementParticipantCurrencyStateChange.js +1 -1
  107. package/{migrations → src/migrations}/400800_settlementStateChange.js +1 -1
  108. package/{migrations → src/migrations}/400900_settlementWindowContent.js +1 -1
  109. package/{migrations → src/migrations}/401000_settlementWindowContentStateChange.js +1 -1
  110. package/{migrations → src/migrations}/401001_settlementWindowContentStateChange-indexes.js +6 -6
  111. package/{migrations → src/migrations}/401200_settlementContentAggregation.js +1 -1
  112. package/{migrations → src/migrations}/401300_settlementModel.js +1 -1
  113. package/{migrations → src/migrations}/410100_transferFulfilmentDuplicateCheck.js +1 -1
  114. package/{migrations → src/migrations}/410102_transferFulfilmentDuplicateCheck-dropForeignTransferId.js +1 -1
  115. package/{migrations → src/migrations}/410103_transferFulfilmentDuplicateCheck-hashNullable.js +1 -1
  116. package/{migrations → src/migrations}/410200_transferFulfilment.js +1 -1
  117. package/{migrations → src/migrations}/410202_transferFulfilment-ilpFulfilment.js +1 -1
  118. package/{migrations → src/migrations}/410203_transferFulfilment-foreignTransferFulfilmentId.js +1 -1
  119. package/{migrations → src/migrations}/410240_bulkTransferFulfilmentDuplicateCheck.js +1 -1
  120. package/{migrations → src/migrations}/410250_bulkTransferFulfilment.js +1 -1
  121. package/{migrations → src/migrations}/410300_transferExtension.js +1 -1
  122. package/{migrations → src/migrations}/410302_transferExtension-transferErrorId.js +1 -1
  123. package/{migrations → src/migrations}/410350_bulkTransferExtension.js +1 -1
  124. package/{migrations → src/migrations}/410400_transferTimeout.js +1 -1
  125. package/{migrations → src/migrations}/410450_transferErrorDuplicateCheck.js +1 -1
  126. package/{migrations → src/migrations}/410500_transferError.js +1 -1
  127. package/{migrations → src/migrations}/410502_transferError-foreignTransferErrorDuplicateCheckId.js +1 -1
  128. package/{migrations → src/migrations}/410550_bulkTransferError.js +1 -1
  129. package/{migrations → src/migrations}/410600_expiringTransfer.js +1 -1
  130. package/{migrations → src/migrations}/500401_quote-hotfix-2719.js +8 -8
  131. package/{migrations → src/migrations}/500501_feature-fixSubIdRef.js +12 -12
  132. package/{migrations → src/migrations}/500601_party-2480.js +3 -3
  133. package/{migrations → src/migrations}/501001_quoteExtension-quote-service-174.js +1 -2
  134. package/{migrations → src/migrations}/501002_quoteExtension-2522.js +3 -3
  135. package/{migrations → src/migrations}/600010_fxTransferType.js +1 -1
  136. package/{migrations → src/migrations}/600012_fxParticipantCurrencyType.js +1 -1
  137. package/{migrations → src/migrations}/600100_fxTransferDuplicateCheck.js +2 -2
  138. package/{migrations → src/migrations}/600110_fxTransferErrorDuplicateCheck.js.js +1 -1
  139. package/{migrations → src/migrations}/600200_fxTransfer.js +2 -2
  140. package/{migrations → src/migrations}/600400_fxTransferStateChange.js +1 -1
  141. package/{migrations → src/migrations}/600501_fxWatchList.js +2 -2
  142. package/{migrations → src/migrations}/600600_fxTransferFulfilmentDuplicateCheck.js +1 -1
  143. package/{migrations → src/migrations}/600700_fxTransferFulfilment.js +1 -1
  144. package/{migrations → src/migrations}/600800_fxTransferExtension.js +1 -1
  145. package/{migrations → src/migrations}/601400_fxTransferTimeout.js +1 -1
  146. package/{migrations → src/migrations}/601500_fxTransferError.js +1 -1
  147. package/{migrations → src/migrations}/610200_fxTransferParticipant.js +1 -1
  148. package/{migrations → src/migrations}/610202_fxTransferParticipant-participantId.js +2 -2
  149. package/{migrations → src/migrations}/610403_participantPositionChange-fxTransfer.js +1 -1
  150. package/{migrations → src/migrations}/800100_quotePartyView.js +1 -1
  151. package/{migrations → src/migrations}/800101_feature-fixSubIdRef.js +1 -1
  152. package/{migrations → src/migrations}/800200_quoteView.js +1 -1
  153. package/{migrations → src/migrations}/800300_quoteResponseView.js +1 -1
  154. package/{migrations → src/migrations}/900100_foreign-keys.js +18 -18
  155. package/{migrations → src/migrations}/900101_foreign-keys.js +6 -6
  156. package/{migrations → src/migrations}/910100_feature904DDL.js +7 -7
  157. package/{migrations → src/migrations}/910101_feature904DataMigration.js +1 -1
  158. package/{migrations → src/migrations}/910102_feature949DataMigration.js +1 -1
  159. package/{migrations → src/migrations}/910103_feature1093SettlementByCurrency.js +3 -3
  160. package/{migrations → src/migrations}/940101_quotePartyIdInfoExtension-2522.js +25 -25
  161. package/{migrations → src/migrations}/950101_transferParticipantStateChange.js +2 -2
  162. package/{migrations → src/migrations}/950102_settlementModel-adjustPosition.js +2 -2
  163. package/{migrations → src/migrations}/950103_dropTransferParticipantStateChange.js +3 -3
  164. package/{migrations → src/migrations}/950104_settlementModel-settlementAccountTypeId.js +3 -3
  165. package/src/migrations/950107_settlementWindowContent_foreign_settlementModel.js +63 -0
  166. package/{migrations → src/migrations}/950108_participantProxy.js +1 -2
  167. package/{migrations → src/migrations}/950120_fxQuoteConversionTerms_sourceAmount_nullable.js +1 -1
  168. package/{migrations → src/migrations}/960100_create_externalParticipant.js +2 -2
  169. package/{migrations → src/migrations}/960110_alter_transferParticipant__addFiled_externalParticipantId.js +2 -2
  170. package/{migrations → src/migrations}/961001_transferForwarded.js +1 -1
  171. package/{migrations → src/migrations}/961002_fxTransferForwarded.js +1 -1
  172. package/src/models/participant/externalParticipant.int.ts +85 -0
  173. package/src/models/participant/facade.js +4 -2
  174. package/src/models/participant/participantCurrencyCached.js +1 -1
  175. package/src/models/position/facade.js +8 -2
  176. package/src/models/transfer/facade.int.ts +139 -0
  177. package/src/models/transfer/facade.js +8 -3
  178. package/src/models/transfer/transferDuplicateCheck.js +2 -2
  179. package/src/models/transfer/transferError.int.ts +112 -0
  180. package/src/models/transfer/transferExtension.int.ts +137 -0
  181. package/src/models/transfer/transferStateChange.int.ts +117 -0
  182. package/{seeds → src/seeds}/participant.js +1 -1
  183. package/{seeds → src/seeds}/settlementInterchange.js +1 -0
  184. package/src/settlement/api/handlerRoutes.js +71 -0
  185. package/src/settlement/api/handlers/health.js +10 -0
  186. package/src/settlement/api/handlers/index.js +57 -0
  187. package/src/settlement/api/handlers/settlementWindows/{id}.js +103 -0
  188. package/src/settlement/api/handlers/settlementWindows.js +77 -0
  189. package/src/settlement/api/handlers/settlements/{id}.js +133 -0
  190. package/src/settlement/api/handlers/settlements/{sid}/participants/{pid}/accounts/{aid}.js +127 -0
  191. package/src/settlement/api/handlers/settlements/{sid}/participants/{pid}.js +127 -0
  192. package/src/settlement/api/handlers/settlements.js +114 -0
  193. package/src/settlement/api/openapiRouting.js +87 -0
  194. package/src/settlement/api/routes.js +176 -0
  195. package/src/settlement/domain/rules/index.js +46 -0
  196. package/src/settlement/domain/settlement/index.js +405 -0
  197. package/src/settlement/domain/settlementWindow/index.js +117 -0
  198. package/src/settlement/domain/transactions/index.js +83 -0
  199. package/src/settlement/domain/transferSettlement/index.js +57 -0
  200. package/src/settlement/handlers/deferredSettlement/handler.js +180 -0
  201. package/src/settlement/handlers/grossSettlement/handler.js +180 -0
  202. package/src/settlement/handlers/index.js +101 -0
  203. package/src/settlement/handlers/register.js +92 -0
  204. package/src/settlement/handlers/rules/handler.js +164 -0
  205. package/src/settlement/interface/swagger-handler.json +34 -0
  206. package/src/settlement/interface/swagger.json +1560 -0
  207. package/src/settlement/lib/db.js +32 -0
  208. package/src/settlement/lib/kafka/index.js +41 -0
  209. package/src/settlement/lib/kafka/producer.js +142 -0
  210. package/src/settlement/lib/scriptEngine.js +99 -0
  211. package/src/settlement/lib/scriptsLoader.js +175 -0
  212. package/src/settlement/lib/utility.js +243 -0
  213. package/src/settlement/models/ilpPackets/ilpPacket.js +42 -0
  214. package/src/settlement/models/lib/enums.js +192 -0
  215. package/src/settlement/models/misc/migrationLock.js +53 -0
  216. package/src/settlement/models/settlement/facade.js +1652 -0
  217. package/src/settlement/models/settlement/index.js +55 -0
  218. package/src/settlement/models/settlement/participantCurrency.js +48 -0
  219. package/src/settlement/models/settlement/settlement.js +49 -0
  220. package/src/settlement/models/settlement/settlementModel.js +44 -0
  221. package/src/settlement/models/settlement/settlementParticipantCurrency.js +61 -0
  222. package/src/settlement/models/settlement/settlementStateChange.js +44 -0
  223. package/src/settlement/models/settlement/settlementTransferParticipant.js +46 -0
  224. package/src/settlement/models/settlementWindow/facade.js +345 -0
  225. package/src/settlement/models/settlementWindow/index.js +47 -0
  226. package/src/settlement/models/settlementWindow/settlementWindowStateChange.js +55 -0
  227. package/src/settlement/models/settlementWindowContent/facade.js +79 -0
  228. package/src/settlement/models/settlementWindowContent/index.js +40 -0
  229. package/src/settlement/models/settlementWindowContent/settlementWindowContentStateChange.js +53 -0
  230. package/src/settlement/models/transferSettlement/facade.js +363 -0
  231. package/src/settlement/models/transferSettlement/index.js +42 -0
  232. package/src/settlement/shared/logger/index.js +7 -0
  233. package/src/settlement/shared/plugins.js +88 -0
  234. package/src/settlement/shared/setup.js +224 -0
  235. package/src/settlement/utils/truthyProperty.js +37 -0
  236. package/src/shared/legacyUtilShim.js +20 -0
  237. package/src/shared/plugins.js +3 -3
  238. package/src/shared/results.ts +22 -0
  239. package/src/shared/results.unit.ts +38 -0
  240. package/src/shared/setup.js +14 -1
  241. package/src/testing/api-helpers.ts +1222 -0
  242. package/src/testing/exec-async.ts +67 -0
  243. package/src/testing/harness.snapshot.sql +3895 -0
  244. package/src/testing/harness.test.ts +15 -0
  245. package/src/testing/harness.ts +1160 -0
  246. package/src/testing/kafka.ts +221 -0
  247. package/src/testing/prng.ts +82 -0
  248. package/src/testing/rdkafka.test.ts +263 -0
  249. package/src/testing/run.ts +559 -0
  250. package/src/testing/snapshot.test.ts +92 -0
  251. package/src/testing/snapshot.ts +549 -0
  252. package/src/testing/tap-stream.ts +164 -0
  253. package/src/testing/types.ts +95 -0
  254. package/src/testing/util.ts +333 -0
  255. package/src/types/central-services-error-handling.d.ts +72 -0
  256. package/src/types/central-services-stream.d.ts +148 -0
  257. package/test-integration.Dockerfile +0 -2
  258. package/test.Dockerfile +0 -2
  259. package/tsconfig.json +40 -0
  260. package/config/knexfile.js +0 -19
  261. package/migrations/950107_settlementWindowContent_foreign_settlementModel.js +0 -63
  262. package/src/api/index.js +0 -16
  263. package/src/lib/config.js +0 -81
  264. /package/{migrations → src/migrations}/110101_contactType-indexes.js +0 -0
  265. /package/{migrations → src/migrations}/110301_endpointType-indexes.js +0 -0
  266. /package/{migrations → src/migrations}/110401_ledgerEntryType-indexes.js +0 -0
  267. /package/{migrations → src/migrations}/110451_ledgerAccountType-indexes.js +0 -0
  268. /package/{migrations → src/migrations}/110501_participantLimitType-indexes.js +0 -0
  269. /package/{migrations → src/migrations}/110601_transferParticipantRoleType-indexes.js +0 -0
  270. /package/{migrations → src/migrations}/110800_settlementWindowState.js +0 -0
  271. /package/{migrations → src/migrations}/111000_amountType.js +0 -0
  272. /package/{migrations → src/migrations}/111001_amountType-indexes.js +0 -0
  273. /package/{migrations → src/migrations}/111100_balanceOfPayments.js +0 -0
  274. /package/{migrations → src/migrations}/111101_balanceOfPayments-indexes.js +0 -0
  275. /package/{migrations → src/migrations}/111200_partyIdentifierType.js +0 -0
  276. /package/{migrations → src/migrations}/111201_partyIdentifierType-indexes.js +0 -0
  277. /package/{migrations → src/migrations}/111300_partyType.js +0 -0
  278. /package/{migrations → src/migrations}/111301_partyType-indexes.js +0 -0
  279. /package/{migrations → src/migrations}/111400_quoteDuplicateCheck.js +0 -0
  280. /package/{migrations → src/migrations}/111500_transactionInitiator.js +0 -0
  281. /package/{migrations → src/migrations}/111501_transactionInitiator-indexes.js +0 -0
  282. /package/{migrations → src/migrations}/111600_transactionInitiatorType.js +0 -0
  283. /package/{migrations → src/migrations}/111601_transactionInitiatorType-indexes.js +0 -0
  284. /package/{migrations → src/migrations}/111700_settlementDelay.js +0 -0
  285. /package/{migrations → src/migrations}/111701_settlementDelay-indexes.js +0 -0
  286. /package/{migrations → src/migrations}/111800_settlementInterchange.js +0 -0
  287. /package/{migrations → src/migrations}/111801_settlementInterchange-indexes.js +0 -0
  288. /package/{migrations → src/migrations}/111900_settlementGranularity.js +0 -0
  289. /package/{migrations → src/migrations}/111901_settlementGranularity-indexes.js +0 -0
  290. /package/{migrations → src/migrations}/112101_bulkProcessingState-indexes.js +0 -0
  291. /package/{migrations → src/migrations}/200101_participant-indexes.js +0 -0
  292. /package/{migrations → src/migrations}/200201_participantContact-indexes.js +0 -0
  293. /package/{migrations → src/migrations}/200301_participantEndpoint-indexes.js +0 -0
  294. /package/{migrations → src/migrations}/200401_participantParty-indexes.js +0 -0
  295. /package/{migrations → src/migrations}/300201_transfer-indexes.js +0 -0
  296. /package/{migrations → src/migrations}/300251_bulkTransfer-indexes.js +0 -0
  297. /package/{migrations → src/migrations}/300276_bulkTransferAssociation-indexes.js +0 -0
  298. /package/{migrations → src/migrations}/300401_transferStateChange-indexes.js +0 -0
  299. /package/{migrations → src/migrations}/300451_bulkTransferStateChange-indexes.js +0 -0
  300. /package/{migrations → src/migrations}/300501_segment-indexes.js +0 -0
  301. /package/{migrations → src/migrations}/310101_participantCurrency-indexes.js +0 -0
  302. /package/{migrations → src/migrations}/310201_transferParticipant-indexes.js +0 -0
  303. /package/{migrations → src/migrations}/310203_transferParticipant-indexes.js +0 -0
  304. /package/{migrations → src/migrations}/310301_participantPosition-indexes.js +0 -0
  305. /package/{migrations → src/migrations}/310401_participantPositionChange-indexes.js +0 -0
  306. /package/{migrations → src/migrations}/310405_participantPositionChange-positionChange.js +0 -0
  307. /package/{migrations → src/migrations}/310406_participantPositionChange-positionChange-revert.js +0 -0
  308. /package/{migrations → src/migrations}/310501_participantLimit-indexes.js +0 -0
  309. /package/{migrations → src/migrations}/310503_participantLimit-indexes-composite.js +0 -0
  310. /package/{migrations → src/migrations}/400401_settlmentWindowStateChange-indexes.js +0 -0
  311. /package/{migrations → src/migrations}/400501_settlementTransferParticipant-indexes.js +0 -0
  312. /package/{migrations → src/migrations}/400601_settlementParticipantCurrency-indexes.js +0 -0
  313. /package/{migrations → src/migrations}/400701_settlementParticipantCurrencyStateChange-indexes.js +0 -0
  314. /package/{migrations → src/migrations}/400801_settlementStateChange-indexes.js +0 -0
  315. /package/{migrations → src/migrations}/401002_settlementWindowContent-indexes.js +0 -0
  316. /package/{migrations → src/migrations}/401201_settlementContentAggregation-indexes.js +0 -0
  317. /package/{migrations → src/migrations}/401301_settlementModel-indexes.js +0 -0
  318. /package/{migrations → src/migrations}/410101_transferFulfilmentDuplicateCheck-indexes.js +0 -0
  319. /package/{migrations → src/migrations}/410201_transferFulfilment-indexes.js +0 -0
  320. /package/{migrations → src/migrations}/410301_transferExtension-indexes.js +0 -0
  321. /package/{migrations → src/migrations}/410351_bulkTransferExtension-indexes.js +0 -0
  322. /package/{migrations → src/migrations}/410401_transferTimeout-indexes.js +0 -0
  323. /package/{migrations → src/migrations}/410501_transferError-indexes.js +0 -0
  324. /package/{migrations → src/migrations}/410551_bulkTransferError-indexes.js +0 -0
  325. /package/{migrations → src/migrations}/410601_expiringTransfer-indexes.js +0 -0
  326. /package/{migrations → src/migrations}/500100_transactionReference.js +0 -0
  327. /package/{migrations → src/migrations}/500101_transactionReference-indexes.js +0 -0
  328. /package/{migrations → src/migrations}/500200_transactionScenario.js +0 -0
  329. /package/{migrations → src/migrations}/500201_transactionScenario-indexes.js +0 -0
  330. /package/{migrations → src/migrations}/500300_transactionSubScenario.js +0 -0
  331. /package/{migrations → src/migrations}/500400_quote.js +0 -0
  332. /package/{migrations → src/migrations}/500500_quoteParty.js +0 -0
  333. /package/{migrations → src/migrations}/500600_party.js +0 -0
  334. /package/{migrations → src/migrations}/500700_quoteResponse.js +0 -0
  335. /package/{migrations → src/migrations}/500800_quoteResponseIlpPacket.js +0 -0
  336. /package/{migrations → src/migrations}/500900_geoCode.js +0 -0
  337. /package/{migrations → src/migrations}/501000_quoteExtension.js +0 -0
  338. /package/{migrations → src/migrations}/501100_quoteResponseDuplicateCheck.js +0 -0
  339. /package/{migrations → src/migrations}/501200_quoteError.js +0 -0
  340. /package/{migrations → src/migrations}/501300_transferRules.js +0 -0
  341. /package/{migrations → src/migrations}/600011_fxTransferType-indexes.js +0 -0
  342. /package/{migrations → src/migrations}/600013_fxParticipantCurrencyType-indexes.js +0 -0
  343. /package/{migrations → src/migrations}/600201_fxTransfer-indexes.js +0 -0
  344. /package/{migrations → src/migrations}/600401_fxTransferStateChange-indexes.js +0 -0
  345. /package/{migrations → src/migrations}/600502_fxWatchList-indexes.js +0 -0
  346. /package/{migrations → src/migrations}/600601_fxTransferFulfilmentDuplicateCheck-indexes.js +0 -0
  347. /package/{migrations → src/migrations}/600701_fxTransferFulfilment-indexes.js +0 -0
  348. /package/{migrations → src/migrations}/601401_fxTransferTimeout-indexes.js +0 -0
  349. /package/{migrations → src/migrations}/601501_fxTransferError-indexes.js +0 -0
  350. /package/{migrations → src/migrations}/610201_fxTransferParticipant-indexes.js +0 -0
  351. /package/{migrations → src/migrations}/910102_hotfix954.js +0 -0
  352. /package/{migrations → src/migrations}/940100_quotePartyIdInfoExtension.js +0 -0
  353. /package/{migrations → src/migrations}/950100_settlementContentAggregation-decimal184.js +0 -0
  354. /package/{migrations → src/migrations}/950106_unique_settlement_model_ledger_account_currency.js +0 -0
  355. /package/{migrations → src/migrations}/950109_fxQuote.js +0 -0
  356. /package/{migrations → src/migrations}/950110_fxQuoteResponse.js +0 -0
  357. /package/{migrations → src/migrations}/950111_fxQuoteError.js +0 -0
  358. /package/{migrations → src/migrations}/950113_fxQuoteDuplicateCheck.js +0 -0
  359. /package/{migrations → src/migrations}/950114_fxQuoteResponseDuplicateCheck.js +0 -0
  360. /package/{migrations → src/migrations}/950115_fxQuoteConversionTerms.js +0 -0
  361. /package/{migrations → src/migrations}/950116_fxQuoteConversionTermsExtension.js +0 -0
  362. /package/{migrations → src/migrations}/950117_fxQuoteResponseConversionTerms.js +0 -0
  363. /package/{migrations → src/migrations}/950118_fxQuoteResponseConversionTermsExtension.js +0 -0
  364. /package/{migrations → src/migrations}/950119_fxCharge.js +0 -0
  365. /package/{migrations → src/migrations}/960111_alter_fxTransferParticipant__addFiled_externalParticipantId.js +0 -0
  366. /package/{migrations → src/migrations}/960112_fxTransfer-indexes2.js +0 -0
  367. /package/{migrations → src/migrations}/960113_fxTransferExtension-indexes.js +0 -0
  368. /package/{migrations → src/migrations}/960114_fxTransferTimeout-indexes2.js +0 -0
  369. /package/{migrations → src/migrations}/961000_participantEndpoint_unique.js +0 -0
  370. /package/{migrations → src/migrations}/migration.template +0 -0
  371. /package/{seeds → src/seeds}/amountType.js +0 -0
  372. /package/{seeds → src/seeds}/balanceOfPayments.js +0 -0
  373. /package/{seeds → src/seeds}/bulkProcessingState.js +0 -0
  374. /package/{seeds → src/seeds}/bulkTransferState.js +0 -0
  375. /package/{seeds → src/seeds}/currency.js +0 -0
  376. /package/{seeds → src/seeds}/endpointType.js +0 -0
  377. /package/{seeds → src/seeds}/fxParticipantCurrencyType.js +0 -0
  378. /package/{seeds → src/seeds}/fxTransferType.js +0 -0
  379. /package/{seeds → src/seeds}/ledgerAccountType.js +0 -0
  380. /package/{seeds → src/seeds}/ledgerEntryType.js +0 -0
  381. /package/{seeds → src/seeds}/participantLimitType.js +0 -0
  382. /package/{seeds → src/seeds}/partyIdentifierType.js +0 -0
  383. /package/{seeds → src/seeds}/partyType.js +0 -0
  384. /package/{seeds → src/seeds}/settlementDelay.js +0 -0
  385. /package/{seeds → src/seeds}/settlementGranularity.js +0 -0
  386. /package/{seeds → src/seeds}/settlementState.js +0 -0
  387. /package/{seeds → src/seeds}/settlementWindow1State.js +0 -0
  388. /package/{seeds → src/seeds}/settlementWindow2Open.js +0 -0
  389. /package/{seeds → src/seeds}/transactionInitiator.js +0 -0
  390. /package/{seeds → src/seeds}/transactionInitiatorType.js +0 -0
  391. /package/{seeds → src/seeds}/transactionScenario.js +0 -0
  392. /package/{seeds → src/seeds}/transferParticipantRoleType.js +0 -0
  393. /package/{seeds → src/seeds}/transferState.js +0 -0
  394. /package/{seeds → src/seeds}/z1000_settlementModel-deprecated.js- +0 -0
@@ -0,0 +1,1283 @@
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2020-2026 Mojaloop Foundation
5
+ The Mojaloop files are made available by the Mojaloop 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
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ 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.
10
+
11
+ Contributors
12
+ --------------
13
+ This is the official list of the Mojaloop project contributors for this file.
14
+ Names of the original copyright holders (individuals or organizations)
15
+ should be listed with a '*' in the first column. People who have
16
+ contributed from an organization can be listed under the organization
17
+ that actually holds the copyright for their contributions (see the
18
+ Mojaloop Foundation for an example). Those individuals should have
19
+ their names indented and be marked with a '-'. Email address can be added
20
+ optionally within square brackets <email>.
21
+
22
+ * TigerBeetle
23
+ - Lewis Daly <lewis@tigerbeetle.com>
24
+ --------------
25
+ ******/
26
+
27
+ import { after, before, describe, it } from "node:test"
28
+ import Harness from '../../testing/harness'
29
+ import { Snapshot } from "../../testing/snapshot"
30
+ import * as ApiHelpers from '../../testing/api-helpers'
31
+ import assert from "node:assert"
32
+ import { assertPositionDiff, sleepSeconds } from "../../testing/util"
33
+ import TimeoutHandler from '../timeouts/handler'
34
+
35
+ const harness = Harness.getInstance()
36
+ let PrepareHandler: any
37
+ let TransferHandler: any
38
+ let PositionBatchHandler: any
39
+ let ExternalParticipantCached: any
40
+ let TransferFacade: any
41
+ let FxTransferService: any
42
+ let proxyCache: any
43
+
44
+ describe('handlers/prepare', () => {
45
+ before(async () => {
46
+ await harness.up('BATCH')
47
+ await harness.setupGlobals()
48
+
49
+ // Import after bringing up the harness, so that global config is overriden.
50
+ TransferHandler = require('./handler')
51
+ PrepareHandler = require('./prepare')
52
+ PositionBatchHandler = require('../positions/handlerBatch')
53
+ TransferFacade = require('../../models/transfer/facade')
54
+ FxTransferService = require('../../domain/fx/index')
55
+ ExternalParticipantCached = require('../../models/participant/externalParticipantCached')
56
+ proxyCache = require('../../lib/proxyCache')
57
+ await proxyCache.connect()
58
+ await TransferHandler.registerPrepareHandler()
59
+ await TransferHandler.registerFulfilHandler()
60
+
61
+ // Create the hub accounts + settlement model.
62
+ const createHubPayload: ApiHelpers.CreateHubPayload = {
63
+ currencies: ['BWP', 'USD'],
64
+ settlementModels: [
65
+ {
66
+ name: `DEFERRED_MULTILATERAL_NET_BWP`,
67
+ settlementGranularity: "NET",
68
+ settlementInterchange: "MULTILATERAL",
69
+ settlementDelay: "DEFERRED",
70
+ currency: 'BWP',
71
+ requireLiquidityCheck: true,
72
+ ledgerAccountType: "POSITION",
73
+ settlementAccountType: "SETTLEMENT",
74
+ autoPositionReset: true
75
+ },
76
+ {
77
+ name: `DEFERRED_MULTILATERAL_NET_USD`,
78
+ settlementGranularity: "NET",
79
+ settlementInterchange: "MULTILATERAL",
80
+ settlementDelay: "DEFERRED",
81
+ currency: 'USD',
82
+ requireLiquidityCheck: true,
83
+ ledgerAccountType: "POSITION",
84
+ settlementAccountType: "SETTLEMENT",
85
+ autoPositionReset: true
86
+ }
87
+ ]
88
+ }
89
+ await ApiHelpers.createHub(harness, createHubPayload)
90
+ // Create 2 test dfsps to transfer between.
91
+ await ApiHelpers.createDfsp(harness, {
92
+ name: 'dfsp_a',
93
+ currencies: ['BWP'],
94
+ isProxy: false,
95
+ initialPostionsAndLimits: [{ initialPosition: 0, value: 100000 }],
96
+ deposits: [10000]
97
+ })
98
+ await ApiHelpers.createDfsp(harness, {
99
+ name: 'dfsp_b',
100
+ currencies: ['BWP', 'USD'],
101
+ isProxy: false,
102
+ initialPostionsAndLimits: [
103
+ { initialPosition: 0, value: 100000 },
104
+ { initialPosition: 0, value: 100000 }
105
+ ],
106
+ deposits: [10000, 10000]
107
+ })
108
+ await ApiHelpers.createDfsp(harness, {
109
+ name: 'dfsp_a_proxy',
110
+ currencies: ['BWP', 'USD'],
111
+ isProxy: true,
112
+ initialPostionsAndLimits: [
113
+ { initialPosition: 0, value: 100000 },
114
+ { initialPosition: 0, value: 100000 }
115
+ ],
116
+ deposits: [10000, 10000]
117
+ })
118
+ await ApiHelpers.createDfsp(harness, {
119
+ name: 'dfsp_b_proxy',
120
+ currencies: ['BWP', 'USD'],
121
+ isProxy: true,
122
+ initialPostionsAndLimits: [
123
+ { initialPosition: 0, value: 100000 },
124
+ { initialPosition: 0, value: 100000 }
125
+ ],
126
+ deposits: [10000, 10000]
127
+ })
128
+ await ApiHelpers.createDfsp(harness, {
129
+ name: 'fxp_a',
130
+ currencies: ['BWP', 'USD'],
131
+ isProxy: false,
132
+ initialPostionsAndLimits: [
133
+ { initialPosition: 0, value: 100000 },
134
+ { initialPosition: 0, value: 100000 }
135
+ ],
136
+ deposits: [10000, 10000]
137
+ })
138
+
139
+ // Set up the proxy1 currencies.
140
+ await proxyCache.getCache().addDfspIdToProxyMapping('external_dfsp_a', 'dfsp_a_proxy')
141
+ await proxyCache.getCache().addDfspIdToProxyMapping('external_dfsp_b', 'dfsp_b_proxy')
142
+
143
+ // Create payment of $100.00 USD from dfsp_a to dfsp_b with id 1000001.
144
+ await ApiHelpers.buildPayment()
145
+ .deps(harness, TransferHandler)
146
+ .parties('dfsp_a', 'dfsp_b')
147
+ .transferId('1000001')
148
+ .amount('1.00', 'BWP')
149
+ .build()
150
+ .prepareAndFulfil()
151
+ })
152
+
153
+ after(async () => {
154
+ await proxyCache.disconnect()
155
+ await harness.teardownGlobals()
156
+ await harness.down()
157
+ })
158
+
159
+ /**
160
+ * fxTransfers
161
+ */
162
+ it('Calculates the obligation between initating and counterparty DFSP.', async () => {
163
+ const payload = {
164
+ commitRequestId: '200001',
165
+ determiningTransferId: '300001',
166
+ initiatingFsp: 'external_dfsp_a',
167
+ counterPartyFsp: 'external_dfsp_b',
168
+ amountType: 'SEND',
169
+ sourceAmount: { currency: 'BWP', amount: '300.33' },
170
+ targetAmount: { currency: 'TZS', amount: '48000' },
171
+ // Mock condition.
172
+ condition: '8x04dj-RKEtfjStajaKXKJ5eL1mWm9iG2ltEKvEDOHc',
173
+ expiration: new Date(Date.now() + (24 * 60 * 60 * 1000))
174
+ }
175
+
176
+ const obligation = await PrepareHandler.calculateProxyObligation({
177
+ payload,
178
+ isFx: true,
179
+ params: {},
180
+ functionality: 'functionality',
181
+ action: 'action'
182
+ })
183
+
184
+ Snapshot.from(`{
185
+ "isFx": true,
186
+ "payloadClone": {
187
+ "commitRequestId": "200001",
188
+ "determiningTransferId": "300001",
189
+ "initiatingFsp": "dfsp_a_proxy",
190
+ "counterPartyFsp": "dfsp_b_proxy",
191
+ "amountType": "SEND",
192
+ "sourceAmount": {
193
+ "currency": "BWP",
194
+ "amount": "300.33"
195
+ },
196
+ "targetAmount": {
197
+ "currency": "TZS",
198
+ "amount": "48000"
199
+ },
200
+ "condition": "8x04dj-RKEtfjStajaKXKJ5eL1mWm9iG2ltEKvEDOHc",
201
+ "expiration": :ignore
202
+ },
203
+ "isInitiatingFspProxy": true,
204
+ "isCounterPartyFspProxy": true,
205
+ "initiatingFspProxyOrParticipantId": {
206
+ "inScheme": false,
207
+ "proxyId": "dfsp_a_proxy",
208
+ "name": "external_dfsp_a"
209
+ },
210
+ "counterPartyFspProxyOrParticipantId": {
211
+ "inScheme": false,
212
+ "proxyId": "dfsp_b_proxy",
213
+ "name": "external_dfsp_b"
214
+ }
215
+ }`).checkUnwrap(obligation)
216
+ })
217
+
218
+ it('Lazy creates external participants.', async () => {
219
+ let payerExternal = await ExternalParticipantCached.getByName('external_dfsp_a')
220
+ let payeeExternal = await ExternalParticipantCached.getByName('external_dfsp_b')
221
+ assert(payerExternal === undefined)
222
+ assert(payeeExternal === undefined)
223
+
224
+ await ApiHelpers.buildForex()
225
+ .deps(harness, TransferHandler)
226
+ .commitRequestId('2000001')
227
+ .determiningTransferId('3000001')
228
+ .parties('external_dfsp_a', 'external_dfsp_b')
229
+ .amountSource('100.00', 'BWP')
230
+ .amountTarget('1.00', 'USD')
231
+ .build()
232
+ .prepareAndFulfil()
233
+
234
+ const payment = ApiHelpers.buildPayment()
235
+ .deps(harness, TransferHandler)
236
+ .parties('external_dfsp_a', 'external_dfsp_b')
237
+ .transferId('3000001')
238
+ .amount('1.00', 'USD')
239
+ .fx()
240
+ .build()
241
+
242
+ await payment.prepare()
243
+
244
+ // Assert that the've been created?
245
+ payerExternal = await ExternalParticipantCached.getByName('external_dfsp_a')
246
+ payeeExternal = await ExternalParticipantCached.getByName('external_dfsp_b')
247
+ assert(payerExternal)
248
+ assert(payeeExternal)
249
+
250
+ const [participant] = await TransferFacade.getTransferParticipant('dfsp_a_proxy', '3000001')
251
+ Snapshot.from(`{
252
+ "transferParticipantId": :ignore,
253
+ "transferId": "3000001",
254
+ "participantCurrencyId": :ignore,
255
+ "transferParticipantRoleTypeId": 1,
256
+ "ledgerEntryTypeId": 1,
257
+ "amount": "1.0000",
258
+ "createdDate": :ignore
259
+ "participantId": 4,
260
+ "externalParticipantId": 1
261
+ }`).checkUnwrap(participant)
262
+
263
+ // Fulfil the payment so it doesn't time out later.
264
+ await payment.fulfil()
265
+ })
266
+
267
+ it('Ignores non COMMITTED/ABORTED fxTransfer on duplicate request.', async () => {
268
+ const forex = ApiHelpers.buildForex()
269
+ .deps(harness, TransferHandler)
270
+ .commitRequestId('2000002')
271
+ .determiningTransferId('3000002')
272
+ .parties('external_dfsp_a', 'external_dfsp_b')
273
+ .amountSource('100.00', 'BWP')
274
+ .amountTarget('1.00', 'USD')
275
+ .build()
276
+
277
+ await forex.prepare()
278
+
279
+ // Manually prepare again.
280
+ const mark = harness.redpandaMark()
281
+ await PrepareHandler.prepare(null, forex.buildMessagePrepare())
282
+ await harness.redpandaDrain(mark, 1)
283
+
284
+ const lastTopics = harness.spoolLastTopic(3)
285
+ Snapshot.from(`[
286
+ "topic-transfer-position-batch",
287
+ "topic-notification-event",
288
+ "topic-notification-event"
289
+ ]`).checkUnwrap(lastTopics)
290
+ })
291
+
292
+ it('Duplicate fxTransfers callback when in `RECEIVED_FULFIL_DEPENDENT` state.', async () => {
293
+ const forex = ApiHelpers.buildForex()
294
+ .deps(harness, TransferHandler)
295
+ .commitRequestId('2000003')
296
+ .determiningTransferId('3000003')
297
+ .parties('external_dfsp_a', 'external_dfsp_b')
298
+ .amountSource('100.00', 'BWP')
299
+ .amountTarget('1.00', 'USD')
300
+ .build()
301
+
302
+ await forex.prepareAndFulfil()
303
+
304
+ // Now send the first message again.
305
+ // await forex.prepare()
306
+ const mark = harness.redpandaMark()
307
+ await PrepareHandler.prepare(null, forex.buildMessagePrepare())
308
+ await harness.redpandaDrain(mark, 1)
309
+
310
+ const lastTopics = harness.spoolLastTopic(1)
311
+ Snapshot.from(`[
312
+ "topic-notification-event"
313
+ ]`).checkUnwrap(lastTopics)
314
+
315
+ Snapshot.from(`{
316
+ "completedTimestamp": :ignore,
317
+ "conversionState": "RESERVED",
318
+ "fulfilment": "uz0FAeutW6o8Mz7OmJh8ALX6mmsZCcIDOqtE01eo4uI"
319
+ }`).checkUnwrap(harness.spoolLastPayload(1)[0])
320
+ })
321
+
322
+ it('Duplicate fxTransfers callback when in `COMMITTED` state.', async () => {
323
+ const forex = ApiHelpers.buildForex()
324
+ .deps(harness, TransferHandler)
325
+ .commitRequestId('2000004')
326
+ .determiningTransferId('3000004')
327
+ .parties('external_dfsp_a', 'external_dfsp_b')
328
+ .amountSource('100.00', 'BWP')
329
+ .amountTarget('1.00', 'USD')
330
+ .build()
331
+
332
+ await forex.prepareAndFulfil()
333
+
334
+ // Make the payment.
335
+ await ApiHelpers.buildPayment()
336
+ .deps(harness, TransferHandler)
337
+ .parties('external_dfsp_a', 'external_dfsp_b')
338
+ .transferId('3000004')
339
+ .amount('1.00', 'USD')
340
+ .fx()
341
+ .build()
342
+ .prepareAndFulfil()
343
+
344
+ // Now send the first message again.
345
+ const mark = harness.redpandaMark()
346
+ await PrepareHandler.prepare(null, forex.buildMessagePrepare())
347
+ await harness.redpandaDrain(mark, 1)
348
+
349
+ const lastTopics = harness.spoolLastTopic(1)
350
+ Snapshot.from(`[
351
+ "topic-notification-event"
352
+ ]`).checkUnwrap(lastTopics)
353
+
354
+ Snapshot.from(`{
355
+ "completedTimestamp": :ignore,
356
+ "conversionState": "COMMITTED",
357
+ "fulfilment": "uz0FAeutW6o8Mz7OmJh8ALX6mmsZCcIDOqtE01eo4uI"
358
+ }`).checkUnwrap(harness.spoolLastPayload(1)[0])
359
+ })
360
+
361
+ it('Duplicate fxTransfers callback when in `ABORTED` state.', async () => {
362
+ const forex = ApiHelpers.buildForex()
363
+ .deps(harness, TransferHandler)
364
+ .commitRequestId('2000005')
365
+ .determiningTransferId('3000005')
366
+ .parties('external_dfsp_a', 'external_dfsp_b')
367
+ .amountSource('100.00', 'BWP')
368
+ .amountTarget('1.00', 'USD')
369
+ .build()
370
+
371
+ await forex.prepare()
372
+ await forex.abort()
373
+
374
+ // Now send the first message again.
375
+ const mark = harness.redpandaMark()
376
+ await PrepareHandler.prepare(null, forex.buildMessagePrepare())
377
+ await harness.redpandaDrain(mark, 1)
378
+
379
+ const lastTopics = harness.spoolLastTopic(1)
380
+ Snapshot.from(`[
381
+ "topic-notification-event"
382
+ ]`).checkUnwrap(lastTopics)
383
+
384
+ Snapshot.from(`{
385
+ "completedTimestamp": :ignore,
386
+ "conversionState": "ABORTED"
387
+ }`).checkUnwrap(harness.spoolLastPayload(1)[0])
388
+ })
389
+
390
+ /**
391
+ * Proxy
392
+ */
393
+ it('Updates the transfer state on prepare forwarded action.', async () => {
394
+ const transferId = '3000006'
395
+ const payment = ApiHelpers.buildPayment()
396
+ .deps(harness, TransferHandler)
397
+ .parties('dfsp_a', 'dfsp_b')
398
+ .transferId(transferId)
399
+ .amount('45.67', 'BWP')
400
+ .build()
401
+
402
+ await payment.prepare()
403
+
404
+ const forwardedMsg = ApiHelpers.buildMessageForwarded(harness, {
405
+ transferId,
406
+ proxyId: 'dfsp_a_proxy'
407
+ })
408
+ await PrepareHandler.prepare(null, forwardedMsg)
409
+
410
+ const transfer = await TransferFacade.getById(transferId)
411
+ assert.equal(transfer.transferState, 'RESERVED_FORWARDED')
412
+
413
+ // Fulfil the payment so it doesn't time out later.
414
+ await payment.fulfil()
415
+ })
416
+
417
+ it('Should not time out the prepared payment in RESERVED_FORWARDED state.', async () => {
418
+ const transferId = '3000007'
419
+ const payment = ApiHelpers.buildPayment()
420
+ .deps(harness, TransferHandler)
421
+ .parties('dfsp_a', 'dfsp_b')
422
+ .transferId(transferId)
423
+ .amount('100.00', 'BWP')
424
+ .expiry(1)
425
+ .build()
426
+
427
+ const [positionPayerA] = await ApiHelpers.getPositions('dfsp_a', 'dfsp_b', 'BWP')
428
+ await payment.prepare()
429
+ const forwardedMsg = ApiHelpers.buildMessageForwarded(harness, {
430
+ transferId: transferId,
431
+ proxyId: 'dfsp_a_proxy'
432
+ })
433
+ await PrepareHandler.prepare(null, forwardedMsg)
434
+
435
+ const [positionPayerB] = await ApiHelpers.getPositions('dfsp_a', 'dfsp_b', 'BWP')
436
+ assertPositionDiff('payer', positionPayerA, positionPayerB, {
437
+ pending: 0,
438
+ posted: 100
439
+ })
440
+
441
+ const transfer = await TransferFacade.getById(transferId)
442
+ assert.equal(transfer.transferState, 'RESERVED_FORWARDED')
443
+
444
+ await sleepSeconds(10)
445
+ // We don't run the timeout handler in these tests, but instead just call timeout()
446
+ // directly. This makes the tests more predictable and deterministic.
447
+ const mark = harness.redpandaMark()
448
+ await TimeoutHandler.timeout()
449
+ await harness.redpandaDrain(mark, 1)
450
+ const [positionPayerC] = await ApiHelpers.getPositions('dfsp_a', 'dfsp_b', 'BWP')
451
+ assertPositionDiff('payer', positionPayerB, positionPayerC, {
452
+ pending: 0,
453
+ posted: 0
454
+ })
455
+ await payment.abort()
456
+ })
457
+
458
+ it('Completes a forwarded payment.', async () => {
459
+ const transferId = '3000008'
460
+ const payment = ApiHelpers.buildPayment()
461
+ .deps(harness, TransferHandler)
462
+ .parties('dfsp_a', 'dfsp_b')
463
+ .transferId(transferId)
464
+ .amount('100.00', 'BWP')
465
+ .expiry(120)
466
+ .build()
467
+
468
+ const [positionPayerA] = await ApiHelpers.getPositions('dfsp_a', 'dfsp_b', 'BWP')
469
+ await payment.prepare()
470
+ const forwardedMsg = ApiHelpers.buildMessageForwarded(harness, {
471
+ transferId: transferId,
472
+ proxyId: 'dfsp_a_proxy'
473
+ })
474
+ await PrepareHandler.prepare(null, forwardedMsg)
475
+
476
+ const [positionPayerB, positionPayeeB] = await ApiHelpers.getPositions('dfsp_a', 'dfsp_b', 'BWP')
477
+ assertPositionDiff('payer', positionPayerA, positionPayerB, {
478
+ pending: 0,
479
+ posted: 100
480
+ })
481
+
482
+ let transfer = await TransferFacade.getById(transferId)
483
+ assert.equal(transfer.transferState, 'RESERVED_FORWARDED')
484
+
485
+ await payment.fulfil()
486
+ transfer = await TransferFacade.getById(transferId)
487
+ assert.equal(transfer.transferState, 'COMMITTED')
488
+
489
+ const [positionPayerC, positionPayeeC] = await ApiHelpers.getPositions('dfsp_a', 'dfsp_b', 'BWP')
490
+ assertPositionDiff('payer', positionPayerB, positionPayerC, {
491
+ pending: 0,
492
+ posted: 0
493
+ })
494
+ assertPositionDiff('payee', positionPayeeB, positionPayeeC, {
495
+ pending: 0,
496
+ posted: -100
497
+ })
498
+ })
499
+
500
+ it('Aborts a forwarded payment.', async () => {
501
+ const transferId = '3000010'
502
+ const payment = ApiHelpers.buildPayment()
503
+ .deps(harness, TransferHandler)
504
+ .parties('dfsp_a', 'dfsp_b')
505
+ .transferId(transferId)
506
+ .amount('100.00', 'BWP')
507
+ .expiry(120)
508
+ .build()
509
+
510
+ const [positionPayerA, positionPayeeA] = await ApiHelpers.getPositions('dfsp_a', 'dfsp_b', 'BWP')
511
+ await payment.prepare()
512
+ const forwardedMsg = ApiHelpers.buildMessageForwarded(harness, {
513
+ transferId: transferId,
514
+ proxyId: 'dfsp_a_proxy'
515
+ })
516
+ await PrepareHandler.prepare(null, forwardedMsg)
517
+
518
+ const [positionPayerB, positionPayeeB] = await ApiHelpers.getPositions('dfsp_a', 'dfsp_b', 'BWP')
519
+ assertPositionDiff('payer', positionPayerA, positionPayerB, {
520
+ pending: 0,
521
+ posted: 100
522
+ })
523
+
524
+ let transfer = await TransferFacade.getById(transferId)
525
+ assert.equal(transfer.transferState, 'RESERVED_FORWARDED')
526
+
527
+ await payment.abort()
528
+ transfer = await TransferFacade.getById(transferId)
529
+ assert.equal(transfer.transferState, 'ABORTED_ERROR')
530
+
531
+ const [positionPayerC, positionPayeeC] = await ApiHelpers.getPositions('dfsp_a', 'dfsp_b', 'BWP')
532
+ // A --> C is the same (Payment was aborted).
533
+ assertPositionDiff('payer', positionPayerA, positionPayerC, {
534
+ pending: 0,
535
+ posted: 0
536
+ })
537
+ assertPositionDiff('payee', positionPayeeB, positionPayeeC, {
538
+ pending: 0,
539
+ posted: 0
540
+ })
541
+ })
542
+
543
+ it('Notifies when the forwarded payment cannot be found.', async () => {
544
+ const transferId = '3000011'
545
+
546
+ const forwardedMsg = ApiHelpers.buildMessageForwarded(harness, {
547
+ transferId: transferId,
548
+ proxyId: 'dfsp_a_proxy'
549
+ })
550
+ let mark = harness.redpandaMark();
551
+ await PrepareHandler.prepare(null, forwardedMsg)
552
+ await harness.redpandaDrain(mark, 1)
553
+
554
+ const lastPayload = harness.spoolLastPayload(1)
555
+ Snapshot.from(`[
556
+ {
557
+ "errorInformation": {
558
+ "errorCode": "3200",
559
+ "errorDescription": "Generic ID not found - Forwarded transfer could not be found.",
560
+ "extensionList": {
561
+ "extension": [
562
+ {
563
+ "key": "cause",
564
+ "value": :ignore
565
+ }
566
+ ]
567
+ }
568
+ }
569
+ }
570
+ ]`).checkUnwrap(lastPayload)
571
+ })
572
+
573
+ it('Notifies if the transfer is in an invalid state.', async () => {
574
+ const transferId = '3000012'
575
+ const paymentA = ApiHelpers.buildPayment()
576
+ .deps(harness, TransferHandler)
577
+ .parties('dfsp_a', 'dfsp_b')
578
+ .transferId(transferId)
579
+ .amount('100.00', 'BWP')
580
+ .expiry(1)
581
+ .build()
582
+
583
+ // Prepare and timeout.
584
+ await paymentA.prepare()
585
+ const transferBefore = await TransferFacade.getById(transferId)
586
+ assert.equal(transferBefore.transferState, 'RESERVED')
587
+ await sleepSeconds(10)
588
+
589
+ let mark = harness.redpandaMark()
590
+ await TimeoutHandler.timeout()
591
+ await harness.redpandaDrain(mark, 2)
592
+
593
+ const transferExpired = await TransferFacade.getById(transferId)
594
+ assert.equal(transferExpired.transferState, 'EXPIRED_RESERVED')
595
+
596
+ const forwardedMsg = ApiHelpers.buildMessageForwarded(harness, {
597
+ transferId: transferId,
598
+ proxyId: 'dfsp_a_proxy'
599
+ })
600
+ mark = harness.redpandaMark()
601
+ await PrepareHandler.prepare(null, forwardedMsg)
602
+ await harness.redpandaDrain(mark, 1)
603
+
604
+ Snapshot.from(`[
605
+ {
606
+ "errorInformation": {
607
+ "errorCode": "2001",
608
+ "errorDescription": "Internal server error - Invalid State: EXPIRED_RESERVED - expected: RESERVED",
609
+ "extensionList": {
610
+ "extension": [
611
+ {
612
+ "key": "cause",
613
+ "value": :ignore
614
+ }
615
+ ]
616
+ }
617
+ }
618
+ }
619
+ ]`).checkUnwrap(harness.spoolLastPayload(1))
620
+ })
621
+
622
+ it('Notifies with GET on timeout if the transfer is RESERVED_FORWARDED', async () => {
623
+ const transferId = '3000013'
624
+ const payment = ApiHelpers.buildPayment()
625
+ .deps(harness, TransferHandler)
626
+ .parties('dfsp_a', 'dfsp_b')
627
+ .transferId(transferId)
628
+ .amount('100.00', 'BWP')
629
+ .expiry(1)
630
+ .build()
631
+
632
+ // Prepare and forward.
633
+ await payment.prepare()
634
+ let transfer = await TransferFacade.getById(transferId)
635
+ assert.equal(transfer.transferState, 'RESERVED')
636
+
637
+ const forwardedMsg = ApiHelpers.buildMessageForwarded(harness, {
638
+ transferId: transferId,
639
+ proxyId: 'dfsp_a_proxy'
640
+ })
641
+ await PrepareHandler.prepare(null, forwardedMsg)
642
+ transfer = await TransferFacade.getById(transferId)
643
+ assert.equal(transfer.transferState, 'RESERVED_FORWARDED')
644
+
645
+ // Time out to trigger notification.
646
+ await sleepSeconds(10)
647
+ const mark = harness.redpandaMark()
648
+ const resultTimeout = await TimeoutHandler.timeout()
649
+ await harness.redpandaDrain(mark, 1)
650
+
651
+ assert(resultTimeout)
652
+ // Annoyingly the timeout handler still includes aborted transfers in timeouts.
653
+ const reservedForwardedTimedout = resultTimeout.transferForwardedList
654
+ .filter((transfer: any) => transfer.transferStateId === 'RESERVED_FORWARDED')
655
+ Snapshot.from(`[
656
+ {
657
+ "transferForwardedId": :ignore
658
+ "transferId": "3000013",
659
+ "expirationDate": :ignore
660
+ "attemptCount": 0,
661
+ "createdDate": :ignore
662
+ "transferStateId": "RESERVED_FORWARDED",
663
+ "payerParticipantCurrencyId": 5,
664
+ "payerFsp": "dfsp_a",
665
+ "payeeFsp": "dfsp_b",
666
+ "payeeParticipantCurrencyId": null,
667
+ "bulkTransferId": null,
668
+ "effectedParticipantCurrencyId": 5,
669
+ "externalPayerName": null,
670
+ "externalPayeeName": null
671
+ }
672
+ ]`).checkUnwrap(reservedForwardedTimedout)
673
+ assert(reservedForwardedTimedout.length === 1)
674
+ // Get has no payload.
675
+ Snapshot.from(`[
676
+ {}
677
+ ]`).checkUnwrap(harness.spoolLastPayload(1))
678
+ Snapshot.from(`[
679
+ "topic-notification-event"
680
+ ]`).checkUnwrap(harness.spoolLastTopic(1))
681
+
682
+ await payment.abort()
683
+ })
684
+
685
+ /**
686
+ * FX + Proxy.
687
+ */
688
+
689
+ it('should update fxTransfer internal state on prepare event fx-forwarded action', async () => {
690
+ const forex = ApiHelpers.buildForex()
691
+ .deps(harness, TransferHandler)
692
+ .commitRequestId('2000010')
693
+ .determiningTransferId('3000010')
694
+ .parties('external_dfsp_a', 'external_dfsp_b')
695
+ .amountSource('100.00', 'BWP')
696
+ .amountTarget('1.00', 'USD')
697
+ .build()
698
+
699
+ await forex.prepare()
700
+
701
+ // Forward the fxPrepare to the proxy.
702
+ const forwardedMsg = ApiHelpers.buildMessageForwardedFx(harness, {
703
+ commitRequestId: '2000010',
704
+ proxyId: 'dfsp_a_proxy'
705
+ })
706
+ await PrepareHandler.prepare(null, forwardedMsg)
707
+
708
+ // Check the result.
709
+ const transfer = await FxTransferService.getByIdLight('2000010')
710
+ assert.equal(transfer.fxTransferState, 'RESERVED_FORWARDED')
711
+ })
712
+
713
+ it('not timeout fxTransfer in RESERVED_FORWARDED internal transfer state', async () => {
714
+ const forex = ApiHelpers.buildForex()
715
+ .deps(harness, TransferHandler)
716
+ .commitRequestId('2000011')
717
+ .determiningTransferId('3000011')
718
+ .parties('external_dfsp_a', 'external_dfsp_b')
719
+ .amountSource('100.00', 'BWP')
720
+ .amountTarget('1.00', 'USD')
721
+ .expiry(5)
722
+ .build()
723
+
724
+ await forex.prepare()
725
+ const forwardedMsg = ApiHelpers.buildMessageForwardedFx(harness, {
726
+ commitRequestId: '2000011',
727
+ proxyId: 'dfsp_a_proxy'
728
+ })
729
+ await PrepareHandler.prepare(null, forwardedMsg)
730
+ let transfer = await FxTransferService.getByIdLight('2000011')
731
+ assert.equal(transfer.fxTransferState, 'RESERVED_FORWARDED')
732
+
733
+ // Now time it out - no message should be produced.
734
+ await sleepSeconds(10)
735
+ const mark = harness.redpandaMark()
736
+ await TimeoutHandler.timeout()
737
+ await harness.redpandaDrain(mark, 0)
738
+
739
+ // Check the result.
740
+ transfer = await FxTransferService.getByIdLight('2000011')
741
+ assert.equal(transfer.fxTransferState, 'RESERVED_FORWARDED')
742
+ })
743
+
744
+ // // This test is broken, there is currently no notification produced.
745
+ // // See note on `test/integration-override/handlers/transfers/handlers.test.js`.
746
+ // it.skip('produces a get notification if fx transfer stuck in RESERVED_FORWARDED', async () => {
747
+ // const forex = ApiHelpers.buildForex()
748
+ // .deps(harness, TransferHandler)
749
+ // .commitRequestId('2000012')
750
+ // .determiningTransferId('3000012')
751
+ // .parties('external_dfsp_a', 'external_dfsp_b')
752
+ // .amountSource('100.00', 'BWP')
753
+ // .amountTarget('1.00', 'USD')
754
+ // .expiry(5)
755
+ // .build()
756
+
757
+ // await forex.prepare()
758
+ // const forwardedMsg = ApiHelpers.buildMessageForwardedFx(harness, {
759
+ // commitRequestId: '2000012',
760
+ // proxyId: 'dfsp_a_proxy'
761
+ // })
762
+ // await PrepareHandler.prepare(null, forwardedMsg)
763
+ // let transfer = await FxTransferService.getByIdLight('2000012')
764
+ // assert.equal(transfer.fxTransferState, 'RESERVED_FORWARDED')
765
+
766
+ // // Now time it out - no message should be produced.
767
+ // await sleepSeconds(10)
768
+ // const mark = harness.redpandaMark()
769
+ // await TimeoutHandler.timeout()
770
+ // await harness.redpandaDrain(mark, 0)
771
+
772
+ // // Check the result.
773
+ // transfer = await FxTransferService.getByIdLight('2000012')
774
+ // assert.equal(transfer.fxTransferState, 'RESERVED_FORWARDED')
775
+ // })
776
+
777
+ it('transitions RESERVED_FORWARDED -> RECEIVED_FULFIL_DEPENDENT on fx-fulfil', async () => {
778
+ const forex = ApiHelpers.buildForex()
779
+ .deps(harness, TransferHandler)
780
+ .commitRequestId('2000012')
781
+ .determiningTransferId('3000012')
782
+ .parties('external_dfsp_a', 'external_dfsp_b')
783
+ .amountSource('100.00', 'BWP')
784
+ .amountTarget('1.00', 'USD')
785
+ .expiry(5)
786
+ .build()
787
+ await forex.prepare()
788
+
789
+ // Forward the message.
790
+ const forwardedMsg = ApiHelpers.buildMessageForwardedFx(harness, {
791
+ commitRequestId: '2000012',
792
+ proxyId: 'dfsp_a_proxy'
793
+ })
794
+ await PrepareHandler.prepare(null, forwardedMsg)
795
+ let transfer = await FxTransferService.getByIdLight('2000012')
796
+ assert.equal(transfer.fxTransferState, 'RESERVED_FORWARDED')
797
+
798
+ await forex.fulfil()
799
+ transfer = await FxTransferService.getByIdLight('2000012')
800
+ assert.equal(transfer.fxTransferState, 'RECEIVED_FULFIL_DEPENDENT')
801
+ })
802
+
803
+ it('transitions RESERVED_FORWARDED -> ABORTED_ERROR on fx-fulfil-error', async () => {
804
+ const forex = ApiHelpers.buildForex()
805
+ .deps(harness, TransferHandler)
806
+ .commitRequestId('2000013')
807
+ .determiningTransferId('3000013')
808
+ .parties('external_dfsp_a', 'external_dfsp_b')
809
+ .amountSource('100.00', 'BWP')
810
+ .amountTarget('1.00', 'USD')
811
+ .expiry(5)
812
+ .build()
813
+ await forex.prepare()
814
+
815
+ // Forward the message.
816
+ const forwardedMsg = ApiHelpers.buildMessageForwardedFx(harness, {
817
+ commitRequestId: '2000013',
818
+ proxyId: 'dfsp_a_proxy'
819
+ })
820
+ await PrepareHandler.prepare(null, forwardedMsg)
821
+ let transfer = await FxTransferService.getByIdLight('2000013')
822
+ assert.equal(transfer.fxTransferState, 'RESERVED_FORWARDED')
823
+
824
+ await forex.abort()
825
+ transfer = await FxTransferService.getByIdLight('2000013')
826
+ assert.equal(transfer.fxTransferState, 'ABORTED_ERROR')
827
+ })
828
+
829
+ it('notifies if fxTransfer is not found', async () => {
830
+ // Forward the message.
831
+ const forwardedMsg = ApiHelpers.buildMessageForwardedFx(harness, {
832
+ commitRequestId: '2000014',
833
+ proxyId: 'dfsp_a_proxy'
834
+ })
835
+ const mark = harness.redpandaMark()
836
+ await PrepareHandler.prepare(null, forwardedMsg)
837
+ await harness.redpandaDrain(mark, 1)
838
+
839
+ Snapshot.from(`[
840
+ {
841
+ "errorInformation": {
842
+ "errorCode": "3200",
843
+ "errorDescription": "Generic ID not found - Forwarded fxTransfer could not be found.",
844
+ "extensionList": {
845
+ "extension": [
846
+ {
847
+ "key": "cause",
848
+ "value": "FSPIOPError: Forwarded fxTransfer could not be found.:ignore
849
+ }
850
+ ]
851
+ }
852
+ }
853
+ }
854
+ ]`).checkUnwrap(harness.spoolLastPayload(1))
855
+ Snapshot.from(`[
856
+ "topic-notification-event"
857
+ ]`).checkUnwrap(harness.spoolLastTopic(1))
858
+ })
859
+
860
+ // // TODO: I'm not sure how to get the timeouts to emit the position change messages.
861
+ // // I think this was actually broken in the original tests.
862
+ // it.skip('notifies if the transfer is in an invalid state', async () => {
863
+ // // Create a forex, time it out, but the problem is the timeouts are broken?
864
+ // const forex = ApiHelpers.buildForex()
865
+ // .deps(harness, TransferHandler)
866
+ // .commitRequestId('2000015')
867
+ // .determiningTransferId('3000015')
868
+ // .parties('external_dfsp_a', 'external_dfsp_b')
869
+ // .amountSource('100.00', 'BWP')
870
+ // .amountTarget('1.00', 'USD')
871
+ // .expiry(1)
872
+ // .build()
873
+ // await forex.prepare()
874
+
875
+ // await sleepSeconds(5)
876
+
877
+ // const knex = Db.getKnex()
878
+ // await knex('segment')
879
+ // .where({ tableName: 'fxTransferStateChange', segmentType: 'timeout' })
880
+ // .update({ value: 0 })
881
+
882
+ // const mark = harness.redpandaMark()
883
+ // // For some reason, timeout isn't producing any messages.
884
+ // const timeoutResult = await TimeoutHandler.timeout()
885
+ // await harness.redpandaDrain(mark, 1)
886
+
887
+ // let fxTransfer = await FxTransferService.getByIdLight('2000015')
888
+ // assert.equal(fxTransfer.fxTransferState, 'EXPIRED_RESERVED')
889
+ // })
890
+
891
+ it(
892
+ `Scheme A: POST /fxTransfer call I.e. Debtor: Payer DFSP → Creditor: Proxy AR
893
+ Payer DFSP position account must be updated (reserved)`,
894
+ async () => {
895
+ const externalFxp = 'external_fxp'
896
+ await proxyCache.getCache().addDfspIdToProxyMapping(externalFxp, 'fxp_proxy')
897
+
898
+ const positionPayerPre = await ApiHelpers.getPositionAccount('dfsp_a', 'BWP')
899
+ const forex = ApiHelpers.buildForex()
900
+ .deps(harness, TransferHandler)
901
+ .commitRequestId('2000017')
902
+ .determiningTransferId('3000017')
903
+ .parties('dfsp_a', 'external_dfsp_b')
904
+ .amountSource('100.00', 'BWP')
905
+ .amountTarget('1.00', 'USD')
906
+ .build()
907
+
908
+ await forex.prepare()
909
+ const fxTransfer = await FxTransferService.getByIdLight('2000017')
910
+ assert.equal(fxTransfer.fxTransferState, 'RESERVED')
911
+
912
+ const positionPayerPost = await ApiHelpers.getPositionAccount('dfsp_a', 'BWP')
913
+ assertPositionDiff('payer', positionPayerPre, positionPayerPost, {
914
+ pending: 0,
915
+ posted: 100
916
+ })
917
+ })
918
+
919
+ it(
920
+ `Scheme A: POST /Transfer call I.e. Debtor: Proxy AR → Creditor: Proxy AR
921
+ Do nothing (produce message with key 0)`,
922
+ async () => {
923
+ const externalFxp = 'external_fxp'
924
+ const externalPayee = 'external_payee'
925
+ await proxyCache.getCache().addDfspIdToProxyMapping(externalFxp, 'dfsp_a_proxy')
926
+ await proxyCache.getCache().addDfspIdToProxyMapping(externalPayee, 'dfsp_a_proxy')
927
+
928
+ const positionPayerPre = await ApiHelpers.getPositionAccount('dfsp_a', 'BWP')
929
+ const forex = ApiHelpers.buildForex()
930
+ .deps(harness, TransferHandler)
931
+ .commitRequestId('2000018')
932
+ .determiningTransferId('3000018')
933
+ .parties('dfsp_a', externalFxp)
934
+ .amountSource('100.00', 'BWP')
935
+ .amountTarget('10.00', 'USD')
936
+ .build()
937
+ await forex.prepareAndFulfil()
938
+
939
+ const positionPayerAfterFx = await ApiHelpers.getPositionAccount('dfsp_a', 'BWP')
940
+ assertPositionDiff('payer', positionPayerPre, positionPayerAfterFx, {
941
+ pending: 0,
942
+ posted: 100
943
+ })
944
+
945
+ // Send the transfer through.
946
+ const payment = ApiHelpers.buildPayment()
947
+ .deps(harness, TransferHandler)
948
+ .parties(externalFxp, externalPayee)
949
+ .transferId('3000018')
950
+ .amount('10.00', 'USD')
951
+ .fx()
952
+ .build()
953
+ await payment.prepare()
954
+
955
+ const positionPayerAfterTransfer = await ApiHelpers.getPositionAccount('dfsp_a', 'BWP')
956
+ // Ensure position doesn't change.
957
+ assertPositionDiff('payer', positionPayerAfterFx, positionPayerAfterTransfer, {
958
+ pending: 0,
959
+ posted: 0
960
+ })
961
+ })
962
+
963
+ it(
964
+ `Scheme R: POST /fxTransfer call I.e. Debtor: Proxy AR → Creditor: FXP
965
+ Proxy AR position account in source currency must be updated (reserved)`,
966
+ async () => {
967
+ const externalPayer = 'external_payer_r'
968
+ await proxyCache.getCache().addDfspIdToProxyMapping(externalPayer, 'dfsp_a_proxy')
969
+
970
+ const positionProxyPre = await ApiHelpers.getPositionAccount('dfsp_a_proxy', 'BWP')
971
+ const forex = ApiHelpers.buildForex()
972
+ .deps(harness, TransferHandler)
973
+ .commitRequestId('2000019')
974
+ .determiningTransferId('3000019')
975
+ .parties(externalPayer, 'dfsp_b')
976
+ .amountSource('100.00', 'BWP')
977
+ .amountTarget('10.00', 'USD')
978
+ .build()
979
+ await forex.prepare()
980
+ const positionProxyPost = await ApiHelpers.getPositionAccount('dfsp_a_proxy', 'BWP')
981
+ assertPositionDiff('payer', positionProxyPre, positionProxyPost, {
982
+ pending: 0,
983
+ posted: 100
984
+ })
985
+ })
986
+
987
+ it(
988
+ `Scheme R: POST /transfer call I.e. Debtor: FXP → Creditor: Proxy RB
989
+ FXP position account in targeted currency must be updated (reserved)`,
990
+ async () => {
991
+ const externalPayer = 'external_payer_r'
992
+ const externalPayee = 'external_payee_r'
993
+ await proxyCache.getCache().addDfspIdToProxyMapping(externalPayer, 'dfsp_a_proxy')
994
+ await proxyCache.getCache().addDfspIdToProxyMapping(externalPayee, 'dfsp_b_proxy')
995
+
996
+ await ApiHelpers.buildForex()
997
+ .deps(harness, TransferHandler)
998
+ .commitRequestId('2000020')
999
+ .determiningTransferId('3000020')
1000
+ .parties(externalPayer, 'fxp_a')
1001
+ .amountSource('100.00', 'BWP')
1002
+ .amountTarget('10.00', 'USD')
1003
+ .build()
1004
+ .prepareAndFulfil()
1005
+
1006
+ const positionFxpPre = await ApiHelpers.getPositionAccount('fxp_a', 'USD')
1007
+ const payment = ApiHelpers.buildPayment()
1008
+ .deps(harness, TransferHandler)
1009
+ .parties(externalPayer, externalPayee)
1010
+ .transferId('3000020')
1011
+ .amount('10.00', 'USD')
1012
+ .fx()
1013
+ .build()
1014
+
1015
+ await payment.prepare()
1016
+
1017
+ const positionFxpPost = await ApiHelpers.getPositionAccount('fxp_a', 'USD')
1018
+ assertPositionDiff('payer', positionFxpPre, positionFxpPost, {
1019
+ pending: 0,
1020
+ posted: 10
1021
+ })
1022
+ })
1023
+
1024
+ it(
1025
+ `Scheme B: POST /transfer call I.e. Debtor: Proxy RB → Creditor: Payee DFSP
1026
+ Proxy RB position account must be updated (reserved)`,
1027
+ async () => {
1028
+ const externalPayer = 'external_payer_scheme_b'
1029
+ await proxyCache.getCache().addDfspIdToProxyMapping(externalPayer, 'dfsp_a_proxy')
1030
+
1031
+ const positionProxyPre = await ApiHelpers.getPositionAccount('dfsp_a_proxy', 'BWP')
1032
+ const payment = ApiHelpers.buildPayment()
1033
+ .deps(harness, TransferHandler)
1034
+ .parties(externalPayer, 'dfsp_b')
1035
+ .transferId('3000021')
1036
+ .amount('93.00', 'BWP')
1037
+ .build()
1038
+
1039
+ await payment.prepare()
1040
+
1041
+ const positionProxyPost = await ApiHelpers.getPositionAccount('dfsp_a_proxy', 'BWP')
1042
+ assertPositionDiff('payer', positionProxyPre, positionProxyPost, {
1043
+ pending: 0,
1044
+ posted: 93
1045
+ })
1046
+ })
1047
+
1048
+ it(
1049
+ `Scheme B: PUT /transfers call I.e. From: Payee DFSP → To: Proxy RB
1050
+ Payee DFSP position account must be updated`,
1051
+ async () => {
1052
+ const externalPayer = 'external_payer_scheme'
1053
+ await proxyCache.getCache().addDfspIdToProxyMapping(externalPayer, 'dfsp_b_proxy')
1054
+
1055
+ const [positionProxyPre, positionPayeePre] = await ApiHelpers.getPositions(
1056
+ 'dfsp_b_proxy', 'dfsp_a', 'BWP'
1057
+ )
1058
+ const payment = ApiHelpers.buildPayment()
1059
+ .deps(harness, TransferHandler)
1060
+ .parties(externalPayer, 'dfsp_a')
1061
+ .transferId('3000022')
1062
+ .amount('38.92', 'BWP')
1063
+ .build()
1064
+ await payment.prepare()
1065
+ const [positionProxyAfterPrepare, positionPayeeAfterPrepare] = await ApiHelpers.getPositions(
1066
+ 'dfsp_b_proxy', 'dfsp_a', 'BWP'
1067
+ )
1068
+ assertPositionDiff('payer', positionProxyPre, positionProxyAfterPrepare, {
1069
+ posted: 38.92
1070
+ })
1071
+
1072
+ await payment.fulfil()
1073
+ const [positionProxyAfterFulfil, positionPayeeAfterFulfil] = await ApiHelpers.getPositions(
1074
+ 'dfsp_b_proxy', 'dfsp_a', 'BWP'
1075
+ )
1076
+ assertPositionDiff('payee', positionPayeeAfterPrepare, positionPayeeAfterFulfil, {
1077
+ posted: -38.92
1078
+ })
1079
+ })
1080
+
1081
+ it(
1082
+ `Scheme R: PUT /transfers call I.e. From: Proxy RB → To: Proxy AR
1083
+ If it is a normal transfer without currency conversion
1084
+ ProxyRB account must be updated`,
1085
+ async () => {
1086
+ const externalPayer = 'external_payer_scheme_r'
1087
+ const externalPayee = 'external_payee_scheme_r'
1088
+ await proxyCache.getCache().addDfspIdToProxyMapping(externalPayer, 'dfsp_a_proxy')
1089
+ await proxyCache.getCache().addDfspIdToProxyMapping(externalPayee, 'dfsp_b_proxy')
1090
+ const [positionProxyAPre, positionProxyBPre] = await ApiHelpers.getPositions(
1091
+ 'dfsp_a_proxy', 'dfsp_b_proxy', 'BWP'
1092
+ )
1093
+
1094
+ // Both parties are external.
1095
+ const payment = ApiHelpers.buildPayment()
1096
+ .deps(harness, TransferHandler)
1097
+ .parties(externalPayer, externalPayee)
1098
+ .transferId('3000023')
1099
+ .amount('12.05', 'BWP')
1100
+ .build()
1101
+ await payment.prepare()
1102
+ const [positionProxyAPostPrepare] = await ApiHelpers.getPositions(
1103
+ 'dfsp_a_proxy', 'dfsp_b_proxy', 'BWP'
1104
+ )
1105
+ assertPositionDiff('payer', positionProxyAPre, positionProxyAPostPrepare, {
1106
+ posted: 12.05
1107
+ })
1108
+
1109
+ await payment.fulfil()
1110
+ const [_, positionProxyBPostFulfil] = await ApiHelpers.getPositions(
1111
+ 'dfsp_a_proxy', 'dfsp_b_proxy', 'BWP'
1112
+ )
1113
+ assertPositionDiff('payee', positionProxyBPre, positionProxyBPostFulfil, {
1114
+ posted: -12.05
1115
+ })
1116
+ })
1117
+
1118
+ it(
1119
+ `Scheme R: PUT /fxTransfer call I.e. From: FXP → To: Proxy AR
1120
+ No position changes should happen`,
1121
+ async () => {
1122
+ const externalPayer = 'external_payer_scheme_r'
1123
+ await proxyCache.getCache().addDfspIdToProxyMapping(externalPayer, 'dfsp_a_proxy')
1124
+ const positionProxyPre = await ApiHelpers.getPositionAccount('dfsp_a_proxy', 'BWP')
1125
+ const positionFxpPreBWP = await ApiHelpers.getPositionAccount('fxp_a', 'BWP')
1126
+ const positionFxpPreUSD = await ApiHelpers.getPositionAccount('fxp_a', 'USD')
1127
+
1128
+ const forex = ApiHelpers.buildForex()
1129
+ .deps(harness, TransferHandler)
1130
+ .commitRequestId('2000030')
1131
+ .determiningTransferId('3000030')
1132
+ .parties(externalPayer, 'fxp_a')
1133
+ .amountSource('100.00', 'BWP')
1134
+ .amountTarget('10.00', 'USD')
1135
+ .build()
1136
+
1137
+ await forex.prepare()
1138
+ const positionProxyAfterPrepare = await ApiHelpers.getPositionAccount('dfsp_a_proxy', 'BWP')
1139
+
1140
+ assertPositionDiff('payer', positionProxyPre, positionProxyAfterPrepare, {
1141
+ posted: 100
1142
+ })
1143
+
1144
+ await forex.fulfil()
1145
+
1146
+ // Fxp positions don't change until the payment gets processed.
1147
+ const positionFxpAfterFulfilBWP = await ApiHelpers.getPositionAccount('fxp_a', 'BWP')
1148
+ const positionFxpAfterFulfilUSD = await ApiHelpers.getPositionAccount('fxp_a', 'USD')
1149
+ assertPositionDiff('payer', positionFxpPreBWP, positionFxpAfterFulfilBWP, { posted: 0 })
1150
+ assertPositionDiff('payee', positionFxpPreUSD, positionFxpAfterFulfilUSD, { posted: 0 })
1151
+ })
1152
+
1153
+ it(
1154
+ `Scheme R: PUT /fxTransfer call I.e. From: FXP → To: Proxy AR
1155
+ with wrong headers - ABORT VALIDATION`,
1156
+ async () => {
1157
+ const externalPayer = 'external_payer_scheme_r'
1158
+ await proxyCache.getCache().addDfspIdToProxyMapping(externalPayer, 'dfsp_a_proxy')
1159
+ const positionProxyPre = await ApiHelpers.getPositionAccount('dfsp_a_proxy', 'BWP')
1160
+
1161
+ const forex = ApiHelpers.buildForex()
1162
+ .deps(harness, TransferHandler)
1163
+ .commitRequestId('2000121')
1164
+ .determiningTransferId('3000121')
1165
+ .parties(externalPayer, 'fxp_a')
1166
+ .amountSource('100.00', 'BWP')
1167
+ .amountTarget('10.00', 'USD')
1168
+ .build()
1169
+
1170
+ await forex.prepare()
1171
+ const positionProxyAfterPrepare = await ApiHelpers.getPositionAccount('dfsp_a_proxy', 'BWP')
1172
+
1173
+ assertPositionDiff('payer', positionProxyPre, positionProxyAfterPrepare, {
1174
+ posted: 100
1175
+ })
1176
+
1177
+ // Manually fulfil with bad headers.
1178
+ const messageFulfil = forex.buildMessageFulfil()
1179
+ messageFulfil.value.content.headers['fspiop-source'] = 'wrongfsp'
1180
+ const mark = harness.redpandaMark()
1181
+ await TransferHandler.fulfil(null, messageFulfil)
1182
+ await harness.redpandaDrain(mark, 2) // maybe 1?
1183
+
1184
+ const fxTransfer = await FxTransferService.getByIdLight('2000121')
1185
+ assert.equal(fxTransfer.fxTransferState, 'ABORTED_ERROR')
1186
+
1187
+ // Position is reverted.
1188
+ const positionProxyAfterAbort = await ApiHelpers.getPositionAccount('dfsp_a_proxy', 'BWP')
1189
+ assertPositionDiff('payer', positionProxyPre, positionProxyAfterAbort, {
1190
+ posted: 0
1191
+ })
1192
+ })
1193
+
1194
+ it(
1195
+ `Scheme R: PUT /transfers call I.e. From: Proxy RB → To: Proxy AR
1196
+ If it is a FX transfer with currency conversion
1197
+ FXP and ProxyRB account must be updated`,
1198
+ async () => {
1199
+ const externalPayer = 'external_payer_scheme_r2'
1200
+ const externalPayee = 'external_payee_scheme_r2'
1201
+ await proxyCache.getCache().addDfspIdToProxyMapping(externalPayer, 'dfsp_a_proxy')
1202
+ await proxyCache.getCache().addDfspIdToProxyMapping(externalPayee, 'dfsp_b_proxy')
1203
+
1204
+ const positionProxyPayerPre = await ApiHelpers.getPositionAccount('dfsp_a_proxy', 'BWP')
1205
+ const positionFxpPreBWP = await ApiHelpers.getPositionAccount('fxp_a', 'BWP')
1206
+ const positionFxpPreUSD = await ApiHelpers.getPositionAccount('fxp_a', 'USD')
1207
+ const positionProxyPayeePre = await ApiHelpers.getPositionAccount('dfsp_b_proxy', 'USD')
1208
+
1209
+ const forex = ApiHelpers.buildForex()
1210
+ .deps(harness, TransferHandler)
1211
+ .commitRequestId('2000022')
1212
+ .determiningTransferId('3000222')
1213
+ .parties(externalPayer, 'fxp_a')
1214
+ .amountSource('100.00', 'BWP')
1215
+ .amountTarget('10.00', 'USD')
1216
+ .build()
1217
+ await forex.prepare()
1218
+ await forex.fulfil()
1219
+
1220
+ const payment = ApiHelpers.buildPayment()
1221
+ .deps(harness, TransferHandler)
1222
+ .parties(externalPayer, externalPayee)
1223
+ .transferId('3000222')
1224
+ .amount('10.00', 'USD')
1225
+ .fx()
1226
+ .build()
1227
+ await payment.prepareAndFulfil()
1228
+
1229
+ const positionProxyPayerPost = await ApiHelpers.getPositionAccount('dfsp_a_proxy', 'BWP')
1230
+ const positionFxpPostBWP = await ApiHelpers.getPositionAccount('fxp_a', 'BWP')
1231
+ const positionFxpPostUSD = await ApiHelpers.getPositionAccount('fxp_a', 'USD')
1232
+ const positionProxyPayeePost = await ApiHelpers.getPositionAccount('dfsp_b_proxy', 'USD')
1233
+
1234
+ assertPositionDiff('payer', positionProxyPayerPre, positionProxyPayerPost, { posted: 100 })
1235
+ assertPositionDiff('payer', positionFxpPreBWP, positionFxpPostBWP, { posted: -100 })
1236
+ assertPositionDiff('payer', positionFxpPreUSD, positionFxpPostUSD, { posted: 10 })
1237
+ assertPositionDiff('payee', positionProxyPayeePre, positionProxyPayeePost, { posted: -10 })
1238
+ }
1239
+ )
1240
+
1241
+ it(
1242
+ `Scheme A: PUT /transfers call I.e. From: Proxy AR → To: Payer FSP
1243
+ If it is a FX transfer with currency conversion
1244
+ PayerFSP and ProxyAR account must be updated`,
1245
+ async () => {
1246
+ const externalFxp = 'external_fxp_schema_a'
1247
+ const externalPayee = 'external_payee_scheme_a'
1248
+ await proxyCache.getCache().addDfspIdToProxyMapping(externalFxp, 'dfsp_a_proxy')
1249
+ await proxyCache.getCache().addDfspIdToProxyMapping(externalPayee, 'dfsp_a_proxy')
1250
+
1251
+ const positionPayerPre = await ApiHelpers.getPositionAccount('dfsp_a', 'BWP')
1252
+ const positionProxyPre = await ApiHelpers.getPositionAccount('dfsp_a_proxy', 'BWP')
1253
+
1254
+ // Set up Forex from in scheme payer to external Fxp.
1255
+ const forex = ApiHelpers.buildForex()
1256
+ .deps(harness, TransferHandler)
1257
+ .commitRequestId('2000033')
1258
+ .determiningTransferId('3000033')
1259
+ .parties('dfsp_a', externalFxp)
1260
+ .amountSource('100.00', 'BWP')
1261
+ .amountTarget('10.00', 'USD')
1262
+ .build()
1263
+ await forex.prepare()
1264
+
1265
+ const positionPayerPostFxPrepare = await ApiHelpers.getPositionAccount('dfsp_a', 'BWP')
1266
+ assertPositionDiff('payer', positionPayerPre, positionPayerPostFxPrepare, { posted: 100 })
1267
+
1268
+ await forex.fulfil()
1269
+
1270
+ const payment = ApiHelpers.buildPayment()
1271
+ .deps(harness, TransferHandler)
1272
+ .parties('dfsp_a', externalPayee)
1273
+ .transferId('3000033')
1274
+ .amount('10.00', 'USD')
1275
+ .fx()
1276
+ .build()
1277
+ await payment.prepareAndFulfil()
1278
+
1279
+ const positionProxyPost = await ApiHelpers.getPositionAccount('dfsp_a_proxy', 'BWP')
1280
+ assertPositionDiff('payee', positionProxyPre, positionProxyPost, { posted: -100 })
1281
+ }
1282
+ )
1283
+ })