From 53abb244822a0e13e97613d017c416ce3a8871a0 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 25 Feb 2026 22:17:12 +0100 Subject: [PATCH] +ledger ops --- api/billing/documents/go.mod | 2 +- api/billing/documents/go.sum | 4 +- api/billing/fees/go.mod | 2 +- api/billing/fees/go.sum | 4 +- api/discovery/go.mod | 2 +- api/discovery/go.sum | 4 +- api/fx/ingestor/go.mod | 2 +- api/fx/ingestor/go.sum | 4 +- api/fx/oracle/go.mod | 2 +- api/fx/oracle/go.sum | 4 +- api/gateway/chain/go.mod | 4 +- api/gateway/chain/go.sum | 8 +- api/gateway/mntx/go.mod | 2 +- api/gateway/mntx/go.sum | 4 +- api/gateway/tgsettle/go.mod | 2 +- api/gateway/tgsettle/go.sum | 4 +- api/gateway/tron/go.mod | 4 +- api/gateway/tron/go.sum | 8 +- api/ledger/go.mod | 2 +- api/ledger/go.sum | 4 +- api/notification/go.mod | 2 +- api/notification/go.sum | 4 +- api/payments/methods/go.mod | 2 +- api/payments/methods/go.sum | 4 +- api/payments/orchestrator/go.mod | 2 +- api/payments/orchestrator/go.sum | 4 +- .../orchestrator/card_payout_executor.go | 419 ++++++++++++++++++ .../orchestrator/card_payout_executor_test.go | 181 ++++++++ .../service/orchestrator/external_runtime.go | 8 + .../orchestrator/external_runtime_test.go | 67 +++ .../service/orchestrator/ledger_executor.go | 371 ++++++++++++++++ .../orchestrator/ledger_executor_test.go | 275 ++++++++++++ .../internal/service/orchestrator/options.go | 22 +- .../internal/service/orchestrator/service.go | 6 + .../service/orchestrator/service_v2.go | 55 ++- .../service/orchestrator/service_v2_test.go | 83 ++++ api/payments/quotation/go.mod | 2 +- api/payments/quotation/go.sum | 4 +- api/pkg/go.mod | 2 +- api/pkg/go.sum | 4 +- api/server/go.mod | 2 +- api/server/go.sum | 4 +- 42 files changed, 1521 insertions(+), 74 deletions(-) create mode 100644 api/payments/orchestrator/internal/service/orchestrator/card_payout_executor.go create mode 100644 api/payments/orchestrator/internal/service/orchestrator/card_payout_executor_test.go create mode 100644 api/payments/orchestrator/internal/service/orchestrator/ledger_executor.go create mode 100644 api/payments/orchestrator/internal/service/orchestrator/ledger_executor_test.go create mode 100644 api/payments/orchestrator/internal/service/orchestrator/service_v2_test.go diff --git a/api/billing/documents/go.mod b/api/billing/documents/go.mod index 18d3f1da..8fb17fef 100644 --- a/api/billing/documents/go.mod +++ b/api/billing/documents/go.mod @@ -53,7 +53,7 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect diff --git a/api/billing/documents/go.sum b/api/billing/documents/go.sum index 0fd9262f..b814da1f 100644 --- a/api/billing/documents/go.sum +++ b/api/billing/documents/go.sum @@ -158,8 +158,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= +github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= diff --git a/api/billing/fees/go.mod b/api/billing/fees/go.mod index 5239fa89..83fe0602 100644 --- a/api/billing/fees/go.mod +++ b/api/billing/fees/go.mod @@ -38,7 +38,7 @@ require ( github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect diff --git a/api/billing/fees/go.sum b/api/billing/fees/go.sum index d26defcf..49be6d17 100644 --- a/api/billing/fees/go.sum +++ b/api/billing/fees/go.sum @@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= +github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= diff --git a/api/discovery/go.mod b/api/discovery/go.mod index 92250bfc..d37352a2 100644 --- a/api/discovery/go.mod +++ b/api/discovery/go.mod @@ -30,7 +30,7 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect diff --git a/api/discovery/go.sum b/api/discovery/go.sum index d26defcf..49be6d17 100644 --- a/api/discovery/go.sum +++ b/api/discovery/go.sum @@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= +github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= diff --git a/api/fx/ingestor/go.mod b/api/fx/ingestor/go.mod index 9cf1a260..1b20b7ad 100644 --- a/api/fx/ingestor/go.mod +++ b/api/fx/ingestor/go.mod @@ -35,7 +35,7 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect diff --git a/api/fx/ingestor/go.sum b/api/fx/ingestor/go.sum index d26defcf..49be6d17 100644 --- a/api/fx/ingestor/go.sum +++ b/api/fx/ingestor/go.sum @@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= +github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= diff --git a/api/fx/oracle/go.mod b/api/fx/oracle/go.mod index c61aa700..f9beac4f 100644 --- a/api/fx/oracle/go.mod +++ b/api/fx/oracle/go.mod @@ -36,7 +36,7 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect diff --git a/api/fx/oracle/go.sum b/api/fx/oracle/go.sum index d26defcf..49be6d17 100644 --- a/api/fx/oracle/go.sum +++ b/api/fx/oracle/go.sum @@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= +github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= diff --git a/api/gateway/chain/go.mod b/api/gateway/chain/go.mod index 571c7048..20c8580b 100644 --- a/api/gateway/chain/go.mod +++ b/api/gateway/chain/go.mod @@ -25,7 +25,7 @@ require ( require ( github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260224171832-d0e24df9ee63 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect @@ -67,7 +67,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/supranational/blst v0.3.16 // indirect diff --git a/api/gateway/chain/go.sum b/api/gateway/chain/go.sum index fdd2e6f0..cb72ee0f 100644 --- a/api/gateway/chain/go.sum +++ b/api/gateway/chain/go.sum @@ -6,8 +6,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260224171832-d0e24df9ee63 h1:kLumy+keYsmuByIG8/G7Iay1vGCd1/WBq8a3vvPJWTM= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260224171832-d0e24df9ee63/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc h1:1stW1OipdBj8Me+nj26SzT8yil7OYve0r3cWobzk1JQ= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -236,8 +236,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= +github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= diff --git a/api/gateway/mntx/go.mod b/api/gateway/mntx/go.mod index 9bff5860..2dbc6da0 100644 --- a/api/gateway/mntx/go.mod +++ b/api/gateway/mntx/go.mod @@ -37,7 +37,7 @@ require ( github.com/nats-io/nuid v1.0.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect diff --git a/api/gateway/mntx/go.sum b/api/gateway/mntx/go.sum index 7ad1510d..e8deda76 100644 --- a/api/gateway/mntx/go.sum +++ b/api/gateway/mntx/go.sum @@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= +github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= diff --git a/api/gateway/tgsettle/go.mod b/api/gateway/tgsettle/go.mod index ac37d177..6207326f 100644 --- a/api/gateway/tgsettle/go.mod +++ b/api/gateway/tgsettle/go.mod @@ -36,7 +36,7 @@ require ( github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect diff --git a/api/gateway/tgsettle/go.sum b/api/gateway/tgsettle/go.sum index d26defcf..49be6d17 100644 --- a/api/gateway/tgsettle/go.sum +++ b/api/gateway/tgsettle/go.sum @@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= +github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= diff --git a/api/gateway/tron/go.mod b/api/gateway/tron/go.mod index fa2176fb..e863f6ff 100644 --- a/api/gateway/tron/go.mod +++ b/api/gateway/tron/go.mod @@ -27,7 +27,7 @@ require ( require ( github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260224171832-d0e24df9ee63 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect @@ -73,7 +73,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.0 // indirect github.com/rjeczalik/notify v0.9.3 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect diff --git a/api/gateway/tron/go.sum b/api/gateway/tron/go.sum index 7fc500da..d5911ef0 100644 --- a/api/gateway/tron/go.sum +++ b/api/gateway/tron/go.sum @@ -6,8 +6,8 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260224171832-d0e24df9ee63 h1:kLumy+keYsmuByIG8/G7Iay1vGCd1/WBq8a3vvPJWTM= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260224171832-d0e24df9ee63/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc h1:1stW1OipdBj8Me+nj26SzT8yil7OYve0r3cWobzk1JQ= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260225065256-91dd007ecddc/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -245,8 +245,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= +github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= diff --git a/api/ledger/go.mod b/api/ledger/go.mod index dc77f0a6..0eef72e4 100644 --- a/api/ledger/go.mod +++ b/api/ledger/go.mod @@ -37,7 +37,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect diff --git a/api/ledger/go.sum b/api/ledger/go.sum index 4c6fc8f0..531bd1b6 100644 --- a/api/ledger/go.sum +++ b/api/ledger/go.sum @@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= +github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= diff --git a/api/notification/go.mod b/api/notification/go.mod index de5a8c30..530e0bac 100644 --- a/api/notification/go.mod +++ b/api/notification/go.mod @@ -37,7 +37,7 @@ require ( github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.0 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect github.com/toorop/go-dkim v0.0.0-20250226130143-9025cce95817 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect diff --git a/api/notification/go.sum b/api/notification/go.sum index 2a3943ad..dd4b4809 100644 --- a/api/notification/go.sum +++ b/api/notification/go.sum @@ -119,8 +119,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= +github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= diff --git a/api/payments/methods/go.mod b/api/payments/methods/go.mod index be3b733e..b167b07a 100644 --- a/api/payments/methods/go.mod +++ b/api/payments/methods/go.mod @@ -36,7 +36,7 @@ require ( github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect diff --git a/api/payments/methods/go.sum b/api/payments/methods/go.sum index 4c6fc8f0..531bd1b6 100644 --- a/api/payments/methods/go.sum +++ b/api/payments/methods/go.sum @@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= +github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= diff --git a/api/payments/orchestrator/go.mod b/api/payments/orchestrator/go.mod index be14e998..b9fde247 100644 --- a/api/payments/orchestrator/go.mod +++ b/api/payments/orchestrator/go.mod @@ -51,7 +51,7 @@ require ( github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect diff --git a/api/payments/orchestrator/go.sum b/api/payments/orchestrator/go.sum index e564a3ee..a1fcaf96 100644 --- a/api/payments/orchestrator/go.sum +++ b/api/payments/orchestrator/go.sum @@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= +github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= diff --git a/api/payments/orchestrator/internal/service/orchestrator/card_payout_executor.go b/api/payments/orchestrator/internal/service/orchestrator/card_payout_executor.go new file mode 100644 index 00000000..5315039c --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrator/card_payout_executor.go @@ -0,0 +1,419 @@ +package orchestrator + +import ( + "context" + "strconv" + "strings" + + "github.com/shopspring/decimal" + mntxclient "github.com/tech/sendico/gateway/mntx/client" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/erecon" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan" + "github.com/tech/sendico/payments/storage/model" + "github.com/tech/sendico/pkg/merrors" + paymenttypes "github.com/tech/sendico/pkg/payments/types" + mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1" +) + +type gatewayCardPayoutExecutor struct { + mntxClient mntxclient.Client +} + +type cardPayoutCustomer struct { + id string + firstName string + middleName string + lastName string + ip string + zip string + country string + state string + city string + address string +} + +func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req sexec.StepRequest) (*sexec.ExecuteOutput, error) { + if req.Payment == nil { + return nil, merrors.InvalidArgument("card payout send: payment is required") + } + if e == nil || e.mntxClient == nil { + return nil, merrors.InvalidArgument("card payout send: mntx client is required") + } + if model.ParseRailOperation(string(req.Step.Action)) != model.RailOperationSend { + return nil, merrors.InvalidArgument("card payout send: unsupported action") + } + + card, err := payoutDestinationCard(req.Payment) + if err != nil { + return nil, err + } + amountMinor, currency, err := cardPayoutAmountMinor(req.Payment) + if err != nil { + return nil, err + } + + stepToken := cardPayoutStepToken(req.Step) + operationRef := cardPayoutOperationRef(req.Payment, stepToken) + payoutRef := cardPayoutRef(req.Payment, stepToken) + idempotencyKey := cardPayoutIdempotencyKey(req.Payment, stepToken) + projectID := cardPayoutProjectID(req.Payment) + customer := cardPayoutCustomerFromPayment(req.Payment, card) + cardHolder := cardPayoutCardholder(card, customer) + metadata := cardPayoutMetadata(req.Payment, req.Step) + intentRef := strings.TrimSpace(req.Payment.IntentSnapshot.Ref) + + var responsePayout *mntxv1.CardPayoutState + if token := strings.TrimSpace(card.Token); token != "" { + resp, createErr := e.mntxClient.CreateCardTokenPayout(ctx, &mntxv1.CardTokenPayoutRequest{ + PayoutId: payoutRef, + ProjectId: projectID, + CustomerId: customer.id, + CustomerFirstName: customer.firstName, + CustomerMiddleName: customer.middleName, + CustomerLastName: customer.lastName, + CustomerIp: customer.ip, + CustomerZip: customer.zip, + CustomerCountry: customer.country, + CustomerState: customer.state, + CustomerCity: customer.city, + CustomerAddress: customer.address, + AmountMinor: amountMinor, + Currency: currency, + CardToken: token, + CardHolder: cardHolder, + MaskedPan: strings.TrimSpace(card.MaskedPan), + Metadata: metadata, + OperationRef: operationRef, + IdempotencyKey: idempotencyKey, + IntentRef: intentRef, + }) + if createErr != nil { + return nil, createErr + } + if resp == nil { + return nil, merrors.Internal("card payout send: card token payout response is missing") + } + responsePayout = resp.GetPayout() + } else { + pan := strings.TrimSpace(card.Pan) + if pan == "" { + return nil, merrors.InvalidArgument("card payout send: card pan is required") + } + if card.ExpMonth == 0 || card.ExpYear == 0 { + return nil, merrors.InvalidArgument("card payout send: card expiry is required") + } + resp, createErr := e.mntxClient.CreateCardPayout(ctx, &mntxv1.CardPayoutRequest{ + PayoutId: payoutRef, + ProjectId: projectID, + CustomerId: customer.id, + CustomerFirstName: customer.firstName, + CustomerMiddleName: customer.middleName, + CustomerLastName: customer.lastName, + CustomerIp: customer.ip, + CustomerZip: customer.zip, + CustomerCountry: customer.country, + CustomerState: customer.state, + CustomerCity: customer.city, + CustomerAddress: customer.address, + AmountMinor: amountMinor, + Currency: currency, + CardPan: pan, + CardExpYear: card.ExpYear, + CardExpMonth: card.ExpMonth, + CardHolder: cardHolder, + Metadata: metadata, + OperationRef: operationRef, + IdempotencyKey: idempotencyKey, + IntentRef: intentRef, + }) + if createErr != nil { + return nil, createErr + } + if resp == nil { + return nil, merrors.Internal("card payout send: card payout response is missing") + } + responsePayout = resp.GetPayout() + } + + resolvedPayoutRef := firstNonEmpty(strings.TrimSpace(responsePayout.GetPayoutId()), payoutRef) + resolvedOperationRef := firstNonEmpty(strings.TrimSpace(responsePayout.GetOperationRef()), operationRef) + gatewayInstanceID := firstNonEmpty(strings.TrimSpace(req.Step.InstanceID), strings.TrimSpace(req.Step.Gateway)) + externalRefs, refsErr := cardPayoutExternalRefs(resolvedPayoutRef, resolvedOperationRef, gatewayInstanceID) + if refsErr != nil { + return nil, refsErr + } + + step := req.StepExecution + step.State = agg.StepStateCompleted + step.ExternalRefs = externalRefs + step.FailureCode = "" + step.FailureMsg = "" + return &sexec.ExecuteOutput{StepExecution: step}, nil +} + +func payoutDestinationCard(payment *agg.Payment) (*model.CardEndpoint, error) { + if payment == nil { + return nil, merrors.InvalidArgument("card payout send: payment is required") + } + destination := payment.IntentSnapshot.Destination + if destination.Type != model.EndpointTypeCard || destination.Card == nil { + return nil, merrors.InvalidArgument("card payout send: destination card is required") + } + return destination.Card, nil +} + +func cardPayoutMoney(payment *agg.Payment) *paymenttypes.Money { + if payment != nil && payment.QuoteSnapshot != nil && payment.QuoteSnapshot.ExpectedSettlementAmount != nil { + return payment.QuoteSnapshot.ExpectedSettlementAmount + } + if payment == nil { + return nil + } + return payment.IntentSnapshot.Amount +} + +func cardPayoutAmountMinor(payment *agg.Payment) (int64, string, error) { + money := cardPayoutMoney(payment) + if money == nil { + return 0, "", merrors.InvalidArgument("card payout send: payout amount is required") + } + amountText := strings.TrimSpace(money.GetAmount()) + currency := strings.ToUpper(strings.TrimSpace(money.GetCurrency())) + if idx := strings.Index(currency, "-"); idx > 0 { + currency = currency[:idx] + } + if amountText == "" || currency == "" { + return 0, "", merrors.InvalidArgument("card payout send: payout amount is invalid") + } + + value, err := decimal.NewFromString(amountText) + if err != nil || !value.IsPositive() { + return 0, "", merrors.InvalidArgument("card payout send: payout amount is invalid") + } + minor := value.Mul(decimal.NewFromInt(100)) + if !minor.Equal(minor.Truncate(0)) { + return 0, "", merrors.InvalidArgument("card payout send: payout amount supports at most 2 fractional digits") + } + return minor.IntPart(), currency, nil +} + +func cardPayoutStepToken(step xplan.Step) string { + return firstNonEmpty(strings.TrimSpace(step.StepRef), strings.TrimSpace(step.StepCode), "card_payout") +} + +func cardPayoutOperationRef(payment *agg.Payment, stepToken string) string { + base := "" + if payment != nil { + base = firstNonEmpty(strings.TrimSpace(payment.PaymentRef), strings.TrimSpace(payment.IdempotencyKey)) + } + return joinRef(base, stepToken) +} + +func cardPayoutRef(payment *agg.Payment, stepToken string) string { + base := "" + if payment != nil { + base = firstNonEmpty(strings.TrimSpace(payment.PaymentRef), strings.TrimSpace(payment.IdempotencyKey), "card_payout") + } + return joinRef(base, stepToken) +} + +func cardPayoutIdempotencyKey(payment *agg.Payment, stepToken string) string { + base := "" + if payment != nil { + base = strings.TrimSpace(payment.IdempotencyKey) + if base == "" { + base = strings.TrimSpace(payment.PaymentRef) + } + } + if base == "" { + base = "card_payout" + } + return joinRef(base, stepToken) +} + +func joinRef(base, suffix string) string { + base = strings.TrimSpace(base) + suffix = strings.TrimSpace(suffix) + switch { + case base == "": + return suffix + case suffix == "": + return base + default: + return base + ":" + suffix + } +} + +func cardPayoutProjectID(payment *agg.Payment) int64 { + if payment == nil { + return 0 + } + raw := cardPayoutAttribute(payment.IntentSnapshot.Attributes, "project_id", "projectId") + if raw == "" { + return 0 + } + value, err := strconv.ParseInt(raw, 10, 64) + if err != nil || value < 0 { + return 0 + } + return value +} + +func cardPayoutCustomerFromPayment(payment *agg.Payment, card *model.CardEndpoint) cardPayoutCustomer { + customer := cardPayoutCustomer{} + if payment == nil { + return customer + } + + cardholder := "" + cardholderSurname := "" + cardCountry := "" + if card != nil { + cardholder = strings.TrimSpace(card.Cardholder) + cardholderSurname = strings.TrimSpace(card.CardholderSurname) + cardCountry = strings.ToUpper(strings.TrimSpace(card.Country)) + } + attrs := payment.IntentSnapshot.Attributes + intentCustomer := payment.IntentSnapshot.Customer + if intentCustomer != nil { + customer.id = strings.TrimSpace(intentCustomer.ID) + customer.firstName = strings.TrimSpace(intentCustomer.FirstName) + customer.middleName = strings.TrimSpace(intentCustomer.MiddleName) + customer.lastName = strings.TrimSpace(intentCustomer.LastName) + customer.ip = strings.TrimSpace(intentCustomer.IP) + customer.zip = strings.TrimSpace(intentCustomer.Zip) + customer.country = strings.ToUpper(strings.TrimSpace(intentCustomer.Country)) + customer.state = strings.TrimSpace(intentCustomer.State) + customer.city = strings.TrimSpace(intentCustomer.City) + customer.address = strings.TrimSpace(intentCustomer.Address) + } + + customer.id = firstNonEmpty(customer.id, + cardPayoutAttribute(attrs, "customer_id", "customerId", "initiator_ref", "initiatorRef"), + strings.TrimSpace(payment.PaymentRef), + strings.TrimSpace(payment.IdempotencyKey), + "unknown_customer") + customer.firstName = firstNonEmpty(customer.firstName, cardholder, "UNKNOWN") + customer.middleName = firstNonEmpty(customer.middleName, cardPayoutAttribute(attrs, "customer_middle_name", "customerMiddleName")) + customer.lastName = firstNonEmpty(customer.lastName, cardholderSurname, "UNKNOWN") + customer.ip = firstNonEmpty(customer.ip, + cardPayoutAttribute(attrs, "customer_ip", "customerIp", "ip", "ip_address", "ipAddress"), + "0.0.0.0") + customer.zip = firstNonEmpty(customer.zip, cardPayoutAttribute(attrs, "customer_zip", "customerZip")) + customer.country = firstNonEmpty(customer.country, cardCountry, cardPayoutAttribute(attrs, "customer_country", "customerCountry")) + customer.state = firstNonEmpty(customer.state, cardPayoutAttribute(attrs, "customer_state", "customerState")) + customer.city = firstNonEmpty(customer.city, cardPayoutAttribute(attrs, "customer_city", "customerCity")) + customer.address = firstNonEmpty(customer.address, cardPayoutAttribute(attrs, "customer_address", "customerAddress")) + + return customer +} + +func cardPayoutCardholder(card *model.CardEndpoint, customer cardPayoutCustomer) string { + holder := "" + if card != nil { + holder = strings.TrimSpace(card.Cardholder) + surname := strings.TrimSpace(card.CardholderSurname) + if holder != "" && surname != "" && !strings.Contains(strings.ToLower(holder), strings.ToLower(surname)) { + holder = holder + " " + surname + } + } + if holder == "" { + holder = strings.TrimSpace(firstNonEmpty(spaceJoin(customer.firstName, customer.lastName), customer.firstName, customer.lastName)) + } + if holder == "" { + holder = "UNKNOWN" + } + return holder +} + +func spaceJoin(values ...string) string { + parts := make([]string, 0, len(values)) + for i := range values { + item := strings.TrimSpace(values[i]) + if item == "" { + continue + } + parts = append(parts, item) + } + return strings.Join(parts, " ") +} + +func cardPayoutAttribute(attrs map[string]string, keys ...string) string { + if len(attrs) == 0 { + return "" + } + for i := range keys { + key := strings.TrimSpace(keys[i]) + if key == "" { + continue + } + if value, ok := attrs[key]; ok { + if trimmed := strings.TrimSpace(value); trimmed != "" { + return trimmed + } + } + for attrKey, value := range attrs { + if !strings.EqualFold(strings.TrimSpace(attrKey), key) { + continue + } + if trimmed := strings.TrimSpace(value); trimmed != "" { + return trimmed + } + } + } + return "" +} + +func cardPayoutMetadata(payment *agg.Payment, step xplan.Step) map[string]string { + out := transferMetadata(step) + if out == nil { + out = map[string]string{} + } + if payment != nil { + if quoteRef := firstNonEmpty( + strings.TrimSpace(payment.QuotationRef), + strings.TrimSpace(quoteRefFromSnapshot(payment.QuoteSnapshot)), + ); quoteRef != "" { + out[settlementMetadataQuoteRef] = quoteRef + } + } + if outgoingLeg := strings.TrimSpace(string(step.Rail)); outgoingLeg != "" { + out[settlementMetadataOutgoingLeg] = outgoingLeg + } + if len(out) == 0 { + return nil + } + return out +} + +func cardPayoutExternalRefs(payoutRef, operationRef, gatewayInstanceID string) ([]agg.ExternalRef, error) { + gatewayInstanceID = strings.TrimSpace(gatewayInstanceID) + refs := make([]agg.ExternalRef, 0, 3) + if operationRef = strings.TrimSpace(operationRef); operationRef != "" { + refs = append(refs, agg.ExternalRef{ + GatewayInstanceID: gatewayInstanceID, + Kind: erecon.ExternalRefKindOperation, + Ref: operationRef, + }) + } + if payoutRef = strings.TrimSpace(payoutRef); payoutRef != "" { + refs = append(refs, agg.ExternalRef{ + GatewayInstanceID: gatewayInstanceID, + Kind: erecon.ExternalRefKindTransfer, + Ref: payoutRef, + }) + refs = append(refs, agg.ExternalRef{ + GatewayInstanceID: gatewayInstanceID, + Kind: erecon.ExternalRefKindCardPayout, + Ref: payoutRef, + }) + } + if len(refs) == 0 { + return nil, merrors.Internal("card payout send: payout response does not contain references") + } + return refs, nil +} + +var _ sexec.CardPayoutExecutor = (*gatewayCardPayoutExecutor)(nil) diff --git a/api/payments/orchestrator/internal/service/orchestrator/card_payout_executor_test.go b/api/payments/orchestrator/internal/service/orchestrator/card_payout_executor_test.go new file mode 100644 index 00000000..87e82f15 --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrator/card_payout_executor_test.go @@ -0,0 +1,181 @@ +package orchestrator + +import ( + "context" + "strings" + "testing" + + mntxclient "github.com/tech/sendico/gateway/mntx/client" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/erecon" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan" + "github.com/tech/sendico/payments/storage/model" + pm "github.com/tech/sendico/pkg/model" + paymenttypes "github.com/tech/sendico/pkg/payments/types" + mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1" + "go.mongodb.org/mongo-driver/v2/bson" +) + +func TestGatewayCardPayoutExecutor_ExecuteCardPayout_SubmitsCardPayout(t *testing.T) { + orgID := bson.NewObjectID() + + var payoutReq *mntxv1.CardPayoutRequest + executor := &gatewayCardPayoutExecutor{ + mntxClient: &mntxclient.Fake{ + CreateCardPayoutFn: func(_ context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) { + payoutReq = req + return &mntxv1.CardPayoutResponse{ + Payout: &mntxv1.CardPayoutState{ + PayoutId: "payout-remote-1", + }, + }, nil + }, + }, + } + + req := sexec.StepRequest{ + Payment: &agg.Payment{ + OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID}, + PaymentRef: "payment-1", + IdempotencyKey: "idem-1", + QuotationRef: "quote-1", + IntentSnapshot: model.PaymentIntent{ + Ref: "intent-1", + Destination: model.PaymentEndpoint{ + Type: model.EndpointTypeCard, + Card: &model.CardEndpoint{ + Pan: "2200700142860161", + Cardholder: "Stephan", + CardholderSurname: "Deshevikh", + ExpMonth: 3, + ExpYear: 2030, + }, + }, + Customer: &model.Customer{ + ID: "cust-1", + FirstName: "Stephan", + LastName: "Deshevikh", + IP: "198.51.100.10", + }, + Amount: &paymenttypes.Money{ + Amount: "1.000000", + Currency: "USDT", + }, + Attributes: map[string]string{ + "initiator_ref": "user-123", + }, + }, + QuoteSnapshot: &model.PaymentQuoteSnapshot{ + ExpectedSettlementAmount: &paymenttypes.Money{ + Amount: "76.50", + Currency: "RUB", + }, + QuoteRef: "quote-1", + }, + }, + Step: xplan.Step{ + StepRef: "hop_4_card_payout_send", + StepCode: "hop.4.card_payout.send", + Action: model.RailOperationSend, + Rail: model.RailCardPayout, + Gateway: "monetix", + InstanceID: "monetix", + }, + StepExecution: agg.StepExecution{ + StepRef: "hop_4_card_payout_send", + StepCode: "hop.4.card_payout.send", + Attempt: 1, + }, + } + + out, err := executor.ExecuteCardPayout(context.Background(), req) + if err != nil { + t.Fatalf("ExecuteCardPayout returned error: %v", err) + } + if out == nil { + t.Fatal("expected output") + } + if out.StepExecution.State != agg.StepStateCompleted { + t.Fatalf("expected completed state, got=%q", out.StepExecution.State) + } + if payoutReq == nil { + t.Fatal("expected payout request to be submitted") + } + if got, want := payoutReq.GetPayoutId(), "payment-1:hop_4_card_payout_send"; got != want { + t.Fatalf("payout_id mismatch: got=%q want=%q", got, want) + } + if got, want := payoutReq.GetOperationRef(), "payment-1:hop_4_card_payout_send"; got != want { + t.Fatalf("operation_ref mismatch: got=%q want=%q", got, want) + } + if got, want := payoutReq.GetIdempotencyKey(), "idem-1:hop_4_card_payout_send"; got != want { + t.Fatalf("idempotency mismatch: got=%q want=%q", got, want) + } + if got, want := payoutReq.GetAmountMinor(), int64(7650); got != want { + t.Fatalf("amount_minor mismatch: got=%d want=%d", got, want) + } + if got, want := payoutReq.GetCurrency(), "RUB"; got != want { + t.Fatalf("currency mismatch: got=%q want=%q", got, want) + } + if got, want := payoutReq.GetMetadata()[settlementMetadataQuoteRef], "quote-1"; got != want { + t.Fatalf("quote_ref metadata mismatch: got=%q want=%q", got, want) + } + if got, want := payoutReq.GetMetadata()[settlementMetadataOutgoingLeg], string(model.RailCardPayout); got != want { + t.Fatalf("outgoing_leg metadata mismatch: got=%q want=%q", got, want) + } + if len(out.StepExecution.ExternalRefs) != 3 { + t.Fatalf("expected 3 external refs, got=%d", len(out.StepExecution.ExternalRefs)) + } + if out.StepExecution.ExternalRefs[0].Kind != erecon.ExternalRefKindOperation { + t.Fatalf("expected first external ref to be operation, got=%q", out.StepExecution.ExternalRefs[0].Kind) + } + if out.StepExecution.ExternalRefs[1].Kind != erecon.ExternalRefKindTransfer { + t.Fatalf("expected second external ref to be transfer, got=%q", out.StepExecution.ExternalRefs[1].Kind) + } + if out.StepExecution.ExternalRefs[2].Kind != erecon.ExternalRefKindCardPayout { + t.Fatalf("expected third external ref to be card payout, got=%q", out.StepExecution.ExternalRefs[2].Kind) + } + if got, want := out.StepExecution.ExternalRefs[1].Ref, "payout-remote-1"; got != want { + t.Fatalf("transfer_ref mismatch: got=%q want=%q", got, want) + } +} + +func TestGatewayCardPayoutExecutor_ExecuteCardPayout_RequiresMntxClient(t *testing.T) { + orgID := bson.NewObjectID() + + executor := &gatewayCardPayoutExecutor{} + _, err := executor.ExecuteCardPayout(context.Background(), sexec.StepRequest{ + Payment: &agg.Payment{ + OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID}, + PaymentRef: "payment-2", + IntentSnapshot: model.PaymentIntent{ + Destination: model.PaymentEndpoint{ + Type: model.EndpointTypeCard, + Card: &model.CardEndpoint{ + Pan: "4111111111111111", + ExpMonth: 3, + ExpYear: 2030, + }, + }, + Amount: &paymenttypes.Money{Amount: "10", Currency: "RUB"}, + }, + }, + Step: xplan.Step{ + StepRef: "hop_4_card_payout_send", + StepCode: "hop.4.card_payout.send", + Action: model.RailOperationSend, + Rail: model.RailCardPayout, + }, + StepExecution: agg.StepExecution{ + StepRef: "hop_4_card_payout_send", + StepCode: "hop.4.card_payout.send", + Attempt: 1, + }, + }) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "mntx client is required") { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/api/payments/orchestrator/internal/service/orchestrator/external_runtime.go b/api/payments/orchestrator/internal/service/orchestrator/external_runtime.go index 6bebc067..bc120c0a 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/external_runtime.go +++ b/api/payments/orchestrator/internal/service/orchestrator/external_runtime.go @@ -192,6 +192,9 @@ func matchExecutionStep(payment *agg.Payment, msg *pmodel.PaymentGatewayExecutio if stepRef, gatewayInstanceID, ok := findStepByExternalRef(payment, erecon.ExternalRefKindTransfer, transferRef); ok { return stepRef, gatewayInstanceID } + if stepRef, gatewayInstanceID, ok := findStepByExternalRef(payment, erecon.ExternalRefKindCardPayout, transferRef); ok { + return stepRef, gatewayInstanceID + } } operationRef := strings.TrimSpace(msg.OperationRef) @@ -361,6 +364,11 @@ func buildObserveCandidate(step agg.StepExecution) (runningObserveCandidate, boo candidate.transferRef = value candidate.gatewayInstanceID = strings.TrimSpace(ref.GatewayInstanceID) } + case strings.EqualFold(kind, erecon.ExternalRefKindCardPayout): + if candidate.transferRef == "" { + candidate.transferRef = value + candidate.gatewayInstanceID = strings.TrimSpace(ref.GatewayInstanceID) + } case strings.EqualFold(kind, erecon.ExternalRefKindOperation): if candidate.operationRef == "" { candidate.operationRef = value diff --git a/api/payments/orchestrator/internal/service/orchestrator/external_runtime_test.go b/api/payments/orchestrator/internal/service/orchestrator/external_runtime_test.go index 6c253042..28e8a8cd 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/external_runtime_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/external_runtime_test.go @@ -96,6 +96,43 @@ func TestBuildGatewayExecutionEvent_FailedSetsTerminalNeedsAttentionHint(t *test } } +func TestBuildGatewayExecutionEvent_MatchesCardObserveByCardPayoutRef(t *testing.T) { + orgID := bson.NewObjectID() + payment := &agg.Payment{ + OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID}, + PaymentRef: "payment-card-1", + StepExecutions: []agg.StepExecution{ + { + StepRef: "hop_4_card_payout_observe", + StepCode: "hop.4.card_payout.observe", + State: agg.StepStateRunning, + ExternalRefs: []agg.ExternalRef{ + { + GatewayInstanceID: "monetix", + Kind: erecon.ExternalRefKindCardPayout, + Ref: "payout-1", + }, + }, + }, + }, + } + + event, ok := buildGatewayExecutionEvent(payment, &pm.PaymentGatewayExecution{ + PaymentRef: payment.PaymentRef, + Status: rail.OperationResultSuccess, + TransferRef: "payout-1", + }) + if !ok { + t.Fatal("expected gateway execution event to be accepted") + } + if got, want := event.StepRef, "hop_4_card_payout_observe"; got != want { + t.Fatalf("step_ref mismatch: got=%q want=%q", got, want) + } + if got, want := event.GatewayInstanceID, "monetix"; got != want { + t.Fatalf("gateway_instance_id mismatch: got=%q want=%q", got, want) + } +} + func TestOnPaymentGatewayExecution_ReconcilesUsingGlobalPaymentLookup(t *testing.T) { orgID := bson.NewObjectID() payment := &agg.Payment{ @@ -273,6 +310,36 @@ func TestRunningObserveCandidates(t *testing.T) { } } +func TestRunningObserveCandidates_UsesCardPayoutRefAsTransfer(t *testing.T) { + payment := &agg.Payment{ + StepExecutions: []agg.StepExecution{ + { + StepRef: "hop_4_card_payout_observe", + StepCode: "hop.4.card_payout.observe", + State: agg.StepStateRunning, + ExternalRefs: []agg.ExternalRef{ + { + GatewayInstanceID: "monetix", + Kind: erecon.ExternalRefKindCardPayout, + Ref: "payout-2", + }, + }, + }, + }, + } + + candidates := runningObserveCandidates(payment) + if len(candidates) != 1 { + t.Fatalf("candidate count mismatch: got=%d want=1", len(candidates)) + } + if got, want := candidates[0].transferRef, "payout-2"; got != want { + t.Fatalf("transfer_ref mismatch: got=%q want=%q", got, want) + } + if got, want := candidates[0].gatewayInstanceID, "monetix"; got != want { + t.Fatalf("gateway_instance_id mismatch: got=%q want=%q", got, want) + } +} + func TestResolveObserveGateway_UsesExternalRefGatewayInstanceAcrossRails(t *testing.T) { svc := &Service{ gatewayRegistry: &fakeGatewayRegistry{ diff --git a/api/payments/orchestrator/internal/service/orchestrator/ledger_executor.go b/api/payments/orchestrator/internal/service/orchestrator/ledger_executor.go new file mode 100644 index 00000000..6cb6ff77 --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrator/ledger_executor.go @@ -0,0 +1,371 @@ +package orchestrator + +import ( + "context" + "fmt" + "strings" + + ledgerclient "github.com/tech/sendico/ledger/client" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/erecon" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan" + "github.com/tech/sendico/payments/storage/model" + "github.com/tech/sendico/pkg/ledgerconv" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/model/account_role" + paymenttypes "github.com/tech/sendico/pkg/payments/types" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" +) + +const ( + ledgerMetadataMode = "mode" +) + +type gatewayLedgerExecutor struct { + ledgerClient ledgerclient.Client +} + +type ledgerRoles struct { + from account_role.AccountRole + to account_role.AccountRole +} + +func (e *gatewayLedgerExecutor) ExecuteLedger(ctx context.Context, req sexec.StepRequest) (*sexec.ExecuteOutput, error) { + if req.Payment == nil { + return nil, merrors.InvalidArgument("ledger step: payment is required") + } + if e == nil || e.ledgerClient == nil { + return nil, merrors.InvalidArgument("ledger step: ledger client is required") + } + + action := model.ParseRailOperation(string(req.Step.Action)) + switch action { + case model.RailOperationDebit, + model.RailOperationCredit, + model.RailOperationExternalDebit, + model.RailOperationExternalCredit, + model.RailOperationMove, + model.RailOperationBlock, + model.RailOperationRelease: + default: + return nil, merrors.InvalidArgument("ledger step: unsupported action") + } + + amount, err := ledgerAmountForStep(req.Payment, req.Step, action) + if err != nil { + return nil, err + } + roles, err := ledgerRolesForStep(req.Step, action) + if err != nil { + return nil, err + } + + transferReq := &ledgerv1.TransferRequest{ + IdempotencyKey: ledgerStepIdempotencyKey(req.Payment, req.Step), + OrganizationRef: req.Payment.OrganizationRef.Hex(), + Money: amount, + Description: ledgerDescription(req.Step), + Metadata: ledgerTransferMetadata(req.Payment, req.Step, roles), + FromRole: ledgerRoleToProto(roles.from), + ToRole: ledgerRoleToProto(roles.to), + } + + resp, err := e.ledgerClient.TransferInternal(ctx, transferReq) + if err != nil { + return nil, err + } + if resp == nil || strings.TrimSpace(resp.GetJournalEntryRef()) == "" { + return nil, merrors.Internal("ledger step: journal entry reference is missing") + } + + step := req.StepExecution + step.State = agg.StepStateCompleted + step.FailureCode = "" + step.FailureMsg = "" + step.ExternalRefs = appendLedgerExternalRef(step.ExternalRefs, agg.ExternalRef{ + GatewayInstanceID: firstNonEmpty(strings.TrimSpace(req.Step.InstanceID), strings.TrimSpace(req.Step.Gateway)), + Kind: erecon.ExternalRefKindLedger, + Ref: strings.TrimSpace(resp.GetJournalEntryRef()), + }) + + return &sexec.ExecuteOutput{StepExecution: step}, nil +} + +func ledgerAmountForStep( + payment *agg.Payment, + step xplan.Step, + action model.RailOperation, +) (*moneyv1.Money, error) { + sourceMoney := sourceMoneyForLedger(payment) + settlementMoney := settlementMoneyForLedger(payment, sourceMoney) + payoutMoney := payoutMoneyForLedger(payment, settlementMoney) + + if fromRail, toRail, ok := ledgerBoundaryRails(payment, step); ok { + switch { + case isLedgerExternalRail(fromRail) && isLedgerExternalRail(toRail): + return protoMoneyRequired(sourceMoney, "ledger step: source amount is required") + case isLedgerExternalRail(fromRail) && isLedgerInternalRail(toRail): + return protoMoneyRequired(settlementMoney, "ledger step: settlement amount is required") + case isLedgerInternalRail(fromRail) && isLedgerExternalRail(toRail): + if toRail == model.RailCardPayout { + return protoMoneyRequired(payoutMoney, "ledger step: payout amount is required") + } + return protoMoneyRequired(settlementMoney, "ledger step: settlement amount is required") + case isLedgerInternalRail(fromRail) && isLedgerInternalRail(toRail): + return protoMoneyRequired(settlementMoney, "ledger step: settlement amount is required") + } + } + + switch action { + case model.RailOperationCredit, model.RailOperationExternalCredit: + return protoMoneyRequired(settlementMoney, "ledger step: settlement amount is required") + case model.RailOperationDebit, model.RailOperationExternalDebit: + if sourceMoney != nil { + return protoMoneyRequired(sourceMoney, "ledger step: source amount is required") + } + return protoMoneyRequired(settlementMoney, "ledger step: settlement amount is required") + case model.RailOperationMove, model.RailOperationBlock, model.RailOperationRelease: + if settlementMoney != nil { + return protoMoneyRequired(settlementMoney, "ledger step: settlement amount is required") + } + return protoMoneyRequired(sourceMoney, "ledger step: source amount is required") + default: + return nil, merrors.InvalidArgument("ledger step: unsupported action") + } +} + +func sourceMoneyForLedger(payment *agg.Payment) *paymenttypes.Money { + if payment == nil { + return nil + } + if payment.QuoteSnapshot != nil && payment.QuoteSnapshot.DebitAmount != nil { + return payment.QuoteSnapshot.DebitAmount + } + return payment.IntentSnapshot.Amount +} + +func settlementMoneyForLedger(payment *agg.Payment, source *paymenttypes.Money) *paymenttypes.Money { + if payment != nil && payment.QuoteSnapshot != nil && payment.QuoteSnapshot.ExpectedSettlementAmount != nil { + return payment.QuoteSnapshot.ExpectedSettlementAmount + } + return source +} + +func payoutMoneyForLedger(_ *agg.Payment, settlement *paymenttypes.Money) *paymenttypes.Money { + return settlement +} + +func protoMoneyRequired(m *paymenttypes.Money, errMsg string) (*moneyv1.Money, error) { + if m == nil { + return nil, merrors.InvalidArgument(errMsg) + } + amount := strings.TrimSpace(m.GetAmount()) + currency := strings.TrimSpace(m.GetCurrency()) + if amount == "" || currency == "" { + return nil, merrors.InvalidArgument(errMsg) + } + return &moneyv1.Money{Amount: amount, Currency: currency}, nil +} + +func ledgerRolesForStep(step xplan.Step, action model.RailOperation) (ledgerRoles, error) { + roles, ok, err := ledgerRolesFromMetadata(step.Metadata) + if err != nil { + return ledgerRoles{}, err + } + if ok { + return roles, nil + } + + mode := strings.ToLower(strings.TrimSpace(step.Metadata[ledgerMetadataMode])) + switch action { + case model.RailOperationBlock: + return ledgerRoles{from: account_role.AccountRoleOperating, to: account_role.AccountRoleHold}, nil + case model.RailOperationRelease: + return ledgerRoles{from: account_role.AccountRoleHold, to: account_role.AccountRoleOperating}, nil + case model.RailOperationCredit, model.RailOperationExternalCredit: + return ledgerRoles{from: account_role.AccountRolePending, to: account_role.AccountRoleOperating}, nil + case model.RailOperationDebit, model.RailOperationExternalDebit: + if mode == "finalize_debit" { + return ledgerRoles{from: account_role.AccountRoleHold, to: account_role.AccountRoleTransit}, nil + } + return ledgerRoles{from: account_role.AccountRoleOperating, to: account_role.AccountRoleTransit}, nil + case model.RailOperationMove: + return ledgerRoles{from: account_role.AccountRoleOperating, to: account_role.AccountRoleTransit}, nil + default: + return ledgerRoles{}, merrors.InvalidArgument("ledger step: unsupported action") + } +} + +func ledgerRolesFromMetadata(metadata map[string]string) (ledgerRoles, bool, error) { + fromValue, fromFound := metadataLookup(metadata, "from_role", "fromRole") + toValue, toFound := metadataLookup(metadata, "to_role", "toRole") + if !fromFound && !toFound { + return ledgerRoles{}, false, nil + } + if strings.TrimSpace(fromValue) == "" || strings.TrimSpace(toValue) == "" { + return ledgerRoles{}, false, merrors.InvalidArgument("ledger step: from_role and to_role must both be provided") + } + + fromRole, ok := account_role.Parse(fromValue) + if !ok || fromRole == "" { + return ledgerRoles{}, false, merrors.InvalidArgument("ledger step: invalid from_role") + } + toRole, ok := account_role.Parse(toValue) + if !ok || toRole == "" { + return ledgerRoles{}, false, merrors.InvalidArgument("ledger step: invalid to_role") + } + if fromRole == toRole { + return ledgerRoles{}, false, merrors.InvalidArgument("ledger step: from_role and to_role must differ") + } + return ledgerRoles{from: fromRole, to: toRole}, true, nil +} + +func metadataLookup(metadata map[string]string, keys ...string) (string, bool) { + if len(metadata) == 0 { + return "", false + } + for i := range keys { + key := strings.TrimSpace(keys[i]) + if key == "" { + continue + } + value, ok := metadata[key] + if !ok { + continue + } + return value, true + } + return "", false +} + +func ledgerRoleToProto(role account_role.AccountRole) ledgerv1.AccountRole { + parsed, _ := ledgerconv.ParseAccountRole(string(role)) + return parsed +} + +func ledgerTransferMetadata(payment *agg.Payment, step xplan.Step, roles ledgerRoles) map[string]string { + out := transferMetadata(step) + if out == nil { + out = map[string]string{} + } + if quoteRef := firstNonEmpty(strings.TrimSpace(payment.QuotationRef), strings.TrimSpace(quoteRefFromSnapshot(payment.QuoteSnapshot))); quoteRef != "" { + out[settlementMetadataQuoteRef] = quoteRef + } + if roles.from != "" { + out[account_role.MetadataKeyFromRole] = string(roles.from) + } + if roles.to != "" { + out[account_role.MetadataKeyToRole] = string(roles.to) + } + if mode := strings.TrimSpace(step.Metadata[ledgerMetadataMode]); mode != "" { + out[ledgerMetadataMode] = mode + } + if len(out) == 0 { + return nil + } + return out +} + +func ledgerStepIdempotencyKey(payment *agg.Payment, step xplan.Step) string { + base := strings.TrimSpace(payment.IdempotencyKey) + if base == "" { + base = strings.TrimSpace(payment.PaymentRef) + } + stepToken := firstNonEmpty(strings.TrimSpace(step.StepRef), strings.TrimSpace(step.StepCode), "ledger") + if base == "" { + return "ledger:" + stepToken + } + return base + ":" + stepToken +} + +func ledgerDescription(step xplan.Step) string { + code := strings.TrimSpace(step.StepCode) + if code == "" { + code = strings.TrimSpace(step.StepRef) + } + action := strings.ToLower(strings.TrimSpace(string(step.Action))) + if code == "" { + return "orchestration ledger " + action + } + return fmt.Sprintf("orchestration ledger %s %s", action, code) +} + +func appendLedgerExternalRef(existing []agg.ExternalRef, ref agg.ExternalRef) []agg.ExternalRef { + ref.GatewayInstanceID = strings.TrimSpace(ref.GatewayInstanceID) + ref.Kind = strings.TrimSpace(ref.Kind) + ref.Ref = strings.TrimSpace(ref.Ref) + if ref.Kind == "" || ref.Ref == "" { + return existing + } + for i := range existing { + item := existing[i] + if strings.EqualFold(strings.TrimSpace(item.GatewayInstanceID), ref.GatewayInstanceID) && + strings.EqualFold(strings.TrimSpace(item.Kind), ref.Kind) && + strings.EqualFold(strings.TrimSpace(item.Ref), ref.Ref) { + return existing + } + } + return append(existing, ref) +} + +func ledgerBoundaryRails(payment *agg.Payment, step xplan.Step) (model.Rail, model.Rail, bool) { + fromIndex, toIndex, ok := parseLedgerEdgeStepCode(step.StepCode) + if !ok || payment == nil || payment.QuoteSnapshot == nil || payment.QuoteSnapshot.Route == nil { + return model.RailUnspecified, model.RailUnspecified, false + } + + fromRail := model.RailUnspecified + toRail := model.RailUnspecified + for i := range payment.QuoteSnapshot.Route.Hops { + hop := payment.QuoteSnapshot.Route.Hops[i] + if hop == nil { + continue + } + if hop.Index == fromIndex { + fromRail = model.ParseRail(hop.Rail) + } + if hop.Index == toIndex { + toRail = model.ParseRail(hop.Rail) + } + } + if fromRail == model.RailUnspecified || toRail == model.RailUnspecified { + return model.RailUnspecified, model.RailUnspecified, false + } + return fromRail, toRail, true +} + +func parseLedgerEdgeStepCode(stepCode string) (uint32, uint32, bool) { + code := strings.ToLower(strings.TrimSpace(stepCode)) + if !strings.HasPrefix(code, "edge.") || !strings.Contains(code, ".ledger.") { + return 0, 0, false + } + var ( + from uint32 + to uint32 + op string + ) + if _, err := fmt.Sscanf(code, "edge.%d_%d.ledger.%s", &from, &to, &op); err != nil { + return 0, 0, false + } + if strings.TrimSpace(op) == "" { + return 0, 0, false + } + return from, to, true +} + +func isLedgerInternalRail(rail model.Rail) bool { + return rail == model.RailLedger +} + +func isLedgerExternalRail(rail model.Rail) bool { + switch rail { + case model.RailCrypto, model.RailProviderSettlement, model.RailCardPayout, model.RailFiatOnRamp: + return true + default: + return false + } +} + +var _ sexec.LedgerExecutor = (*gatewayLedgerExecutor)(nil) diff --git a/api/payments/orchestrator/internal/service/orchestrator/ledger_executor_test.go b/api/payments/orchestrator/internal/service/orchestrator/ledger_executor_test.go new file mode 100644 index 00000000..37dc7aa4 --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrator/ledger_executor_test.go @@ -0,0 +1,275 @@ +package orchestrator + +import ( + "context" + "strings" + "testing" + + ledgerclient "github.com/tech/sendico/ledger/client" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/erecon" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan" + "github.com/tech/sendico/payments/storage/model" + pm "github.com/tech/sendico/pkg/model" + paymenttypes "github.com/tech/sendico/pkg/payments/types" + ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" + "go.mongodb.org/mongo-driver/v2/bson" +) + +func TestGatewayLedgerExecutor_ExecuteLedger_CreditUsesSourceAmountAndDefaultRoles(t *testing.T) { + orgID := bson.NewObjectID() + payment := testLedgerExecutorPayment(orgID) + + var transferReq *ledgerv1.TransferRequest + executor := &gatewayLedgerExecutor{ + ledgerClient: &ledgerclient.Fake{ + TransferInternalFn: func(_ context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) { + transferReq = req + return &ledgerv1.PostResponse{JournalEntryRef: "entry-1"}, nil + }, + }, + } + + out, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{ + Payment: payment, + Step: xplan.Step{ + StepRef: "edge_1_2_ledger_credit", + StepCode: "edge.1_2.ledger.credit", + Action: model.RailOperationCredit, + Rail: model.RailLedger, + }, + StepExecution: agg.StepExecution{ + StepRef: "edge_1_2_ledger_credit", + StepCode: "edge.1_2.ledger.credit", + Attempt: 1, + }, + }) + if err != nil { + t.Fatalf("ExecuteLedger returned error: %v", err) + } + if out == nil { + t.Fatal("expected output") + } + if transferReq == nil { + t.Fatal("expected ledger transfer request") + } + if got, want := transferReq.GetMoney().GetAmount(), "1.000000"; got != want { + t.Fatalf("money.amount mismatch: got=%q want=%q", got, want) + } + if got, want := transferReq.GetMoney().GetCurrency(), "USDT"; got != want { + t.Fatalf("money.currency mismatch: got=%q want=%q", got, want) + } + if got, want := transferReq.GetFromRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING; got != want { + t.Fatalf("from_role mismatch: got=%v want=%v", got, want) + } + if got, want := transferReq.GetToRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING; got != want { + t.Fatalf("to_role mismatch: got=%v want=%v", got, want) + } + if got, want := out.StepExecution.State, agg.StepStateCompleted; got != want { + t.Fatalf("state mismatch: got=%q want=%q", got, want) + } + if len(out.StepExecution.ExternalRefs) != 1 { + t.Fatalf("expected one external ref, got=%d", len(out.StepExecution.ExternalRefs)) + } + if got, want := out.StepExecution.ExternalRefs[0].Kind, erecon.ExternalRefKindLedger; got != want { + t.Fatalf("external ref kind mismatch: got=%q want=%q", got, want) + } + if got, want := out.StepExecution.ExternalRefs[0].Ref, "entry-1"; got != want { + t.Fatalf("external ref value mismatch: got=%q want=%q", got, want) + } +} + +func TestGatewayLedgerExecutor_ExecuteLedger_FinalizeDebitUsesHoldToTransitAndSettlementAmount(t *testing.T) { + orgID := bson.NewObjectID() + payment := testLedgerExecutorPayment(orgID) + + var transferReq *ledgerv1.TransferRequest + executor := &gatewayLedgerExecutor{ + ledgerClient: &ledgerclient.Fake{ + TransferInternalFn: func(_ context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) { + transferReq = req + return &ledgerv1.PostResponse{JournalEntryRef: "entry-2"}, nil + }, + }, + } + + out, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{ + Payment: payment, + Step: xplan.Step{ + StepRef: "edge_3_4_ledger_debit", + StepCode: "edge.3_4.ledger.debit", + Action: model.RailOperationDebit, + Rail: model.RailLedger, + Metadata: map[string]string{"mode": "finalize_debit"}, + HopIndex: 4, + HopRole: paymenttypes.QuoteRouteHopRoleTransit, + DependsOn: []string{"hop_4_card_payout_observe"}, + }, + StepExecution: agg.StepExecution{ + StepRef: "edge_3_4_ledger_debit", + StepCode: "edge.3_4.ledger.debit", + Attempt: 1, + }, + }) + if err != nil { + t.Fatalf("ExecuteLedger returned error: %v", err) + } + if out == nil { + t.Fatal("expected output") + } + if transferReq == nil { + t.Fatal("expected ledger transfer request") + } + if got, want := transferReq.GetMoney().GetAmount(), "76.5"; got != want { + t.Fatalf("money.amount mismatch: got=%q want=%q", got, want) + } + if got, want := transferReq.GetMoney().GetCurrency(), "RUB"; got != want { + t.Fatalf("money.currency mismatch: got=%q want=%q", got, want) + } + if got, want := transferReq.GetFromRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD; got != want { + t.Fatalf("from_role mismatch: got=%v want=%v", got, want) + } + if got, want := transferReq.GetToRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT; got != want { + t.Fatalf("to_role mismatch: got=%v want=%v", got, want) + } + if got, want := transferReq.GetMetadata()[ledgerMetadataMode], "finalize_debit"; got != want { + t.Fatalf("mode metadata mismatch: got=%q want=%q", got, want) + } +} + +func TestGatewayLedgerExecutor_ExecuteLedger_UsesMetadataRoleOverrides(t *testing.T) { + orgID := bson.NewObjectID() + payment := testLedgerExecutorPayment(orgID) + + var transferReq *ledgerv1.TransferRequest + executor := &gatewayLedgerExecutor{ + ledgerClient: &ledgerclient.Fake{ + TransferInternalFn: func(_ context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) { + transferReq = req + return &ledgerv1.PostResponse{JournalEntryRef: "entry-3"}, nil + }, + }, + } + + _, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{ + Payment: payment, + Step: xplan.Step{ + StepRef: "edge_2_3_ledger_credit", + StepCode: "edge.2_3.ledger.credit", + Action: model.RailOperationCredit, + Rail: model.RailLedger, + Metadata: map[string]string{ + "from_role": "reserve", + "to_role": "liquidity", + }, + }, + StepExecution: agg.StepExecution{ + StepRef: "edge_2_3_ledger_credit", + StepCode: "edge.2_3.ledger.credit", + Attempt: 1, + }, + }) + if err != nil { + t.Fatalf("ExecuteLedger returned error: %v", err) + } + if transferReq == nil { + t.Fatal("expected ledger transfer request") + } + if got, want := transferReq.GetFromRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE; got != want { + t.Fatalf("from_role mismatch: got=%v want=%v", got, want) + } + if got, want := transferReq.GetToRole(), ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY; got != want { + t.Fatalf("to_role mismatch: got=%v want=%v", got, want) + } +} + +func TestGatewayLedgerExecutor_ExecuteLedger_ValidatesMetadataRoles(t *testing.T) { + orgID := bson.NewObjectID() + payment := testLedgerExecutorPayment(orgID) + + executor := &gatewayLedgerExecutor{ + ledgerClient: &ledgerclient.Fake{}, + } + + _, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{ + Payment: payment, + Step: xplan.Step{ + StepRef: "edge_2_3_ledger_credit", + StepCode: "edge.2_3.ledger.credit", + Action: model.RailOperationCredit, + Rail: model.RailLedger, + Metadata: map[string]string{ + "from_role": "bad_role", + "to_role": "operating", + }, + }, + StepExecution: agg.StepExecution{StepRef: "edge_2_3_ledger_credit", StepCode: "edge.2_3.ledger.credit", Attempt: 1}, + }) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "invalid from_role") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestGatewayLedgerExecutor_ExecuteLedger_RequiresLedgerClient(t *testing.T) { + orgID := bson.NewObjectID() + payment := testLedgerExecutorPayment(orgID) + + executor := &gatewayLedgerExecutor{} + + _, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{ + Payment: payment, + Step: xplan.Step{ + StepRef: "edge_1_2_ledger_credit", + StepCode: "edge.1_2.ledger.credit", + Action: model.RailOperationCredit, + Rail: model.RailLedger, + }, + StepExecution: agg.StepExecution{StepRef: "edge_1_2_ledger_credit", StepCode: "edge.1_2.ledger.credit", Attempt: 1}, + }) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "ledger client is required") { + t.Fatalf("unexpected error: %v", err) + } +} + +func testLedgerExecutorPayment(orgID bson.ObjectID) *agg.Payment { + return &agg.Payment{ + OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID}, + PaymentRef: "payment-ledger-1", + IdempotencyKey: "idem-ledger-1", + QuotationRef: "quote-ledger-1", + IntentSnapshot: model.PaymentIntent{ + Ref: "intent-ledger-1", + Source: model.PaymentEndpoint{ + Type: model.EndpointTypeManagedWallet, + ManagedWallet: &model.ManagedWalletEndpoint{ + ManagedWalletRef: "wallet-src", + }, + }, + Destination: model.PaymentEndpoint{ + Type: model.EndpointTypeCard, + Card: &model.CardEndpoint{Pan: "4111111111111111"}, + }, + Amount: &paymenttypes.Money{Amount: "1", Currency: "USDT"}, + }, + QuoteSnapshot: &model.PaymentQuoteSnapshot{ + DebitAmount: &paymenttypes.Money{Amount: "1.000000", Currency: "USDT"}, + ExpectedSettlementAmount: &paymenttypes.Money{Amount: "76.5", Currency: "RUB"}, + QuoteRef: "quote-ledger-1", + Route: &paymenttypes.QuoteRouteSpecification{ + Hops: []*paymenttypes.QuoteRouteHop{ + {Index: 1, Rail: "CRYPTO", Role: paymenttypes.QuoteRouteHopRoleSource}, + {Index: 2, Rail: "SETTLEMENT", Role: paymenttypes.QuoteRouteHopRoleTransit}, + {Index: 3, Rail: "LEDGER", Role: paymenttypes.QuoteRouteHopRoleTransit}, + {Index: 4, Rail: "CARD", Role: paymenttypes.QuoteRouteHopRoleDestination}, + }, + }, + }, + } +} diff --git a/api/payments/orchestrator/internal/service/orchestrator/options.go b/api/payments/orchestrator/internal/service/orchestrator/options.go index d2eed75d..f3ec7237 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/options.go +++ b/api/payments/orchestrator/internal/service/orchestrator/options.go @@ -44,14 +44,24 @@ func WithFeeEngine(_ feesv1.FeeEngineClient, _ time.Duration) Option { return func(*Service) {} } -// WithLedgerClient is retained for backward-compatible wiring and is currently a no-op. -func WithLedgerClient(_ ledgerclient.Client) Option { - return func(*Service) {} +// WithLedgerClient configures internal ledger execution for ledger-bound steps. +func WithLedgerClient(client ledgerclient.Client) Option { + return func(s *Service) { + if s == nil { + return + } + s.ledgerClient = client + } } -// WithMntxGateway is retained for backward-compatible wiring and is currently a no-op. -func WithMntxGateway(_ mntxclient.Client) Option { - return func(*Service) {} +// WithMntxGateway configures card payout execution for card-bound steps. +func WithMntxGateway(client mntxclient.Client) Option { + return func(s *Service) { + if s == nil { + return + } + s.mntxClient = client + } } // WithPaymentGatewayBroker wires broker subscription for payment gateway execution events. diff --git a/api/payments/orchestrator/internal/service/orchestrator/service.go b/api/payments/orchestrator/internal/service/orchestrator/service.go index 4601d1b0..0d4d1b1b 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/service.go +++ b/api/payments/orchestrator/internal/service/orchestrator/service.go @@ -3,6 +3,8 @@ package orchestrator import ( "context" + mntxclient "github.com/tech/sendico/gateway/mntx/client" + ledgerclient "github.com/tech/sendico/ledger/client" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/prepo" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/psvc" "github.com/tech/sendico/payments/storage" @@ -22,6 +24,8 @@ type Service struct { v2 psvc.Service paymentRepo prepo.Repository + ledgerClient ledgerclient.Client + mntxClient mntxclient.Client gatewayInvokeResolver GatewayInvokeResolver gatewayRegistry GatewayRegistry cardGatewayRoutes map[string]CardGatewayRoute @@ -49,6 +53,8 @@ func NewService(logger mlogger.Logger, repo storage.Repository, opts ...Option) var err error svc.v2, svc.paymentRepo, err = newOrchestrationV2Service(svc.logger, repo, v2RuntimeDeps{ + LedgerClient: svc.ledgerClient, + MntxClient: svc.mntxClient, GatewayInvokeResolver: svc.gatewayInvokeResolver, GatewayRegistry: svc.gatewayRegistry, CardGatewayRoutes: svc.cardGatewayRoutes, diff --git a/api/payments/orchestrator/internal/service/orchestrator/service_v2.go b/api/payments/orchestrator/internal/service/orchestrator/service_v2.go index 03ed9a66..639b7c9f 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/service_v2.go +++ b/api/payments/orchestrator/internal/service/orchestrator/service_v2.go @@ -3,6 +3,8 @@ package orchestrator import ( "context" + mntxclient "github.com/tech/sendico/gateway/mntx/client" + ledgerclient "github.com/tech/sendico/ledger/client" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/oobs" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/pquery" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/prepo" @@ -22,6 +24,8 @@ type v2MongoDBProvider interface { } type v2RuntimeDeps struct { + LedgerClient ledgerclient.Client + MntxClient mntxclient.Client GatewayInvokeResolver GatewayInvokeResolver GatewayRegistry GatewayRegistry CardGatewayRoutes map[string]CardGatewayRoute @@ -34,6 +38,14 @@ func newOrchestrationV2Service(logger mlogger.Logger, repo storage.Repository, r if repo == nil { return nil, nil, merrors.Internal("No repo for orchestrator v2 provided") } + if runtimeDeps.LedgerClient == nil { + logger.Error("Orchestration v2 disabled: ledger client is missing") + return nil, nil, merrors.Internal("ledger client is required") + } + if checker, ok := runtimeDeps.LedgerClient.(interface{ Available() bool }); ok && !checker.Available() { + logger.Error("Orchestration v2 disabled: ledger client is unavailable") + return nil, nil, merrors.Internal("ledger client is unavailable") + } paymentRepo, err := buildPaymentRepositoryV2(repo, logger) if paymentRepo == nil || err != nil { @@ -72,27 +84,42 @@ func newOrchestrationV2Service(logger mlogger.Logger, repo storage.Repository, r } func buildOrchestrationV2Executors(logger mlogger.Logger, runtimeDeps v2RuntimeDeps) sexec.Registry { - if runtimeDeps.GatewayInvokeResolver == nil || runtimeDeps.GatewayRegistry == nil { - return nil - } execLogger := logger.Named("v2") - cryptoExecutor := &gatewayCryptoExecutor{ - gatewayInvokeResolver: runtimeDeps.GatewayInvokeResolver, - gatewayRegistry: runtimeDeps.GatewayRegistry, - cardGatewayRoutes: cloneCardGatewayRoutes(runtimeDeps.CardGatewayRoutes), + + var cryptoExecutor sexec.CryptoExecutor + var providerSettlementExecutor sexec.ProviderSettlementExecutor + var guardExecutor sexec.GuardExecutor + if runtimeDeps.GatewayInvokeResolver != nil && runtimeDeps.GatewayRegistry != nil { + cryptoExecutor = &gatewayCryptoExecutor{ + gatewayInvokeResolver: runtimeDeps.GatewayInvokeResolver, + gatewayRegistry: runtimeDeps.GatewayRegistry, + cardGatewayRoutes: cloneCardGatewayRoutes(runtimeDeps.CardGatewayRoutes), + } + providerSettlementExecutor = &gatewayProviderSettlementExecutor{ + gatewayInvokeResolver: runtimeDeps.GatewayInvokeResolver, + gatewayRegistry: runtimeDeps.GatewayRegistry, + } + guardExecutor = &gatewayGuardExecutor{ + logger: execLogger.Named("guard"), + gatewayInvokeResolver: runtimeDeps.GatewayInvokeResolver, + gatewayRegistry: runtimeDeps.GatewayRegistry, + } } - providerSettlementExecutor := &gatewayProviderSettlementExecutor{ - gatewayInvokeResolver: runtimeDeps.GatewayInvokeResolver, - gatewayRegistry: runtimeDeps.GatewayRegistry, + + ledgerExecutor := &gatewayLedgerExecutor{ + ledgerClient: runtimeDeps.LedgerClient, } - guardExecutor := &gatewayGuardExecutor{ - logger: execLogger.Named("guard"), - gatewayInvokeResolver: runtimeDeps.GatewayInvokeResolver, - gatewayRegistry: runtimeDeps.GatewayRegistry, + var cardPayoutExecutor sexec.CardPayoutExecutor + if runtimeDeps.MntxClient != nil { + cardPayoutExecutor = &gatewayCardPayoutExecutor{ + mntxClient: runtimeDeps.MntxClient, + } } return psvc.NewDefaultExecutors(execLogger, sexec.Dependencies{ + Ledger: ledgerExecutor, Crypto: cryptoExecutor, ProviderSettlement: providerSettlementExecutor, + CardPayout: cardPayoutExecutor, Guard: guardExecutor, }) } diff --git a/api/payments/orchestrator/internal/service/orchestrator/service_v2_test.go b/api/payments/orchestrator/internal/service/orchestrator/service_v2_test.go new file mode 100644 index 00000000..f5a61010 --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrator/service_v2_test.go @@ -0,0 +1,83 @@ +package orchestrator + +import ( + "context" + "strings" + "testing" + + ledgerclient "github.com/tech/sendico/ledger/client" + "github.com/tech/sendico/payments/storage" + quotestorage "github.com/tech/sendico/payments/storage/quote" + "go.uber.org/zap" +) + +func TestNewOrchestrationV2Service_FailsWhenLedgerClientMissing(t *testing.T) { + svc, repo, err := newOrchestrationV2Service(zap.NewNop(), fakeStorageRepo{}, v2RuntimeDeps{}) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "ledger client is required") { + t.Fatalf("unexpected error: %v", err) + } + if svc != nil { + t.Fatal("expected nil service") + } + if repo != nil { + t.Fatal("expected nil payment repo") + } +} + +func TestNewOrchestrationV2Service_FailsWhenLedgerClientUnavailable(t *testing.T) { + ledger := unavailableLedgerClient{Fake: &ledgerclient.Fake{}} + svc, repo, err := newOrchestrationV2Service(zap.NewNop(), fakeStorageRepo{}, v2RuntimeDeps{ + LedgerClient: ledger, + }) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "ledger client is unavailable") { + t.Fatalf("unexpected error: %v", err) + } + if svc != nil { + t.Fatal("expected nil service") + } + if repo != nil { + t.Fatal("expected nil payment repo") + } +} + +type unavailableLedgerClient struct { + *ledgerclient.Fake +} + +func (u unavailableLedgerClient) Available() bool { + return false +} + +type fakeStorageRepo struct{} + +func (fakeStorageRepo) Ping(context.Context) error { + return nil +} + +func (fakeStorageRepo) Payments() storage.PaymentsStore { + return nil +} + +func (fakeStorageRepo) PaymentMethods() storage.PaymentMethodsStore { + return nil +} + +func (fakeStorageRepo) Quotes() quotestorage.QuotesStore { + return nil +} + +func (fakeStorageRepo) Routes() storage.RoutesStore { + return nil +} + +func (fakeStorageRepo) PlanTemplates() storage.PlanTemplatesStore { + return nil +} + +var _ storage.Repository = fakeStorageRepo{} diff --git a/api/payments/quotation/go.mod b/api/payments/quotation/go.mod index e83786c5..ea25e748 100644 --- a/api/payments/quotation/go.mod +++ b/api/payments/quotation/go.mod @@ -50,7 +50,7 @@ require ( github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect diff --git a/api/payments/quotation/go.sum b/api/payments/quotation/go.sum index 3800b6fb..ce1a2bb9 100644 --- a/api/payments/quotation/go.sum +++ b/api/payments/quotation/go.sum @@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= +github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= diff --git a/api/pkg/go.mod b/api/pkg/go.mod index 39a0374a..a803f28b 100644 --- a/api/pkg/go.mod +++ b/api/pkg/go.mod @@ -66,7 +66,7 @@ require ( github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.0 // indirect github.com/shirou/gopsutil/v3 v3.24.5 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect diff --git a/api/pkg/go.sum b/api/pkg/go.sum index 4e72b695..a8298edd 100644 --- a/api/pkg/go.sum +++ b/api/pkg/go.sum @@ -128,8 +128,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= +github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= diff --git a/api/server/go.mod b/api/server/go.mod index eb24f4a1..cee4b5fb 100644 --- a/api/server/go.mod +++ b/api/server/go.mod @@ -118,7 +118,7 @@ require ( github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/procfs v0.20.0 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/shirou/gopsutil/v3 v3.24.5 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect diff --git a/api/server/go.sum b/api/server/go.sum index 34698344..0fb933c3 100644 --- a/api/server/go.sum +++ b/api/server/go.sum @@ -200,8 +200,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.0 h1:AA7aCvjxwAquZAlonN7888f2u4IN8WVeFgBi4k82M4Q= +github.com/prometheus/procfs v0.20.0/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=