21 Commits

Author SHA1 Message Date
Arseni
0ecd17d2dc Updated Settings Page 2025-12-18 15:15:33 +03:00
d649748f6f Merge pull request 'server endpoint' (#115) from quotes-115 into main
All checks were successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
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/db Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
Reviewed-on: #115
2025-12-17 17:15:40 +00:00
Stephan D
61177a4e30 server endpoint 2025-12-17 18:15:02 +01:00
c7b9b70d57 Merge pull request 'multiple quotes payment' (#114) from quotes-114 into main
All checks were successful
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/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
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/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #114
2025-12-17 15:53:29 +00:00
Stephan D
5030453807 multiple quotes payment 2025-12-17 16:53:03 +01:00
5565081b69 Merge pull request 'PostHog last fixes hopefully' (#109) from SEND006 into main
All checks were successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
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/ledger Pipeline was successful
ci/woodpecker/push/mntx_gateway Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
Reviewed-on: #109
2025-12-17 12:22:33 +00:00
b216aa68b7 Merge pull request 'Empty state for Recipient Address Book' (#110) from SEND007 into main
Reviewed-on: #110
2025-12-17 11:48:07 +00:00
7ac1c519e3 Merge pull request 'Made clear massages for errors in recipient registration' (#111) from SEND008 into main
Some checks are pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/mntx_gateway Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
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/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline is running
ci/woodpecker/push/frontend Pipeline is running
ci/woodpecker/push/chain_gateway Pipeline was successful
Reviewed-on: #111
2025-12-17 11:13:16 +00:00
076b0c6434 Merge pull request 'Wallet update for correct name and symbol appearance' (#112) from SEND009 into main
Some checks failed
ci/woodpecker/push/bff Pipeline failed
ci/woodpecker/push/chain_gateway Pipeline is pending
ci/woodpecker/push/db Pipeline is pending
ci/woodpecker/push/frontend Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/mntx_gateway Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline failed
Reviewed-on: #112
2025-12-17 11:12:26 +00:00
Arseni
9a90e6a03b Wallet update for correct name and symbol appearance 2025-12-16 19:37:28 +03:00
Arseni
5218632c00 Made clear massages for errors in recipient registration 2025-12-16 18:42:57 +03:00
Arseni
a2c05745ad A 2025-12-16 18:21:49 +03:00
Arseni
82b2f88122 Changed the spelling of the word adress) 2025-12-12 19:39:36 +03:00
Arseni
28d74d058b Empty state for Recipient Adress Book 2025-12-12 19:29:10 +03:00
Arseni
6ee146b95a PostHog last fixes hopefully 2025-12-12 16:39:18 +03:00
67b52af150 Merge pull request 'fixed dropping of settlement mode' (#108) from settlement-106 into main
Some checks failed
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/mntx_gateway Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
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 failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #108
2025-12-12 13:19:29 +00:00
Stephan D
058a3fefaf fixed dropping of settlement mode 2025-12-12 14:19:09 +01:00
c8a97d940c Merge pull request 'quotation rate display' (#105) from fees-104 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/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/mntx_gateway Pipeline failed
ci/woodpecker/push/ledger Pipeline failed
Reviewed-on: #105
2025-12-12 12:46:21 +00:00
Stephan D
00045c1e65 quotation rate display 2025-12-12 13:45:58 +01:00
d64d7dab58 Merge pull request 'fixed quotation calculation logic' (#103) from fees-102 into main
Some checks failed
ci/woodpecker/push/chain_gateway Pipeline is pending
ci/woodpecker/push/db Pipeline is pending
ci/woodpecker/push/frontend Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/mntx_gateway Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline failed
ci/woodpecker/push/bff Pipeline failed
Reviewed-on: #103
2025-12-12 12:42:13 +00:00
Stephan D
4746a00eee fixed quotation calculation logic 2025-12-12 13:41:37 +01:00
134 changed files with 2863 additions and 670 deletions

3
.gitignore vendored
View File

@@ -9,4 +9,5 @@ untranslated.txt
generate_protos.sh
update_dep.sh
.vscode/
.gocache/
.gocache/
.cache/

View File

@@ -31,7 +31,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_golang v1.23.2
@@ -49,6 +49,6 @@ require (
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/protobuf v1.36.11
)

View File

@@ -95,8 +95,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -212,8 +212,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -32,7 +32,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
@@ -49,7 +49,7 @@ require (
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

View File

@@ -95,8 +95,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -212,8 +212,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -33,7 +33,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
@@ -50,5 +50,5 @@ require (
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
)

View File

@@ -95,8 +95,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -212,8 +212,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -22,7 +22,7 @@ require (
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251211224604-2e727cd2e6fe // indirect
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251213223233-751f36331c62 // 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.9.1 // indirect
@@ -60,7 +60,7 @@ require (
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -86,5 +86,5 @@ require (
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
)

View File

@@ -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-20251211224604-2e727cd2e6fe h1:Z93WiwkZABbBBb0hGVFSF9nofjiYRvdF7PUxB75oeyE=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251211224604-2e727cd2e6fe/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251213223233-751f36331c62 h1:Rge3uIIO891+nLqKTfMulCw+tWHtTl16Oudi0yUcAoE=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251213223233-751f36331c62/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=
@@ -207,8 +207,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -362,8 +362,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -30,7 +30,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
@@ -50,5 +50,5 @@ require (
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
)

View File

@@ -95,8 +95,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -214,8 +214,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -33,7 +33,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -51,5 +51,5 @@ require (
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
)

View File

@@ -95,8 +95,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -214,8 +214,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -33,7 +33,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
@@ -52,7 +52,7 @@ require (
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

View File

@@ -99,8 +99,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -227,8 +227,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -17,6 +17,8 @@ import (
// Client exposes typed helpers around the payment orchestrator gRPC API.
type Client interface {
QuotePayment(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.QuotePaymentResponse, error)
QuotePayments(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error)
InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error)
InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
CancelPayment(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error)
GetPayment(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error)
@@ -29,6 +31,8 @@ type Client interface {
type grpcOrchestratorClient interface {
QuotePayment(ctx context.Context, in *orchestratorv1.QuotePaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.QuotePaymentResponse, error)
QuotePayments(ctx context.Context, in *orchestratorv1.QuotePaymentsRequest, opts ...grpc.CallOption) (*orchestratorv1.QuotePaymentsResponse, error)
InitiatePayments(ctx context.Context, in *orchestratorv1.InitiatePaymentsRequest, opts ...grpc.CallOption) (*orchestratorv1.InitiatePaymentsResponse, error)
InitiatePayment(ctx context.Context, in *orchestratorv1.InitiatePaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.InitiatePaymentResponse, error)
CancelPayment(ctx context.Context, in *orchestratorv1.CancelPaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.CancelPaymentResponse, error)
GetPayment(ctx context.Context, in *orchestratorv1.GetPaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.GetPaymentResponse, error)
@@ -97,6 +101,18 @@ func (c *orchestratorClient) QuotePayment(ctx context.Context, req *orchestrator
return c.client.QuotePayment(ctx, req)
}
func (c *orchestratorClient) QuotePayments(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()
return c.client.QuotePayments(ctx, req)
}
func (c *orchestratorClient) InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()
return c.client.InitiatePayments(ctx, req)
}
func (c *orchestratorClient) InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) {
ctx, cancel := c.callContext(ctx)
defer cancel()

View File

@@ -9,6 +9,8 @@ import (
// Fake implements Client for tests.
type Fake struct {
QuotePaymentFn func(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.QuotePaymentResponse, error)
QuotePaymentsFn func(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error)
InitiatePaymentsFn func(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error)
InitiatePaymentFn func(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
CancelPaymentFn func(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error)
GetPaymentFn func(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error)
@@ -26,6 +28,20 @@ func (f *Fake) QuotePayment(ctx context.Context, req *orchestratorv1.QuotePaymen
return &orchestratorv1.QuotePaymentResponse{}, nil
}
func (f *Fake) QuotePayments(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error) {
if f.QuotePaymentsFn != nil {
return f.QuotePaymentsFn(ctx, req)
}
return &orchestratorv1.QuotePaymentsResponse{}, nil
}
func (f *Fake) InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error) {
if f.InitiatePaymentsFn != nil {
return f.InitiatePaymentsFn(ctx, req)
}
return &orchestratorv1.InitiatePaymentsResponse{}, nil
}
func (f *Fake) InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) {
if f.InitiatePaymentFn != nil {
return f.InitiatePaymentFn(ctx, req)

View File

@@ -45,7 +45,7 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
@@ -62,5 +62,5 @@ require (
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
)

View File

@@ -95,8 +95,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -215,8 +215,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -61,6 +61,13 @@ func (f *paymentCommandFactory) QuotePayment() *quotePaymentCommand {
}
}
func (f *paymentCommandFactory) QuotePayments() *quotePaymentsCommand {
return &quotePaymentsCommand{
engine: f.engine,
logger: f.logger.Named("quote_payments"),
}
}
func (f *paymentCommandFactory) InitiatePayment() *initiatePaymentCommand {
return &initiatePaymentCommand{
engine: f.engine,
@@ -68,6 +75,13 @@ func (f *paymentCommandFactory) InitiatePayment() *initiatePaymentCommand {
}
}
func (f *paymentCommandFactory) InitiatePayments() *initiatePaymentsCommand {
return &initiatePaymentsCommand{
engine: f.engine,
logger: f.logger.Named("initiate_payments"),
}
}
func (f *paymentCommandFactory) CancelPayment() *cancelPaymentCommand {
return &cancelPaymentCommand{
engine: f.engine,

View File

@@ -20,13 +20,14 @@ func intentFromProto(src *orchestratorv1.PaymentIntent) model.PaymentIntent {
return model.PaymentIntent{}
}
intent := model.PaymentIntent{
Kind: modelKindFromProto(src.GetKind()),
Source: endpointFromProto(src.GetSource()),
Destination: endpointFromProto(src.GetDestination()),
Amount: cloneMoney(src.GetAmount()),
RequiresFX: src.GetRequiresFx(),
FeePolicy: src.GetFeePolicy(),
Attributes: cloneMetadata(src.GetAttributes()),
Kind: modelKindFromProto(src.GetKind()),
Source: endpointFromProto(src.GetSource()),
Destination: endpointFromProto(src.GetDestination()),
Amount: cloneMoney(src.GetAmount()),
RequiresFX: src.GetRequiresFx(),
FeePolicy: src.GetFeePolicy(),
SettlementMode: src.GetSettlementMode(),
Attributes: cloneMetadata(src.GetAttributes()),
}
if src.GetFx() != nil {
intent.FX = fxIntentFromProto(src.GetFx())
@@ -153,13 +154,14 @@ func toProtoPayment(src *model.Payment) *orchestratorv1.Payment {
func protoIntentFromModel(src model.PaymentIntent) *orchestratorv1.PaymentIntent {
intent := &orchestratorv1.PaymentIntent{
Kind: protoKindFromModel(src.Kind),
Source: protoEndpointFromModel(src.Source),
Destination: protoEndpointFromModel(src.Destination),
Amount: cloneMoney(src.Amount),
RequiresFx: src.RequiresFX,
FeePolicy: src.FeePolicy,
Attributes: cloneMetadata(src.Attributes),
Kind: protoKindFromModel(src.Kind),
Source: protoEndpointFromModel(src.Source),
Destination: protoEndpointFromModel(src.Destination),
Amount: cloneMoney(src.Amount),
RequiresFx: src.RequiresFX,
FeePolicy: src.FeePolicy,
SettlementMode: src.SettlementMode,
Attributes: cloneMetadata(src.Attributes),
}
if src.FX != nil {
intent.Fx = protoFXIntentFromModel(src.FX)

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"strings"
"time"
"github.com/tech/sendico/payments/orchestrator/storage"
"github.com/tech/sendico/payments/orchestrator/storage/model"
@@ -66,6 +67,177 @@ func (h *quotePaymentCommand) Execute(ctx context.Context, req *orchestratorv1.Q
return gsresponse.Success(&orchestratorv1.QuotePaymentResponse{Quote: quote})
}
type quotePaymentsCommand struct {
engine paymentEngine
logger mlogger.Logger
}
func (h *quotePaymentsCommand) Execute(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) gsresponse.Responder[orchestratorv1.QuotePaymentsResponse] {
if err := h.engine.EnsureRepository(ctx); err != nil {
return gsresponse.Unavailable[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
if req == nil {
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request"))
}
orgRef, orgID, err := validateMetaAndOrgRef(req.GetMeta())
if err != nil {
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
intents := req.GetIntents()
if len(intents) == 0 {
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("intents are required"))
}
baseKey := strings.TrimSpace(req.GetIdempotencyKey())
quotes := make([]*orchestratorv1.PaymentQuote, 0, len(intents))
expires := make([]time.Time, 0, len(intents))
for i, intent := range intents {
if err := requireNonNilIntent(intent); err != nil {
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
quoteReq := &orchestratorv1.QuotePaymentRequest{
Meta: req.GetMeta(),
IdempotencyKey: perIntentIdempotencyKey(baseKey, i, len(intents)),
Intent: intent,
PreviewOnly: req.GetPreviewOnly(),
}
quote, expiresAt, err := h.engine.BuildPaymentQuote(ctx, orgRef, quoteReq)
if err != nil {
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
quotes = append(quotes, quote)
expires = append(expires, expiresAt)
}
aggregate, err := aggregatePaymentQuotes(quotes)
if err != nil {
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InternalWrap(err, "quote aggregation failed"))
}
expiresAt, ok := minQuoteExpiry(expires)
if !ok {
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.Internal("quote expiry missing"))
}
quoteRef := ""
if !req.GetPreviewOnly() {
quotesStore, err := ensureQuotesStore(h.engine.Repository())
if err != nil {
return gsresponse.Unavailable[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
quoteRef = primitive.NewObjectID().Hex()
record := &model.PaymentQuoteRecord{
QuoteRef: quoteRef,
Intents: intentsFromProto(intents),
Quotes: quoteSnapshotsFromProto(quotes),
ExpiresAt: expiresAt,
}
record.SetID(primitive.NewObjectID())
record.SetOrganizationRef(orgID)
if err := quotesStore.Create(ctx, record); err != nil {
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
h.logger.Info("stored payment quotes", zap.String("quote_ref", quoteRef), zap.String("org_ref", orgID.Hex()))
}
return gsresponse.Success(&orchestratorv1.QuotePaymentsResponse{
QuoteRef: quoteRef,
Aggregate: aggregate,
Quotes: quotes,
})
}
type initiatePaymentsCommand struct {
engine paymentEngine
logger mlogger.Logger
}
func (h *initiatePaymentsCommand) Execute(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) gsresponse.Responder[orchestratorv1.InitiatePaymentsResponse] {
if err := h.engine.EnsureRepository(ctx); err != nil {
return gsresponse.Unavailable[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
if req == nil {
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request"))
}
_, orgID, err := validateMetaAndOrgRef(req.GetMeta())
if err != nil {
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
idempotencyKey, err := requireIdempotencyKey(req.GetIdempotencyKey())
if err != nil {
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
quoteRef := strings.TrimSpace(req.GetQuoteRef())
if quoteRef == "" {
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("quote_ref is required"))
}
quotesStore, err := ensureQuotesStore(h.engine.Repository())
if err != nil {
return gsresponse.Unavailable[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
record, err := quotesStore.GetByRef(ctx, orgID, quoteRef)
if err != nil {
if errors.Is(err, storage.ErrQuoteNotFound) {
return gsresponse.FailedPrecondition[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, "quote_not_found", merrors.InvalidArgument("quote_ref not found or expired"))
}
return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
intents := record.Intents
quotes := record.Quotes
if len(intents) == 0 && record.Intent.Kind != "" && record.Intent.Kind != model.PaymentKindUnspecified {
intents = []model.PaymentIntent{record.Intent}
}
if len(quotes) == 0 && record.Quote != nil {
quotes = []*model.PaymentQuoteSnapshot{record.Quote}
}
if len(intents) == 0 || len(quotes) == 0 || len(intents) != len(quotes) {
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("stored quote payload is incomplete"))
}
store, err := ensurePaymentsStore(h.engine.Repository())
if err != nil {
return gsresponse.Unavailable[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
payments := make([]*orchestratorv1.Payment, 0, len(intents))
for i := range intents {
intentProto := protoIntentFromModel(intents[i])
if err := requireNonNilIntent(intentProto); err != nil {
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
quoteProto := modelQuoteToProto(quotes[i])
if quoteProto == nil {
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("stored quote is empty"))
}
quoteProto.QuoteRef = quoteRef
perKey := perIntentIdempotencyKey(idempotencyKey, i, len(intents))
if existing, err := getPaymentByIdempotencyKey(ctx, store, orgID, perKey); err == nil && existing != nil {
payments = append(payments, toProtoPayment(existing))
continue
} else if err != nil && !errors.Is(err, storage.ErrPaymentNotFound) {
return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
entity := newPayment(orgID, intentProto, perKey, req.GetMetadata(), quoteProto)
if err = store.Create(ctx, entity); err != nil {
if errors.Is(err, storage.ErrDuplicatePayment) {
return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.DataConflict("payment already exists"))
}
return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
if err := h.engine.ExecutePayment(ctx, store, entity, quoteProto); err != nil {
return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
payments = append(payments, toProtoPayment(entity))
}
return gsresponse.Success(&orchestratorv1.InitiatePaymentsResponse{Payments: payments})
}
type initiatePaymentCommand struct {
engine paymentEngine
logger mlogger.Logger

View File

@@ -145,7 +145,7 @@ func resolveTradeAmounts(intentAmount *moneyv1.Money, fxQuote *oraclev1.Quote, s
}
}
func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.EstimateTransferFeeResponse, fxQuote *oraclev1.Quote) (*moneyv1.Money, *moneyv1.Money) {
func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.EstimateTransferFeeResponse, fxQuote *oraclev1.Quote, mode orchestratorv1.SettlementMode) (*moneyv1.Money, *moneyv1.Money) {
if pay == nil {
return nil, nil
}
@@ -166,7 +166,7 @@ func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.Est
}
}
adjustDebit := func(m *moneyv1.Money) {
applyChargeToDebit := func(m *moneyv1.Money) {
converted, err := ensureCurrency(m, pay.GetCurrency(), fxQuote)
if err != nil || converted == nil {
return
@@ -176,7 +176,7 @@ func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.Est
}
}
adjustSettlement := func(m *moneyv1.Money) {
applyChargeToSettlement := func(m *moneyv1.Money) {
converted, err := ensureCurrency(m, settlementCurrency, fxQuote)
if err != nil || converted == nil {
return
@@ -186,12 +186,22 @@ func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.Est
}
}
adjustDebit(fee)
adjustSettlement(fee)
switch mode {
case orchestratorv1.SettlementMode_SETTLEMENT_MODE_FIX_RECEIVED:
// Sender pays the fee: keep settlement fixed, increase debit.
applyChargeToDebit(fee)
default:
// Recipient pays the fee (default): reduce settlement, keep debit fixed.
applyChargeToSettlement(fee)
}
if network != nil && network.GetNetworkFee() != nil {
adjustDebit(network.GetNetworkFee())
adjustSettlement(network.GetNetworkFee())
switch mode {
case orchestratorv1.SettlementMode_SETTLEMENT_MODE_FIX_RECEIVED:
applyChargeToDebit(network.GetNetworkFee())
default:
applyChargeToSettlement(network.GetNetworkFee())
}
}
return makeMoney(pay.GetCurrency(), debitDecimal), makeMoney(settlementCurrency, settlementDecimal)

View File

@@ -7,6 +7,7 @@ import (
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"
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
)
func TestResolveTradeAmountsBuyBase(t *testing.T) {
@@ -47,11 +48,32 @@ func TestComputeAggregatesConvertsCurrencies(t *testing.T) {
},
}
debit, settlement := computeAggregates(pay, settle, fee, network, fxQuote)
debit, settlement := computeAggregates(pay, settle, fee, network, fxQuote, orchestratorv1.SettlementMode_SETTLEMENT_MODE_FIX_RECEIVED)
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())
if settlement.GetCurrency() != "EUR" || settlement.GetAmount() != "50" {
t.Fatalf("expected settlement 50 EUR, got %s %s", settlement.GetCurrency(), settlement.GetAmount())
}
}
func TestComputeAggregatesRecipientPaysFee(t *testing.T) {
pay := &moneyv1.Money{Currency: "USDT", Amount: "100"}
settle := &moneyv1.Money{Currency: "RUB", Amount: "7932"} // 100 * 79.32
fee := &moneyv1.Money{Currency: "USDT", Amount: "7"} // 7% of 100
fxQuote := &oraclev1.Quote{
Pair: &fxv1.CurrencyPair{Base: "USDT", Quote: "RUB"},
Side: fxv1.Side_SELL_BASE_BUY_QUOTE,
Price: &moneyv1.Decimal{
Value: "79.32",
},
}
debit, settlement := computeAggregates(pay, settle, fee, nil, fxQuote, orchestratorv1.SettlementMode_SETTLEMENT_MODE_FIX_SOURCE)
if debit.GetCurrency() != "USDT" || debit.GetAmount() != "100" {
t.Fatalf("expected debit 100 USDT, got %s %s", debit.GetCurrency(), debit.GetAmount())
}
if settlement.GetCurrency() != "RUB" || settlement.GetAmount() != "7376.76" {
t.Fatalf("expected settlement 7376.76 RUB, got %s %s", settlement.GetCurrency(), settlement.GetAmount())
}
}

View File

@@ -0,0 +1,145 @@
package orchestrator
import (
"fmt"
"sort"
"strings"
"time"
"github.com/shopspring/decimal"
"github.com/tech/sendico/payments/orchestrator/storage/model"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
)
func perIntentIdempotencyKey(base string, index int, total int) string {
base = strings.TrimSpace(base)
if base == "" {
return ""
}
if total <= 1 {
return base
}
return fmt.Sprintf("%s:%d", base, index+1)
}
func minQuoteExpiry(expires []time.Time) (time.Time, bool) {
var min time.Time
for _, exp := range expires {
if exp.IsZero() {
continue
}
if min.IsZero() || exp.Before(min) {
min = exp
}
}
if min.IsZero() {
return time.Time{}, false
}
return min, true
}
func aggregatePaymentQuotes(quotes []*orchestratorv1.PaymentQuote) (*orchestratorv1.PaymentQuoteAggregate, error) {
if len(quotes) == 0 {
return nil, nil
}
debitTotals := map[string]decimal.Decimal{}
settlementTotals := map[string]decimal.Decimal{}
feeTotals := map[string]decimal.Decimal{}
networkTotals := map[string]decimal.Decimal{}
for _, quote := range quotes {
if quote == nil {
continue
}
if err := accumulateMoney(debitTotals, quote.GetDebitAmount()); err != nil {
return nil, err
}
if err := accumulateMoney(settlementTotals, quote.GetExpectedSettlementAmount()); err != nil {
return nil, err
}
if err := accumulateMoney(feeTotals, quote.GetExpectedFeeTotal()); err != nil {
return nil, err
}
if nf := quote.GetNetworkFee(); nf != nil {
if err := accumulateMoney(networkTotals, nf.GetNetworkFee()); err != nil {
return nil, err
}
}
}
return &orchestratorv1.PaymentQuoteAggregate{
DebitAmounts: totalsToMoney(debitTotals),
ExpectedSettlementAmounts: totalsToMoney(settlementTotals),
ExpectedFeeTotals: totalsToMoney(feeTotals),
NetworkFeeTotals: totalsToMoney(networkTotals),
}, nil
}
func accumulateMoney(totals map[string]decimal.Decimal, money *moneyv1.Money) error {
if money == nil {
return nil
}
currency := strings.TrimSpace(money.GetCurrency())
if currency == "" {
return nil
}
amount, err := decimal.NewFromString(money.GetAmount())
if err != nil {
return err
}
if current, ok := totals[currency]; ok {
totals[currency] = current.Add(amount)
return nil
}
totals[currency] = amount
return nil
}
func totalsToMoney(totals map[string]decimal.Decimal) []*moneyv1.Money {
if len(totals) == 0 {
return nil
}
currencies := make([]string, 0, len(totals))
for currency := range totals {
currencies = append(currencies, currency)
}
sort.Strings(currencies)
result := make([]*moneyv1.Money, 0, len(currencies))
for _, currency := range currencies {
amount := totals[currency]
result = append(result, &moneyv1.Money{
Amount: amount.String(),
Currency: currency,
})
}
return result
}
func intentsFromProto(intents []*orchestratorv1.PaymentIntent) []model.PaymentIntent {
if len(intents) == 0 {
return nil
}
result := make([]model.PaymentIntent, 0, len(intents))
for _, intent := range intents {
result = append(result, intentFromProto(intent))
}
return result
}
func quoteSnapshotsFromProto(quotes []*orchestratorv1.PaymentQuote) []*model.PaymentQuoteSnapshot {
if len(quotes) == 0 {
return nil
}
result := make([]*model.PaymentQuoteSnapshot, 0, len(quotes))
for _, quote := range quotes {
if quote == nil {
continue
}
if snapshot := quoteSnapshotToModel(quote); snapshot != nil {
result = append(result, snapshot)
}
}
return result
}

View File

@@ -0,0 +1,102 @@
package orchestrator
import (
"testing"
"time"
"github.com/shopspring/decimal"
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"
)
func TestAggregatePaymentQuotes(t *testing.T) {
quotes := []*orchestratorv1.PaymentQuote{
{
DebitAmount: &moneyv1.Money{Amount: "10", Currency: "USD"},
ExpectedSettlementAmount: &moneyv1.Money{Amount: "8", Currency: "EUR"},
ExpectedFeeTotal: &moneyv1.Money{Amount: "1", Currency: "USD"},
NetworkFee: &chainv1.EstimateTransferFeeResponse{
NetworkFee: &moneyv1.Money{Amount: "0.5", Currency: "USD"},
},
},
{
DebitAmount: &moneyv1.Money{Amount: "5", Currency: "USD"},
ExpectedSettlementAmount: &moneyv1.Money{Amount: "1000", Currency: "NGN"},
ExpectedFeeTotal: &moneyv1.Money{Amount: "2", Currency: "USD"},
NetworkFee: &chainv1.EstimateTransferFeeResponse{
NetworkFee: &moneyv1.Money{Amount: "100", Currency: "NGN"},
},
},
}
agg, err := aggregatePaymentQuotes(quotes)
if err != nil {
t.Fatalf("aggregatePaymentQuotes returned error: %v", err)
}
assertMoneyTotals(t, agg.GetDebitAmounts(), map[string]string{"USD": "15"})
assertMoneyTotals(t, agg.GetExpectedSettlementAmounts(), map[string]string{"EUR": "8", "NGN": "1000"})
assertMoneyTotals(t, agg.GetExpectedFeeTotals(), map[string]string{"USD": "3"})
assertMoneyTotals(t, agg.GetNetworkFeeTotals(), map[string]string{"USD": "0.5", "NGN": "100"})
}
func TestAggregatePaymentQuotesInvalidAmount(t *testing.T) {
quotes := []*orchestratorv1.PaymentQuote{
{
DebitAmount: &moneyv1.Money{Amount: "bad", Currency: "USD"},
},
}
if _, err := aggregatePaymentQuotes(quotes); err == nil {
t.Fatal("expected error for invalid amount")
}
}
func TestMinQuoteExpiry(t *testing.T) {
now := time.Now().UTC()
later := now.Add(10 * time.Minute)
earliest := now.Add(5 * time.Minute)
min, ok := minQuoteExpiry([]time.Time{later, time.Time{}, earliest})
if !ok {
t.Fatal("expected min expiry to be set")
}
if !min.Equal(earliest) {
t.Fatalf("expected min expiry %v, got %v", earliest, min)
}
if _, ok := minQuoteExpiry([]time.Time{time.Time{}}); ok {
t.Fatal("expected min expiry to be unset")
}
}
func assertMoneyTotals(t *testing.T, list []*moneyv1.Money, expected map[string]string) {
t.Helper()
got := make(map[string]decimal.Decimal, len(list))
for _, item := range list {
if item == nil {
continue
}
val, err := decimal.NewFromString(item.GetAmount())
if err != nil {
t.Fatalf("invalid money amount %q: %v", item.GetAmount(), err)
}
got[item.GetCurrency()] = val
}
if len(got) != len(expected) {
t.Fatalf("expected %d totals, got %d", len(expected), len(got))
}
for currency, amount := range expected {
val, err := decimal.NewFromString(amount)
if err != nil {
t.Fatalf("invalid expected amount %q: %v", amount, err)
}
gotVal, ok := got[currency]
if !ok {
t.Fatalf("missing currency %s", currency)
}
if !gotVal.Equal(val) {
t.Fatalf("expected %s %s, got %s", amount, currency, gotVal.String())
}
}
}

View File

@@ -65,7 +65,7 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orc
s.logger.Debug("network fee estimated", zap.String("org_ref", orgRef))
}
debitAmount, settlementAmount := computeAggregates(payAmount, settlementAmountBeforeFees, feeTotal, networkFee, fxQuote)
debitAmount, settlementAmount := computeAggregates(payAmount, settlementAmountBeforeFees, feeTotal, networkFee, fxQuote, intent.GetSettlementMode())
quote := &orchestratorv1.PaymentQuote{
DebitAmount: debitAmount,

View File

@@ -117,12 +117,24 @@ func (s *Service) QuotePayment(ctx context.Context, req *orchestratorv1.QuotePay
return executeUnary(ctx, s, "QuotePayment", s.h.commands.QuotePayment().Execute, req)
}
// QuotePayments aggregates downstream quotes for multiple intents.
func (s *Service) QuotePayments(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error) {
s.ensureHandlers()
return executeUnary(ctx, s, "QuotePayments", s.h.commands.QuotePayments().Execute, req)
}
// InitiatePayment captures a payment intent and reserves funds orchestration.
func (s *Service) InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) {
s.ensureHandlers()
return executeUnary(ctx, s, "InitiatePayment", s.h.commands.InitiatePayment().Execute, req)
}
// InitiatePayments executes multiple payments using a stored quote reference.
func (s *Service) InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error) {
s.ensureHandlers()
return executeUnary(ctx, s, "InitiatePayments", s.h.commands.InitiatePayments().Execute, req)
}
// CancelPayment attempts to cancel an in-flight payment.
func (s *Service) CancelPayment(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error) {
s.ensureHandlers()

View File

@@ -48,7 +48,8 @@ func TestRequireIdempotencyKey(t *testing.T) {
func TestNewPayment(t *testing.T) {
org := primitive.NewObjectID()
intent := &orchestratorv1.PaymentIntent{
Amount: &moneyv1.Money{Currency: "USD", Amount: "10"},
Amount: &moneyv1.Money{Currency: "USD", Amount: "10"},
SettlementMode: orchestratorv1.SettlementMode_SETTLEMENT_MODE_FIX_RECEIVED,
}
quote := &orchestratorv1.PaymentQuote{QuoteRef: "q1"}
p := newPayment(org, intent, "idem", map[string]string{"k": "v"}, quote)
@@ -58,6 +59,9 @@ func TestNewPayment(t *testing.T) {
if p.Intent.Amount == nil || p.Intent.Amount.GetAmount() != "10" {
t.Fatalf("intent not copied")
}
if p.Intent.SettlementMode != orchestratorv1.SettlementMode_SETTLEMENT_MODE_FIX_RECEIVED {
t.Fatalf("settlement mode not preserved")
}
if p.LastQuote == nil || p.LastQuote.QuoteRef != "q1" {
t.Fatalf("quote not copied")
}

View File

@@ -11,6 +11,7 @@ import (
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"
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
)
// PaymentKind captures the orchestrator intent type.
@@ -124,14 +125,15 @@ type FXIntent struct {
// PaymentIntent models the requested payment operation.
type PaymentIntent struct {
Kind PaymentKind `bson:"kind" json:"kind"`
Source PaymentEndpoint `bson:"source" json:"source"`
Destination PaymentEndpoint `bson:"destination" json:"destination"`
Amount *moneyv1.Money `bson:"amount" json:"amount"`
RequiresFX bool `bson:"requiresFx,omitempty" json:"requiresFx,omitempty"`
FX *FXIntent `bson:"fx,omitempty" json:"fx,omitempty"`
FeePolicy *feesv1.PolicyOverrides `bson:"feePolicy,omitempty" json:"feePolicy,omitempty"`
Attributes map[string]string `bson:"attributes,omitempty" json:"attributes,omitempty"`
Kind PaymentKind `bson:"kind" json:"kind"`
Source PaymentEndpoint `bson:"source" json:"source"`
Destination PaymentEndpoint `bson:"destination" json:"destination"`
Amount *moneyv1.Money `bson:"amount" json:"amount"`
RequiresFX bool `bson:"requiresFx,omitempty" json:"requiresFx,omitempty"`
FX *FXIntent `bson:"fx,omitempty" json:"fx,omitempty"`
FeePolicy *feesv1.PolicyOverrides `bson:"feePolicy,omitempty" json:"feePolicy,omitempty"`
SettlementMode orchestratorv1.SettlementMode `bson:"settlementMode,omitempty" json:"settlementMode,omitempty"`
Attributes map[string]string `bson:"attributes,omitempty" json:"attributes,omitempty"`
}
// PaymentQuoteSnapshot stores the latest quote info.

View File

@@ -12,10 +12,12 @@ type PaymentQuoteRecord struct {
storable.Base `bson:",inline" json:",inline"`
model.OrganizationBoundBase `bson:",inline" json:",inline"`
QuoteRef string `bson:"quoteRef" json:"quoteRef"`
Intent PaymentIntent `bson:"intent" json:"intent"`
Quote *PaymentQuoteSnapshot `bson:"quote" json:"quote"`
ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
QuoteRef string `bson:"quoteRef" json:"quoteRef"`
Intent PaymentIntent `bson:"intent,omitempty" json:"intent,omitempty"`
Intents []PaymentIntent `bson:"intents,omitempty" json:"intents,omitempty"`
Quote *PaymentQuoteSnapshot `bson:"quote,omitempty" json:"quote,omitempty"`
Quotes []*PaymentQuoteSnapshot `bson:"quotes,omitempty" json:"quotes,omitempty"`
ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
}
// Collection implements storable.Storable.

View File

@@ -73,6 +73,16 @@ func (q *Quotes) Create(ctx context.Context, quote *model.PaymentQuoteRecord) er
quote.Intent.Attributes[k] = strings.TrimSpace(v)
}
}
if len(quote.Intents) > 0 {
for i := range quote.Intents {
if quote.Intents[i].Attributes == nil {
continue
}
for k, v := range quote.Intents[i].Attributes {
quote.Intents[i].Attributes[k] = strings.TrimSpace(v)
}
}
}
quote.Update()
filter := repository.OrgFilter(quote.OrganizationRef).And(

View File

@@ -9,7 +9,7 @@ require (
github.com/google/uuid v1.6.0
github.com/mattn/go-colorable v0.1.14
github.com/mitchellh/mapstructure v1.5.0
github.com/nats-io/nats.go v1.47.0
github.com/nats-io/nats.go v1.48.0
github.com/prometheus/client_golang v1.23.2
github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go v0.33.0
@@ -93,6 +93,6 @@ require (
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -106,8 +106,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -269,8 +269,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -125,6 +125,13 @@ message PaymentQuote {
string quote_ref = 8;
}
message PaymentQuoteAggregate {
repeated common.money.v1.Money debit_amounts = 1;
repeated common.money.v1.Money expected_settlement_amounts = 2;
repeated common.money.v1.Money expected_fee_totals = 3;
repeated common.money.v1.Money network_fee_totals = 4;
}
message ExecutionRefs {
string debit_entry_ref = 1;
string credit_entry_ref = 2;
@@ -172,6 +179,30 @@ message QuotePaymentResponse {
PaymentQuote quote = 1;
}
message QuotePaymentsRequest {
RequestMeta meta = 1;
string idempotency_key = 2;
repeated PaymentIntent intents = 3;
bool preview_only = 4;
}
message QuotePaymentsResponse {
string quote_ref = 1;
PaymentQuoteAggregate aggregate = 2;
repeated PaymentQuote quotes = 3;
}
message InitiatePaymentsRequest {
RequestMeta meta = 1;
string idempotency_key = 2;
string quote_ref = 3;
map<string, string> metadata = 4;
}
message InitiatePaymentsResponse {
repeated Payment payments = 1;
}
message InitiatePaymentRequest {
RequestMeta meta = 1;
string idempotency_key = 2;
@@ -259,6 +290,8 @@ message InitiateConversionResponse {
service PaymentOrchestrator {
rpc QuotePayment(QuotePaymentRequest) returns (QuotePaymentResponse);
rpc QuotePayments(QuotePaymentsRequest) returns (QuotePaymentsResponse);
rpc InitiatePayments(InitiatePaymentsRequest) returns (InitiatePaymentsResponse);
rpc InitiatePayment(InitiatePaymentRequest) returns (InitiatePaymentResponse);
rpc CancelPayment(CancelPaymentRequest) returns (CancelPaymentResponse);
rpc GetPayment(GetPaymentRequest) returns (GetPaymentResponse);

View File

@@ -12,9 +12,9 @@ replace github.com/tech/sendico/gateway/chain => ../gateway/chain
require (
github.com/aws/aws-sdk-go-v2 v1.41.0
github.com/aws/aws-sdk-go-v2/config v1.32.5
github.com/aws/aws-sdk-go-v2/credentials v1.19.5
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.2
github.com/aws/aws-sdk-go-v2/config v1.32.6
github.com/aws/aws-sdk-go-v2/credentials v1.19.6
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2
github.com/go-chi/jwtauth/v5 v5.3.3
@@ -58,7 +58,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
@@ -103,7 +103,7 @@ require (
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.47.0 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect
github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
@@ -139,6 +139,6 @@ require (
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
google.golang.org/grpc v1.77.0 // indirect
)

View File

@@ -10,10 +10,10 @@ github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgP
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.32.5 h1:pz3duhAfUgnxbtVhIK39PGF/AHYyrzGEyRD9Og0QrE8=
github.com/aws/aws-sdk-go-v2/config v1.32.5/go.mod h1:xmDjzSUs/d0BB7ClzYPAZMmgQdrodNjPPhd6bGASwoE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.5 h1:xMo63RlqP3ZZydpJDMBsH9uJ10hgHYfQFIk1cHDXrR4=
github.com/aws/aws-sdk-go-v2/credentials v1.19.5/go.mod h1:hhbH6oRcou+LpXfA/0vPElh/e0M3aFeOblE1sssAAEk=
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
@@ -32,12 +32,12 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.2 h1:U3ygWUhCpiSPYSHOrRhb3gOl9T5Y3kB8k5Vjs//57bE=
github.com/aws/aws-sdk-go-v2/service/s3 v1.93.2/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 h1:SWTxh/EcUCDVqi/0s26V6pVUq0BBG7kx0tDTmF/hCgA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 h1:eYnlt6QxnFINKzwxP5/Ucs1vkG7VT3Iezmvfgc2waUw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
@@ -175,8 +175,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -361,8 +361,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

View File

@@ -36,6 +36,27 @@ func (r *QuotePayment) Validate() error {
return nil
}
type QuotePayments struct {
PaymentBase `json:",inline"`
Intents []PaymentIntent `json:"intents"`
PreviewOnly bool `json:"previewOnly"`
}
func (r *QuotePayments) Validate() error {
if err := r.PaymentBase.Validate(); err != nil {
return err
}
if len(r.Intents) == 0 {
return merrors.InvalidArgument("intents are required", "intents")
}
for i := range r.Intents {
if err := r.Intents[i].Validate(); err != nil {
return err
}
}
return nil
}
type InitiatePayment struct {
PaymentBase `json:",inline"`
Intent *PaymentIntent `json:"intent,omitempty"`
@@ -68,3 +89,18 @@ func (r InitiatePayment) Validate() error {
return nil
}
type InitiatePayments struct {
PaymentBase `json:",inline"`
QuoteRef string `json:"quoteRef,omitempty"`
}
func (r InitiatePayments) Validate() error {
if err := r.PaymentBase.Validate(); err != nil {
return err
}
if r.QuoteRef == "" {
return merrors.InvalidArgument("quoteRef is required", "quoteRef")
}
return nil
}

View File

@@ -14,3 +14,19 @@ func toMoney(m *moneyv1.Money) *model.Money {
Currency: m.GetCurrency(),
}
}
func toMoneyList(list []*moneyv1.Money) []*model.Money {
if len(list) == 0 {
return nil
}
result := make([]*model.Money, 0, len(list))
for _, item := range list {
if m := toMoney(item); m != nil {
result = append(result, m)
}
}
if len(result) == 0 {
return nil
}
return result
}

View File

@@ -49,6 +49,19 @@ type PaymentQuote struct {
FxQuote *FxQuote `json:"fxQuote,omitempty"`
}
type PaymentQuoteAggregate struct {
DebitAmounts []*model.Money `json:"debitAmounts,omitempty"`
ExpectedSettlementAmounts []*model.Money `json:"expectedSettlementAmounts,omitempty"`
ExpectedFeeTotals []*model.Money `json:"expectedFeeTotals,omitempty"`
NetworkFeeTotals []*model.Money `json:"networkFeeTotals,omitempty"`
}
type PaymentQuotes struct {
QuoteRef string `json:"quoteRef,omitempty"`
Aggregate *PaymentQuoteAggregate `json:"aggregate,omitempty"`
Quotes []PaymentQuote `json:"quotes,omitempty"`
}
type Payment struct {
PaymentRef string `json:"paymentRef,omitempty"`
IdempotencyKey string `json:"idempotencyKey,omitempty"`
@@ -63,6 +76,16 @@ type paymentQuoteResponse struct {
Quote *PaymentQuote `json:"quote"`
}
type paymentQuotesResponse struct {
authResponse `json:",inline"`
Quote *PaymentQuotes `json:"quote"`
}
type paymentsResponse struct {
authResponse `json:",inline"`
Payments []Payment `json:"payments"`
}
type paymentResponse struct {
authResponse `json:",inline"`
Payment *Payment `json:"payment"`
@@ -76,6 +99,22 @@ func PaymentQuoteResponse(logger mlogger.Logger, quote *orchestratorv1.PaymentQu
})
}
// PaymentQuotes wraps batch quotes with refreshed access token.
func PaymentQuotesResponse(logger mlogger.Logger, resp *orchestratorv1.QuotePaymentsResponse, token *TokenData) http.HandlerFunc {
return response.Ok(logger, paymentQuotesResponse{
Quote: toPaymentQuotes(resp),
authResponse: authResponse{AccessToken: *token},
})
}
// Payments wraps a list of payments with refreshed access token.
func PaymentsResponse(logger mlogger.Logger, payments []*orchestratorv1.Payment, token *TokenData) http.HandlerFunc {
return response.Ok(logger, paymentsResponse{
Payments: toPayments(payments),
authResponse: authResponse{AccessToken: *token},
})
}
// Payment wraps a payment with refreshed access token.
func PaymentResponse(logger mlogger.Logger, payment *orchestratorv1.Payment, token *TokenData) http.HandlerFunc {
return response.Ok(logger, paymentResponse{
@@ -158,6 +197,54 @@ func toPaymentQuote(q *orchestratorv1.PaymentQuote) *PaymentQuote {
}
}
func toPaymentQuoteAggregate(q *orchestratorv1.PaymentQuoteAggregate) *PaymentQuoteAggregate {
if q == nil {
return nil
}
return &PaymentQuoteAggregate{
DebitAmounts: toMoneyList(q.GetDebitAmounts()),
ExpectedSettlementAmounts: toMoneyList(q.GetExpectedSettlementAmounts()),
ExpectedFeeTotals: toMoneyList(q.GetExpectedFeeTotals()),
NetworkFeeTotals: toMoneyList(q.GetNetworkFeeTotals()),
}
}
func toPaymentQuotes(resp *orchestratorv1.QuotePaymentsResponse) *PaymentQuotes {
if resp == nil {
return nil
}
quotes := make([]PaymentQuote, 0, len(resp.GetQuotes()))
for _, quote := range resp.GetQuotes() {
if dto := toPaymentQuote(quote); dto != nil {
quotes = append(quotes, *dto)
}
}
if len(quotes) == 0 {
quotes = nil
}
return &PaymentQuotes{
QuoteRef: resp.GetQuoteRef(),
Aggregate: toPaymentQuoteAggregate(resp.GetAggregate()),
Quotes: quotes,
}
}
func toPayments(items []*orchestratorv1.Payment) []Payment {
if len(items) == 0 {
return nil
}
result := make([]Payment, 0, len(items))
for _, item := range items {
if p := toPayment(item); p != nil {
result = append(result, *p)
}
}
if len(result) == 0 {
return nil
}
return result
}
func toPayment(p *orchestratorv1.Payment) *Payment {
if p == nil {
return nil

View File

@@ -0,0 +1,74 @@
package paymentapiimp
import (
"encoding/json"
"net/http"
"strings"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
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"
)
func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
orgRef, err := a.oph.GetRef(r)
if err != nil {
a.logger.Warn("Failed to parse organization reference for batch payment initiation", zap.Error(err), zap.String(a.oph.Name(), a.oph.GetID(r)))
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
}
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), mutil.PLog(a.oph, r))
return response.Auto(a.logger, a.Name(), err)
}
if !allowed {
a.logger.Debug("Access denied when initiating batch payments", mutil.PLog(a.oph, r))
return response.AccessDenied(a.logger, a.Name(), "payments write permission denied")
}
payload, err := decodeInitiatePaymentsPayload(r)
if err != nil {
return response.BadPayload(a.logger, a.Name(), err)
}
req := &orchestratorv1.InitiatePaymentsRequest{
Meta: &orchestratorv1.RequestMeta{
OrganizationRef: orgRef.Hex(),
},
IdempotencyKey: strings.TrimSpace(payload.IdempotencyKey),
QuoteRef: strings.TrimSpace(payload.QuoteRef),
Metadata: payload.Metadata,
}
resp, err := a.client.InitiatePayments(ctx, req)
if err != nil {
a.logger.Warn("Failed to initiate batch payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
return response.Auto(a.logger, a.Name(), err)
}
return sresponse.PaymentsResponse(a.logger, resp.GetPayments(), token)
}
func decodeInitiatePaymentsPayload(r *http.Request) (*srequest.InitiatePayments, error) {
defer r.Body.Close()
payload := &srequest.InitiatePayments{}
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
return nil, merrors.InvalidArgument("invalid payload: " + err.Error())
}
payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey)
payload.QuoteRef = strings.TrimSpace(payload.QuoteRef)
if err := payload.Validate(); err != nil {
return nil, err
}
return payload, nil
}

View File

@@ -67,6 +67,62 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token
return sresponse.PaymentQuoteResponse(a.logger, resp.GetQuote(), token)
}
func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
orgRef, err := a.oph.GetRef(r)
if err != nil {
a.logger.Warn("Failed to parse organization reference for quotes", zap.Error(err), zap.String(a.oph.Name(), a.oph.GetID(r)))
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
}
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), mutil.PLog(a.oph, r))
return response.Auto(a.logger, a.Name(), err)
}
if !allowed {
a.logger.Debug("Access denied when quoting payments", mutil.PLog(a.oph, r))
return response.AccessDenied(a.logger, a.Name(), "payments write permission denied")
}
payload, err := decodeQuotePaymentsPayload(r)
if err != nil {
a.logger.Debug("Failed to decode payload", zap.Error(err), mutil.PLog(a.oph, r))
return response.BadPayload(a.logger, a.Name(), err)
}
if err := payload.Validate(); err != nil {
a.logger.Debug("Failed to validate payload", zap.Error(err), mutil.PLog(a.oph, r))
return response.Auto(a.logger, a.Name(), err)
}
intents := make([]*orchestratorv1.PaymentIntent, 0, len(payload.Intents))
for i := range payload.Intents {
intent, err := mapPaymentIntent(&payload.Intents[i])
if err != nil {
a.logger.Debug("Failed to map payment intent", zap.Error(err), mutil.PLog(a.oph, r))
return response.BadPayload(a.logger, a.Name(), err)
}
intents = append(intents, intent)
}
req := &orchestratorv1.QuotePaymentsRequest{
Meta: &orchestratorv1.RequestMeta{
OrganizationRef: orgRef.Hex(),
},
IdempotencyKey: payload.IdempotencyKey,
Intents: intents,
PreviewOnly: payload.PreviewOnly,
}
resp, err := a.client.QuotePayments(ctx, req)
if err != nil {
a.logger.Warn("Failed to quote payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
return response.Auto(a.logger, a.Name(), err)
}
return sresponse.PaymentQuotesResponse(a.logger, resp, token)
}
func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) {
defer r.Body.Close()
@@ -80,3 +136,17 @@ func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) {
}
return payload, nil
}
func decodeQuotePaymentsPayload(r *http.Request) (*srequest.QuotePayments, error) {
defer r.Body.Close()
payload := &srequest.QuotePayments{}
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
return nil, merrors.InvalidArgument("invalid payload: "+err.Error(), "payload")
}
payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey)
if err := payload.Validate(); err != nil {
return nil, err
}
return payload, nil
}

View File

@@ -22,6 +22,8 @@ import (
type paymentClient interface {
QuotePayment(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.QuotePaymentResponse, error)
QuotePayments(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) (*orchestratorv1.QuotePaymentsResponse, error)
InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error)
InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
Close() error
}
@@ -66,8 +68,10 @@ func CreateAPI(apiCtx eapi.API) (*PaymentAPI, error) {
}
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/quote"), api.Post, p.quotePayment)
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/multiquote"), api.Post, p.quotePayments)
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/immediate"), api.Post, p.initiateImmediate)
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/by-quote"), api.Post, p.initiateByQuote)
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/by-multiquote"), api.Post, p.initiatePaymentsByQuote)
return p, nil
}

View File

@@ -0,0 +1,20 @@
import 'package:json_annotation/json_annotation.dart';
part 'username.g.dart';
@JsonSerializable()
class ResetUserNameRequest {
final String userName;
const ResetUserNameRequest({
required this.userName,
});
factory ResetUserNameRequest.fromJson(Map<String, dynamic> json) => _$ResetUserNameRequestFromJson(json);
Map<String, dynamic> toJson() => _$ResetUserNameRequestToJson(this);
static ResetUserNameRequest build({
required String userName,
}) => ResetUserNameRequest(userName: userName);
}

View File

@@ -11,6 +11,8 @@ class CommonConstants {
static String apiEndpoint = '/api/v1';
static String amplitudeSecret = 'c3d75b3e2520d708440acbb16b923e79';
static String amplitudeServerZone = 'EU';
static String posthogApiKey = 'phc_lVhbruaZpxiQxppHBJpL36ARnPlkqbCewv6cauoceTN';
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

@@ -2,6 +2,7 @@ import 'package:pshared/data/dto/wallet/balance.dart';
import 'package:pshared/data/dto/wallet/wallet.dart';
import 'package:pshared/data/mapper/wallet/balance.dart';
import 'package:pshared/data/mapper/wallet/money.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/wallet/wallet.dart';
@@ -22,5 +23,9 @@ extension WalletDTOMapper on WalletDTO {
updatedAt: (updatedAt == null || updatedAt!.isEmpty) ? null : DateTime.tryParse(updatedAt!),
balance: balance?.toDomain(),
availableMoney: balance?.available?.toDomain(),
describable: newDescribable(
name: metadata?['name'] ?? 'Crypto Wallet',
description: metadata?['description'],
),
);
}

View File

@@ -0,0 +1,18 @@
import 'package:pshared/models/currency.dart';
import 'package:pshared/utils/currency.dart';
class Asset {
final Currency currency;
final double amount;
const Asset({
required this.currency,
required this.amount,
});
}
Asset createAsset(String currencyCode, String amount) => Asset(
currency: currencyStringToCode(currencyCode),
amount: double.parse(amount),
);

View File

@@ -0,0 +1,7 @@
enum AuthState {
idle,
checking,
ready,
empty,
error,
}

View File

@@ -79,12 +79,13 @@ enum ResourceType {
@JsonValue('payments')
payments,
@JsonValue('payment_methods')
paymentMethods,
/// Represents payment orchestration service
@JsonValue('payment_orchestrator')
paymentOrchestrator,
@JsonValue('payment_methods')
paymentMethods,
/// Represents permissions service
@JsonValue('permissions')
permissions,

View File

@@ -1,5 +1,6 @@
import 'package:pshared/models/wallet/balance.dart';
import 'package:pshared/models/wallet/money.dart';
import 'package:pshared/models/describable.dart';
class WalletAsset {
@@ -14,7 +15,7 @@ class WalletAsset {
});
}
class WalletModel {
class WalletModel implements Describable {
final String walletRef;
final String organizationRef;
final String ownerRef;
@@ -26,6 +27,13 @@ class WalletModel {
final DateTime? updatedAt;
final WalletBalance? balance;
final WalletMoney? availableMoney;
final Describable describable;
@override
String get name => describable.name;
@override
String? get description => describable.description;
const WalletModel({
required this.walletRef,
@@ -39,11 +47,13 @@ class WalletModel {
this.updatedAt,
this.balance,
this.availableMoney,
required this.describable,
});
WalletModel copyWith({
WalletBalance? balance,
WalletMoney? availableMoney,
Describable? describable,
}) {
return WalletModel(
walletRef: walletRef,
@@ -57,6 +67,7 @@ class WalletModel {
updatedAt: updatedAt,
balance: balance ?? this.balance,
availableMoney: availableMoney ?? this.availableMoney,
describable: describable ?? this.describable,
);
}
}

View File

@@ -1,4 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:pshared/models/auth/state.dart';
import 'package:share_plus/share_plus.dart';
@@ -20,8 +23,14 @@ import 'package:pshared/utils/exception.dart';
class AccountProvider extends ChangeNotifier {
AccountProvider();
static String get currentUserRef => Constants.nilObjectRef;
/// Auth lifecycle state to avoid multiple ad-hoc flags.
AuthState _authState = AuthState.idle;
AuthState get authState => _authState;
// The resource now wraps our Account? state along with its loading/error state.
Resource<Account?> _resource = Resource(data: null);
Resource<Account?> get resource => _resource;
@@ -52,9 +61,18 @@ class AccountProvider extends ChangeNotifier {
);
}
// Private helper to update the resource and notify listeners.
@protected
Future<void> onAccountChanged(Account? previous, Account? current) => Future<void>.value();
void _setResource(Resource<Account?> newResource) {
final previousAccount = _resource.data;
_resource = newResource;
final currentAccount = newResource.data;
if (previousAccount != currentAccount) {
unawaited(onAccountChanged(previousAccount, currentAccount));
}
notifyListeners();
}
@@ -75,6 +93,7 @@ class AccountProvider extends ChangeNotifier {
locale: locale,
));
if (outcome.account != null) {
_authState = AuthState.ready;
_setResource(Resource(data: outcome.account, isLoading: false));
_pickupLocale(outcome.account!.locale);
} else {
@@ -84,10 +103,12 @@ class AccountProvider extends ChangeNotifier {
}
await VerificationService.requestLoginCode(pending);
_pendingLogin = pending;
_authState = AuthState.idle;
_setResource(_resource.copyWith(isLoading: false));
}
return outcome;
} catch (e) {
_authState = AuthState.error;
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
rethrow;
}
@@ -95,6 +116,7 @@ class AccountProvider extends ChangeNotifier {
void completePendingLogin(Account account) {
_pendingLogin = null;
_authState = AuthState.ready;
_setResource(Resource(data: account, isLoading: false, error: null));
_pickupLocale(account.locale);
}
@@ -102,13 +124,17 @@ class AccountProvider extends ChangeNotifier {
Future<bool> isAuthorizationStored() async => AuthorizationService.isAuthorizationStored();
Future<Account?> restore() async {
_authState = AuthState.checking;
notifyListeners();
_setResource(_resource.copyWith(isLoading: true, error: null));
try {
final acc = await AccountService.restore();
_authState = AuthState.ready;
_setResource(Resource(data: acc, isLoading: false));
_pickupLocale(acc.locale);
return acc;
} catch (e) {
_authState = AuthState.error;
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
rethrow;
}
@@ -140,11 +166,14 @@ class AccountProvider extends ChangeNotifier {
}
Future<void> logout() async {
_authState = AuthState.empty;
_setResource(_resource.copyWith(isLoading: true, error: null));
_pendingLogin = null;
try {
await AccountService.logout();
_setResource(Resource(data: null, isLoading: false));
} catch (e) {
_authState = AuthState.error;
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
rethrow;
}
@@ -199,6 +228,19 @@ class AccountProvider extends ChangeNotifier {
}
}
Future<Account> resetUsername(String userName) async {
if (account == null) throw ErrorUnauthorized();
_setResource(_resource.copyWith(isLoading: true, error: null));
try {
final updated = await AccountService.resetUsername(account!, userName);
_setResource(Resource(data: updated, isLoading: false));
return updated;
} catch (e) {
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
rethrow;
}
}
Future<void> forgotPassword(String email) async {
_setResource(_resource.copyWith(isLoading: true, error: null));
try {
@@ -220,4 +262,15 @@ class AccountProvider extends ChangeNotifier {
rethrow;
}
}
Future<void> restoreIfPossible() async {
if (_authState == AuthState.checking || _authState == AuthState.ready) return;
final hasAuth = await AuthorizationService.isAuthorizationStored();
if (!hasAuth) {
_authState = AuthState.empty;
notifyListeners();
return;
}
await restore();
}
}

View File

@@ -80,4 +80,12 @@ class OrganizationsProvider extends ChangeNotifier {
notifyListeners();
return true;
}
Future<void> reset() async {
_resource = Resource(data: []);
_currentOrg = null;
notifyListeners();
// Best-effort cleanup of stored selection to avoid using stale org on next login.
await SecureStorageService.delete(Constants.currentOrgKey);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
class PaymentAmountProvider with ChangeNotifier {
double _amount = 10.0;
bool _payerCoversFee = true;
double get amount => _amount;
bool get payerCoversFee => _payerCoversFee;
void setAmount(double value) {
_amount = value;
notifyListeners();
}
void setPayerCoversFee(bool value) {
_payerCoversFee = value;
notifyListeners();
}
}

View File

@@ -1,5 +1,17 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/asset.dart';
import 'package:uuid/uuid.dart';
import 'package:pshared/models/payment/currency_pair.dart';
import 'package:pshared/models/payment/fx/intent.dart';
import 'package:pshared/models/payment/fx/side.dart';
import 'package:pshared/models/payment/kind.dart';
import 'package:pshared/models/payment/methods/card.dart';
import 'package:pshared/models/payment/methods/managed_wallet.dart';
import 'package:pshared/models/payment/money.dart';
import 'package:pshared/models/payment/settlement_mode.dart';
import 'package:pshared/provider/payment/amount.dart';
import 'package:pshared/api/requests/payment/quote.dart';
import 'package:pshared/data/mapper/payment/intent/payment.dart';
import 'package:pshared/models/payment/intent.dart';
@@ -7,7 +19,6 @@ import 'package:pshared/models/payment/quote.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/payment/quotation.dart';
import 'package:uuid/uuid.dart';
class QuotationProvider extends ChangeNotifier {
@@ -15,14 +26,46 @@ class QuotationProvider extends ChangeNotifier {
late OrganizationsProvider _organizations;
bool _isLoaded = false;
void update(OrganizationsProvider venue) {
void update(OrganizationsProvider venue, PaymentAmountProvider payment) {
_organizations = venue;
getQuotation(PaymentIntent(
kind: PaymentKind.payout,
amount: Money(
amount: payment.amount.toString(),
currency: 'USDT',
),
destination: CardPaymentMethod(
pan: '4000000000000077',
firstName: 'John',
lastName: 'Doe',
),
source: ManagedWalletPaymentMethod(
managedWalletRef: '',
),
fx: FxIntent(
pair: CurrencyPair(
base: 'USDT',
quote: 'RUB',
),
side: FxSide.sellBaseBuyQuote,
),
settlementMode: payment.payerCoversFee ? SettlementMode.fixReceived : SettlementMode.fixSource,
));
}
PaymentQuote? get quotation => _quotation.data;
bool get isReady => _isLoaded && !_quotation.isLoading && _quotation.error == null;
Asset? get fee => quotation == null ? null : createAsset(quotation!.expectedFeeTotal!.currency, quotation!.expectedFeeTotal!.amount);
Asset? get total => quotation == null ? null : createAsset(quotation!.debitAmount!.currency, quotation!.debitAmount!.amount);
Asset? get recipientGets => quotation == null ? null : createAsset(quotation!.expectedSettlementAmount!.currency, quotation!.expectedSettlementAmount!.amount);
void _setResource(Resource<PaymentQuote> quotation) {
_quotation = quotation;
notifyListeners();
}
Future<PaymentQuote?> getQuotation(PaymentIntent intent) async {
if (!_organizations.isOrganizationSet) throw StateError('Organization is not set');
try {
@@ -35,19 +78,20 @@ class QuotationProvider extends ChangeNotifier {
),
);
_isLoaded = true;
_quotation = _quotation.copyWith(data: response, isLoading: false);
_setResource(_quotation.copyWith(data: response, isLoading: false, error: null));
} catch (e) {
_quotation = _quotation.copyWith(
_setResource(_quotation.copyWith(
data: null,
error: e is Exception ? e : Exception(e.toString()),
isLoading: false,
);
));
}
notifyListeners();
return _quotation.data;
}
void reset() {
_quotation = Resource(data: null, isLoading: false, error: null);
_setResource(Resource(data: null, isLoading: false, error: null));
_isLoaded = false;
notifyListeners();
}

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
@@ -21,9 +23,17 @@ class PermissionsProvider extends ChangeNotifier {
Resource<UserAccess> _userAccess = Resource(data: null, isLoading: false, error: null);
late OrganizationsProvider _organizations;
bool _isLoaded = false;
String? _loadedOrgRef;
//For permissions to auto-load when an organization is set, so the dashboard no longer hangs waiting for permissions to become ready.
void update(OrganizationsProvider venue) {
_organizations = venue;
// Trigger a reload when organization changes or when permissions were never loaded.
if (_organizations.isOrganizationSet &&
_loadedOrgRef != _organizations.current.id &&
!_userAccess.isLoading) {
unawaited(load());
}
}
// Generic wrapper to perform service calls and reload state
@@ -42,7 +52,11 @@ class PermissionsProvider extends ChangeNotifier {
}
/// Load the [UserAccess] for the current venue.
Future<UserAccess?> load() async {
Future<UserAccess?> load() async {
if (!_organizations.isOrganizationSet) {
// Organization is not ready yet; skip loading until it becomes available.
return _userAccess.data;
}
_userAccess = _userAccess.copyWith(isLoading: true, error: null);
notifyListeners();
@@ -56,6 +70,7 @@ class PermissionsProvider extends ChangeNotifier {
_userAccess = _userAccess.copyWith(data: allAccess, isLoading: false);
}
_isLoaded = true;
_loadedOrgRef = orgRef;
} catch (e) {
_userAccess = _userAccess.copyWith(
error: e is Exception ? e : Exception(e.toString()),
@@ -164,9 +179,14 @@ class PermissionsProvider extends ChangeNotifier {
void reset() {
_userAccess = Resource(data: null, isLoading: false, error: null);
_isLoaded = false;
_loadedOrgRef = null;
notifyListeners();
}
Future<void> resetAsync() async {
reset();
}
bool canRead(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.read, objectRef: objectRef);
bool canUpdate(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.update, objectRef: objectRef);
bool canDelete(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.delete, objectRef: objectRef);

View File

@@ -1,5 +1,4 @@
import 'package:logging/logging.dart';
import 'package:share_plus/share_plus.dart';
import 'package:pshared/api/requests/signup.dart';
@@ -10,6 +9,7 @@ import 'package:pshared/api/requests/password/forgot.dart';
import 'package:pshared/api/requests/password/reset.dart';
import 'package:pshared/data/mapper/account/account.dart';
import 'package:pshared/models/account/account.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/auth/login_outcome.dart';
import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/service/files.dart';
@@ -61,6 +61,14 @@ class AccountService {
await getPOSTResponse(_objectType, 'password/reset/$accountRef/$token', ResetPasswordRequest.build(password: newPassword).toJson());
}
static Future<Account> resetUsername(Account account, String userName) async {
_logger.fine('Updating username for account: ${account.id}');
final updatedAccount = account.copyWith(
describable: account.describable.copyWith(name: userName),
);
return update(updatedAccount);
}
static Future<Account> changePassword(String oldPassword, String newPassword) async {
_logger.fine('Changing password');
return _getAccount(AuthorizationService.getPATCHResponse(

View File

@@ -4,8 +4,8 @@ import 'package:pshared/api/requests/payment/quote.dart';
import 'package:pshared/api/responses/payment/quotation.dart';
import 'package:pshared/data/mapper/payment/payment_quote.dart';
import 'package:pshared/models/payment/quote.dart';
import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/service/services.dart';
import 'package:pshared/utils/http/requests.dart';
class QuotationService {
@@ -14,7 +14,11 @@ class QuotationService {
static Future<PaymentQuote> getQuotation(String organizationRef, QuotePaymentRequest request) async {
_logger.fine('Quoting payment for organization $organizationRef');
final response = await getPOSTResponse(_objectType, '/quote/$organizationRef', request.toJson());
final response = await AuthorizationService.getPOSTResponse(
_objectType,
'/quote/$organizationRef',
request.toJson(),
);
return PaymentQuoteResponse.fromJson(response).quote.toDomain();
}
}

View File

@@ -1,22 +1,64 @@
String currencyCodeToSymbol(String currencyCode) {
import 'package:flutter/material.dart';
import 'package:pshared/models/asset.dart';
import 'package:pshared/models/currency.dart';
String currencyCodeToSymbol(Currency currencyCode) {
switch (currencyCode) {
case 'USD':
case Currency.usd:
return '\$';
case 'PLN':
return '';
case 'EUR':
return '';
case 'GBP':
return '£';
case 'HUF':
return 'Ft';
case 'RUB':
case Currency.usdt:
return '';
case Currency.usdc:
return '\$';
case Currency.rub:
return '';
default:
return currencyCode;
case Currency.eur:
return '';
}
}
String currencyToString(String currencyCode, double amount) {
return '${currencyCodeToSymbol(currencyCode)}\u00A0${amount.toStringAsFixed(2)}';
String amountToString(double amount) {
return amount.toStringAsFixed(2);
}
String currencyToString(Currency currencyCode, double amount) {
return '${currencyCodeToSymbol(currencyCode)}\u00A0${amountToString(amount)}';
}
String assetToString(Asset asset) {
return currencyToString(asset.currency, asset.amount);
}
Currency currencyStringToCode(String currencyCode) {
switch (currencyCode) {
case 'USD':
return Currency.usd;
case 'USDT':
return Currency.usdt;
case 'USDC':
return Currency.usdc;
case 'RUB':
return Currency.rub;
case 'EUR':
return Currency.eur;
default:
throw ArgumentError('Unknown currency code: $currencyCode');
}
}
IconData iconForCurrencyType(Currency currencyCode) {
switch (currencyCode) {
case Currency.usd:
return Icons.currency_exchange;
case Currency.eur:
return Icons.currency_exchange;
case Currency.rub:
return Icons.currency_ruble;
case Currency.usdt:
return Icons.currency_exchange;
case Currency.usdc:
return Icons.money;
}
}

View File

@@ -92,7 +92,7 @@ RouteBase payoutShellRoute() => ShellRoute(
pageBuilder: (context, _) {
final recipient = context.read<RecipientsProvider>().currentObject;
return NoTransitionPage(
child: AdressBookRecipientForm(
child: AddressBookRecipientForm(
recipient: recipient,
onSaved: (_) => context.goToPayout(PayoutDestination.recipients),
),

View File

@@ -1,6 +1,8 @@
import 'package:pshared/models/wallet/wallet.dart' as domain;
import 'package:pweb/models/currency.dart';
import 'package:pshared/models/currency.dart';
import 'package:pshared/models/describable.dart';
import 'package:pweb/models/wallet.dart';
@@ -16,7 +18,6 @@ extension WalletUiMapper on domain.WalletModel {
return Wallet(
id: walletRef,
walletUserID: walletRef,
name: metadata?['name'] ?? walletRef,
balance: parsedAmount,
currency: currency,
isHidden: true,
@@ -25,6 +26,10 @@ extension WalletUiMapper on domain.WalletModel {
network: asset.chain,
tokenSymbol: asset.tokenSymbol,
contractAddress: asset.contractAddress,
describable: newDescribable(
name: metadata?['name'] ?? 'Crypto Wallet',
description: metadata?['description'],
),
);
}
}

View File

@@ -9,7 +9,13 @@
"usernameErrorInvalid": "Provide a valid email address",
"usernameUnknownTLD": "Domain .{domain} is not known, please, check it",
"password": "Password",
"oldPassword": "Current password",
"newPassword": "New password",
"confirmPassword": "Confirm password",
"changePassword": "Change password",
"savePassword": "Save changed password",
"changePasswordSuccess": "Password updated",
"changePasswordError": "Could not update password",
"passwordValidationRuleDigit": "has digit",
"passwordValidationRuleUpperCase": "has uppercase letter",
"passwordValidationRuleLowerCase": "has lowercase letter",
@@ -311,6 +317,7 @@
"paymentTypeBankAccount": "Russian Bank Account",
"paymentTypeIban": "IBAN",
"paymentTypeWallet": "Wallet",
"paymentTypeCryptoWallet": "Crypto Wallet",
"paymentTypeCryptoAddress": "Crypto address",
"paymentTypeLedger": "Ledger account",
"paymentTypeManagedWallet": "Managed wallet",
@@ -378,7 +385,7 @@
"send": "Send Payout",
"recipientPaysFee": "Recipient pays the fee",
"sentAmount": "Sent amount: ${amount}",
"sentAmount": "Sent amount: {amount}",
"@sentAmount": {
"description": "Label showing the amount sent",
"placeholders": {
@@ -388,7 +395,7 @@
}
},
"fee": "Fee: ${fee}",
"fee": "Fee: {fee}",
"@fee": {
"description": "Label showing the transaction fee",
"placeholders": {
@@ -398,7 +405,7 @@
}
},
"recipientWillReceive": "Recipient will receive: ${amount}",
"recipientWillReceive": "Recipient will receive: {amount}",
"@recipientWillReceive": {
"description": "Label showing how much the recipient will receive",
"placeholders": {
@@ -408,7 +415,7 @@
}
},
"total": "Total: ${total}",
"total": "Total: {total}",
"@total": {
"description": "Label showing the total amount of the transaction",
"placeholders": {
@@ -425,9 +432,13 @@
"addRecipient": "Add Recipient",
"editRecipient": "Edit Recipient",
"saveRecipient": "Save Recipient",
"choosePaymentMethod": "Payment Methods (choose at least 1)",
"recipientFormRule": "Recipient must have at least one payment method",
"recipientFormValidationError": "Fill in the required fields to save the recipient",
"@recipientFormValidationError": {
"description": "Error message shown when recipient form fields are invalid or missing"
},
"allStatus": "All",
"readyStatus": "Ready",
@@ -443,6 +454,10 @@
"@errorSaveRecipient": {
"description": "Error message displayed when saving a recipient fails"
},
"recipientSavedSuccessfully": "Recipient saved successfully",
"@recipientSavedSuccessfully": {
"description": "Success message displayed when a recipient is saved"
},
"recipientDeletedSuccessfully": "Recipient deleted successfully",
"@recipientDeletedSuccessfully": {
"description": "Success message displayed when a recipient is deleted"

View File

@@ -9,7 +9,13 @@
"usernameErrorInvalid": "Укажите действительный адрес электронной почты",
"usernameUnknownTLD": "Домен .{domain} неизвестен, пожалуйста, проверьте его",
"password": "Пароль",
"oldPassword": "Текущий пароль",
"newPassword": "Новый пароль",
"confirmPassword": "Подтвердите пароль",
"changePassword": "Изменить пароль",
"savePassword": "Сохранить пароль",
"changePasswordSuccess": "Пароль обновлен",
"changePasswordError": "Не удалось обновить пароль",
"passwordValidationRuleDigit": "содержит цифру",
"passwordValidationRuleUpperCase": "содержит заглавную букву",
"passwordValidationRuleLowerCase": "содержит строчную букву",
@@ -311,6 +317,7 @@
"paymentTypeBankAccount": "Российский банковский счет",
"paymentTypeIban": "IBAN",
"paymentTypeWallet": "Кошелек",
"paymentTypeCryptoWallet": "Криптокошелек",
"paymentTypeCryptoAddress": "Крипто-адрес",
"paymentTypeLedger": "Леджер счет",
"paymentTypeManagedWallet": "Управляемый кошелек",
@@ -378,7 +385,7 @@
"send": "Отправить выплату",
"recipientPaysFee": "Получатель оплачивает комиссию",
"sentAmount": "Отправленная сумма: ${amount}",
"sentAmount": "Отправленная сумма: {amount}",
"@sentAmount": {
"description": "Метка, показывающая отправленную сумму",
"placeholders": {
@@ -388,7 +395,7 @@
}
},
"fee": "Комиссия: ${fee}",
"fee": "Комиссия: {fee}",
"@fee": {
"description": "Метка, показывающая комиссию за транзакцию",
"placeholders": {
@@ -398,7 +405,7 @@
}
},
"recipientWillReceive": "Получатель получит: ${amount}",
"recipientWillReceive": "Получатель получит: {amount}",
"@recipientWillReceive": {
"description": "Метка, показывающая, сколько получит получатель",
"placeholders": {
@@ -408,7 +415,7 @@
}
},
"total": "Итого: ${total}",
"total": "Итого: {total}",
"@total": {
"description": "Метка, показывающая общую сумму транзакции",
"placeholders": {
@@ -425,9 +432,13 @@
"addRecipient": "Добавить получателя",
"editRecipient": "Редактировать получателя",
"saveRecipient": "Сохранить получателя",
"choosePaymentMethod": "Способы оплаты (выберите хотя бы 1)",
"recipientFormRule": "Получатель должен иметь хотя бы один способ оплаты",
"recipientFormValidationError": "Заполните обязательные поля, чтобы сохранить получателя",
"@recipientFormValidationError": {
"description": "Сообщение об ошибке, если обязательные поля формы получателя не заполнены"
},
"allStatus": "Все",
"readyStatus": "Готов",
@@ -443,6 +454,10 @@
"@errorSaveRecipient": {
"description": "Сообщение об ошибке при неудачном сохранении получателя"
},
"recipientSavedSuccessfully": "Получатель успешно сохранен",
"@recipientSavedSuccessfully": {
"description": "Сообщение об успешном сохранении получателя"
},
"recipientDeletedSuccessfully": "Получатель успешно удален",
"@recipientDeletedSuccessfully": {
"description": "Сообщение об успешном удалении получателя"

View File

@@ -12,6 +12,7 @@ import 'package:pshared/provider/locale.dart';
import 'package:pshared/provider/permissions.dart';
import 'package:pshared/provider/account.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/payment/amount.dart';
import 'package:pshared/provider/payment/quotation.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
@@ -27,8 +28,10 @@ import 'package:pweb/providers/wallets.dart';
import 'package:pweb/providers/wallet_transactions.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';
import 'package:pweb/providers/account.dart';
void _setupLogging() {
@@ -40,11 +43,9 @@ void _setupLogging() {
}
void main() async {
await Constants.initialize();
WidgetsFlutterBinding.ensureInitialized();
// await AmplitudeService.initialize();
await Constants.initialize();
await PosthogService.initialize();
_setupLogging();
@@ -57,7 +58,7 @@ void main() async {
providers: [
ChangeNotifierProvider(create: (_) => LocaleProvider(null)),
ChangeNotifierProxyProvider<LocaleProvider, AccountProvider>(
create: (_) => AccountProvider(),
create: (_) => PwebAccountProvider(),
update: (context, localeProvider, provider) => provider!..updateProvider(localeProvider),
),
ChangeNotifierProxyProvider<AccountProvider, TwoFactorProvider>(
@@ -70,6 +71,7 @@ void main() async {
update: (context, orgnization, provider) => provider!..update(orgnization),
),
ChangeNotifierProvider(create: (_) => CarouselIndexProvider()),
ChangeNotifierProvider(
create: (_) => UploadHistoryProvider(service: MockUploadHistoryService())..load(),
),
@@ -91,12 +93,16 @@ void main() async {
ChangeNotifierProvider(
create: (_) => MockPaymentProvider(),
),
ChangeNotifierProvider(
create: (_) => OperationProvider(OperationService())..loadOperations(),
),
ChangeNotifierProxyProvider<OrganizationsProvider, QuotationProvider>(
ChangeNotifierProvider(
create: (_) => PaymentAmountProvider(),
),
ChangeNotifierProxyProvider2<OrganizationsProvider, PaymentAmountProvider, QuotationProvider>(
create: (_) => QuotationProvider(),
update: (context, orgnization, provider) => provider!..update(orgnization),
update: (context, orgnization, payment, provider) => provider!..update(orgnization, payment),
),
],
child: const PayApp(),

View File

@@ -0,0 +1 @@
enum EditState { view, edit, saving }

View File

@@ -1,10 +1,10 @@
import 'package:pweb/models/currency.dart';
import 'package:pshared/models/currency.dart';
import 'package:pshared/models/describable.dart';
class Wallet {
class Wallet implements Describable {
final String id;
final String walletUserID; // ID or number that we show the user
final String name;
final double balance;
final Currency currency;
final bool isHidden;
@@ -13,14 +13,21 @@ class Wallet {
final String? network;
final String? tokenSymbol;
final String? contractAddress;
final Describable describable;
@override
String get name => describable.name;
@override
String? get description => describable.description;
Wallet({
required this.id,
required this.walletUserID,
required this.name,
required this.balance,
required this.currency,
required this.calculatedAt,
required this.describable,
this.isHidden = true,
this.depositAddress,
this.network,
@@ -30,7 +37,6 @@ class Wallet {
Wallet copyWith({
String? id,
String? name,
double? balance,
Currency? currency,
String? walletUserID,
@@ -39,9 +45,11 @@ class Wallet {
String? network,
String? tokenSymbol,
String? contractAddress,
Describable? describable,
String? name,
String? Function()? description,
}) => Wallet(
id: id ?? this.id,
name: name ?? this.name,
balance: balance ?? this.balance,
currency: currency ?? this.currency,
walletUserID: walletUserID ?? this.walletUserID,
@@ -51,5 +59,9 @@ class Wallet {
network: network ?? this.network,
tokenSymbol: tokenSymbol ?? this.tokenSymbol,
contractAddress: contractAddress ?? this.contractAddress,
describable: describable
?? (name != null || description != null
? this.describable.copyWith(name: name, description: description)
: this.describable),
);
}

View File

@@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart';
import 'package:pshared/models/payment/status.dart';
import 'package:pweb/models/currency.dart';
import 'package:pshared/models/currency.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';

View File

@@ -7,7 +7,7 @@ import 'package:pweb/pages/payment_methods/form.dart';
import 'package:pweb/pages/payment_methods/icon.dart';
class AdressBookPaymentMethodTile extends StatefulWidget {
class AddressBookPaymentMethodTile extends StatefulWidget {
final PaymentType type;
final String title;
final MethodMap methods;
@@ -18,7 +18,7 @@ class AdressBookPaymentMethodTile extends StatefulWidget {
final double sizeM;
final TextStyle? titleTextStyle;
const AdressBookPaymentMethodTile({
const AddressBookPaymentMethodTile({
super.key,
required this.type,
required this.title,
@@ -31,10 +31,10 @@ class AdressBookPaymentMethodTile extends StatefulWidget {
});
@override
State<AdressBookPaymentMethodTile> createState() => _AdressBookPaymentMethodTileState();
State<AddressBookPaymentMethodTile> createState() => _AddressBookPaymentMethodTileState();
}
class _AdressBookPaymentMethodTileState extends State<AdressBookPaymentMethodTile> {
class _AddressBookPaymentMethodTileState extends State<AddressBookPaymentMethodTile> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
@@ -14,6 +16,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/posthog.dart';
import 'package:pweb/utils/error/snackbar.dart';
import 'package:pweb/utils/payment/label.dart';
import 'package:pweb/utils/snackbar.dart';
@@ -21,17 +24,17 @@ import 'package:pweb/utils/snackbar.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class AdressBookRecipientForm extends StatefulWidget {
class AddressBookRecipientForm extends StatefulWidget {
final Recipient? recipient;
final ValueChanged<Recipient?>? onSaved;
const AdressBookRecipientForm({super.key, this.recipient, this.onSaved});
const AddressBookRecipientForm({super.key, this.recipient, this.onSaved});
@override
State<AdressBookRecipientForm> createState() => _AdressBookRecipientFormState();
State<AddressBookRecipientForm> createState() => _AddressBookRecipientFormState();
}
class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
class _AddressBookRecipientFormState extends State<AddressBookRecipientForm> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nameCtrl;
late TextEditingController _emailCtrl;
@@ -101,21 +104,28 @@ class _AdressBookRecipientFormState extends State<AdressBookRecipientForm> {
//TODO: Change when registration is ready
Future<void> _save() async {
if (!_formKey.currentState!.validate() || _methods.isEmpty) {
notifyUser(context, AppLocalizations.of(context)!.errorSaveRecipient);
final l10n = AppLocalizations.of(context)!;
if (!_formKey.currentState!.validate()) {
notifyUser(context, l10n.recipientFormValidationError);
return;
}
// AmplitudeService.recipientAddCompleted(
// _type,
// _status,
// _methods.keys.toSet(),
// );
if (_methods.isEmpty) {
notifyUser(context, l10n.recipientFormRule);
return;
}
unawaited(PosthogService.recipientAddCompleted(
_type,
_status,
_methods.keys.toSet(),
));
final recipient = await executeActionWithNotification(
context: context,
action: _doSave,
errorMessage: AppLocalizations.of(context)!.errorSaveRecipient,
successMessage: AppLocalizations.of(context)!.recipientFormRule,
errorMessage: l10n.errorSaveRecipient,
successMessage: l10n.recipientSavedSuccessfully,
);

View File

@@ -103,7 +103,7 @@ class FormView extends StatelessWidget {
),
SizedBox(height: spacingFields),
...PaymentType.values.map(
(p) => AdressBookPaymentMethodTile(
(p) => AddressBookPaymentMethodTile(
type: p,
title: getPaymentTypeLabel(context, p),
methods: methods,

View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
class RecipientAddressBookEmptyState extends StatelessWidget {
final String message;
final String actionLabel;
final VoidCallback onAction;
const RecipientAddressBookEmptyState({
required this.message,
required this.actionLabel,
required this.onAction,
});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.contact_mail_outlined, size: 42),
const SizedBox(height: 12),
Text(message, style: textTheme.titleMedium),
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: onAction,
icon: const Icon(Icons.add),
label: Text(actionLabel),
),
],
),
);
}
}

View File

@@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/models/recipient/filter.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/address_book/page/empty.dart';
import 'package:pweb/pages/address_book/page/filter_button.dart';
import 'package:pweb/pages/address_book/page/header.dart';
@@ -72,6 +73,7 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
final loc = AppLocalizations.of(context)!;
final provider = context.watch<RecipientsProvider>();
_syncSearchField(provider);
final filteredRecipients = provider.filteredRecipients;
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
@@ -124,15 +126,23 @@ class _RecipientAddressBookPageState extends State<RecipientAddressBookPage> {
height: RecipientAddressBookPage._expandedHeight,
child: Padding(
padding: const EdgeInsets.all(RecipientAddressBookPage._paddingAll),
child: RecipientAddressBookList(
filteredRecipients: provider.filteredRecipients,
onEdit: (recipient) => widget.onEditRecipient?.call(recipient),
onDelete: (recipient) => widget.onDeleteRecipient?.call(recipient),
onSelected: widget.onRecipientSelected,
),
child: provider.recipients.isEmpty
? RecipientAddressBookEmptyState(
message: loc.noRecipientsYet,
actionLabel: loc.addRecipient,
onAction: widget.onAddRecipient,
)
: filteredRecipients.isEmpty
? Center(child: Text(loc.noRecipientsFound))
: RecipientAddressBookList(
filteredRecipients: filteredRecipients,
onEdit: (recipient) => widget.onEditRecipient?.call(recipient),
onDelete: (recipient) => widget.onDeleteRecipient?.call(recipient),
onSelected: widget.onRecipientSelected,
),
),
),
],
);
}
}
}

View File

@@ -6,7 +6,7 @@ import 'package:pweb/pages/address_book/page/recipient/actions.dart';
import 'package:pweb/pages/address_book/page/recipient/info_column.dart';
import 'package:pweb/pages/address_book/page/recipient/payment_row.dart';
import 'package:pweb/pages/address_book/page/recipient/status.dart';
import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart';
import 'package:pweb/pages/dashboard/payouts/single/address_book/avatar.dart';
class RecipientAddressBookItem extends StatefulWidget {

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pweb/models/wallet.dart';
import 'package:pweb/utils/currency.dart';
class BalanceAmount extends StatelessWidget {
@@ -25,7 +27,7 @@ class BalanceAmount extends StatelessWidget {
return Row(
children: [
Text(
wallet.isHidden ? '•••• $currencyBalance' : '${wallet.balance.toStringAsFixed(2)} $currencyBalance',
wallet.isHidden ? '•••• $currencyBalance' : '${amountToString(wallet.balance)} $currencyBalance',
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,

View File

@@ -34,8 +34,8 @@ class WalletCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BalanceHeader(
walletName: wallet.name,
walletId: wallet.walletUserID,
walletNetwork: wallet.network,
tokenSymbol: wallet.tokenSymbol,
),
BalanceAmount(
wallet: wallet,

View File

@@ -2,37 +2,46 @@ import 'package:flutter/material.dart';
class BalanceHeader extends StatelessWidget {
final String walletName;
final String walletId;
final String? walletNetwork;
final String? tokenSymbol;
const BalanceHeader({
super.key,
required this.walletName,
required this.walletId,
this.walletNetwork,
this.tokenSymbol,
});
static const double _spacing = 8.0;
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
final colorScheme = Theme.of(context).colorScheme;
final symbol = tokenSymbol?.trim();
return Row(
children: [
Text(
walletName,
'Crypto Wallet',
style: textTheme.titleMedium?.copyWith(
color: colorScheme.onSurface,
),
),
const SizedBox(width: _spacing),
Text(
walletId,
style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurface,
if (symbol != null && symbol.isNotEmpty) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(999),
),
child: Text(
symbol,
style: textTheme.bodyMedium?.copyWith(
color: colorScheme.onPrimaryContainer,
fontWeight: FontWeight.w600,
),
),
),
),
],
],
);
}

View File

@@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/payment/amount.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentAmountWidget extends StatefulWidget {
const PaymentAmountWidget({super.key});
@override
State<PaymentAmountWidget> createState() => _PaymentAmountWidgetState();
}
class _PaymentAmountWidgetState extends State<PaymentAmountWidget> {
late final TextEditingController _controller;
bool _isSyncingText = false;
@override
void initState() {
super.initState();
final initialAmount = context.read<PaymentAmountProvider>().amount;
_controller = TextEditingController(text: amountToString(initialAmount));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
double? _parseAmount(String value) => double.tryParse(value.replaceAll(',', '.'));
void _syncTextWithAmount(double amount) {
final parsedText = _parseAmount(_controller.text);
if (parsedText != null && parsedText == amount) return;
final nextText = amountToString(amount);
_isSyncingText = true;
_controller.value = TextEditingValue(
text: nextText,
selection: TextSelection.collapsed(offset: nextText.length),
);
_isSyncingText = false;
}
void _onChanged(String value) {
if (_isSyncingText) return;
final parsed = _parseAmount(value);
if (parsed != null) {
context.read<PaymentAmountProvider>().setAmount(parsed);
}
}
@override
Widget build(BuildContext context) {
final amount = context.select<PaymentAmountProvider, double>((provider) => provider.amount);
_syncTextWithAmount(amount);
return TextField(
controller: _controller,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.amount,
border: const OutlineInputBorder(),
),
onChanged: _onChanged,
);
}
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/payment/amount.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class FeePayerSwitch extends StatelessWidget {
final double spacing;
final TextStyle? style;
const FeePayerSwitch({super.key, required this.spacing, TextStyle? this.style});
@override
Widget build(BuildContext context) => Consumer<PaymentAmountProvider>(
builder: (context, provider, _) => Row(
spacing: spacing,
children: [
Text(AppLocalizations.of(context)!.recipientPaysFee, style: style),
Switch(
value: !provider.payerCoversFee,
onChanged: (val) => provider.setPayerCoversFee(!val),
),
],
),
);
}

View File

@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:pweb/pages/dashboard/payouts/amount.dart';
import 'package:pweb/pages/dashboard/payouts/fee_payer.dart';
import 'package:pweb/pages/dashboard/payouts/summary/widget.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentFormWidget extends StatelessWidget {
const PaymentFormWidget({super.key});
static const double _smallSpacing = 5;
static const double _mediumSpacing = 10;
static const double _largeSpacing = 16;
static const double _extraSpacing = 15;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final loc = AppLocalizations.of(context)!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(loc.details, style: theme.textTheme.titleMedium),
const SizedBox(height: _smallSpacing),
const PaymentAmountWidget(),
const SizedBox(height: _mediumSpacing),
FeePayerSwitch(spacing: _mediumSpacing, style: theme.textTheme.titleMedium),
const SizedBox(height: _largeSpacing),
const PaymentSummary(spacing: _extraSpacing),
],
);
}
}

View File

@@ -1,92 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pweb/providers/mock_payment.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentFormWidget extends StatelessWidget {
const PaymentFormWidget({super.key});
static const double _smallSpacing = 5;
static const double _mediumSpacing = 10;
static const double _largeSpacing = 16;
static const double _extraSpacing = 15;
String _formatAmount(double amount) => amount.toStringAsFixed(2);
@override
Widget build(BuildContext context) {
final provider = Provider.of<MockPaymentProvider>(context);
final theme = Theme.of(context);
final loc = AppLocalizations.of(context)!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(loc.details, style: theme.textTheme.titleMedium),
const SizedBox(height: _smallSpacing),
TextField(
keyboardType: const TextInputType.numberWithOptions(decimal: true),
decoration: InputDecoration(
labelText: loc.amount,
border: const OutlineInputBorder(),
),
onChanged: (val) {
final parsed = double.tryParse(val.replaceAll(',', '.')) ?? 0.0;
provider.setAmount(parsed);
},
),
const SizedBox(height: _mediumSpacing),
Row(
spacing: _mediumSpacing,
children: [
Text(loc.recipientPaysFee, style: theme.textTheme.titleMedium),
Switch(
value: !provider.payerCoversFee,
onChanged: (val) => provider.setPayerCoversFee(!val),
),
],
),
const SizedBox(height: _largeSpacing),
Align(
alignment: Alignment.center,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_SummaryRow(label: loc.sentAmount(_formatAmount(provider.amount)), style: theme.textTheme.titleMedium),
_SummaryRow(label: loc.fee(_formatAmount(provider.fee)), style: theme.textTheme.titleMedium),
_SummaryRow(label: loc.recipientWillReceive(_formatAmount(provider.recipientGets)), style: theme.textTheme.titleMedium),
const SizedBox(height: _extraSpacing),
_SummaryRow(
label: loc.total(_formatAmount(provider.total)),
style: theme.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600),
),
],
),
),
],
);
}
}
class _SummaryRow extends StatelessWidget {
final String label;
final TextStyle? style;
const _SummaryRow({required this.label, this.style});
@override
Widget build(BuildContext context) {
return Text(label, style: style);
}
}

View File

@@ -6,8 +6,8 @@ import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/recipient/pmethods.dart';
import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart';
import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/info_row.dart';
import 'package:pweb/pages/dashboard/payouts/single/address_book/avatar.dart';
import 'package:pweb/pages/dashboard/payouts/single/address_book/long_list/info_row.dart';
import 'package:pweb/utils/payment/label.dart';

View File

@@ -2,14 +2,14 @@ import 'package:flutter/material.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/item.dart';
import 'package:pweb/pages/dashboard/payouts/single/address_book/long_list/item.dart';
class LongListAdressBookPayout extends StatelessWidget {
class LongListAddressBookPayout extends StatelessWidget {
final List<Recipient> filteredRecipients;
final ValueChanged<Recipient>? onSelected;
const LongListAdressBookPayout({
const LongListAddressBookPayout({
super.key,
required this.filteredRecipients,
this.onSelected,

View File

@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
class AddressBookPlaceholder extends StatelessWidget {
final String text;
const AddressBookPlaceholder({required this.text});
@override
Widget build(BuildContext context) => Center(
child: Text(
text,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
);
}

View File

@@ -1,14 +1,14 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart';
import 'package:pweb/pages/dashboard/payouts/single/address_book/avatar.dart';
class ShortListAdressBookPayout extends StatelessWidget {
class ShortListAddressBookPayout extends StatelessWidget {
final List<Recipient> recipients;
final ValueChanged<Recipient> onSelected;
const ShortListAdressBookPayout({
const ShortListAddressBookPayout({
super.key,
required this.recipients,
required this.onSelected,

View File

@@ -6,25 +6,26 @@ import 'package:pshared/models/recipient/recipient.dart';
import 'package:pshared/provider/recipient/provider.dart';
import 'package:pweb/pages/address_book/page/search.dart';
import 'package:pweb/pages/dashboard/payouts/single/adress_book/long_list/widget.dart';
import 'package:pweb/pages/dashboard/payouts/single/adress_book/short_list.dart';
import 'package:pweb/pages/dashboard/payouts/single/address_book/long_list/widget.dart';
import 'package:pweb/pages/dashboard/payouts/single/address_book/placeholder.dart';
import 'package:pweb/pages/dashboard/payouts/single/address_book/short_list.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class AdressBookPayout extends StatefulWidget {
class AddressBookPayout extends StatefulWidget {
final ValueChanged<Recipient> onSelected;
const AdressBookPayout({
const AddressBookPayout({
super.key,
required this.onSelected,
});
@override
State<AdressBookPayout> createState() => _AdressBookPayoutState();
State<AddressBookPayout> createState() => _AddressBookPayoutState();
}
class _AdressBookPayoutState extends State<AdressBookPayout> {
class _AddressBookPayoutState extends State<AddressBookPayout> {
static const double _expandedHeight = 400;
static const double _collapsedHeight = 200;
static const double _cardMargin = 1;
@@ -58,6 +59,8 @@ class _AdressBookPayoutState extends State<AdressBookPayout> {
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context)!;
final provider = context.watch<RecipientsProvider>();
final recipients = provider.recipients;
final filteredRecipients = provider.filteredRecipients;
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
@@ -87,15 +90,19 @@ class _AdressBookPayoutState extends State<AdressBookPayout> {
),
const SizedBox(height: _spacingBetween),
Expanded(
child: _isExpanded
? LongListAdressBookPayout(
filteredRecipients: provider.filteredRecipients,
onSelected: widget.onSelected,
)
: ShortListAdressBookPayout(
recipients: provider.recipients,
onSelected: widget.onSelected,
),
child: recipients.isEmpty
? AddressBookPlaceholder(text: loc.noRecipientsYet)
: _isExpanded && filteredRecipients.isEmpty
? AddressBookPlaceholder(text: loc.noRecipientsFound)
: _isExpanded
? LongListAddressBookPayout(
filteredRecipients: filteredRecipients,
onSelected: widget.onSelected,
)
: ShortListAddressBookPayout(
recipients: recipients,
onSelected: widget.onSelected,
),
),
],
),
@@ -103,4 +110,4 @@ class _AdressBookPayoutState extends State<AdressBookPayout> {
),
);
}
}
}

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pweb/pages/dashboard/payouts/single/adress_book/avatar.dart';
import 'package:pweb/pages/dashboard/payouts/single/address_book/avatar.dart';
class RecipientHeader extends StatelessWidget{

View File

@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:pshared/models/payment/type.dart';
import 'package:pshared/models/recipient/recipient.dart';
import 'package:pweb/pages/dashboard/payouts/single/adress_book/widget.dart';
import 'package:pweb/pages/dashboard/payouts/single/address_book/widget.dart';
import 'package:pweb/pages/dashboard/payouts/single/new_recipient/payout.dart';
@@ -25,7 +25,7 @@ class SinglePayoutForm extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AdressBookPayout(onSelected: onRecipientSelected),
AddressBookPayout(onSelected: onRecipientSelected),
const SizedBox(height: _spacingBetweenAddressAndForm),
SinglePayout(onGoToPayment: onGoToPayment),
const SizedBox(height: _bottomSpacing),

View File

@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/payment/quotation.dart';
import 'package:pweb/pages/dashboard/payouts/summary/row.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentFeeRow extends StatelessWidget {
const PaymentFeeRow({super.key});
@override
Widget build(BuildContext context) => Consumer<QuotationProvider>(
builder: (context, provider, _) => PaymentSummaryRow(
labelFactory: AppLocalizations.of(context)!.fee,
asset: provider.fee,
style: Theme.of(context).textTheme.titleMedium,
),
);
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/payment/quotation.dart';
import 'package:pweb/pages/dashboard/payouts/summary/row.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentRecipientReceivesRow extends StatelessWidget {
const PaymentRecipientReceivesRow({super.key});
@override
Widget build(BuildContext context) => Consumer<QuotationProvider>(
builder: (context, provider, _) => PaymentSummaryRow(
labelFactory: AppLocalizations.of(context)!.recipientWillReceive,
asset: provider.recipientGets,
style: Theme.of(context).textTheme.titleMedium,
),
);
}

View File

@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:pshared/models/asset.dart';
import 'package:pshared/utils/currency.dart';
class PaymentSummaryRow extends StatelessWidget {
final String Function(String) labelFactory;
final Asset? asset;
final TextStyle? style;
const PaymentSummaryRow({
super.key,
required this.labelFactory,
required this.asset,
this.style,
});
@override
Widget build(BuildContext context) => Text(
labelFactory(asset == null ? 'N/A' : assetToString(asset!)),
style: style,
);
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/asset.dart';
import 'package:pshared/models/currency.dart';
import 'package:pshared/provider/payment/amount.dart';
import 'package:pweb/pages/dashboard/payouts/summary/row.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentSentAmountRow extends StatelessWidget {
final Currency currency;
const PaymentSentAmountRow({super.key, required this.currency});
@override
Widget build(BuildContext context) => Consumer<PaymentAmountProvider>(
builder: (context, provider, _) => PaymentSummaryRow(
labelFactory: AppLocalizations.of(context)!.sentAmount,
asset: Asset(currency: currency, amount: provider.amount),
style: Theme.of(context).textTheme.titleMedium,
),
);
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/payment/quotation.dart';
import 'package:pweb/pages/dashboard/payouts/summary/row.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class PaymentTotalRow extends StatelessWidget {
const PaymentTotalRow({super.key});
@override
Widget build(BuildContext context) => Consumer<QuotationProvider>(
builder: (context, provider, _) => PaymentSummaryRow(
labelFactory: AppLocalizations.of(context)!.total,
asset: provider.total,
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.w600),
),
);
}

View File

@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/utils/currency.dart';
import 'package:pweb/pages/dashboard/payouts/summary/fee.dart';
import 'package:pweb/pages/dashboard/payouts/summary/recipient_receives.dart';
import 'package:pweb/pages/dashboard/payouts/summary/sent_amount.dart';
import 'package:pweb/pages/dashboard/payouts/summary/total.dart';
import 'package:pweb/providers/wallets.dart';
class PaymentSummary extends StatelessWidget {
final double spacing;
const PaymentSummary({super.key, required this.spacing});
@override
Widget build(BuildContext context) => Align(
alignment: Alignment.center,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
PaymentSentAmountRow(currency: currencyStringToCode(context.read<WalletsProvider>().selectedWallet?.tokenSymbol ?? 'USDT')),
const PaymentFeeRow(),
const PaymentRecipientReceivesRow(),
SizedBox(height: spacing),
const PaymentTotalRow(),
],
),
);
}

View File

@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pshared/provider/payment/amount.dart';
import 'package:pshared/provider/payment/quotation.dart';
import 'package:pweb/pages/dashboard/payouts/form.dart';
class PaymentFromWrappingWidget extends StatelessWidget {
const PaymentFromWrappingWidget({super.key});
@override
Widget build(BuildContext context) => MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => PaymentAmountProvider(),
),
ChangeNotifierProxyProvider2<OrganizationsProvider, PaymentAmountProvider, QuotationProvider>(
create: (_) => QuotationProvider(),
update: (context, orgnization, payment, provider) => provider!..update(orgnization, payment),
),
],
child: const PaymentFormWidget(),
);
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pshared/models/auth/state.dart';
import 'package:pshared/provider/account.dart';
import 'package:pweb/app/router/pages.dart';
@@ -10,26 +11,64 @@ import 'package:pweb/widgets/error/snackbar.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
class AccountLoader extends StatelessWidget {
class AccountLoader extends StatefulWidget {
final Widget child;
const AccountLoader({super.key, required this.child});
@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!,
);
State<AccountLoader> createState() => _AccountLoaderState();
}
class _AccountLoaderState extends State<AccountLoader> {
AuthState? _handledState;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
Provider.of<AccountProvider>(context, listen: false).restoreIfPossible();
});
}
void _handleSideEffects(AccountProvider provider) {
if (_handledState == provider.authState) return;
_handledState = provider.authState;
void goToLogin() {
if (!mounted) return;
navigateAndReplace(context, Pages.login);
}
if (provider.account == null) {
WidgetsBinding.instance.addPostFrameCallback((_) => navigateAndReplace(context, Pages.login));
return const Center(child: CircularProgressIndicator());
switch (provider.authState) {
case AuthState.error:
final error = provider.error ?? Exception('Authorization failed');
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
postNotifyUserOfErrorX(
context: context,
errorSituation: AppLocalizations.of(context)!.errorLogin,
exception: error,
);
goToLogin();
});
break;
case AuthState.empty:
WidgetsBinding.instance.addPostFrameCallback((_) => goToLogin());
break;
default:
break;
}
return child;
});
}
@override
Widget build(BuildContext context) {
return Consumer<AccountProvider>(builder: (context, provider, _) {
_handleSideEffects(provider);
if (provider.authState == AuthState.ready && provider.account != null) {
return widget.child;
}
return const Center(child: CircularProgressIndicator());
});
}
}

View File

@@ -4,9 +4,6 @@ import 'package:provider/provider.dart';
import 'package:pshared/provider/organizations.dart';
import 'package:pweb/app/router/pages.dart';
import 'package:pweb/widgets/error/snackbar.dart';
import 'package:pweb/generated/i18n/app_localizations.dart';
@@ -19,12 +16,20 @@ class OrganizationLoader extends StatelessWidget {
Widget build(BuildContext context) => Consumer<OrganizationsProvider>(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!,
final loc = AppLocalizations.of(context)!;
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(loc.errorLogin),
const SizedBox(height: 12),
ElevatedButton(
onPressed: provider.load,
child: Text(loc.retry),
),
],
),
);
navigateAndReplace(context, Pages.login);
}
if ((provider.error == null) && (!provider.isOrganizationSet)) {
WidgetsBinding.instance.addPostFrameCallback((_) {

Some files were not shown because too many files have changed in this diff Show More