idempotency key usage fix
This commit is contained in:
@@ -49,6 +49,6 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||||
google.golang.org/grpc v1.78.0 // indirect
|
google.golang.org/grpc v1.78.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||||
google.golang.org/grpc v1.78.0 // indirect
|
google.golang.org/grpc v1.78.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -50,5 +50,5 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -86,5 +86,5 @@ require (
|
|||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -50,5 +50,5 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -47,5 +47,5 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -51,5 +51,5 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
pmodel "github.com/tech/sendico/pkg/model"
|
pmodel "github.com/tech/sendico/pkg/model"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
"go.uber.org/zap"
|
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
"go.uber.org/zap"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -96,7 +97,7 @@ func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.Create
|
|||||||
if lookupErr != nil {
|
if lookupErr != nil {
|
||||||
s.logger.Warn("duplicate account create but failed to load existing",
|
s.logger.Warn("duplicate account create but failed to load existing",
|
||||||
zap.Error(lookupErr),
|
zap.Error(lookupErr),
|
||||||
zap.String("organizationRef", orgRef.Hex()),
|
mzap.ObjRef("organization_ref", orgRef),
|
||||||
zap.String("accountCode", accountCode),
|
zap.String("accountCode", accountCode),
|
||||||
zap.String("currency", currency))
|
zap.String("currency", currency))
|
||||||
return nil, merrors.Internal("failed to load existing account after conflict")
|
return nil, merrors.Internal("failed to load existing account after conflict")
|
||||||
@@ -109,7 +110,7 @@ func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.Create
|
|||||||
recordAccountOperation("create", "error")
|
recordAccountOperation("create", "error")
|
||||||
s.logger.Warn("failed to create account",
|
s.logger.Warn("failed to create account",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.String("organizationRef", orgRef.Hex()),
|
mzap.ObjRef("organization_ref", orgRef),
|
||||||
zap.String("accountCode", accountCode),
|
zap.String("accountCode", accountCode),
|
||||||
zap.String("currency", currency))
|
zap.String("currency", currency))
|
||||||
return nil, merrors.Internal("failed to create account")
|
return nil, merrors.Internal("failed to create account")
|
||||||
@@ -279,7 +280,7 @@ func (s *Service) ensureSettlementAccount(ctx context.Context, orgRef primitive.
|
|||||||
if !errors.Is(err, storage.ErrAccountNotFound) {
|
if !errors.Is(err, storage.ErrAccountNotFound) {
|
||||||
s.logger.Warn("failed to resolve default settlement account",
|
s.logger.Warn("failed to resolve default settlement account",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.String("organizationRef", orgRef.Hex()),
|
mzap.ObjRef("organization_ref", orgRef),
|
||||||
zap.String("currency", normalizedCurrency))
|
zap.String("currency", normalizedCurrency))
|
||||||
return nil, merrors.Internal("failed to resolve settlement account")
|
return nil, merrors.Internal("failed to resolve settlement account")
|
||||||
}
|
}
|
||||||
@@ -306,20 +307,20 @@ func (s *Service) ensureSettlementAccount(ctx context.Context, orgRef primitive.
|
|||||||
}
|
}
|
||||||
s.logger.Warn("duplicate settlement account create but failed to load existing",
|
s.logger.Warn("duplicate settlement account create but failed to load existing",
|
||||||
zap.Error(lookupErr),
|
zap.Error(lookupErr),
|
||||||
zap.String("organizationRef", orgRef.Hex()),
|
mzap.ObjRef("organization_ref", orgRef),
|
||||||
zap.String("currency", normalizedCurrency))
|
zap.String("currency", normalizedCurrency))
|
||||||
return nil, merrors.Internal("failed to resolve settlement account after conflict")
|
return nil, merrors.Internal("failed to resolve settlement account after conflict")
|
||||||
}
|
}
|
||||||
s.logger.Warn("failed to create default settlement account",
|
s.logger.Warn("failed to create default settlement account",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.String("organizationRef", orgRef.Hex()),
|
mzap.ObjRef("organization_ref", orgRef),
|
||||||
zap.String("currency", normalizedCurrency),
|
zap.String("currency", normalizedCurrency),
|
||||||
zap.String("accountCode", accountCode))
|
zap.String("accountCode", accountCode))
|
||||||
return nil, merrors.Internal("failed to create settlement account")
|
return nil, merrors.Internal("failed to create settlement account")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Info("default settlement account created",
|
s.logger.Info("default settlement account created",
|
||||||
zap.String("organizationRef", orgRef.Hex()),
|
mzap.ObjRef("organization_ref", orgRef),
|
||||||
zap.String("currency", normalizedCurrency),
|
zap.String("currency", normalizedCurrency),
|
||||||
zap.String("accountCode", accountCode))
|
zap.String("accountCode", accountCode))
|
||||||
return account, nil
|
return account, nil
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -31,7 +32,7 @@ func (s *Service) listAccountsResponder(_ context.Context, req *ledgerv1.ListAcc
|
|||||||
// No pagination requested; return all accounts for the organization.
|
// No pagination requested; return all accounts for the organization.
|
||||||
accounts, err := s.storage.Accounts().ListByOrganization(ctx, orgRef, 0, 0)
|
accounts, err := s.storage.Accounts().ListByOrganization(ctx, orgRef, 0, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("failed to list ledger accounts", zap.Error(err), zap.String("organizationRef", orgRef.Hex()))
|
s.logger.Warn("failed to list ledger accounts", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package ledger
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/ledger/storage"
|
"github.com/tech/sendico/ledger/storage"
|
||||||
@@ -10,6 +11,7 @@ import (
|
|||||||
storageMongo "github.com/tech/sendico/ledger/storage/mongo"
|
storageMongo "github.com/tech/sendico/ledger/storage/mongo"
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -41,12 +43,20 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
logger := s.logger.With(
|
||||||
|
zap.String("idempotency_key", req.IdempotencyKey),
|
||||||
|
mzap.ObjRef("organization_ref", orgRef),
|
||||||
|
mzap.ObjRef("ledger_account_ref", accountRef),
|
||||||
|
zap.String("currency", req.Money.Currency),
|
||||||
|
)
|
||||||
|
if strings.TrimSpace(req.ContraLedgerAccountRef) != "" {
|
||||||
|
logger = logger.With(zap.String("contra_ledger_account_ref", strings.TrimSpace(req.ContraLedgerAccountRef)))
|
||||||
|
}
|
||||||
|
|
||||||
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
|
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
|
||||||
if err == nil && existingEntry != nil {
|
if err == nil && existingEntry != nil {
|
||||||
recordDuplicateRequest("credit")
|
recordDuplicateRequest("credit")
|
||||||
s.logger.Info("duplicate credit request (idempotency)",
|
logger.Info("duplicate credit request (idempotency)",
|
||||||
zap.String("idempotencyKey", req.IdempotencyKey),
|
|
||||||
zap.String("existingEntryID", existingEntry.GetID().Hex()))
|
zap.String("existingEntryID", existingEntry.GetID().Hex()))
|
||||||
return &ledgerv1.PostResponse{
|
return &ledgerv1.PostResponse{
|
||||||
JournalEntryRef: existingEntry.GetID().Hex(),
|
JournalEntryRef: existingEntry.GetID().Hex(),
|
||||||
@@ -56,7 +66,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
|
|||||||
}
|
}
|
||||||
if err != nil && err != storage.ErrJournalEntryNotFound {
|
if err != nil && err != storage.ErrJournalEntryNotFound {
|
||||||
recordJournalEntryError("credit", "idempotency_check_failed")
|
recordJournalEntryError("credit", "idempotency_check_failed")
|
||||||
s.logger.Warn("failed to check idempotency", zap.Error(err))
|
logger.Warn("failed to check idempotency", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to check idempotency")
|
return nil, merrors.Internal("failed to check idempotency")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +77,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
|
|||||||
return nil, merrors.NoData("account not found")
|
return nil, merrors.NoData("account not found")
|
||||||
}
|
}
|
||||||
recordJournalEntryError("credit", "account_lookup_failed")
|
recordJournalEntryError("credit", "account_lookup_failed")
|
||||||
s.logger.Warn("failed to get account", zap.Error(err))
|
logger.Warn("failed to get account", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to get account")
|
return nil, merrors.Internal("failed to get account")
|
||||||
}
|
}
|
||||||
if err := validateAccountForOrg(account, orgRef, req.Money.Currency); err != nil {
|
if err := validateAccountForOrg(account, orgRef, req.Money.Currency); err != nil {
|
||||||
@@ -84,7 +94,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
|
|||||||
charges := req.Charges
|
charges := req.Charges
|
||||||
if len(charges) == 0 {
|
if len(charges) == 0 {
|
||||||
if computed, err := s.quoteFeesForCredit(ctx, req); err != nil {
|
if computed, err := s.quoteFeesForCredit(ctx, req); err != nil {
|
||||||
s.logger.Warn("failed to quote fees", zap.Error(err))
|
logger.Warn("failed to quote fees", zap.Error(err))
|
||||||
} else if len(computed) > 0 {
|
} else if len(computed) > 0 {
|
||||||
charges = computed
|
charges = computed
|
||||||
}
|
}
|
||||||
@@ -118,7 +128,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
|
|||||||
if err == storage.ErrAccountNotFound {
|
if err == storage.ErrAccountNotFound {
|
||||||
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
|
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
|
||||||
}
|
}
|
||||||
s.logger.Warn("failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex()))
|
logger.Warn("failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex()))
|
||||||
return nil, merrors.Internal("failed to get charge account")
|
return nil, merrors.Internal("failed to get charge account")
|
||||||
}
|
}
|
||||||
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {
|
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {
|
||||||
@@ -189,7 +199,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
|
|||||||
entry.OrganizationRef = orgRef
|
entry.OrganizationRef = orgRef
|
||||||
|
|
||||||
if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil {
|
if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil {
|
||||||
s.logger.Warn("failed to create journal entry", zap.Error(err))
|
logger.Warn("failed to create journal entry", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to create journal entry")
|
return nil, merrors.Internal("failed to create journal entry")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +217,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil {
|
if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil {
|
||||||
s.logger.Warn("failed to create posting lines", zap.Error(err))
|
logger.Warn("failed to create posting lines", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to create posting lines")
|
return nil, merrors.Internal("failed to create posting lines")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package ledger
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tech/sendico/ledger/storage"
|
"github.com/tech/sendico/ledger/storage"
|
||||||
@@ -10,6 +11,7 @@ import (
|
|||||||
storageMongo "github.com/tech/sendico/ledger/storage/mongo"
|
storageMongo "github.com/tech/sendico/ledger/storage/mongo"
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -39,12 +41,20 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
logger := s.logger.With(
|
||||||
|
zap.String("idempotency_key", req.IdempotencyKey),
|
||||||
|
mzap.ObjRef("organization_ref", orgRef),
|
||||||
|
mzap.ObjRef("ledger_account_ref", accountRef),
|
||||||
|
zap.String("currency", req.Money.Currency),
|
||||||
|
)
|
||||||
|
if strings.TrimSpace(req.ContraLedgerAccountRef) != "" {
|
||||||
|
logger = logger.With(zap.String("contra_ledger_account_ref", strings.TrimSpace(req.ContraLedgerAccountRef)))
|
||||||
|
}
|
||||||
|
|
||||||
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
|
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
|
||||||
if err == nil && existingEntry != nil {
|
if err == nil && existingEntry != nil {
|
||||||
recordDuplicateRequest("debit")
|
recordDuplicateRequest("debit")
|
||||||
s.logger.Info("duplicate debit request (idempotency)",
|
logger.Info("duplicate debit request (idempotency)",
|
||||||
zap.String("idempotencyKey", req.IdempotencyKey),
|
|
||||||
zap.String("existingEntryID", existingEntry.GetID().Hex()))
|
zap.String("existingEntryID", existingEntry.GetID().Hex()))
|
||||||
return &ledgerv1.PostResponse{
|
return &ledgerv1.PostResponse{
|
||||||
JournalEntryRef: existingEntry.GetID().Hex(),
|
JournalEntryRef: existingEntry.GetID().Hex(),
|
||||||
@@ -53,7 +63,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
if err != nil && err != storage.ErrJournalEntryNotFound {
|
if err != nil && err != storage.ErrJournalEntryNotFound {
|
||||||
s.logger.Warn("failed to check idempotency", zap.Error(err))
|
logger.Warn("failed to check idempotency", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to check idempotency")
|
return nil, merrors.Internal("failed to check idempotency")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +72,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
|
|||||||
if err == storage.ErrAccountNotFound {
|
if err == storage.ErrAccountNotFound {
|
||||||
return nil, merrors.NoData("account not found")
|
return nil, merrors.NoData("account not found")
|
||||||
}
|
}
|
||||||
s.logger.Warn("failed to get account", zap.Error(err))
|
logger.Warn("failed to get account", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to get account")
|
return nil, merrors.Internal("failed to get account")
|
||||||
}
|
}
|
||||||
if err := validateAccountForOrg(account, orgRef, req.Money.Currency); err != nil {
|
if err := validateAccountForOrg(account, orgRef, req.Money.Currency); err != nil {
|
||||||
@@ -78,7 +88,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
|
|||||||
charges := req.Charges
|
charges := req.Charges
|
||||||
if len(charges) == 0 {
|
if len(charges) == 0 {
|
||||||
if computed, err := s.quoteFeesForDebit(ctx, req); err != nil {
|
if computed, err := s.quoteFeesForDebit(ctx, req); err != nil {
|
||||||
s.logger.Warn("failed to quote fees", zap.Error(err))
|
logger.Warn("failed to quote fees", zap.Error(err))
|
||||||
} else if len(computed) > 0 {
|
} else if len(computed) > 0 {
|
||||||
charges = computed
|
charges = computed
|
||||||
}
|
}
|
||||||
@@ -112,7 +122,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
|
|||||||
if err == storage.ErrAccountNotFound {
|
if err == storage.ErrAccountNotFound {
|
||||||
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
|
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
|
||||||
}
|
}
|
||||||
s.logger.Warn("failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex()))
|
logger.Warn("failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex()))
|
||||||
return nil, merrors.Internal("failed to get charge account")
|
return nil, merrors.Internal("failed to get charge account")
|
||||||
}
|
}
|
||||||
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {
|
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {
|
||||||
@@ -183,7 +193,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
|
|||||||
entry.OrganizationRef = orgRef
|
entry.OrganizationRef = orgRef
|
||||||
|
|
||||||
if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil {
|
if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil {
|
||||||
s.logger.Warn("failed to create journal entry", zap.Error(err))
|
logger.Warn("failed to create journal entry", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to create journal entry")
|
return nil, merrors.Internal("failed to create journal entry")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +211,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil {
|
if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil {
|
||||||
s.logger.Warn("failed to create posting lines", zap.Error(err))
|
logger.Warn("failed to create posting lines", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to create posting lines")
|
return nil, merrors.Internal("failed to create posting lines")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
storageMongo "github.com/tech/sendico/ledger/storage/mongo"
|
storageMongo "github.com/tech/sendico/ledger/storage/mongo"
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -62,13 +63,21 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
logger := s.logger.With(
|
||||||
|
zap.String("idempotency_key", req.IdempotencyKey),
|
||||||
|
mzap.ObjRef("organization_ref", orgRef),
|
||||||
|
mzap.ObjRef("from_account_ref", fromAccountRef),
|
||||||
|
mzap.ObjRef("to_account_ref", toAccountRef),
|
||||||
|
zap.String("from_currency", req.FromMoney.Currency),
|
||||||
|
zap.String("to_currency", req.ToMoney.Currency),
|
||||||
|
zap.String("rate", req.Rate),
|
||||||
|
)
|
||||||
|
|
||||||
// Check for duplicate idempotency key
|
// Check for duplicate idempotency key
|
||||||
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
|
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
|
||||||
if err == nil && existingEntry != nil {
|
if err == nil && existingEntry != nil {
|
||||||
recordDuplicateRequest("fx")
|
recordDuplicateRequest("fx")
|
||||||
s.logger.Info("duplicate FX request (idempotency)",
|
logger.Info("duplicate FX request (idempotency)",
|
||||||
zap.String("idempotencyKey", req.IdempotencyKey),
|
|
||||||
zap.String("existingEntryID", existingEntry.GetID().Hex()))
|
zap.String("existingEntryID", existingEntry.GetID().Hex()))
|
||||||
return &ledgerv1.PostResponse{
|
return &ledgerv1.PostResponse{
|
||||||
JournalEntryRef: existingEntry.GetID().Hex(),
|
JournalEntryRef: existingEntry.GetID().Hex(),
|
||||||
@@ -77,7 +86,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
if err != nil && err != storage.ErrJournalEntryNotFound {
|
if err != nil && err != storage.ErrJournalEntryNotFound {
|
||||||
s.logger.Warn("failed to check idempotency", zap.Error(err))
|
logger.Warn("failed to check idempotency", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to check idempotency")
|
return nil, merrors.Internal("failed to check idempotency")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +96,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
|
|||||||
if err == storage.ErrAccountNotFound {
|
if err == storage.ErrAccountNotFound {
|
||||||
return nil, merrors.NoData("from_account not found")
|
return nil, merrors.NoData("from_account not found")
|
||||||
}
|
}
|
||||||
s.logger.Warn("failed to get from_account", zap.Error(err))
|
logger.Warn("failed to get from_account", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to get from_account")
|
return nil, merrors.Internal("failed to get from_account")
|
||||||
}
|
}
|
||||||
if err := validateAccountForOrg(fromAccount, orgRef, req.FromMoney.Currency); err != nil {
|
if err := validateAccountForOrg(fromAccount, orgRef, req.FromMoney.Currency); err != nil {
|
||||||
@@ -99,7 +108,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
|
|||||||
if err == storage.ErrAccountNotFound {
|
if err == storage.ErrAccountNotFound {
|
||||||
return nil, merrors.NoData("to_account not found")
|
return nil, merrors.NoData("to_account not found")
|
||||||
}
|
}
|
||||||
s.logger.Warn("failed to get to_account", zap.Error(err))
|
logger.Warn("failed to get to_account", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to get to_account")
|
return nil, merrors.Internal("failed to get to_account")
|
||||||
}
|
}
|
||||||
if err := validateAccountForOrg(toAccount, orgRef, req.ToMoney.Currency); err != nil {
|
if err := validateAccountForOrg(toAccount, orgRef, req.ToMoney.Currency); err != nil {
|
||||||
@@ -153,7 +162,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
|
|||||||
if err == storage.ErrAccountNotFound {
|
if err == storage.ErrAccountNotFound {
|
||||||
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
|
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
|
||||||
}
|
}
|
||||||
s.logger.Warn("failed to get FX charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex()))
|
logger.Warn("failed to get FX charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex()))
|
||||||
return nil, merrors.Internal("failed to get charge account")
|
return nil, merrors.Internal("failed to get charge account")
|
||||||
}
|
}
|
||||||
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {
|
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {
|
||||||
@@ -206,7 +215,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
|
|||||||
entry.OrganizationRef = orgRef
|
entry.OrganizationRef = orgRef
|
||||||
|
|
||||||
if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil {
|
if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil {
|
||||||
s.logger.Warn("failed to create journal entry", zap.Error(err))
|
logger.Warn("failed to create journal entry", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to create journal entry")
|
return nil, merrors.Internal("failed to create journal entry")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +229,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil {
|
if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil {
|
||||||
s.logger.Warn("failed to create posting lines", zap.Error(err))
|
logger.Warn("failed to create posting lines", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to create posting lines")
|
return nil, merrors.Internal("failed to create posting lines")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/tech/sendico/ledger/storage"
|
"github.com/tech/sendico/ledger/storage"
|
||||||
"github.com/tech/sendico/ledger/storage/model"
|
"github.com/tech/sendico/ledger/storage/model"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -90,7 +91,7 @@ func (s *Service) resolveSettlementAccount(ctx context.Context, orgRef primitive
|
|||||||
}
|
}
|
||||||
s.logger.Warn("failed to resolve default settlement account",
|
s.logger.Warn("failed to resolve default settlement account",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.String("organizationRef", orgRef.Hex()),
|
mzap.ObjRef("organization_ref", orgRef),
|
||||||
zap.String("currency", currency))
|
zap.String("currency", currency))
|
||||||
return nil, merrors.Internal("failed to resolve settlement account")
|
return nil, merrors.Internal("failed to resolve settlement account")
|
||||||
}
|
}
|
||||||
@@ -132,7 +133,7 @@ func (s *Service) upsertBalances(ctx context.Context, lines []*model.PostingLine
|
|||||||
for accountRef, delta := range balanceDeltas {
|
for accountRef, delta := range balanceDeltas {
|
||||||
account := accounts[accountRef]
|
account := accounts[accountRef]
|
||||||
if account == nil {
|
if account == nil {
|
||||||
s.logger.Warn("account cache missing for balance update", zap.String("accountRef", accountRef.Hex()))
|
s.logger.Warn("account cache missing for balance update", mzap.ObjRef("account_ref", accountRef))
|
||||||
return merrors.Internal("account cache missing for balance update")
|
return merrors.Internal("account cache missing for balance update")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +141,7 @@ func (s *Service) upsertBalances(ctx context.Context, lines []*model.PostingLine
|
|||||||
if err != nil && !errors.Is(err, storage.ErrBalanceNotFound) {
|
if err != nil && !errors.Is(err, storage.ErrBalanceNotFound) {
|
||||||
s.logger.Warn("failed to fetch account balance",
|
s.logger.Warn("failed to fetch account balance",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.String("accountRef", accountRef.Hex()))
|
mzap.ObjRef("account_ref", accountRef))
|
||||||
return merrors.Internal("failed to update balance")
|
return merrors.Internal("failed to update balance")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +170,7 @@ func (s *Service) upsertBalances(ctx context.Context, lines []*model.PostingLine
|
|||||||
newBalance.OrganizationRef = account.OrganizationRef
|
newBalance.OrganizationRef = account.OrganizationRef
|
||||||
|
|
||||||
if err := balancesStore.Upsert(ctx, newBalance); err != nil {
|
if err := balancesStore.Upsert(ctx, newBalance); err != nil {
|
||||||
s.logger.Warn("failed to upsert account balance", zap.Error(err), zap.String("accountRef", accountRef.Hex()))
|
s.logger.Warn("failed to upsert account balance", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
|
||||||
return merrors.Internal("failed to update balance")
|
return merrors.Internal("failed to update balance")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
storageMongo "github.com/tech/sendico/ledger/storage/mongo"
|
storageMongo "github.com/tech/sendico/ledger/storage/mongo"
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -53,13 +54,19 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
logger := s.logger.With(
|
||||||
|
zap.String("idempotency_key", req.IdempotencyKey),
|
||||||
|
mzap.ObjRef("organization_ref", orgRef),
|
||||||
|
mzap.ObjRef("from_account_ref", fromAccountRef),
|
||||||
|
mzap.ObjRef("to_account_ref", toAccountRef),
|
||||||
|
zap.String("currency", req.Money.Currency),
|
||||||
|
)
|
||||||
|
|
||||||
// Check for duplicate idempotency key
|
// Check for duplicate idempotency key
|
||||||
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
|
existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
|
||||||
if err == nil && existingEntry != nil {
|
if err == nil && existingEntry != nil {
|
||||||
recordDuplicateRequest("transfer")
|
recordDuplicateRequest("transfer")
|
||||||
s.logger.Info("duplicate transfer request (idempotency)",
|
logger.Info("duplicate transfer request (idempotency)",
|
||||||
zap.String("idempotencyKey", req.IdempotencyKey),
|
|
||||||
zap.String("existingEntryID", existingEntry.GetID().Hex()))
|
zap.String("existingEntryID", existingEntry.GetID().Hex()))
|
||||||
return &ledgerv1.PostResponse{
|
return &ledgerv1.PostResponse{
|
||||||
JournalEntryRef: existingEntry.GetID().Hex(),
|
JournalEntryRef: existingEntry.GetID().Hex(),
|
||||||
@@ -68,7 +75,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
if err != nil && err != storage.ErrJournalEntryNotFound {
|
if err != nil && err != storage.ErrJournalEntryNotFound {
|
||||||
s.logger.Warn("failed to check idempotency", zap.Error(err))
|
logger.Warn("failed to check idempotency", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to check idempotency")
|
return nil, merrors.Internal("failed to check idempotency")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +85,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
|
|||||||
if err == storage.ErrAccountNotFound {
|
if err == storage.ErrAccountNotFound {
|
||||||
return nil, merrors.NoData("from_account not found")
|
return nil, merrors.NoData("from_account not found")
|
||||||
}
|
}
|
||||||
s.logger.Warn("failed to get from_account", zap.Error(err))
|
logger.Warn("failed to get from_account", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to get from_account")
|
return nil, merrors.Internal("failed to get from_account")
|
||||||
}
|
}
|
||||||
if err := validateAccountForOrg(fromAccount, orgRef, req.Money.Currency); err != nil {
|
if err := validateAccountForOrg(fromAccount, orgRef, req.Money.Currency); err != nil {
|
||||||
@@ -90,7 +97,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
|
|||||||
if err == storage.ErrAccountNotFound {
|
if err == storage.ErrAccountNotFound {
|
||||||
return nil, merrors.NoData("to_account not found")
|
return nil, merrors.NoData("to_account not found")
|
||||||
}
|
}
|
||||||
s.logger.Warn("failed to get to_account", zap.Error(err))
|
logger.Warn("failed to get to_account", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to get to_account")
|
return nil, merrors.Internal("failed to get to_account")
|
||||||
}
|
}
|
||||||
if err := validateAccountForOrg(toAccount, orgRef, req.Money.Currency); err != nil {
|
if err := validateAccountForOrg(toAccount, orgRef, req.Money.Currency); err != nil {
|
||||||
@@ -147,7 +154,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
|
|||||||
if err == storage.ErrAccountNotFound {
|
if err == storage.ErrAccountNotFound {
|
||||||
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
|
return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
|
||||||
}
|
}
|
||||||
s.logger.Warn("failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex()))
|
logger.Warn("failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex()))
|
||||||
return nil, merrors.Internal("failed to get charge account")
|
return nil, merrors.Internal("failed to get charge account")
|
||||||
}
|
}
|
||||||
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {
|
if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {
|
||||||
@@ -188,7 +195,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
|
|||||||
entry.OrganizationRef = orgRef
|
entry.OrganizationRef = orgRef
|
||||||
|
|
||||||
if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil {
|
if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil {
|
||||||
s.logger.Warn("failed to create journal entry", zap.Error(err))
|
logger.Warn("failed to create journal entry", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to create journal entry")
|
return nil, merrors.Internal("failed to create journal entry")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +213,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil {
|
if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil {
|
||||||
s.logger.Warn("failed to create posting lines", zap.Error(err))
|
logger.Warn("failed to create posting lines", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to create posting lines")
|
return nil, merrors.Internal("failed to create posting lines")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/tech/sendico/ledger/storage"
|
"github.com/tech/sendico/ledger/storage"
|
||||||
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
"github.com/tech/sendico/pkg/api/routers/gsresponse"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -27,6 +28,7 @@ func (s *Service) getBalanceResponder(_ context.Context, req *ledgerv1.GetBalanc
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
logger := s.logger.With(mzap.ObjRef("ledger_account_ref", accountRef))
|
||||||
|
|
||||||
// Get account to verify it exists
|
// Get account to verify it exists
|
||||||
account, err := s.storage.Accounts().Get(ctx, accountRef)
|
account, err := s.storage.Accounts().Get(ctx, accountRef)
|
||||||
@@ -34,7 +36,7 @@ func (s *Service) getBalanceResponder(_ context.Context, req *ledgerv1.GetBalanc
|
|||||||
if err == storage.ErrAccountNotFound {
|
if err == storage.ErrAccountNotFound {
|
||||||
return nil, merrors.NoData("account not found")
|
return nil, merrors.NoData("account not found")
|
||||||
}
|
}
|
||||||
s.logger.Warn("failed to get account", zap.Error(err))
|
logger.Warn("failed to get account", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to get account")
|
return nil, merrors.Internal("failed to get account")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +55,7 @@ func (s *Service) getBalanceResponder(_ context.Context, req *ledgerv1.GetBalanc
|
|||||||
LastUpdated: timestamppb.Now(),
|
LastUpdated: timestamppb.Now(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
s.logger.Warn("failed to get balance", zap.Error(err))
|
logger.Warn("failed to get balance", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to get balance")
|
return nil, merrors.Internal("failed to get balance")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +84,7 @@ func (s *Service) getJournalEntryResponder(_ context.Context, req *ledgerv1.GetE
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
logger := s.logger.With(mzap.ObjRef("entry_ref", entryRef))
|
||||||
|
|
||||||
// Get journal entry
|
// Get journal entry
|
||||||
entry, err := s.storage.JournalEntries().Get(ctx, entryRef)
|
entry, err := s.storage.JournalEntries().Get(ctx, entryRef)
|
||||||
@@ -89,14 +92,14 @@ func (s *Service) getJournalEntryResponder(_ context.Context, req *ledgerv1.GetE
|
|||||||
if err == storage.ErrJournalEntryNotFound {
|
if err == storage.ErrJournalEntryNotFound {
|
||||||
return nil, merrors.NoData("journal entry not found")
|
return nil, merrors.NoData("journal entry not found")
|
||||||
}
|
}
|
||||||
s.logger.Warn("failed to get journal entry", zap.Error(err))
|
logger.Warn("failed to get journal entry", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to get journal entry")
|
return nil, merrors.Internal("failed to get journal entry")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get posting lines for this entry
|
// Get posting lines for this entry
|
||||||
lines, err := s.storage.PostingLines().ListByJournalEntry(ctx, entryRef)
|
lines, err := s.storage.PostingLines().ListByJournalEntry(ctx, entryRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("failed to get posting lines", zap.Error(err))
|
logger.Warn("failed to get posting lines", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to get posting lines")
|
return nil, merrors.Internal("failed to get posting lines")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +143,7 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
logger := s.logger.With(mzap.ObjRef("ledger_account_ref", accountRef))
|
||||||
|
|
||||||
// Verify account exists
|
// Verify account exists
|
||||||
_, err = s.storage.Accounts().Get(ctx, accountRef)
|
_, err = s.storage.Accounts().Get(ctx, accountRef)
|
||||||
@@ -147,7 +151,7 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat
|
|||||||
if err == storage.ErrAccountNotFound {
|
if err == storage.ErrAccountNotFound {
|
||||||
return nil, merrors.NoData("account not found")
|
return nil, merrors.NoData("account not found")
|
||||||
}
|
}
|
||||||
s.logger.Warn("failed to get account", zap.Error(err))
|
logger.Warn("failed to get account", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to get account")
|
return nil, merrors.Internal("failed to get account")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,11 +171,12 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat
|
|||||||
return nil, merrors.InvalidArgument(fmt.Sprintf("invalid cursor: %v", err))
|
return nil, merrors.InvalidArgument(fmt.Sprintf("invalid cursor: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logger = logger.With(zap.Int("limit", limit), zap.Int("offset", offset))
|
||||||
|
|
||||||
// Get posting lines for account
|
// Get posting lines for account
|
||||||
postingLines, err := s.storage.PostingLines().ListByAccount(ctx, accountRef, limit+1, offset)
|
postingLines, err := s.storage.PostingLines().ListByAccount(ctx, accountRef, limit+1, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("failed to get posting lines", zap.Error(err))
|
logger.Warn("failed to get posting lines", zap.Error(err))
|
||||||
return nil, merrors.Internal("failed to get posting lines")
|
return nil, merrors.Internal("failed to get posting lines")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,18 +194,22 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat
|
|||||||
|
|
||||||
entries := make([]*ledgerv1.JournalEntryResponse, 0)
|
entries := make([]*ledgerv1.JournalEntryResponse, 0)
|
||||||
for entryRefHex := range entryMap {
|
for entryRefHex := range entryMap {
|
||||||
entryRef, _ := parseObjectID(entryRefHex)
|
entryRef, err := parseObjectID(entryRefHex)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("invalid journal entry ref in posting lines", zap.String("entry_ref", entryRefHex), zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
entry, err := s.storage.JournalEntries().Get(ctx, entryRef)
|
entry, err := s.storage.JournalEntries().Get(ctx, entryRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("failed to get journal entry for statement", zap.Error(err), zap.String("entryRef", entryRefHex))
|
logger.Warn("failed to get journal entry for statement", zap.Error(err), zap.String("entry_ref", entryRefHex))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all lines for this entry
|
// Get all lines for this entry
|
||||||
lines, err := s.storage.PostingLines().ListByJournalEntry(ctx, entryRef)
|
lines, err := s.storage.PostingLines().ListByJournalEntry(ctx, entryRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Warn("failed to get posting lines for entry", zap.Error(err), zap.String("entryRef", entryRefHex))
|
logger.Warn("failed to get posting lines for entry", zap.Error(err), zap.String("entry_ref", entryRefHex))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -87,14 +88,14 @@ func (a *accountsStore) Get(ctx context.Context, accountRef primitive.ObjectID)
|
|||||||
result := &model.Account{}
|
result := &model.Account{}
|
||||||
if err := a.repo.Get(ctx, accountRef, result); err != nil {
|
if err := a.repo.Get(ctx, accountRef, result); err != nil {
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
a.logger.Debug("account not found", zap.String("accountRef", accountRef.Hex()))
|
a.logger.Debug("account not found", mzap.ObjRef("account_ref", accountRef))
|
||||||
return nil, storage.ErrAccountNotFound
|
return nil, storage.ErrAccountNotFound
|
||||||
}
|
}
|
||||||
a.logger.Warn("failed to get account", zap.Error(err), zap.String("accountRef", accountRef.Hex()))
|
a.logger.Warn("failed to get account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
a.logger.Debug("account loaded", zap.String("accountRef", accountRef.Hex()),
|
a.logger.Debug("account loaded", mzap.ObjRef("account_ref", accountRef),
|
||||||
zap.String("accountCode", result.AccountCode))
|
zap.String("accountCode", result.AccountCode))
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
@@ -156,11 +157,11 @@ func (a *accountsStore) GetDefaultSettlement(ctx context.Context, orgRef primiti
|
|||||||
if errors.Is(err, merrors.ErrNoData) {
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
a.logger.Debug("default settlement account not found",
|
a.logger.Debug("default settlement account not found",
|
||||||
zap.String("currency", currency),
|
zap.String("currency", currency),
|
||||||
zap.String("organizationRef", orgRef.Hex()))
|
mzap.ObjRef("organization_ref", orgRef))
|
||||||
return nil, storage.ErrAccountNotFound
|
return nil, storage.ErrAccountNotFound
|
||||||
}
|
}
|
||||||
a.logger.Warn("failed to get default settlement account", zap.Error(err),
|
a.logger.Warn("failed to get default settlement account", zap.Error(err),
|
||||||
zap.String("organizationRef", orgRef.Hex()),
|
mzap.ObjRef("organization_ref", orgRef),
|
||||||
zap.String("currency", currency))
|
zap.String("currency", currency))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -210,11 +211,11 @@ func (a *accountsStore) UpdateStatus(ctx context.Context, accountRef primitive.O
|
|||||||
|
|
||||||
patch := repository.Patch().Set(repository.Field("status"), status)
|
patch := repository.Patch().Set(repository.Field("status"), status)
|
||||||
if err := a.repo.Patch(ctx, accountRef, patch); err != nil {
|
if err := a.repo.Patch(ctx, accountRef, patch); err != nil {
|
||||||
a.logger.Warn("failed to update account status", zap.Error(err), zap.String("accountRef", accountRef.Hex()))
|
a.logger.Warn("failed to update account status", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
a.logger.Debug("account status updated", zap.String("accountRef", accountRef.Hex()),
|
a.logger.Debug("account status updated", mzap.ObjRef("account_ref", accountRef),
|
||||||
zap.String("status", string(status)))
|
zap.String("status", string(status)))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -55,14 +56,14 @@ func (b *balancesStore) Get(ctx context.Context, accountRef primitive.ObjectID)
|
|||||||
result := &model.AccountBalance{}
|
result := &model.AccountBalance{}
|
||||||
if err := b.repo.FindOneByFilter(ctx, query, result); err != nil {
|
if err := b.repo.FindOneByFilter(ctx, query, result); err != nil {
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
b.logger.Debug("balance not found", zap.String("accountRef", accountRef.Hex()))
|
b.logger.Debug("balance not found", mzap.ObjRef("account_ref", accountRef))
|
||||||
return nil, storage.ErrBalanceNotFound
|
return nil, storage.ErrBalanceNotFound
|
||||||
}
|
}
|
||||||
b.logger.Warn("failed to get balance", zap.Error(err), zap.String("accountRef", accountRef.Hex()))
|
b.logger.Warn("failed to get balance", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.logger.Debug("balance loaded", zap.String("accountRef", accountRef.Hex()),
|
b.logger.Debug("balance loaded", mzap.ObjRef("account_ref", accountRef),
|
||||||
zap.String("balance", result.Balance))
|
zap.String("balance", result.Balance))
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -65,14 +66,14 @@ func (j *journalEntriesStore) Create(ctx context.Context, entry *model.JournalEn
|
|||||||
|
|
||||||
if err := j.repo.Insert(ctx, entry, nil); err != nil {
|
if err := j.repo.Insert(ctx, entry, nil); err != nil {
|
||||||
if mongo.IsDuplicateKeyError(err) {
|
if mongo.IsDuplicateKeyError(err) {
|
||||||
j.logger.Warn("duplicate idempotency key", zap.String("idempotencyKey", entry.IdempotencyKey))
|
j.logger.Warn("duplicate idempotency key", zap.String("idempotency_key", entry.IdempotencyKey))
|
||||||
return storage.ErrDuplicateIdempotency
|
return storage.ErrDuplicateIdempotency
|
||||||
}
|
}
|
||||||
j.logger.Warn("failed to create journal entry", zap.Error(err))
|
j.logger.Warn("failed to create journal entry", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
j.logger.Debug("journal entry created", zap.String("idempotencyKey", entry.IdempotencyKey),
|
j.logger.Debug("journal entry created", zap.String("idempotency_key", entry.IdempotencyKey),
|
||||||
zap.String("entryType", string(entry.EntryType)))
|
zap.String("entryType", string(entry.EntryType)))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -86,15 +87,15 @@ func (j *journalEntriesStore) Get(ctx context.Context, entryRef primitive.Object
|
|||||||
result := &model.JournalEntry{}
|
result := &model.JournalEntry{}
|
||||||
if err := j.repo.Get(ctx, entryRef, result); err != nil {
|
if err := j.repo.Get(ctx, entryRef, result); err != nil {
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
j.logger.Debug("journal entry not found", zap.String("entryRef", entryRef.Hex()))
|
j.logger.Debug("journal entry not found", mzap.ObjRef("entry_ref", entryRef))
|
||||||
return nil, storage.ErrJournalEntryNotFound
|
return nil, storage.ErrJournalEntryNotFound
|
||||||
}
|
}
|
||||||
j.logger.Warn("failed to get journal entry", zap.Error(err), zap.String("entryRef", entryRef.Hex()))
|
j.logger.Warn("failed to get journal entry", zap.Error(err), mzap.ObjRef("entry_ref", entryRef))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
j.logger.Debug("journal entry loaded", zap.String("entryRef", entryRef.Hex()),
|
j.logger.Debug("journal entry loaded", mzap.ObjRef("entry_ref", entryRef),
|
||||||
zap.String("idempotencyKey", result.IdempotencyKey))
|
zap.String("idempotency_key", result.IdempotencyKey))
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,15 +116,15 @@ func (j *journalEntriesStore) GetByIdempotencyKey(ctx context.Context, orgRef pr
|
|||||||
result := &model.JournalEntry{}
|
result := &model.JournalEntry{}
|
||||||
if err := j.repo.FindOneByFilter(ctx, query, result); err != nil {
|
if err := j.repo.FindOneByFilter(ctx, query, result); err != nil {
|
||||||
if errors.Is(err, merrors.ErrNoData) {
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
j.logger.Debug("journal entry not found by idempotency key", zap.String("idempotencyKey", idempotencyKey))
|
j.logger.Debug("journal entry not found by idempotency key", zap.String("idempotency_key", idempotencyKey))
|
||||||
return nil, storage.ErrJournalEntryNotFound
|
return nil, storage.ErrJournalEntryNotFound
|
||||||
}
|
}
|
||||||
j.logger.Warn("failed to get journal entry by idempotency key", zap.Error(err),
|
j.logger.Warn("failed to get journal entry by idempotency key", zap.Error(err),
|
||||||
zap.String("idempotencyKey", idempotencyKey))
|
zap.String("idempotency_key", idempotencyKey))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
j.logger.Debug("journal entry loaded by idempotency key", zap.String("idempotencyKey", idempotencyKey))
|
j.logger.Debug("journal entry loaded by idempotency key", zap.String("idempotency_key", idempotencyKey))
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/db/storable"
|
"github.com/tech/sendico/pkg/db/storable"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -97,11 +98,11 @@ func (p *postingLinesStore) ListByJournalEntry(ctx context.Context, entryRef pri
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.logger.Warn("failed to list posting lines by entry", zap.Error(err), zap.String("entryRef", entryRef.Hex()))
|
p.logger.Warn("failed to list posting lines by entry", zap.Error(err), mzap.ObjRef("entry_ref", entryRef))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
p.logger.Debug("listed posting lines by entry", zap.Int("count", len(lines)), zap.String("entryRef", entryRef.Hex()))
|
p.logger.Debug("listed posting lines by entry", zap.Int("count", len(lines)), mzap.ObjRef("entry_ref", entryRef))
|
||||||
return lines, nil
|
return lines, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,10 +130,10 @@ func (p *postingLinesStore) ListByAccount(ctx context.Context, accountRef primit
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.logger.Warn("failed to list posting lines by account", zap.Error(err), zap.String("accountRef", accountRef.Hex()))
|
p.logger.Warn("failed to list posting lines by account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
p.logger.Debug("listed posting lines by account", zap.Int("count", len(lines)), zap.String("accountRef", accountRef.Hex()))
|
p.logger.Debug("listed posting lines by account", zap.Int("count", len(lines)), mzap.ObjRef("account_ref", accountRef))
|
||||||
return lines, nil
|
return lines, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ require (
|
|||||||
golang.org/x/net v0.49.0 // indirect
|
golang.org/x/net v0.49.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||||
google.golang.org/grpc v1.78.0 // indirect
|
google.golang.org/grpc v1.78.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -229,8 +229,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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -62,5 +62,5 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -3,13 +3,9 @@ package serverimp
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
chainclient "github.com/tech/sendico/gateway/chain/client"
|
|
||||||
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
|
"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrator"
|
||||||
"github.com/tech/sendico/payments/orchestrator/storage/model"
|
|
||||||
"github.com/tech/sendico/pkg/discovery"
|
"github.com/tech/sendico/pkg/discovery"
|
||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/payments/rail"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildCardGatewayRoutes(src map[string]cardGatewayRouteConfig) map[string]orchestrator.CardGatewayRoute {
|
func buildCardGatewayRoutes(src map[string]cardGatewayRouteConfig) map[string]orchestrator.CardGatewayRoute {
|
||||||
@@ -62,183 +58,3 @@ func buildGatewayRegistry(logger mlogger.Logger, src []gatewayInstanceConfig, re
|
|||||||
}
|
}
|
||||||
return orchestrator.NewDiscoveryGatewayRegistry(logger, registry)
|
return orchestrator.NewDiscoveryGatewayRegistry(logger, registry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildRailGateways(chainClient chainclient.Client, paymentGatewayClient chainclient.Client, src []gatewayInstanceConfig) map[string]rail.RailGateway {
|
|
||||||
if len(src) == 0 || (chainClient == nil && paymentGatewayClient == nil) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
instances := buildGatewayInstances(nil, src)
|
|
||||||
if len(instances) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
result := map[string]rail.RailGateway{}
|
|
||||||
for _, inst := range instances {
|
|
||||||
if inst == nil || !inst.IsEnabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cfg := chainclient.RailGatewayConfig{
|
|
||||||
Rail: string(inst.Rail),
|
|
||||||
Network: inst.Network,
|
|
||||||
Capabilities: rail.RailCapabilities{
|
|
||||||
CanPayIn: inst.Capabilities.CanPayIn,
|
|
||||||
CanPayOut: inst.Capabilities.CanPayOut,
|
|
||||||
CanReadBalance: inst.Capabilities.CanReadBalance,
|
|
||||||
CanSendFee: inst.Capabilities.CanSendFee,
|
|
||||||
RequiresObserveConfirm: inst.Capabilities.RequiresObserveConfirm,
|
|
||||||
CanBlock: inst.Capabilities.CanBlock,
|
|
||||||
CanRelease: inst.Capabilities.CanRelease,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
switch inst.Rail {
|
|
||||||
case model.RailCrypto:
|
|
||||||
if chainClient == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result[inst.ID] = chainclient.NewRailGateway(chainClient, cfg)
|
|
||||||
case model.RailProviderSettlement:
|
|
||||||
if paymentGatewayClient == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result[inst.ID] = orchestrator.NewProviderSettlementGateway(paymentGatewayClient, cfg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(result) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildGatewayInstances(logger mlogger.Logger, src []gatewayInstanceConfig) []*model.GatewayInstanceDescriptor {
|
|
||||||
if len(src) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if logger != nil {
|
|
||||||
logger = logger.Named("gateway_instances")
|
|
||||||
}
|
|
||||||
result := make([]*model.GatewayInstanceDescriptor, 0, len(src))
|
|
||||||
for _, cfg := range src {
|
|
||||||
id := strings.TrimSpace(cfg.ID)
|
|
||||||
if id == "" {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Warn("Gateway instance skipped: missing id")
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rail := parseRail(cfg.Rail)
|
|
||||||
if rail == model.RailUnspecified {
|
|
||||||
if logger != nil {
|
|
||||||
logger.Warn("Gateway instance skipped: invalid rail", zap.String("id", id), zap.String("rail", cfg.Rail))
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
enabled := true
|
|
||||||
if cfg.IsEnabled != nil {
|
|
||||||
enabled = *cfg.IsEnabled
|
|
||||||
}
|
|
||||||
result = append(result, &model.GatewayInstanceDescriptor{
|
|
||||||
ID: id,
|
|
||||||
Rail: rail,
|
|
||||||
Network: strings.ToUpper(strings.TrimSpace(cfg.Network)),
|
|
||||||
Currencies: normalizeCurrencies(cfg.Currencies),
|
|
||||||
Capabilities: model.RailCapabilities{
|
|
||||||
CanPayIn: cfg.Capabilities.CanPayIn,
|
|
||||||
CanPayOut: cfg.Capabilities.CanPayOut,
|
|
||||||
CanReadBalance: cfg.Capabilities.CanReadBalance,
|
|
||||||
CanSendFee: cfg.Capabilities.CanSendFee,
|
|
||||||
RequiresObserveConfirm: cfg.Capabilities.RequiresObserveConfirm,
|
|
||||||
CanBlock: cfg.Capabilities.CanBlock,
|
|
||||||
CanRelease: cfg.Capabilities.CanRelease,
|
|
||||||
},
|
|
||||||
Limits: buildGatewayLimits(cfg.Limits),
|
|
||||||
Version: strings.TrimSpace(cfg.Version),
|
|
||||||
IsEnabled: enabled,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseRail(value string) model.Rail {
|
|
||||||
switch strings.ToUpper(strings.TrimSpace(value)) {
|
|
||||||
case string(model.RailCrypto):
|
|
||||||
return model.RailCrypto
|
|
||||||
case string(model.RailProviderSettlement):
|
|
||||||
return model.RailProviderSettlement
|
|
||||||
case string(model.RailLedger):
|
|
||||||
return model.RailLedger
|
|
||||||
case string(model.RailCardPayout):
|
|
||||||
return model.RailCardPayout
|
|
||||||
case string(model.RailFiatOnRamp):
|
|
||||||
return model.RailFiatOnRamp
|
|
||||||
default:
|
|
||||||
return model.RailUnspecified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeCurrencies(values []string) []string {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
seen := map[string]bool{}
|
|
||||||
result := make([]string, 0, len(values))
|
|
||||||
for _, value := range values {
|
|
||||||
clean := strings.ToUpper(strings.TrimSpace(value))
|
|
||||||
if clean == "" || seen[clean] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[clean] = true
|
|
||||||
result = append(result, clean)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildGatewayLimits(cfg limitsConfig) model.Limits {
|
|
||||||
limits := model.Limits{
|
|
||||||
MinAmount: strings.TrimSpace(cfg.MinAmount),
|
|
||||||
MaxAmount: strings.TrimSpace(cfg.MaxAmount),
|
|
||||||
PerTxMaxFee: strings.TrimSpace(cfg.PerTxMaxFee),
|
|
||||||
PerTxMinAmount: strings.TrimSpace(cfg.PerTxMinAmount),
|
|
||||||
PerTxMaxAmount: strings.TrimSpace(cfg.PerTxMaxAmount),
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.VolumeLimit) > 0 {
|
|
||||||
limits.VolumeLimit = map[string]string{}
|
|
||||||
for key, value := range cfg.VolumeLimit {
|
|
||||||
bucket := strings.TrimSpace(key)
|
|
||||||
amount := strings.TrimSpace(value)
|
|
||||||
if bucket == "" || amount == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
limits.VolumeLimit[bucket] = amount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.VelocityLimit) > 0 {
|
|
||||||
limits.VelocityLimit = map[string]int{}
|
|
||||||
for key, value := range cfg.VelocityLimit {
|
|
||||||
bucket := strings.TrimSpace(key)
|
|
||||||
if bucket == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
limits.VelocityLimit[bucket] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.CurrencyLimits) > 0 {
|
|
||||||
limits.CurrencyLimits = map[string]model.LimitsOverride{}
|
|
||||||
for key, override := range cfg.CurrencyLimits {
|
|
||||||
currency := strings.ToUpper(strings.TrimSpace(key))
|
|
||||||
if currency == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
limits.CurrencyLimits[currency] = model.LimitsOverride{
|
|
||||||
MaxVolume: strings.TrimSpace(override.MaxVolume),
|
|
||||||
MinAmount: strings.TrimSpace(override.MinAmount),
|
|
||||||
MaxAmount: strings.TrimSpace(override.MaxAmount),
|
|
||||||
MaxFee: strings.TrimSpace(override.MaxFee),
|
|
||||||
MaxOps: override.MaxOps,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return limits
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,159 +1,5 @@
|
|||||||
package serverimp
|
package serverimp
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
|
|
||||||
oracleclient "github.com/tech/sendico/fx/oracle/client"
|
|
||||||
chainclient "github.com/tech/sendico/gateway/chain/client"
|
|
||||||
mntxclient "github.com/tech/sendico/gateway/mntx/client"
|
|
||||||
ledgerclient "github.com/tech/sendico/ledger/client"
|
|
||||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/credentials"
|
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (i *Imp) initFeesClient(cfg clientConfig) (feesv1.FeeEngineClient, *grpc.ClientConn) {
|
|
||||||
addr := cfg.address()
|
|
||||||
if addr == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dialCtx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
creds := credentials.NewTLS(&tls.Config{})
|
|
||||||
if cfg.InsecureTransport {
|
|
||||||
creds = insecure.NewCredentials()
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := grpc.DialContext(dialCtx, addr, grpc.WithTransportCredentials(creds))
|
|
||||||
if err != nil {
|
|
||||||
i.logger.Warn("Failed to connect to fees service", zap.String("address", addr), zap.Error(err))
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
i.logger.Info("Connected to fees service", zap.String("address", addr))
|
|
||||||
return feesv1.NewFeeEngineClient(conn), conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Imp) initLedgerClient(cfg clientConfig) ledgerclient.Client {
|
|
||||||
addr := cfg.address()
|
|
||||||
if addr == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
client, err := ledgerclient.New(ctx, ledgerclient.Config{
|
|
||||||
Address: addr,
|
|
||||||
DialTimeout: cfg.dialTimeout(),
|
|
||||||
CallTimeout: cfg.callTimeout(),
|
|
||||||
Insecure: cfg.InsecureTransport,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
i.logger.Warn("Failed to connect to ledger service", zap.String("address", addr), zap.Error(err))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
i.logger.Info("Connected to ledger service", zap.String("address", addr))
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Imp) initGatewayClient(cfg clientConfig) chainclient.Client {
|
|
||||||
addr := cfg.address()
|
|
||||||
if addr == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
client, err := chainclient.New(ctx, chainclient.Config{
|
|
||||||
Address: addr,
|
|
||||||
DialTimeout: cfg.dialTimeout(),
|
|
||||||
CallTimeout: cfg.callTimeout(),
|
|
||||||
Insecure: cfg.InsecureTransport,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
i.logger.Warn("failed to connect to chain gateway service", zap.String("address", addr), zap.Error(err))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
i.logger.Info("connected to chain gateway service", zap.String("address", addr))
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Imp) initPaymentGatewayClient(cfg clientConfig) chainclient.Client {
|
|
||||||
addr := cfg.address()
|
|
||||||
if addr == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
client, err := chainclient.New(ctx, chainclient.Config{
|
|
||||||
Address: addr,
|
|
||||||
DialTimeout: cfg.dialTimeout(),
|
|
||||||
CallTimeout: cfg.callTimeout(),
|
|
||||||
Insecure: cfg.InsecureTransport,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
i.logger.Warn("failed to connect to payment gateway service", zap.String("address", addr), zap.Error(err))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
i.logger.Info("connected to payment gateway service", zap.String("address", addr))
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Imp) initMntxClient(cfg clientConfig) mntxclient.Client {
|
|
||||||
addr := cfg.address()
|
|
||||||
if addr == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
client, err := mntxclient.New(ctx, mntxclient.Config{
|
|
||||||
Address: addr,
|
|
||||||
DialTimeout: cfg.dialTimeout(),
|
|
||||||
CallTimeout: cfg.callTimeout(),
|
|
||||||
Logger: i.logger.Named("client.mntx"),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
i.logger.Warn("Failed to connect to mntx gateway service", zap.String("address", addr), zap.Error(err))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
i.logger.Info("Connected to mntx gateway service", zap.String("address", addr))
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Imp) initOracleClient(cfg clientConfig) oracleclient.Client {
|
|
||||||
addr := cfg.address()
|
|
||||||
if addr == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.dialTimeout())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
client, err := oracleclient.New(ctx, oracleclient.Config{
|
|
||||||
Address: addr,
|
|
||||||
DialTimeout: cfg.dialTimeout(),
|
|
||||||
CallTimeout: cfg.callTimeout(),
|
|
||||||
Insecure: cfg.InsecureTransport,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
i.logger.Warn("Failed to connect to oracle service", zap.String("address", addr), zap.Error(err))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
i.logger.Info("Connected to oracle service", zap.String("address", addr))
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Imp) closeClients() {
|
func (i *Imp) closeClients() {
|
||||||
if i.discoveryClients != nil {
|
if i.discoveryClients != nil {
|
||||||
i.discoveryClients.Close()
|
i.discoveryClients.Close()
|
||||||
|
|||||||
@@ -77,17 +77,6 @@ type limitsOverrideCfg struct {
|
|||||||
MaxOps int `yaml:"max_ops"`
|
MaxOps int `yaml:"max_ops"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c clientConfig) address() string {
|
|
||||||
return strings.TrimSpace(c.Address)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c clientConfig) dialTimeout() time.Duration {
|
|
||||||
if c.DialTimeoutSecs <= 0 {
|
|
||||||
return 5 * time.Second
|
|
||||||
}
|
|
||||||
return time.Duration(c.DialTimeoutSecs) * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c clientConfig) callTimeout() time.Duration {
|
func (c clientConfig) callTimeout() time.Duration {
|
||||||
if c.CallTimeoutSecs <= 0 {
|
if c.CallTimeoutSecs <= 0 {
|
||||||
return 3 * time.Second
|
return 3 * time.Second
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type orchestratorDeps struct {
|
|||||||
gatewayInvokeResolver orchestrator.GatewayInvokeResolver
|
gatewayInvokeResolver orchestrator.GatewayInvokeResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Imp) initDependencies(cfg *config) *orchestratorDeps {
|
func (i *Imp) initDependencies(_ *config) *orchestratorDeps {
|
||||||
deps := &orchestratorDeps{}
|
deps := &orchestratorDeps{}
|
||||||
if i.discoveryReg == nil {
|
if i.discoveryReg == nil {
|
||||||
if i.logger != nil {
|
if i.logger != nil {
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package orchestrator
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -16,6 +19,7 @@ import (
|
|||||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type quotePaymentCommand struct {
|
type quotePaymentCommand struct {
|
||||||
@@ -23,55 +27,194 @@ type quotePaymentCommand struct {
|
|||||||
logger mlogger.Logger
|
logger mlogger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *quotePaymentCommand) Execute(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) gsresponse.Responder[orchestratorv1.QuotePaymentResponse] {
|
var (
|
||||||
|
errIdempotencyRequired = errors.New("idempotency key is required")
|
||||||
|
errPreviewWithIdempotency = errors.New("preview requests must not use idempotency key")
|
||||||
|
errIdempotencyParamMismatch = errors.New("idempotency key reuse with different parameters")
|
||||||
|
)
|
||||||
|
|
||||||
|
type quoteCtx struct {
|
||||||
|
orgID string
|
||||||
|
orgRef primitive.ObjectID
|
||||||
|
intent *orchestratorv1.PaymentIntent
|
||||||
|
previewOnly bool
|
||||||
|
idempotencyKey string
|
||||||
|
hash string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *quotePaymentCommand) Execute(
|
||||||
|
ctx context.Context,
|
||||||
|
req *orchestratorv1.QuotePaymentRequest,
|
||||||
|
) gsresponse.Responder[orchestratorv1.QuotePaymentResponse] {
|
||||||
|
|
||||||
if err := h.engine.EnsureRepository(ctx); err != nil {
|
if err := h.engine.EnsureRepository(ctx); err != nil {
|
||||||
return gsresponse.Unavailable[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
|
return gsresponse.Unavailable[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
}
|
}
|
||||||
if req == nil {
|
if req == nil {
|
||||||
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request"))
|
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qc, err := h.prepareQuoteCtx(req)
|
||||||
|
if err != nil {
|
||||||
|
return h.mapQuoteErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
quotesStore, err := ensureQuotesStore(h.engine.Repository())
|
||||||
|
if err != nil {
|
||||||
|
return gsresponse.Unavailable[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
quoteProto, err := h.quotePayment(ctx, quotesStore, qc, req)
|
||||||
|
if err != nil {
|
||||||
|
return h.mapQuoteErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gsresponse.Success(&orchestratorv1.QuotePaymentResponse{Quote: quoteProto})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *quotePaymentCommand) prepareQuoteCtx(req *orchestratorv1.QuotePaymentRequest) (*quoteCtx, error) {
|
||||||
orgRef, orgID, err := validateMetaAndOrgRef(req.GetMeta())
|
orgRef, orgID, err := validateMetaAndOrgRef(req.GetMeta())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := requireNonNilIntent(req.GetIntent()); err != nil {
|
if err := requireNonNilIntent(req.GetIntent()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
intent := req.GetIntent()
|
||||||
|
preview := req.GetPreviewOnly()
|
||||||
|
idem := strings.TrimSpace(req.GetIdempotencyKey())
|
||||||
|
|
||||||
|
if preview && idem != "" {
|
||||||
|
return nil, errPreviewWithIdempotency
|
||||||
|
}
|
||||||
|
if !preview && idem == "" {
|
||||||
|
return nil, errIdempotencyRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
return "eCtx{
|
||||||
|
orgID: orgRef,
|
||||||
|
orgRef: orgID,
|
||||||
|
intent: intent,
|
||||||
|
previewOnly: preview,
|
||||||
|
idempotencyKey: idem,
|
||||||
|
hash: hashQuoteRequest(req),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *quotePaymentCommand) quotePayment(
|
||||||
|
ctx context.Context,
|
||||||
|
quotesStore storage.QuotesStore,
|
||||||
|
qc *quoteCtx,
|
||||||
|
req *orchestratorv1.QuotePaymentRequest,
|
||||||
|
) (*orchestratorv1.PaymentQuote, error) {
|
||||||
|
|
||||||
|
if qc.previewOnly {
|
||||||
|
quote, _, err := h.engine.BuildPaymentQuote(ctx, qc.orgID, req)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warn("Failed to build preview payment quote", zap.Error(err), zap.String("org_ref", qc.orgID))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
quote.QuoteRef = primitive.NewObjectID().Hex()
|
||||||
|
return quote, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
existing, err := quotesStore.GetByIdempotencyKey(ctx, qc.idempotencyKey)
|
||||||
|
if err != nil && !errors.Is(err, storage.ErrQuoteNotFound) {
|
||||||
|
h.logger.Warn(
|
||||||
|
"Failed to lookup quote by idempotency key",
|
||||||
|
zap.Error(err),
|
||||||
|
mzap.ObjRef("org_ref", qc.orgRef),
|
||||||
|
zap.String("idempotency_key", qc.idempotencyKey),
|
||||||
|
)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if existing != nil {
|
||||||
|
if existing.Hash != qc.hash {
|
||||||
|
return nil, errIdempotencyParamMismatch
|
||||||
|
}
|
||||||
|
h.logger.Debug(
|
||||||
|
"Idempotent quote reused",
|
||||||
|
mzap.ObjRef("org_ref", qc.orgRef),
|
||||||
|
zap.String("idempotency_key", qc.idempotencyKey),
|
||||||
|
zap.String("quote_ref", existing.QuoteRef),
|
||||||
|
)
|
||||||
|
return modelQuoteToProto(existing.Quote), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
quote, expiresAt, err := h.engine.BuildPaymentQuote(ctx, qc.orgID, req)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warn(
|
||||||
|
"Failed to build payment quote",
|
||||||
|
zap.Error(err),
|
||||||
|
mzap.ObjRef("org_ref", qc.orgRef),
|
||||||
|
zap.String("idempotency_key", qc.idempotencyKey),
|
||||||
|
)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
quoteRef := primitive.NewObjectID().Hex()
|
||||||
|
quote.QuoteRef = quoteRef
|
||||||
|
|
||||||
|
record := &model.PaymentQuoteRecord{
|
||||||
|
QuoteRef: quoteRef,
|
||||||
|
IdempotencyKey: qc.idempotencyKey,
|
||||||
|
Hash: qc.hash,
|
||||||
|
Intent: intentFromProto(qc.intent),
|
||||||
|
Quote: quoteSnapshotToModel(quote),
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
}
|
||||||
|
record.SetID(primitive.NewObjectID())
|
||||||
|
record.SetOrganizationRef(qc.orgRef)
|
||||||
|
|
||||||
|
if err := quotesStore.Create(ctx, record); err != nil {
|
||||||
|
if errors.Is(err, storage.ErrDuplicateQuote) {
|
||||||
|
existing, getErr := quotesStore.GetByIdempotencyKey(ctx, qc.idempotencyKey)
|
||||||
|
if getErr == nil && existing != nil {
|
||||||
|
if existing.Hash != qc.hash {
|
||||||
|
return nil, errIdempotencyParamMismatch
|
||||||
|
}
|
||||||
|
return modelQuoteToProto(existing.Quote), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Info(
|
||||||
|
"Stored payment quote",
|
||||||
|
zap.String("quote_ref", quoteRef),
|
||||||
|
mzap.ObjRef("org_ref", qc.orgRef),
|
||||||
|
zap.String("idempotency_key", qc.idempotencyKey),
|
||||||
|
zap.String("kind", qc.intent.GetKind().String()),
|
||||||
|
)
|
||||||
|
|
||||||
|
return quote, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *quotePaymentCommand) mapQuoteErr(err error) gsresponse.Responder[orchestratorv1.QuotePaymentResponse] {
|
||||||
|
if errors.Is(err, errIdempotencyRequired) ||
|
||||||
|
errors.Is(err, errPreviewWithIdempotency) ||
|
||||||
|
errors.Is(err, errIdempotencyParamMismatch) {
|
||||||
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
|
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
}
|
}
|
||||||
intent := req.GetIntent()
|
return gsresponse.Auto[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
|
||||||
quote, expiresAt, err := h.engine.BuildPaymentQuote(ctx, orgRef, req)
|
// TODO: temprorarary hashing function, replace with a proper solution later
|
||||||
|
func hashQuoteRequest(req *orchestratorv1.QuotePaymentRequest) string {
|
||||||
|
cloned := proto.Clone(req).(*orchestratorv1.QuotePaymentRequest)
|
||||||
|
cloned.Meta = nil
|
||||||
|
cloned.IdempotencyKey = ""
|
||||||
|
cloned.PreviewOnly = false
|
||||||
|
|
||||||
|
b, err := proto.MarshalOptions{Deterministic: true}.Marshal(cloned)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gsresponse.Auto[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
|
sum := sha256.Sum256([]byte("marshal_error"))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
if !req.GetPreviewOnly() {
|
sum := sha256.Sum256(b)
|
||||||
quotesStore, err := ensureQuotesStore(h.engine.Repository())
|
return hex.EncodeToString(sum[:])
|
||||||
if err != nil {
|
|
||||||
return gsresponse.Unavailable[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
|
|
||||||
}
|
|
||||||
quoteRef := primitive.NewObjectID().Hex()
|
|
||||||
quote.QuoteRef = quoteRef
|
|
||||||
record := &model.PaymentQuoteRecord{
|
|
||||||
QuoteRef: quoteRef,
|
|
||||||
Intent: intentFromProto(intent),
|
|
||||||
Quote: quoteSnapshotToModel(quote),
|
|
||||||
ExpiresAt: expiresAt,
|
|
||||||
}
|
|
||||||
record.SetID(primitive.NewObjectID())
|
|
||||||
record.SetOrganizationRef(orgID)
|
|
||||||
if err := quotesStore.Create(ctx, record); err != nil {
|
|
||||||
return gsresponse.Auto[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
|
|
||||||
}
|
|
||||||
h.logger.Info(
|
|
||||||
"Stored payment quote",
|
|
||||||
zap.String("quote_ref", quoteRef),
|
|
||||||
mzap.ObjRef("org_ref", orgID),
|
|
||||||
zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
|
|
||||||
zap.String("kind", intent.GetKind().String()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return gsresponse.Success(&orchestratorv1.QuotePaymentResponse{Quote: quote})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type quotePaymentsCommand struct {
|
type quotePaymentsCommand struct {
|
||||||
@@ -79,76 +222,98 @@ type quotePaymentsCommand struct {
|
|||||||
logger mlogger.Logger
|
logger mlogger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *quotePaymentsCommand) Execute(ctx context.Context, req *orchestratorv1.QuotePaymentsRequest) gsresponse.Responder[orchestratorv1.QuotePaymentsResponse] {
|
var (
|
||||||
|
errBatchIdempotencyRequired = errors.New("idempotency key is required")
|
||||||
|
errBatchPreviewWithIdempotency = errors.New("preview requests must not use idempotency key")
|
||||||
|
errBatchIdempotencyParamMismatch = errors.New("idempotency key reuse with different parameters")
|
||||||
|
errBatchIdempotencyShapeMismatch = errors.New("idempotency key already used for a different quote shape")
|
||||||
|
)
|
||||||
|
|
||||||
|
type quotePaymentsCtx struct {
|
||||||
|
orgID string
|
||||||
|
orgRef primitive.ObjectID
|
||||||
|
previewOnly bool
|
||||||
|
idempotencyKey string
|
||||||
|
hash string
|
||||||
|
intentCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *quotePaymentsCommand) Execute(
|
||||||
|
ctx context.Context,
|
||||||
|
req *orchestratorv1.QuotePaymentsRequest,
|
||||||
|
) gsresponse.Responder[orchestratorv1.QuotePaymentsResponse] {
|
||||||
|
|
||||||
if err := h.engine.EnsureRepository(ctx); err != nil {
|
if err := h.engine.EnsureRepository(ctx); err != nil {
|
||||||
return gsresponse.Unavailable[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
return gsresponse.Unavailable[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
}
|
}
|
||||||
if req == nil {
|
if req == nil {
|
||||||
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request"))
|
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request"))
|
||||||
}
|
}
|
||||||
orgID, orgRef, err := validateMetaAndOrgRef(req.GetMeta())
|
|
||||||
|
qc, intents, err := h.prepare(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
return h.mapErr(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())
|
quotesStore, err := ensureQuotesStore(h.engine.Repository())
|
||||||
quotes := make([]*orchestratorv1.PaymentQuote, 0, len(intents))
|
if err != nil {
|
||||||
expires := make([]time.Time, 0, len(intents))
|
return gsresponse.Unavailable[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
for i, intent := range intents {
|
}
|
||||||
if err := requireNonNilIntent(intent); err != nil {
|
|
||||||
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
if qc.previewOnly {
|
||||||
}
|
quotes, expires, err := h.buildQuotes(ctx, req.GetMeta(), qc.idempotencyKey, intents, true)
|
||||||
quoteReq := &orchestratorv1.QuotePaymentRequest{
|
|
||||||
Meta: req.GetMeta(),
|
|
||||||
IdempotencyKey: perIntentIdempotencyKey(baseKey, i, len(intents)),
|
|
||||||
Intent: intent,
|
|
||||||
PreviewOnly: req.GetPreviewOnly(),
|
|
||||||
}
|
|
||||||
quote, expiresAt, err := h.engine.BuildPaymentQuote(ctx, orgID, quoteReq)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
}
|
}
|
||||||
quotes = append(quotes, quote)
|
aggregate, expiresAt, err := h.aggregate(quotes, expires)
|
||||||
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 {
|
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(orgRef)
|
|
||||||
if err := quotesStore.Create(ctx, record); err != nil {
|
|
||||||
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
}
|
}
|
||||||
h.logger.Info("Stored payment quotes",
|
_ = expiresAt
|
||||||
zap.String("quote_ref", quoteRef), mzap.ObjRef("org_ref", orgRef),
|
return gsresponse.Success(&orchestratorv1.QuotePaymentsResponse{
|
||||||
zap.String("idempotency_key", baseKey), zap.Int("quote_count", len(quotes)),
|
QuoteRef: "",
|
||||||
)
|
Aggregate: aggregate,
|
||||||
|
Quotes: quotes,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rec, ok, err := h.tryReuse(ctx, quotesStore, qc); err != nil {
|
||||||
|
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
} else if ok {
|
||||||
|
return gsresponse.Success(h.responseFromRecord(rec))
|
||||||
|
}
|
||||||
|
|
||||||
|
quotes, expires, err := h.buildQuotes(ctx, req.GetMeta(), qc.idempotencyKey, intents, false)
|
||||||
|
if err != nil {
|
||||||
|
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregate, expiresAt, err := h.aggregate(quotes, expires)
|
||||||
|
if err != nil {
|
||||||
|
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
quoteRef := primitive.NewObjectID().Hex()
|
||||||
|
for _, q := range quotes {
|
||||||
|
if q != nil {
|
||||||
|
q.QuoteRef = quoteRef
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rec, err := h.storeBatch(ctx, quotesStore, qc, quoteRef, intents, quotes, expiresAt)
|
||||||
|
if err != nil {
|
||||||
|
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rec != nil {
|
||||||
|
return gsresponse.Success(h.responseFromRecord(rec))
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Info(
|
||||||
|
"Stored payment quotes",
|
||||||
|
h.logFields(qc, quoteRef, expiresAt, len(quotes))...,
|
||||||
|
)
|
||||||
|
|
||||||
return gsresponse.Success(&orchestratorv1.QuotePaymentsResponse{
|
return gsresponse.Success(&orchestratorv1.QuotePaymentsResponse{
|
||||||
QuoteRef: quoteRef,
|
QuoteRef: quoteRef,
|
||||||
Aggregate: aggregate,
|
Aggregate: aggregate,
|
||||||
@@ -156,6 +321,256 @@ func (h *quotePaymentsCommand) Execute(ctx context.Context, req *orchestratorv1.
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *quotePaymentsCommand) prepare(req *orchestratorv1.QuotePaymentsRequest) (*quotePaymentsCtx, []*orchestratorv1.PaymentIntent, error) {
|
||||||
|
orgRefStr, orgID, err := validateMetaAndOrgRef(req.GetMeta())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
intents := req.GetIntents()
|
||||||
|
if len(intents) == 0 {
|
||||||
|
return nil, nil, merrors.InvalidArgument("intents are required")
|
||||||
|
}
|
||||||
|
for _, intent := range intents {
|
||||||
|
if err := requireNonNilIntent(intent); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preview := req.GetPreviewOnly()
|
||||||
|
idem := strings.TrimSpace(req.GetIdempotencyKey())
|
||||||
|
|
||||||
|
if preview && idem != "" {
|
||||||
|
return nil, nil, errBatchPreviewWithIdempotency
|
||||||
|
}
|
||||||
|
if !preview && idem == "" {
|
||||||
|
return nil, nil, errBatchIdempotencyRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := hashQuotePaymentsIntents(intents)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return "ePaymentsCtx{
|
||||||
|
orgID: orgRefStr,
|
||||||
|
orgRef: orgID,
|
||||||
|
previewOnly: preview,
|
||||||
|
idempotencyKey: idem,
|
||||||
|
hash: hash,
|
||||||
|
intentCount: len(intents),
|
||||||
|
}, intents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *quotePaymentsCommand) tryReuse(
|
||||||
|
ctx context.Context,
|
||||||
|
quotesStore storage.QuotesStore,
|
||||||
|
qc *quotePaymentsCtx,
|
||||||
|
) (*model.PaymentQuoteRecord, bool, error) {
|
||||||
|
|
||||||
|
rec, err := quotesStore.GetByIdempotencyKey(ctx, qc.idempotencyKey)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, storage.ErrQuoteNotFound) {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
h.logger.Warn(
|
||||||
|
"Failed to lookup payment quotes by idempotency key",
|
||||||
|
h.logFields(qc, "", time.Time{}, 0)...,
|
||||||
|
)
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rec.Quotes) == 0 {
|
||||||
|
return nil, false, errBatchIdempotencyShapeMismatch
|
||||||
|
}
|
||||||
|
if rec.Hash != qc.hash {
|
||||||
|
return nil, false, errBatchIdempotencyParamMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.Debug(
|
||||||
|
"Idempotent payment quotes reused",
|
||||||
|
h.logFields(qc, rec.QuoteRef, rec.ExpiresAt, len(rec.Quotes))...,
|
||||||
|
)
|
||||||
|
|
||||||
|
return rec, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *quotePaymentsCommand) buildQuotes(
|
||||||
|
ctx context.Context,
|
||||||
|
meta *orchestratorv1.RequestMeta,
|
||||||
|
baseKey string,
|
||||||
|
intents []*orchestratorv1.PaymentIntent,
|
||||||
|
preview bool,
|
||||||
|
) ([]*orchestratorv1.PaymentQuote, []time.Time, error) {
|
||||||
|
|
||||||
|
quotes := make([]*orchestratorv1.PaymentQuote, 0, len(intents))
|
||||||
|
expires := make([]time.Time, 0, len(intents))
|
||||||
|
|
||||||
|
for i, intent := range intents {
|
||||||
|
req := &orchestratorv1.QuotePaymentRequest{
|
||||||
|
Meta: meta,
|
||||||
|
IdempotencyKey: perIntentIdempotencyKey(baseKey, i, len(intents)),
|
||||||
|
Intent: intent,
|
||||||
|
PreviewOnly: preview,
|
||||||
|
}
|
||||||
|
q, exp, err := h.engine.BuildPaymentQuote(ctx, meta.GetOrganizationRef(), req)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warn(
|
||||||
|
"Failed to build payment quote (batch item)",
|
||||||
|
zap.Int("idx", i),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
quotes = append(quotes, q)
|
||||||
|
expires = append(expires, exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return quotes, expires, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *quotePaymentsCommand) aggregate(
|
||||||
|
quotes []*orchestratorv1.PaymentQuote,
|
||||||
|
expires []time.Time,
|
||||||
|
) (*orchestratorv1.PaymentQuoteAggregate, time.Time, error) {
|
||||||
|
|
||||||
|
agg, err := aggregatePaymentQuotes(quotes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, time.Time{}, merrors.InternalWrap(err, "quote aggregation failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
expiresAt, ok := minQuoteExpiry(expires)
|
||||||
|
if !ok {
|
||||||
|
return nil, time.Time{}, merrors.Internal("quote expiry missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
return agg, expiresAt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *quotePaymentsCommand) storeBatch(
|
||||||
|
ctx context.Context,
|
||||||
|
quotesStore storage.QuotesStore,
|
||||||
|
qc *quotePaymentsCtx,
|
||||||
|
quoteRef string,
|
||||||
|
intents []*orchestratorv1.PaymentIntent,
|
||||||
|
quotes []*orchestratorv1.PaymentQuote,
|
||||||
|
expiresAt time.Time,
|
||||||
|
) (*model.PaymentQuoteRecord, error) {
|
||||||
|
|
||||||
|
record := &model.PaymentQuoteRecord{
|
||||||
|
QuoteRef: quoteRef,
|
||||||
|
IdempotencyKey: qc.idempotencyKey,
|
||||||
|
Hash: qc.hash,
|
||||||
|
Intents: intentsFromProto(intents),
|
||||||
|
Quotes: quoteSnapshotsFromProto(quotes),
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
}
|
||||||
|
record.SetID(primitive.NewObjectID())
|
||||||
|
record.SetOrganizationRef(qc.orgRef)
|
||||||
|
|
||||||
|
if err := quotesStore.Create(ctx, record); err != nil {
|
||||||
|
if errors.Is(err, storage.ErrDuplicateQuote) {
|
||||||
|
rec, ok, reuseErr := h.tryReuse(ctx, quotesStore, qc)
|
||||||
|
if reuseErr != nil {
|
||||||
|
return nil, reuseErr
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
return rec, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *quotePaymentsCommand) responseFromRecord(rec *model.PaymentQuoteRecord) *orchestratorv1.QuotePaymentsResponse {
|
||||||
|
quotes := modelQuotesToProto(rec.Quotes)
|
||||||
|
for _, q := range quotes {
|
||||||
|
if q != nil {
|
||||||
|
q.QuoteRef = rec.QuoteRef
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aggregate, _ := aggregatePaymentQuotes(quotes)
|
||||||
|
|
||||||
|
return &orchestratorv1.QuotePaymentsResponse{
|
||||||
|
QuoteRef: rec.QuoteRef,
|
||||||
|
Aggregate: aggregate,
|
||||||
|
Quotes: quotes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *quotePaymentsCommand) logFields(qc *quotePaymentsCtx, quoteRef string, expiresAt time.Time, quoteCount int) []zap.Field {
|
||||||
|
fields := []zap.Field{
|
||||||
|
mzap.ObjRef("org_ref", qc.orgRef),
|
||||||
|
zap.String("org_ref_str", qc.orgID),
|
||||||
|
zap.String("idempotency_key", qc.idempotencyKey),
|
||||||
|
zap.String("hash", qc.hash),
|
||||||
|
zap.Bool("preview_only", qc.previewOnly),
|
||||||
|
zap.Int("intent_count", qc.intentCount),
|
||||||
|
}
|
||||||
|
if quoteRef != "" {
|
||||||
|
fields = append(fields, zap.String("quote_ref", quoteRef))
|
||||||
|
}
|
||||||
|
if !expiresAt.IsZero() {
|
||||||
|
fields = append(fields, zap.Time("expires_at", expiresAt))
|
||||||
|
}
|
||||||
|
if quoteCount > 0 {
|
||||||
|
fields = append(fields, zap.Int("quote_count", quoteCount))
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *quotePaymentsCommand) mapErr(err error) gsresponse.Responder[orchestratorv1.QuotePaymentsResponse] {
|
||||||
|
if errors.Is(err, errBatchIdempotencyRequired) ||
|
||||||
|
errors.Is(err, errBatchPreviewWithIdempotency) ||
|
||||||
|
errors.Is(err, errBatchIdempotencyParamMismatch) ||
|
||||||
|
errors.Is(err, errBatchIdempotencyShapeMismatch) {
|
||||||
|
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func modelQuotesToProto(snaps []*model.PaymentQuoteSnapshot) []*orchestratorv1.PaymentQuote {
|
||||||
|
if len(snaps) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*orchestratorv1.PaymentQuote, 0, len(snaps))
|
||||||
|
for _, s := range snaps {
|
||||||
|
out = append(out, modelQuoteToProto(s))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashQuotePaymentsIntents(intents []*orchestratorv1.PaymentIntent) (string, error) {
|
||||||
|
type item struct {
|
||||||
|
Idx int
|
||||||
|
H [32]byte
|
||||||
|
}
|
||||||
|
items := make([]item, 0, len(intents))
|
||||||
|
|
||||||
|
for i, intent := range intents {
|
||||||
|
b, err := proto.MarshalOptions{Deterministic: true}.Marshal(intent)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
items = append(items, item{Idx: i, H: sha256.Sum256(b)})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(items, func(i, j int) bool { return items[i].Idx < items[j].Idx })
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write([]byte("quote-payments-fp/v1"))
|
||||||
|
h.Write([]byte{0})
|
||||||
|
for _, it := range items {
|
||||||
|
h.Write(it.H[:])
|
||||||
|
h.Write([]byte{0})
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(h.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
type initiatePaymentsCommand struct {
|
type initiatePaymentsCommand struct {
|
||||||
engine paymentEngine
|
engine paymentEngine
|
||||||
logger mlogger.Logger
|
logger mlogger.Logger
|
||||||
|
|||||||
@@ -429,3 +429,15 @@ func (s *helperQuotesStore) GetByRef(_ context.Context, _ primitive.ObjectID, re
|
|||||||
}
|
}
|
||||||
return nil, storage.ErrQuoteNotFound
|
return nil, storage.ErrQuoteNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *helperQuotesStore) GetByIdempotencyKey(_ context.Context, ref string) (*model.PaymentQuoteRecord, error) {
|
||||||
|
if s.records == nil {
|
||||||
|
return nil, storage.ErrQuoteNotFound
|
||||||
|
}
|
||||||
|
for _, rec := range s.records {
|
||||||
|
if rec.IdempotencyKey == ref {
|
||||||
|
return rec, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, storage.ErrQuoteNotFound
|
||||||
|
}
|
||||||
|
|||||||
@@ -423,6 +423,18 @@ func (s *stubQuotesStore) GetByRef(ctx context.Context, orgRef primitive.ObjectI
|
|||||||
return nil, storage.ErrQuoteNotFound
|
return nil, storage.ErrQuoteNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *stubQuotesStore) GetByIdempotencyKey(ctx context.Context, idempotencyKey string) (*model.PaymentQuoteRecord, error) {
|
||||||
|
if s.quotes == nil {
|
||||||
|
return nil, storage.ErrQuoteNotFound
|
||||||
|
}
|
||||||
|
for _, q := range s.quotes {
|
||||||
|
if q.IdempotencyKey == idempotencyKey {
|
||||||
|
return q, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, storage.ErrQuoteNotFound
|
||||||
|
}
|
||||||
|
|
||||||
type stubRoutesStore struct {
|
type stubRoutesStore struct {
|
||||||
routes []*model.PaymentRoute
|
routes []*model.PaymentRoute
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ type PaymentQuoteRecord struct {
|
|||||||
storable.Base `bson:",inline" json:",inline"`
|
storable.Base `bson:",inline" json:",inline"`
|
||||||
model.OrganizationBoundBase `bson:",inline" json:",inline"`
|
model.OrganizationBoundBase `bson:",inline" json:",inline"`
|
||||||
|
|
||||||
QuoteRef string `bson:"quoteRef" json:"quoteRef"`
|
QuoteRef string `bson:"quoteRef" json:"quoteRef"`
|
||||||
Intent PaymentIntent `bson:"intent,omitempty" json:"intent,omitempty"`
|
IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"`
|
||||||
Intents []PaymentIntent `bson:"intents,omitempty" json:"intents,omitempty"`
|
Intent PaymentIntent `bson:"intent,omitempty" json:"intent,omitempty"`
|
||||||
Quote *PaymentQuoteSnapshot `bson:"quote,omitempty" json:"quote,omitempty"`
|
Intents []PaymentIntent `bson:"intents,omitempty" json:"intents,omitempty"`
|
||||||
Quotes []*PaymentQuoteSnapshot `bson:"quotes,omitempty" json:"quotes,omitempty"`
|
Quote *PaymentQuoteSnapshot `bson:"quote,omitempty" json:"quote,omitempty"`
|
||||||
ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
|
Quotes []*PaymentQuoteSnapshot `bson:"quotes,omitempty" json:"quotes,omitempty"`
|
||||||
|
ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
|
||||||
|
Hash string `bson:"hash" json:"hash"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collection implements storable.Storable.
|
// Collection implements storable.Storable.
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ func (q *Quotes) Create(ctx context.Context, quote *model.PaymentQuoteRecord) er
|
|||||||
if quote.OrganizationRef == primitive.NilObjectID {
|
if quote.OrganizationRef == primitive.NilObjectID {
|
||||||
return merrors.InvalidArgument("quotesStore: organization_ref is required")
|
return merrors.InvalidArgument("quotesStore: organization_ref is required")
|
||||||
}
|
}
|
||||||
|
if quote.IdempotencyKey == "" {
|
||||||
|
return merrors.InvalidArgument("quotesStore: idempotency key is required")
|
||||||
|
}
|
||||||
if quote.ExpiresAt.IsZero() {
|
if quote.ExpiresAt.IsZero() {
|
||||||
return merrors.InvalidArgument("quotesStore: expires_at is required")
|
return merrors.InvalidArgument("quotesStore: expires_at is required")
|
||||||
}
|
}
|
||||||
@@ -120,6 +123,25 @@ func (q *Quotes) GetByRef(ctx context.Context, orgRef primitive.ObjectID, quoteR
|
|||||||
return entity, nil
|
return entity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *Quotes) GetByIdempotencyKey(ctx context.Context, idempotencyKey string) (*model.PaymentQuoteRecord, error) {
|
||||||
|
idempotencyKey = strings.TrimSpace(idempotencyKey)
|
||||||
|
if idempotencyKey == "" {
|
||||||
|
return nil, merrors.InvalidArgument("quotesStore: empty idempotency key")
|
||||||
|
}
|
||||||
|
entity := &model.PaymentQuoteRecord{}
|
||||||
|
query := repository.Filter("idempotencyKey", idempotencyKey)
|
||||||
|
if err := q.repo.FindOneByFilter(ctx, query, entity); err != nil {
|
||||||
|
if errors.Is(err, merrors.ErrNoData) {
|
||||||
|
return nil, storage.ErrQuoteNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !entity.ExpiresAt.IsZero() && time.Now().After(entity.ExpiresAt) {
|
||||||
|
return nil, storage.ErrQuoteNotFound
|
||||||
|
}
|
||||||
|
return entity, nil
|
||||||
|
}
|
||||||
|
|
||||||
var _ storage.QuotesStore = (*Quotes)(nil)
|
var _ storage.QuotesStore = (*Quotes)(nil)
|
||||||
|
|
||||||
func int32Ptr(v int32) *int32 {
|
func int32Ptr(v int32) *int32 {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ type PaymentsStore interface {
|
|||||||
type QuotesStore interface {
|
type QuotesStore interface {
|
||||||
Create(ctx context.Context, quote *model.PaymentQuoteRecord) error
|
Create(ctx context.Context, quote *model.PaymentQuoteRecord) error
|
||||||
GetByRef(ctx context.Context, orgRef primitive.ObjectID, quoteRef string) (*model.PaymentQuoteRecord, error)
|
GetByRef(ctx context.Context, orgRef primitive.ObjectID, quoteRef string) (*model.PaymentQuoteRecord, error)
|
||||||
|
GetByIdempotencyKey(ctx context.Context, idempotencyKey string) (*model.PaymentQuoteRecord, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoutesStore manages allowed routing transitions.
|
// RoutesStore manages allowed routing transitions.
|
||||||
|
|||||||
@@ -93,6 +93,6 @@ require (
|
|||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.5.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ enum PaymentKind {
|
|||||||
// SettlementMode defines how to treat fees/FX variance for payouts.
|
// SettlementMode defines how to treat fees/FX variance for payouts.
|
||||||
enum SettlementMode {
|
enum SettlementMode {
|
||||||
SETTLEMENT_UNSPECIFIED = 0;
|
SETTLEMENT_UNSPECIFIED = 0;
|
||||||
SETTLEMENT_FIX_SOURCE = 1; // customer pays fees; sent amount fixed
|
SETTLEMENT_FIX_SOURCE = 1; // customer pays fees; sent amount fixed
|
||||||
SETTLEMENT_FIX_RECEIVED = 2; // receiver gets fixed amount; source flexes
|
SETTLEMENT_FIX_RECEIVED = 2; // receiver gets fixed amount; source flexes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ message ExternalChainEndpoint {
|
|||||||
// Card payout destination.
|
// Card payout destination.
|
||||||
message CardEndpoint {
|
message CardEndpoint {
|
||||||
oneof card {
|
oneof card {
|
||||||
string pan = 1; // raw PAN
|
string pan = 1; // raw PAN
|
||||||
string token = 2; // network or gateway-issued token
|
string token = 2; // network or gateway-issued token
|
||||||
}
|
}
|
||||||
string cardholder_name = 3;
|
string cardholder_name = 3;
|
||||||
@@ -234,6 +234,7 @@ message QuotePaymentRequest {
|
|||||||
|
|
||||||
message QuotePaymentResponse {
|
message QuotePaymentResponse {
|
||||||
PaymentQuote quote = 1;
|
PaymentQuote quote = 1;
|
||||||
|
string idempotency_key = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message QuotePaymentsRequest {
|
message QuotePaymentsRequest {
|
||||||
|
|||||||
@@ -139,6 +139,6 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||||
google.golang.org/grpc v1.78.0 // indirect
|
google.golang.org/grpc v1.78.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type FxQuote struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PaymentQuote struct {
|
type PaymentQuote struct {
|
||||||
|
IdempotencyKey string `json:"idempotencyKey"`
|
||||||
QuoteRef string `json:"quoteRef,omitempty"`
|
QuoteRef string `json:"quoteRef,omitempty"`
|
||||||
DebitAmount *model.Money `json:"debitAmount,omitempty"`
|
DebitAmount *model.Money `json:"debitAmount,omitempty"`
|
||||||
ExpectedSettlementAmount *model.Money `json:"expectedSettlementAmount,omitempty"`
|
ExpectedSettlementAmount *model.Money `json:"expectedSettlementAmount,omitempty"`
|
||||||
@@ -50,9 +51,10 @@ type PaymentQuoteAggregate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PaymentQuotes struct {
|
type PaymentQuotes struct {
|
||||||
QuoteRef string `json:"quoteRef,omitempty"`
|
IdempotencyKey string `json:"idempotencyKey"`
|
||||||
Aggregate *PaymentQuoteAggregate `json:"aggregate,omitempty"`
|
QuoteRef string `json:"quoteRef,omitempty"`
|
||||||
Quotes []PaymentQuote `json:"quotes,omitempty"`
|
Aggregate *PaymentQuoteAggregate `json:"aggregate,omitempty"`
|
||||||
|
Quotes []PaymentQuote `json:"quotes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Payment struct {
|
type Payment struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user