5 Commits

Author SHA1 Message Date
Arseni
32cccc7895 Added PostHog 2025-12-09 21:02:37 +03:00
81d2db394b Merge pull request 'removed auto-generated code' (#51) from interface-#50 into main
Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/mntx_gateway Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline failed
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #51
2025-12-09 17:36:05 +00:00
Stephan D
8d6a302cb8 removed auto-generated code 2025-12-09 18:35:42 +01:00
0e48d2a318 Merge pull request 'double-sided quotation + fixed tests' (#49) from quote-#45 into main
Some checks failed
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline failed
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/mntx_gateway Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
Reviewed-on: #49
2025-12-09 16:46:02 +00:00
Stephan D
32653e11fc double-sided quotation + fixed tests 2025-12-09 17:45:29 +01:00
51 changed files with 876 additions and 127 deletions

View File

@@ -18,7 +18,7 @@ require (
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/casbin/casbin/v2 v2.134.0 // indirect
github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect

View File

@@ -9,8 +9,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=

View File

@@ -20,7 +20,7 @@ require (
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/casbin/casbin/v2 v2.134.0 // indirect
github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect

View File

@@ -9,8 +9,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=

View File

@@ -21,7 +21,7 @@ require (
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/casbin/casbin/v2 v2.134.0 // indirect
github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect

View File

@@ -9,8 +9,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=

View File

@@ -12,7 +12,7 @@ require (
require (
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/casbin/casbin/v2 v2.134.0 // indirect
github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
github.com/golang/snappy v1.0.0 // indirect

View File

@@ -7,8 +7,8 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=

View File

@@ -26,7 +26,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/casbin/casbin/v2 v2.134.0 // indirect
github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect

View File

@@ -17,8 +17,8 @@ github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=

View File

@@ -19,7 +19,7 @@ require (
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/casbin/casbin/v2 v2.134.0 // indirect
github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect

View File

@@ -9,8 +9,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=

View File

@@ -19,7 +19,7 @@ require (
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/casbin/casbin/v2 v2.134.0 // indirect
github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect

View File

@@ -9,8 +9,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=

View File

@@ -21,7 +21,7 @@ require (
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/casbin/casbin/v2 v2.134.0 // indirect
github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect

View File

@@ -13,8 +13,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=

View File

@@ -29,7 +29,7 @@ require (
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/casbin/casbin/v2 v2.134.0 // indirect
github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect

View File

@@ -9,8 +9,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=

View File

@@ -10,6 +10,8 @@ import (
"github.com/tech/sendico/payments/orchestrator/storage/model"
"github.com/tech/sendico/pkg/merrors"
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
@@ -22,12 +24,38 @@ import (
func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.PaymentQuote, time.Time, error) {
intent := req.GetIntent()
amount := intent.GetAmount()
baseAmount := cloneMoney(amount)
feeQuote, err := s.quoteFees(ctx, orgRef, req)
fxSide := fxv1.Side_SIDE_UNSPECIFIED
if intent.GetFx() != nil {
fxSide = intent.GetFx().GetSide()
}
var fxQuote *oraclev1.Quote
var err error
if shouldRequestFX(intent) {
fxQuote, err = s.requestFXQuote(ctx, orgRef, req)
if err != nil {
return nil, time.Time{}, err
}
}
payAmount, settlementAmountBeforeFees := resolveTradeAmounts(amount, fxQuote, fxSide)
feeBaseAmount := payAmount
if feeBaseAmount == nil {
feeBaseAmount = cloneMoney(amount)
}
feeQuote, err := s.quoteFees(ctx, orgRef, req, feeBaseAmount)
if err != nil {
return nil, time.Time{}, err
}
feeTotal := extractFeeTotal(feeQuote.GetLines(), amount.GetCurrency())
feeCurrency := ""
if feeBaseAmount != nil {
feeCurrency = feeBaseAmount.GetCurrency()
} else if amount != nil {
feeCurrency = amount.GetCurrency()
}
feeTotal := extractFeeTotal(feeQuote.GetLines(), feeCurrency)
var networkFee *chainv1.EstimateTransferFeeResponse
if shouldEstimateNetworkFee(intent) {
@@ -37,15 +65,7 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orc
}
}
var fxQuote *oraclev1.Quote
if shouldRequestFX(intent) {
fxQuote, err = s.requestFXQuote(ctx, orgRef, req)
if err != nil {
return nil, time.Time{}, err
}
}
debitAmount, settlementAmount := computeAggregates(baseAmount, feeTotal, networkFee)
debitAmount, settlementAmount := computeAggregates(payAmount, settlementAmountBeforeFees, feeTotal, networkFee, fxQuote)
quote := &orchestratorv1.PaymentQuote{
DebitAmount: debitAmount,
@@ -63,14 +83,18 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orc
return quote, expiresAt, nil
}
func (s *Service) quoteFees(ctx context.Context, orgRef string, req *orchestratorv1.QuotePaymentRequest) (*feesv1.PrecomputeFeesResponse, error) {
func (s *Service) quoteFees(ctx context.Context, orgRef string, req *orchestratorv1.QuotePaymentRequest, baseAmount *moneyv1.Money) (*feesv1.PrecomputeFeesResponse, error) {
if !s.fees.available() {
return &feesv1.PrecomputeFeesResponse{}, nil
}
intent := req.GetIntent()
amount := cloneMoney(baseAmount)
if amount == nil {
amount = cloneMoney(intent.GetAmount())
}
feeIntent := &feesv1.Intent{
Trigger: triggerFromKind(intent.GetKind(), intent.GetRequiresFx()),
BaseAmount: cloneMoney(intent.GetAmount()),
BaseAmount: amount,
BookedAt: timestamppb.New(s.clock.Now()),
OriginType: "payments.orchestrator.quote",
OriginRef: strings.TrimSpace(req.GetIdempotencyKey()),
@@ -164,7 +188,19 @@ func (s *Service) requestFXQuote(ctx context.Context, orgRef string, req *orches
}
if amount := intent.GetAmount(); amount != nil {
params.BaseAmount = cloneMoney(amount)
pair := fxIntent.GetPair()
if pair != nil {
switch {
case strings.EqualFold(amount.GetCurrency(), pair.GetBase()):
params.BaseAmount = cloneMoney(amount)
case strings.EqualFold(amount.GetCurrency(), pair.GetQuote()):
params.QuoteAmount = cloneMoney(amount)
default:
params.BaseAmount = cloneMoney(amount)
}
} else {
params.BaseAmount = cloneMoney(amount)
}
}
quote, err := s.oracle.client.GetQuote(ctx, params)
@@ -291,11 +327,14 @@ func (s *Service) applyFX(ctx context.Context, payment *model.Payment, quote *or
if fq == nil {
return merrors.InvalidArgument("ledger: fx quote missing")
}
fromMoney := cloneMoney(fq.GetBaseAmount())
fxSide := fxv1.Side_SIDE_UNSPECIFIED
if intent.FX != nil {
fxSide = intent.FX.Side
}
fromMoney, toMoney := resolveTradeAmounts(intent.Amount, fq, fxSide)
if fromMoney == nil {
fromMoney = cloneMoney(intent.Amount)
}
toMoney := cloneMoney(fq.GetQuoteAmount())
if toMoney == nil {
toMoney = cloneMoney(quote.GetExpectedSettlementAmount())
}

View File

@@ -14,6 +14,7 @@ import (
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1"
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
)
@@ -109,30 +110,91 @@ func extractFeeTotal(lines []*feesv1.DerivedPostingLine, currency string) *money
}
}
func computeAggregates(base, fee *moneyv1.Money, network *chainv1.EstimateTransferFeeResponse) (*moneyv1.Money, *moneyv1.Money) {
if base == nil {
func resolveTradeAmounts(intentAmount *moneyv1.Money, fxQuote *oraclev1.Quote, side fxv1.Side) (*moneyv1.Money, *moneyv1.Money) {
if fxQuote == nil {
return cloneMoney(intentAmount), cloneMoney(intentAmount)
}
qSide := fxQuote.GetSide()
if qSide == fxv1.Side_SIDE_UNSPECIFIED {
qSide = side
}
switch qSide {
case fxv1.Side_BUY_BASE_SELL_QUOTE:
pay := cloneMoney(fxQuote.GetQuoteAmount())
settle := cloneMoney(fxQuote.GetBaseAmount())
if pay == nil {
pay = cloneMoney(intentAmount)
}
if settle == nil {
settle = cloneMoney(intentAmount)
}
return pay, settle
case fxv1.Side_SELL_BASE_BUY_QUOTE:
pay := cloneMoney(fxQuote.GetBaseAmount())
settle := cloneMoney(fxQuote.GetQuoteAmount())
if pay == nil {
pay = cloneMoney(intentAmount)
}
if settle == nil {
settle = cloneMoney(intentAmount)
}
return pay, settle
default:
return cloneMoney(intentAmount), cloneMoney(intentAmount)
}
}
func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.EstimateTransferFeeResponse, fxQuote *oraclev1.Quote) (*moneyv1.Money, *moneyv1.Money) {
if pay == nil {
return nil, nil
}
baseDecimal, err := decimalFromMoney(base)
debitDecimal, err := decimalFromMoney(pay)
if err != nil {
return cloneMoney(base), cloneMoney(base)
}
debit := baseDecimal
settlement := baseDecimal
if feeDecimal, err := decimalFromMoneyMatching(base, fee); err == nil && feeDecimal != nil {
debit = debit.Add(*feeDecimal)
settlement = settlement.Sub(*feeDecimal)
return cloneMoney(pay), cloneMoney(settlement)
}
if network != nil && network.GetNetworkFee() != nil {
if networkDecimal, err := decimalFromMoneyMatching(base, network.GetNetworkFee()); err == nil && networkDecimal != nil {
debit = debit.Add(*networkDecimal)
settlement = settlement.Sub(*networkDecimal)
settlementCurrency := pay.GetCurrency()
if settlement != nil && strings.TrimSpace(settlement.GetCurrency()) != "" {
settlementCurrency = settlement.GetCurrency()
}
settlementDecimal := debitDecimal
if settlement != nil {
if val, err := decimalFromMoney(settlement); err == nil {
settlementDecimal = val
}
}
return makeMoney(base.GetCurrency(), debit), makeMoney(base.GetCurrency(), settlement)
adjustDebit := func(m *moneyv1.Money) {
converted, err := ensureCurrency(m, pay.GetCurrency(), fxQuote)
if err != nil || converted == nil {
return
}
if val, err := decimalFromMoney(converted); err == nil {
debitDecimal = debitDecimal.Add(val)
}
}
adjustSettlement := func(m *moneyv1.Money) {
converted, err := ensureCurrency(m, settlementCurrency, fxQuote)
if err != nil || converted == nil {
return
}
if val, err := decimalFromMoney(converted); err == nil {
settlementDecimal = settlementDecimal.Sub(val)
}
}
adjustDebit(fee)
adjustSettlement(fee)
if network != nil && network.GetNetworkFee() != nil {
adjustDebit(network.GetNetworkFee())
adjustSettlement(network.GetNetworkFee())
}
return makeMoney(pay.GetCurrency(), debitDecimal), makeMoney(settlementCurrency, settlementDecimal)
}
func decimalFromMoney(m *moneyv1.Money) (decimal.Decimal, error) {
@@ -163,6 +225,46 @@ func makeMoney(currency string, value decimal.Decimal) *moneyv1.Money {
}
}
func ensureCurrency(m *moneyv1.Money, targetCurrency string, quote *oraclev1.Quote) (*moneyv1.Money, error) {
if m == nil || strings.TrimSpace(targetCurrency) == "" {
return nil, nil
}
if strings.EqualFold(m.GetCurrency(), targetCurrency) {
return cloneMoney(m), nil
}
return convertWithQuote(m, quote, targetCurrency)
}
func convertWithQuote(m *moneyv1.Money, quote *oraclev1.Quote, targetCurrency string) (*moneyv1.Money, error) {
if m == nil || quote == nil || quote.GetPair() == nil || quote.GetPrice() == nil {
return nil, nil
}
base := strings.TrimSpace(quote.GetPair().GetBase())
qt := strings.TrimSpace(quote.GetPair().GetQuote())
if base == "" || qt == "" || strings.TrimSpace(targetCurrency) == "" {
return nil, nil
}
price, err := decimal.NewFromString(quote.GetPrice().GetValue())
if err != nil || price.IsZero() {
return nil, err
}
value, err := decimalFromMoney(m)
if err != nil {
return nil, err
}
switch {
case strings.EqualFold(m.GetCurrency(), base) && strings.EqualFold(targetCurrency, qt):
return makeMoney(targetCurrency, value.Mul(price)), nil
case strings.EqualFold(m.GetCurrency(), qt) && strings.EqualFold(targetCurrency, base):
return makeMoney(targetCurrency, value.Div(price)), nil
default:
return nil, nil
}
}
func quoteToProto(src *oracleclient.Quote) *oraclev1.Quote {
if src == nil {
return nil

View File

@@ -0,0 +1,57 @@
package orchestrator
import (
"testing"
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
)
func TestResolveTradeAmountsBuyBase(t *testing.T) {
fxQuote := &oraclev1.Quote{
Side: fxv1.Side_BUY_BASE_SELL_QUOTE,
Pair: &fxv1.CurrencyPair{Base: "EUR", Quote: "USD"},
BaseAmount: &moneyv1.Money{
Currency: "EUR",
Amount: "100",
},
QuoteAmount: &moneyv1.Money{
Currency: "USD",
Amount: "110",
},
}
pay, settle := resolveTradeAmounts(nil, fxQuote, fxv1.Side_SIDE_UNSPECIFIED)
if pay.GetCurrency() != "USD" || pay.GetAmount() != "110" {
t.Fatalf("expected pay amount in USD 110, got %s %s", pay.GetCurrency(), pay.GetAmount())
}
if settle.GetCurrency() != "EUR" || settle.GetAmount() != "100" {
t.Fatalf("expected settlement in EUR 100, got %s %s", settle.GetCurrency(), settle.GetAmount())
}
}
func TestComputeAggregatesConvertsCurrencies(t *testing.T) {
pay := &moneyv1.Money{Currency: "USD", Amount: "100"}
settle := &moneyv1.Money{Currency: "EUR", Amount: "50"}
fee := &moneyv1.Money{Currency: "USD", Amount: "10"}
network := &chainv1.EstimateTransferFeeResponse{
NetworkFee: &moneyv1.Money{Currency: "USD", Amount: "5"},
}
fxQuote := &oraclev1.Quote{
Pair: &fxv1.CurrencyPair{Base: "EUR", Quote: "USD"},
Side: fxv1.Side_BUY_BASE_SELL_QUOTE,
Price: &moneyv1.Decimal{
Value: "2",
},
}
debit, settlement := computeAggregates(pay, settle, fee, network, fxQuote)
if debit.GetCurrency() != "USD" || debit.GetAmount() != "115" {
t.Fatalf("expected debit 115 USD, got %s %s", debit.GetCurrency(), debit.GetAmount())
}
if settlement.GetCurrency() != "EUR" || settlement.GetAmount() != "42.5" {
t.Fatalf("expected settlement 42.5 EUR, got %s %s", settlement.GetCurrency(), settlement.GetAmount())
}
}

View File

@@ -0,0 +1,64 @@
package orchestrator
import (
"context"
"testing"
"time"
oracleclient "github.com/tech/sendico/fx/oracle/client"
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
"go.uber.org/zap"
)
func TestRequestFXQuoteUsesQuoteAmountWhenCurrencyMatchesQuote(t *testing.T) {
ctx := context.Background()
var captured oracleclient.GetQuoteParams
svc := &Service{
logger: zap.NewNop(),
clock: testClock{now: time.Now()},
oracle: oracleDependency{
client: &oracleclient.Fake{
GetQuoteFn: func(ctx context.Context, params oracleclient.GetQuoteParams) (*oracleclient.Quote, error) {
captured = params
return &oracleclient.Quote{
QuoteRef: "q",
Pair: params.Pair,
Side: params.Side,
Price: "1.1",
BaseAmount: params.BaseAmount,
QuoteAmount: params.QuoteAmount,
ExpiresAt: time.Now(),
}, nil
},
},
},
}
req := &orchestratorv1.QuotePaymentRequest{
Meta: &orchestratorv1.RequestMeta{OrganizationRef: "org"},
Intent: &orchestratorv1.PaymentIntent{
Amount: &moneyv1.Money{Currency: "USD", Amount: "100"},
Fx: &orchestratorv1.FXIntent{
Pair: &fxv1.CurrencyPair{Base: "EUR", Quote: "USD"},
Side: fxv1.Side_BUY_BASE_SELL_QUOTE,
},
},
}
if _, err := svc.requestFXQuote(ctx, "org", req); err != nil {
t.Fatalf("requestFXQuote returned error: %v", err)
}
if captured.QuoteAmount == nil {
t.Fatal("expected quote amount to be populated")
}
if captured.BaseAmount != nil {
t.Fatal("expected base amount to be nil when using quote amount input")
}
if captured.QuoteAmount.GetCurrency() != "USD" {
t.Fatalf("expected quote amount currency USD, got %s", captured.QuoteAmount.GetCurrency())
}
}

View File

@@ -36,8 +36,8 @@ func NewQuotes(logger mlogger.Logger, repo repository.Repository) (*Quotes, erro
Keys: []ri.Key{{Field: "organizationRef", Sort: ri.Asc}},
},
{
Keys: []ri.Key{{Field: "expiresAt", Sort: ri.Asc}},
ExpireAfterSeconds: 0,
Keys: []ri.Key{{Field: "expiresAt", Sort: ri.Asc}},
TTL: int32Ptr(0),
},
}
@@ -111,3 +111,7 @@ func (q *Quotes) GetByRef(ctx context.Context, orgRef primitive.ObjectID, quoteR
}
var _ storage.QuotesStore = (*Quotes)(nil)
func int32Ptr(v int32) *int32 {
return &v
}

View File

@@ -3,7 +3,7 @@ module github.com/tech/sendico/pkg
go 1.24.0
require (
github.com/casbin/casbin/v2 v2.134.0
github.com/casbin/casbin/v2 v2.135.0
github.com/casbin/mongodb-adapter/v3 v3.7.0
github.com/go-chi/chi/v5 v5.2.3
github.com/google/uuid v1.6.0

View File

@@ -11,8 +11,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=

View File

@@ -38,7 +38,7 @@ require (
require (
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/casbin/casbin/v2 v2.134.0 // indirect
github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
)

View File

@@ -50,8 +50,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/casbin/casbin/v2 v2.134.0 h1:wyO3hZb487GzlGVAI2hUoHQT0ehFD+9B5P+HVG9BVTM=
github.com/casbin/casbin/v2 v2.134.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0=
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=

View File

@@ -1,19 +1,17 @@
package srequest
import orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
type QuotePaymentPayload struct {
IdempotencyKey string `json:"idempotencyKey"`
Intent *orchestratorv1.PaymentIntent `json:"intent"`
PreviewOnly bool `json:"previewOnly"`
Metadata map[string]string `json:"metadata,omitempty"`
type QuotePayment struct {
IdempotencyKey string `json:"idempotencyKey"`
Intent *PaymentIntent `json:"intent"`
PreviewOnly bool `json:"previewOnly"`
Metadata map[string]string `json:"metadata,omitempty"`
}
type InitiatePaymentPayload struct {
IdempotencyKey string `json:"idempotencyKey"`
Intent *orchestratorv1.PaymentIntent `json:"intent"`
Metadata map[string]string `json:"metadata,omitempty"`
FeeQuoteToken string `json:"feeQuoteToken,omitempty"`
FxQuoteRef string `json:"fxQuoteRef,omitempty"`
QuoteRef string `json:"quoteRef,omitempty"`
type InitiatePayment struct {
IdempotencyKey string `json:"idempotencyKey"`
Intent *PaymentIntent `json:"intent"`
Metadata map[string]string `json:"metadata,omitempty"`
FeeQuoteToken string `json:"feeQuoteToken,omitempty"`
FxQuoteRef string `json:"fxQuoteRef,omitempty"`
QuoteRef string `json:"quoteRef,omitempty"`
}

View File

@@ -0,0 +1,103 @@
package srequest
// PaymentKind mirrors the orchestrator payment kinds without importing generated proto types.
type PaymentKind int32
const (
PaymentKindUnspecified PaymentKind = 0
PaymentKindPayout PaymentKind = 1
PaymentKindInternalTransfer PaymentKind = 2
PaymentKindFxConversion PaymentKind = 3
)
// FXSide mirrors the common FX side enum.
type FXSide int32
const (
FXSideUnspecified FXSide = 0
FXSideBuyBaseSellQuote FXSide = 1
FXSideSellBaseBuyQuote FXSide = 2
)
// ChainNetwork mirrors the chain network enum used by managed wallets.
type ChainNetwork int32
const (
ChainNetworkUnspecified ChainNetwork = 0
ChainNetworkEthereumMainnet ChainNetwork = 1
ChainNetworkArbitrumOne ChainNetwork = 2
ChainNetworkOtherEVM ChainNetwork = 3
)
// InsufficientNetPolicy mirrors the fee engine policy override.
type InsufficientNetPolicy int32
const (
InsufficientNetPolicyUnspecified InsufficientNetPolicy = 0
InsufficientNetPolicyBlockPosting InsufficientNetPolicy = 1
InsufficientNetPolicySweepOrgCash InsufficientNetPolicy = 2
InsufficientNetPolicyInvoiceLater InsufficientNetPolicy = 3
)
type PaymentIntent struct {
Kind PaymentKind `json:"kind,omitempty"`
Source *PaymentEndpoint `json:"source,omitempty"`
Destination *PaymentEndpoint `json:"destination,omitempty"`
Amount *Money `json:"amount,omitempty"`
RequiresFX bool `json:"requires_fx,omitempty"`
FX *FXIntent `json:"fx,omitempty"`
FeePolicy *PolicyOverrides `json:"fee_policy,omitempty"`
Attributes map[string]string `json:"attributes,omitempty"`
}
type PaymentEndpoint struct {
Ledger *LedgerEndpoint `json:"ledger,omitempty"`
ManagedWallet *ManagedWalletEndpoint `json:"managed_wallet,omitempty"`
ExternalChain *ExternalChainEndpoint `json:"external_chain,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
type LedgerEndpoint struct {
LedgerAccountRef string `json:"ledger_account_ref"`
ContraLedgerAccountRef string `json:"contra_ledger_account_ref,omitempty"`
}
type ManagedWalletEndpoint struct {
ManagedWalletRef string `json:"managed_wallet_ref"`
Asset *Asset `json:"asset,omitempty"`
}
type ExternalChainEndpoint struct {
Asset *Asset `json:"asset,omitempty"`
Address string `json:"address"`
Memo string `json:"memo,omitempty"`
}
type Asset struct {
Chain ChainNetwork `json:"chain"`
TokenSymbol string `json:"token_symbol"`
ContractAddress string `json:"contract_address,omitempty"`
}
type Money struct {
Amount string `json:"amount"`
Currency string `json:"currency"`
}
type CurrencyPair struct {
Base string `json:"base"`
Quote string `json:"quote"`
}
type FXIntent struct {
Pair *CurrencyPair `json:"pair,omitempty"`
Side FXSide `json:"side,omitempty"`
Firm bool `json:"firm,omitempty"`
TTLms int64 `json:"ttl_ms,omitempty"`
PreferredProvider string `json:"preferred_provider,omitempty"`
MaxAgeMs int32 `json:"max_age_ms,omitempty"`
}
type PolicyOverrides struct {
InsufficientNet InsufficientNetPolicy `json:"insufficient_net,omitempty"`
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/tech/sendico/pkg/mservice"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
@@ -29,11 +30,11 @@ func (a *LedgerAPI) getBalance(r *http.Request, account *model.Account, token *s
ctx := r.Context()
res, err := a.enf.Enforce(ctx, a.balancePerm, account.ID, orgRef, primitive.NilObjectID, model.ActionRead)
if err != nil {
a.logger.Warn("Failed to check ledger balance access permissions", zap.Error(err), zap.String(a.oph.Name(), orgRef.Hex()), zap.String("ledger_account_ref", accountRef))
a.logger.Warn("Failed to check ledger balance access permissions", zap.Error(err), mutil.PLog(a.oph, r), zap.String("ledger_account_ref", accountRef))
return response.Auto(a.logger, a.Name(), err)
}
if !res {
a.logger.Debug("Access denied when reading ledger balance", zap.String(a.oph.Name(), orgRef.Hex()), zap.String("ledger_account_ref", accountRef))
a.logger.Debug("Access denied when reading ledger balance", mutil.PLog(a.oph, r), zap.String("ledger_account_ref", accountRef))
return response.AccessDenied(a.logger, a.Name(), "ledger balance read permission denied")
}
if a.client == nil {

View File

@@ -9,6 +9,7 @@ import (
"github.com/tech/sendico/pkg/mservice"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
@@ -23,11 +24,11 @@ func (a *LedgerAPI) listAccounts(r *http.Request, account *model.Account, token
ctx := r.Context()
res, err := a.enf.Enforce(ctx, a.permissionRef, account.ID, orgRef, primitive.NilObjectID, model.ActionRead)
if err != nil {
a.logger.Warn("Failed to check ledger accounts access permissions", zap.Error(err), zap.String(a.oph.Name(), orgRef.Hex()))
a.logger.Warn("Failed to check ledger accounts access permissions", zap.Error(err), mutil.PLog(a.oph, r))
return response.Auto(a.logger, a.Name(), err)
}
if !res {
a.logger.Debug("Access denied when listing ledger accounts", zap.String(a.oph.Name(), orgRef.Hex()))
a.logger.Debug("Access denied when listing ledger accounts", mutil.PLog(a.oph, r))
return response.AccessDenied(a.logger, a.Name(), "ledger accounts read permission denied")
}
if a.client == nil {

View File

@@ -0,0 +1,175 @@
package paymentapiimp
import (
"github.com/tech/sendico/pkg/merrors"
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
"github.com/tech/sendico/server/interface/api/srequest"
)
func mapPaymentIntent(intent *srequest.PaymentIntent) (*orchestratorv1.PaymentIntent, error) {
if intent == nil {
return nil, merrors.InvalidArgument("intent is required")
}
source, err := mapPaymentEndpoint(intent.Source, "source")
if err != nil {
return nil, err
}
destination, err := mapPaymentEndpoint(intent.Destination, "destination")
if err != nil {
return nil, err
}
fx, err := mapFXIntent(intent.FX)
if err != nil {
return nil, err
}
return &orchestratorv1.PaymentIntent{
Kind: orchestratorv1.PaymentKind(intent.Kind),
Source: source,
Destination: destination,
Amount: mapMoney(intent.Amount),
RequiresFx: intent.RequiresFX,
Fx: fx,
FeePolicy: mapPolicyOverrides(intent.FeePolicy),
Attributes: copyStringMap(intent.Attributes),
}, nil
}
func mapPaymentEndpoint(endpoint *srequest.PaymentEndpoint, field string) (*orchestratorv1.PaymentEndpoint, error) {
if endpoint == nil {
return nil, nil
}
var (
count int
result orchestratorv1.PaymentEndpoint
)
if endpoint.Ledger != nil {
count++
result.Endpoint = &orchestratorv1.PaymentEndpoint_Ledger{
Ledger: mapLedgerEndpoint(endpoint.Ledger),
}
}
if endpoint.ManagedWallet != nil {
count++
result.Endpoint = &orchestratorv1.PaymentEndpoint_ManagedWallet{
ManagedWallet: mapManagedWalletEndpoint(endpoint.ManagedWallet),
}
}
if endpoint.ExternalChain != nil {
count++
result.Endpoint = &orchestratorv1.PaymentEndpoint_ExternalChain{
ExternalChain: mapExternalChainEndpoint(endpoint.ExternalChain),
}
}
if count > 1 {
return nil, merrors.InvalidArgument(field + " endpoint must set only one of ledger, managed_wallet, external_chain")
}
result.Metadata = copyStringMap(endpoint.Metadata)
return &result, nil
}
func mapLedgerEndpoint(endpoint *srequest.LedgerEndpoint) *orchestratorv1.LedgerEndpoint {
if endpoint == nil {
return nil
}
return &orchestratorv1.LedgerEndpoint{
LedgerAccountRef: endpoint.LedgerAccountRef,
ContraLedgerAccountRef: endpoint.ContraLedgerAccountRef,
}
}
func mapManagedWalletEndpoint(endpoint *srequest.ManagedWalletEndpoint) *orchestratorv1.ManagedWalletEndpoint {
if endpoint == nil {
return nil
}
return &orchestratorv1.ManagedWalletEndpoint{
ManagedWalletRef: endpoint.ManagedWalletRef,
Asset: mapAsset(endpoint.Asset),
}
}
func mapExternalChainEndpoint(endpoint *srequest.ExternalChainEndpoint) *orchestratorv1.ExternalChainEndpoint {
if endpoint == nil {
return nil
}
return &orchestratorv1.ExternalChainEndpoint{
Asset: mapAsset(endpoint.Asset),
Address: endpoint.Address,
Memo: endpoint.Memo,
}
}
func mapAsset(asset *srequest.Asset) *chainv1.Asset {
if asset == nil {
return nil
}
return &chainv1.Asset{
Chain: chainv1.ChainNetwork(asset.Chain),
TokenSymbol: asset.TokenSymbol,
ContractAddress: asset.ContractAddress,
}
}
func mapMoney(m *srequest.Money) *moneyv1.Money {
if m == nil {
return nil
}
return &moneyv1.Money{
Amount: m.Amount,
Currency: m.Currency,
}
}
func mapFXIntent(fx *srequest.FXIntent) (*orchestratorv1.FXIntent, error) {
if fx == nil {
return nil, nil
}
return &orchestratorv1.FXIntent{
Pair: mapCurrencyPair(fx.Pair),
Side: fxv1.Side(fx.Side),
Firm: fx.Firm,
TtlMs: fx.TTLms,
PreferredProvider: fx.PreferredProvider,
MaxAgeMs: fx.MaxAgeMs,
}, nil
}
func mapCurrencyPair(pair *srequest.CurrencyPair) *fxv1.CurrencyPair {
if pair == nil {
return nil
}
return &fxv1.CurrencyPair{
Base: pair.Base,
Quote: pair.Quote,
}
}
func mapPolicyOverrides(policy *srequest.PolicyOverrides) *feesv1.PolicyOverrides {
if policy == nil {
return nil
}
return &feesv1.PolicyOverrides{
InsufficientNet: feesv1.InsufficientNetPolicy(policy.InsufficientNet),
}
}
func copyStringMap(src map[string]string) map[string]string {
if len(src) == 0 {
return nil
}
dst := make(map[string]string, len(src))
for k, v := range src {
dst[k] = v
}
return dst
}

View File

@@ -11,6 +11,7 @@ import (
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
"github.com/tech/sendico/server/interface/api/srequest"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
@@ -26,11 +27,11 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
ctx := r.Context()
allowed, err := a.enf.Enforce(ctx, a.permissionRef, account.ID, orgRef, primitive.NilObjectID, model.ActionCreate)
if err != nil {
a.logger.Warn("Failed to check payments access permissions", zap.Error(err), zap.String(a.oph.Name(), orgRef.Hex()))
a.logger.Warn("Failed to check payments access permissions", zap.Error(err), mutil.PLog(a.oph, r))
return response.Auto(a.logger, a.Name(), err)
}
if !allowed {
a.logger.Debug("Access denied when initiating payment", zap.String(a.oph.Name(), orgRef.Hex()))
a.logger.Debug("Access denied when initiating payment", mutil.PLog(a.oph, r))
return response.AccessDenied(a.logger, a.Name(), "payments write permission denied")
}
@@ -45,12 +46,17 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
payload.QuoteRef = ""
}
intent, err := mapPaymentIntent(payload.Intent)
if err != nil {
return response.BadPayload(a.logger, a.Name(), err)
}
req := &orchestratorv1.InitiatePaymentRequest{
Meta: &orchestratorv1.RequestMeta{
OrganizationRef: orgRef.Hex(),
},
IdempotencyKey: strings.TrimSpace(payload.IdempotencyKey),
Intent: payload.Intent,
Intent: intent,
FeeQuoteToken: strings.TrimSpace(payload.FeeQuoteToken),
FxQuoteRef: strings.TrimSpace(payload.FxQuoteRef),
QuoteRef: strings.TrimSpace(payload.QuoteRef),
@@ -66,10 +72,10 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
return sresponse.PaymentResponse(a.logger, resp.GetPayment(), token)
}
func decodeInitiatePayload(r *http.Request) (*srequest.InitiatePaymentPayload, error) {
func decodeInitiatePayload(r *http.Request) (*srequest.InitiatePayment, error) {
defer r.Body.Close()
payload := &srequest.InitiatePaymentPayload{}
payload := &srequest.InitiatePayment{}
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
return nil, merrors.InvalidArgument("invalid payload: " + err.Error())
}

View File

@@ -11,6 +11,7 @@ import (
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
"github.com/tech/sendico/server/interface/api/srequest"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
@@ -25,11 +26,11 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token
ctx := r.Context()
allowed, err := a.enf.Enforce(ctx, a.permissionRef, account.ID, orgRef, primitive.NilObjectID, model.ActionCreate)
if err != nil {
a.logger.Warn("Failed to check payments access permissions", zap.Error(err), zap.String(a.oph.Name(), orgRef.Hex()))
a.logger.Warn("Failed to check payments access permissions", zap.Error(err), mutil.PLog(a.oph, r))
return response.Auto(a.logger, a.Name(), err)
}
if !allowed {
a.logger.Debug("Access denied when quoting payment", zap.String(a.oph.Name(), orgRef.Hex()))
a.logger.Debug("Access denied when quoting payment", mutil.PLog(a.oph, r))
return response.AccessDenied(a.logger, a.Name(), "payments write permission denied")
}
@@ -38,12 +39,17 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token
return response.BadPayload(a.logger, a.Name(), err)
}
intent, err := mapPaymentIntent(payload.Intent)
if err != nil {
return response.BadPayload(a.logger, a.Name(), err)
}
req := &orchestratorv1.QuotePaymentRequest{
Meta: &orchestratorv1.RequestMeta{
OrganizationRef: orgRef.Hex(),
},
IdempotencyKey: payload.IdempotencyKey,
Intent: payload.Intent,
Intent: intent,
PreviewOnly: payload.PreviewOnly,
}
@@ -56,10 +62,10 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token
return sresponse.PaymentQuoteResponse(a.logger, resp.GetQuote(), token)
}
func decodeQuotePayload(r *http.Request) (*srequest.QuotePaymentPayload, error) {
func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) {
defer r.Body.Close()
payload := &srequest.QuotePaymentPayload{}
payload := &srequest.QuotePayment{}
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
return nil, merrors.InvalidArgument("invalid payload: " + err.Error())
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/tech/sendico/pkg/mservice"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
@@ -28,11 +29,11 @@ func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, to
ctx := r.Context()
res, err := a.enf.Enforce(ctx, a.balancesPermissionRef, account.ID, orgRef, primitive.NilObjectID, model.ActionRead)
if err != nil {
a.logger.Warn("Failed to check wallet balance permissions", zap.Error(err), zap.String(a.oph.Name(), orgRef.Hex()), zap.String("wallet_ref", walletRef))
a.logger.Warn("Failed to check wallet balance permissions", zap.Error(err), mutil.PLog(a.oph, r), zap.String("wallet_ref", walletRef))
return response.Auto(a.logger, a.Name(), err)
}
if !res {
a.logger.Debug("Access denied when reading wallet balance", zap.String(a.oph.Name(), orgRef.Hex()), zap.String("wallet_ref", walletRef))
a.logger.Debug("Access denied when reading wallet balance", mutil.PLog(a.oph, r), zap.String("wallet_ref", walletRef))
return response.AccessDenied(a.logger, a.Name(), "wallet balance read permission denied")
}
if a.chainGateway == nil {

View File

@@ -10,6 +10,7 @@ import (
"github.com/tech/sendico/pkg/mservice"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
@@ -24,11 +25,11 @@ func (a *WalletAPI) listWallets(r *http.Request, account *model.Account, token *
ctx := r.Context()
res, err := a.enf.Enforce(ctx, a.walletsPermissionRef, account.ID, orgRef, primitive.NilObjectID, model.ActionRead)
if err != nil {
a.logger.Warn("Failed to check chain wallet access permissions", zap.Error(err), zap.String(a.oph.Name(), orgRef.Hex()))
a.logger.Warn("Failed to check chain wallet access permissions", zap.Error(err), mutil.PLog(a.oph, r))
return response.Auto(a.logger, a.Name(), err)
}
if !res {
a.logger.Debug("Access denied when listing organization wallets", zap.String(a.oph.Name(), orgRef.Hex()))
a.logger.Debug("Access denied when listing organization wallets", mutil.PLog(a.oph, r))
return response.AccessDenied(a.logger, a.Name(), "wallets read permission denied")
}
if a.chainGateway == nil {

View File

@@ -11,6 +11,8 @@ class CommonConstants {
static String apiEndpoint = '/api/v1';
static String amplitudeSecret = 'c3d75b3e2520d708440acbb16b923e79';
static String amplitudeServerZone = 'EU';
static String posthogApiKey = '';
static String posthogHost = 'https://eu.i.posthog.com';
static Locale defaultLocale = const Locale('en');
static String defaultCurrency = 'EUR';
static int defaultDimensionLength = 500;
@@ -36,6 +38,8 @@ class CommonConstants {
apiEndpoint = configJson['apiEndpoint'] ?? apiEndpoint;
amplitudeSecret = configJson['amplitudeSecret'] ?? amplitudeSecret;
amplitudeServerZone = configJson['amplitudeServerZone'] ?? amplitudeServerZone;
posthogApiKey = configJson['posthogApiKey'] ?? posthogApiKey;
posthogHost = configJson['posthogHost'] ?? posthogHost;
defaultLocale = Locale(configJson['defaultLocale'] ?? defaultLocale.languageCode);
defaultCurrency = configJson['defaultCurrency'] ?? defaultCurrency;
wsProto = configJson['wsProto'] ?? wsProto;

View File

@@ -15,6 +15,8 @@ class Constants extends CommonConstants {
static String get currentOrgKey => CommonConstants.currentOrgKey;
static String get apiUrl => CommonConstants.apiUrl;
static String get serviceUrl => CommonConstants.serviceUrl;
static String get posthogApiKey => CommonConstants.posthogApiKey;
static String get posthogHost => CommonConstants.posthogHost;
static int get defaultDimensionLength => CommonConstants.defaultDimensionLength;
static String get deviceIdStorageKey => CommonConstants.deviceIdStorageKey;
static String get nilObjectRef => CommonConstants.nilObjectRef;

View File

@@ -21,6 +21,8 @@ extension AppConfigExtension on AppConfig {
external String? get apiEndpoint;
external String? get amplitudeSecret;
external String? get amplitudeServerZone;
external String? get posthogApiKey;
external String? get posthogHost;
external String? get defaultLocale;
external String? get wsProto;
external String? get wsEndpoint;
@@ -40,6 +42,8 @@ class Constants extends CommonConstants {
static String get currentOrgKey => CommonConstants.currentOrgKey;
static String get apiUrl => CommonConstants.apiUrl;
static String get serviceUrl => CommonConstants.serviceUrl;
static String get posthogApiKey => CommonConstants.posthogApiKey;
static String get posthogHost => CommonConstants.posthogHost;
static int get defaultDimensionLength => CommonConstants.defaultDimensionLength;
static String get deviceIdStorageKey => CommonConstants.deviceIdStorageKey;
static String get nilObjectRef => CommonConstants.nilObjectRef;
@@ -57,6 +61,8 @@ class Constants extends CommonConstants {
'apiEndpoint': config.apiEndpoint,
'amplitudeSecret': config.amplitudeSecret,
'amplitudeServerZone': config.amplitudeServerZone,
'posthogApiKey': config.posthogApiKey,
'posthogHost': config.posthogHost,
'defaultLocale': config.defaultLocale,
'wsProto': config.wsProto,
'wsEndpoint': config.wsEndpoint,

View File

@@ -27,6 +27,7 @@ class AccountProvider extends ChangeNotifier {
Resource<Account?> get resource => _resource;
late LocaleProvider _localeProvider;
PendingLogin? _pendingLogin;
Future<void>? _restoreFuture;
Account? get account => _resource.data;
PendingLogin? get pendingLogin => _pendingLogin;
@@ -34,6 +35,7 @@ class AccountProvider extends ChangeNotifier {
bool get isLoading => _resource.isLoading;
Object? get error => _resource.error;
bool get isReady => (!isLoading) && (account != null);
Future<void>? get restoreFuture => _restoreFuture;
Account? currentUser() {
final acc = account;
@@ -220,4 +222,12 @@ class AccountProvider extends ChangeNotifier {
rethrow;
}
}
}
Future<void> restoreIfPossible() {
return _restoreFuture ??= () async {
final hasAuth = await AuthorizationService.isAuthorizationStored();
if (!hasAuth) return;
await restore();
}();
}
}

View File

@@ -24,9 +24,9 @@ import 'package:pweb/providers/two_factor.dart';
import 'package:pweb/providers/upload_history.dart';
import 'package:pweb/providers/wallets.dart';
import 'package:pweb/providers/wallet_transactions.dart';
// import 'package:pweb/services/amplitude.dart';
import 'package:pweb/services/operations.dart';
import 'package:pweb/services/payments/history.dart';
import 'package:pweb/services/posthog.dart';
import 'package:pweb/services/wallet_transactions.dart';
import 'package:pweb/services/wallets.dart';
@@ -40,11 +40,9 @@ void _setupLogging() {
}
void main() async {
await Constants.initialize();
WidgetsFlutterBinding.ensureInitialized();
// await AmplitudeService.initialize();
await Constants.initialize();
await PosthogService.initialize();
_setupLogging();

View File

@@ -14,7 +14,7 @@ import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/address_book/form/view.dart';
// import 'package:pweb/services/amplitude.dart';
import 'package:pweb/services/posthog.dart';
import 'package:pweb/utils/error/snackbar.dart';
import 'package:pweb/utils/payment/label.dart';
import 'package:pweb/utils/snackbar.dart';
@@ -105,11 +105,11 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
return;
}
// AmplitudeService.recipientAddCompleted(
// _type,
// _status,
// _methods.keys.toSet(),
// );
await PosthogService.recipientAddCompleted(
_type,
_status,
_methods.keys.toSet(),
);
final recipient = await executeActionWithNotification(
context: context,
action: _doSave,

View File

@@ -6,6 +6,7 @@ import 'package:pshared/provider/account.dart';
import 'package:pweb/app/router/pages.dart';
import 'package:pweb/widgets/error/snackbar.dart';
import 'package:pweb/services/posthog.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -17,15 +18,38 @@ class AccountLoader extends StatelessWidget {
@override
Widget build(BuildContext context) => Consumer<AccountProvider>(builder: (context, provider, _) {
if (provider.isLoading) return const Center(child: CircularProgressIndicator());
if (provider.error != null) {
postNotifyUserOfErrorX(
context: context,
errorSituation: AppLocalizations.of(context)!.errorLogin,
exception: provider.error!,
);
navigateAndReplace(context, Pages.login);
if (provider.account != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final account = provider.account;
if (account != null) {
PosthogService.identify(account);
}
});
return child;
}
if (provider.error != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
postNotifyUserOfErrorX(
context: context,
errorSituation: AppLocalizations.of(context)!.errorLogin,
exception: provider.error!,
);
navigateAndReplace(context, Pages.login);
});
return const Center(child: CircularProgressIndicator());
}
if (provider.restoreFuture == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
provider.restoreIfPossible().catchError((error, stack) {
debugPrint('Account restore failed: $error');
});
});
return const Center(child: CircularProgressIndicator());
}
if (provider.isLoading) return const Center(child: CircularProgressIndicator());
if (provider.account == null) {
WidgetsBinding.instance.addPostFrameCallback((_) => navigateAndReplace(context, Pages.login));
return const Center(child: CircularProgressIndicator());

View File

@@ -14,6 +14,7 @@ import 'package:pweb/widgets/password/password.dart';
import 'package:pweb/widgets/username.dart';
import 'package:pweb/widgets/vspacer.dart';
import 'package:pweb/widgets/error/snackbar.dart';
import 'package:pweb/services/posthog.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -43,10 +44,14 @@ class _LoginFormState extends State<LoginForm> {
password: _passwordController.text,
locale: context.read<LocaleProvider>().locale.languageCode,
);
await PosthogService.login(pending: outcome.isPending);
if (outcome.isPending) {
// TODO: fix context usage
navigateAndReplace(context, Pages.sfactor);
} else {
if (outcome.account != null) {
await PosthogService.identify(outcome.account!);
}
onLogin();
}
return 'ok';

View File

@@ -16,6 +16,7 @@ import 'package:pweb/providers/payment_flow.dart';
import 'package:pweb/pages/payment_methods/payment_page/body.dart';
import 'package:pweb/providers/wallets.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
import 'package:pweb/services/posthog.dart';
class PaymentPage extends StatefulWidget {
@@ -109,7 +110,7 @@ class _PaymentPageState extends State<PaymentPage> {
void _handleSendPayment() {
// TODO: Handle Payment logic
// AmplitudeService.paymentInitiated();
PosthogService.paymentInitiated(method: _flowProvider.selectedType);
}
@override
@@ -195,4 +196,4 @@ class _PaymentPageState extends State<PaymentPage> {
(method.description?.contains(wallet.walletUserID) ?? false),
);
}
}
}

View File

@@ -4,7 +4,7 @@ import 'package:provider/provider.dart';
import 'package:pshared/provider/locale.dart';
// import 'package:pweb/services/amplitude.dart';
import 'package:pweb/services/posthog.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -58,7 +58,7 @@ class LocalePicker extends StatelessWidget {
onChanged: (locale) {
if (locale != null) {
localeProvider.setLocale(locale);
// AmplitudeService.localeChanged(locale);
PosthogService.localeChanged(locale);
}
},
decoration: const InputDecoration(

View File

@@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:posthog_flutter/posthog_flutter.dart';
import 'package:pshared/config/constants.dart';
import 'package:pshared/models/account/account.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/status.dart';
import 'package:pshared/models/recipient/type.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
class PosthogService {
static final _logger = Logger('service.posthog');
static String? _identifiedUserId;
static bool _initialized = false;
static bool get isEnabled => _initialized;
static Future<void> initialize() async {
final apiKey = Constants.posthogApiKey;
if (apiKey.isEmpty) {
_logger.warning('PostHog API key is not configured, analytics disabled.');
return;
}
try {
final config = PostHogConfig(apiKey)
..host = Constants.posthogHost
..captureApplicationLifecycleEvents = true;
await Posthog().setup(config);
await Posthog().register('client_id', Constants.clientId);
_initialized = true;
_logger.info('PostHog initialized with host ${Constants.posthogHost}');
} catch (e, st) {
_initialized = false;
_logger.warning('Failed to initialize PostHog: $e', e, st);
}
}
static Future<void> identify(Account account) async {
if (!_initialized) return;
if (_identifiedUserId == account.id) return;
await Posthog().identify(
userId: account.id,
userProperties: {
'email': account.login,
'name': account.name,
'locale': account.locale,
'created_at': account.createdAt.toIso8601String(),
},
);
_identifiedUserId = account.id;
}
static Future<void> reset() async {
if (!_initialized) return;
_identifiedUserId = null;
await Posthog().reset();
}
static Future<void> login({required bool pending}) async {
if (!_initialized) return;
await _capture(
'login',
properties: {
'result': pending ? 'pending' : 'success',
},
);
}
static Future<void> pageOpened(PayoutDestination page, {String? path, String? uiSource}) async {
if (!_initialized) return;
return _capture(
'pageOpened',
properties: {
'page': page.name,
if (path != null) 'path': path,
if (uiSource != null) 'uiSource': uiSource,
},
);
}
static Future<void> localeChanged(Locale locale) async {
if (!_initialized) return;
return _capture(
'localeChanged',
properties: {'locale': locale.toLanguageTag()},
);
}
static Future<void> recipientAddCompleted(
RecipientType type,
RecipientStatus status,
Set<PaymentType> methods,
) async {
if (!_initialized) return;
return _capture(
'recipientAddCompleted',
properties: {
'methods': methods.map((m) => m.name).toList(),
'type': type.name,
'status': status.name,
},
);
}
static Future<void> paymentInitiated({PaymentType? method}) async {
if (!_initialized) return;
return _capture(
'paymentInitiated',
properties: {
if (method != null) 'method': method.name,
},
);
}
static Future<void> _capture(
String eventName, {
Map<String, Object?>? properties,
}) async {
if (!_initialized) return;
final filtered = <String, Object>{};
if (properties != null) {
for (final entry in properties.entries) {
final value = entry.value;
if (value != null) filtered[entry.key] = value;
}
}
await Posthog().capture(eventName: eventName, properties: filtered.isEmpty ? null : filtered);
}
}

View File

@@ -6,10 +6,12 @@ import 'package:pshared/provider/account.dart';
import 'package:pshared/provider/permissions.dart';
import 'package:pweb/app/router/pages.dart';
import 'package:pweb/services/posthog.dart';
void logoutUtil(BuildContext context) {
context.read<AccountProvider>().logout();
context.read<PermissionsProvider>().reset();
PosthogService.reset();
navigateAndReplace(context, Pages.login);
}

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
// import 'package:pweb/services/amplitude.dart';
import 'package:pweb/services/posthog.dart';
import 'package:pweb/widgets/sidebar/destinations.dart';
@@ -49,7 +49,7 @@ class SideMenuColumn extends StatelessWidget {
child: InkWell(
onTap: () {
onSelected(item);
// AmplitudeService.pageOpened(item, uiSource: 'sidebar');
PosthogService.pageOpened(item, uiSource: 'sidebar');
},
borderRadius: BorderRadius.circular(12),
hoverColor: theme.colorScheme.primaryContainer,
@@ -76,4 +76,4 @@ class SideMenuColumn extends StatelessWidget {
),
);
}
}
}

View File

@@ -9,6 +9,7 @@ import amplitude_flutter
import file_selector_macos
import flutter_timezone
import path_provider_foundation
import posthog_flutter
import share_plus
import shared_preferences_foundation
import sqflite_darwin
@@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
PosthogFlutterPlugin.register(with: registry.registrar(forPlugin: "PosthogFlutterPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))

View File

@@ -35,6 +35,7 @@ dependencies:
sdk: flutter
pshared:
path: ../pshared
posthog_flutter: ^4.0.1
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.