diff --git a/api/billing/fees/go.mod b/api/billing/fees/go.mod index bb520c4d..da4ca91d 100644 --- a/api/billing/fees/go.mod +++ b/api/billing/fees/go.mod @@ -49,6 +49,6 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.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 ) diff --git a/api/billing/fees/go.sum b/api/billing/fees/go.sum index 6c366ee7..d919df05 100644 --- a/api/billing/fees/go.sum +++ b/api/billing/fees/go.sum @@ -212,8 +212,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0= -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 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/discovery/go.mod b/api/discovery/go.mod index 1f9502b0..c78308b3 100644 --- a/api/discovery/go.mod +++ b/api/discovery/go.mod @@ -45,7 +45,7 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.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/protobuf v1.36.11 // indirect ) diff --git a/api/discovery/go.sum b/api/discovery/go.sum index 6c366ee7..d919df05 100644 --- a/api/discovery/go.sum +++ b/api/discovery/go.sum @@ -212,8 +212,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0= -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 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/fx/ingestor/go.mod b/api/fx/ingestor/go.mod index fe2c2307..0d77bb8a 100644 --- a/api/fx/ingestor/go.mod +++ b/api/fx/ingestor/go.mod @@ -49,7 +49,7 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.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/protobuf v1.36.11 // indirect ) diff --git a/api/fx/ingestor/go.sum b/api/fx/ingestor/go.sum index 6c366ee7..d919df05 100644 --- a/api/fx/ingestor/go.sum +++ b/api/fx/ingestor/go.sum @@ -212,8 +212,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0= -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 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/fx/oracle/go.mod b/api/fx/oracle/go.mod index 8a687451..2e5a0c9c 100644 --- a/api/fx/oracle/go.mod +++ b/api/fx/oracle/go.mod @@ -50,5 +50,5 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.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 ) diff --git a/api/fx/oracle/go.sum b/api/fx/oracle/go.sum index 6c366ee7..d919df05 100644 --- a/api/fx/oracle/go.sum +++ b/api/fx/oracle/go.sum @@ -212,8 +212,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0= -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 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/gateway/chain/go.mod b/api/gateway/chain/go.mod index 964b39b5..ed3243bc 100644 --- a/api/gateway/chain/go.mod +++ b/api/gateway/chain/go.mod @@ -86,5 +86,5 @@ require ( golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.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 ) diff --git a/api/gateway/chain/go.sum b/api/gateway/chain/go.sum index f8bfe9ce..55fa8020 100644 --- a/api/gateway/chain/go.sum +++ b/api/gateway/chain/go.sum @@ -362,8 +362,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0= -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 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/gateway/mntx/go.mod b/api/gateway/mntx/go.mod index 9ccfe1dc..df4dae87 100644 --- a/api/gateway/mntx/go.mod +++ b/api/gateway/mntx/go.mod @@ -50,5 +50,5 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.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 ) diff --git a/api/gateway/mntx/go.sum b/api/gateway/mntx/go.sum index 35298ad4..809b11c6 100644 --- a/api/gateway/mntx/go.sum +++ b/api/gateway/mntx/go.sum @@ -214,8 +214,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0= -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 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/gateway/tgsettle/go.mod b/api/gateway/tgsettle/go.mod index d44133e1..b347fbd9 100644 --- a/api/gateway/tgsettle/go.mod +++ b/api/gateway/tgsettle/go.mod @@ -47,5 +47,5 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.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 ) diff --git a/api/gateway/tgsettle/go.sum b/api/gateway/tgsettle/go.sum index 6c366ee7..d919df05 100644 --- a/api/gateway/tgsettle/go.sum +++ b/api/gateway/tgsettle/go.sum @@ -212,8 +212,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0= -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 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/ledger/go.mod b/api/ledger/go.mod index 764ee67e..792bb587 100644 --- a/api/ledger/go.mod +++ b/api/ledger/go.mod @@ -51,5 +51,5 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.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 ) diff --git a/api/ledger/go.sum b/api/ledger/go.sum index 1571bd2e..0be597c9 100644 --- a/api/ledger/go.sum +++ b/api/ledger/go.sum @@ -214,8 +214,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0= -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 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/ledger/internal/service/ledger/accounts.go b/api/ledger/internal/service/ledger/accounts.go index 585bccc2..c8875aca 100644 --- a/api/ledger/internal/service/ledger/accounts.go +++ b/api/ledger/internal/service/ledger/accounts.go @@ -11,10 +11,11 @@ import ( "github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/merrors" pmodel "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mutil/mzap" describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" - "go.uber.org/zap" "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -96,7 +97,7 @@ func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.Create if lookupErr != nil { s.logger.Warn("duplicate account create but failed to load existing", zap.Error(lookupErr), - zap.String("organizationRef", orgRef.Hex()), + mzap.ObjRef("organization_ref", orgRef), zap.String("accountCode", accountCode), zap.String("currency", currency)) 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") s.logger.Warn("failed to create account", zap.Error(err), - zap.String("organizationRef", orgRef.Hex()), + mzap.ObjRef("organization_ref", orgRef), zap.String("accountCode", accountCode), zap.String("currency", currency)) 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) { s.logger.Warn("failed to resolve default settlement account", zap.Error(err), - zap.String("organizationRef", orgRef.Hex()), + mzap.ObjRef("organization_ref", orgRef), zap.String("currency", normalizedCurrency)) 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", zap.Error(lookupErr), - zap.String("organizationRef", orgRef.Hex()), + mzap.ObjRef("organization_ref", orgRef), zap.String("currency", normalizedCurrency)) return nil, merrors.Internal("failed to resolve settlement account after conflict") } s.logger.Warn("failed to create default settlement account", zap.Error(err), - zap.String("organizationRef", orgRef.Hex()), + mzap.ObjRef("organization_ref", orgRef), zap.String("currency", normalizedCurrency), zap.String("accountCode", accountCode)) return nil, merrors.Internal("failed to create settlement account") } s.logger.Info("default settlement account created", - zap.String("organizationRef", orgRef.Hex()), + mzap.ObjRef("organization_ref", orgRef), zap.String("currency", normalizedCurrency), zap.String("accountCode", accountCode)) return account, nil diff --git a/api/ledger/internal/service/ledger/list_accounts.go b/api/ledger/internal/service/ledger/list_accounts.go index 02e352eb..91d8ead3 100644 --- a/api/ledger/internal/service/ledger/list_accounts.go +++ b/api/ledger/internal/service/ledger/list_accounts.go @@ -6,6 +6,7 @@ import ( "github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mutil/mzap" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" "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. accounts, err := s.storage.Accounts().ListByOrganization(ctx, orgRef, 0, 0) 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 } diff --git a/api/ledger/internal/service/ledger/posting.go b/api/ledger/internal/service/ledger/posting.go index 7655b6e5..5f76f771 100644 --- a/api/ledger/internal/service/ledger/posting.go +++ b/api/ledger/internal/service/ledger/posting.go @@ -3,6 +3,7 @@ package ledger import ( "context" "fmt" + "strings" "time" "github.com/tech/sendico/ledger/storage" @@ -10,6 +11,7 @@ import ( storageMongo "github.com/tech/sendico/ledger/storage/mongo" "github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mutil/mzap" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" "go.mongodb.org/mongo-driver/bson/primitive" "go.uber.org/zap" @@ -41,12 +43,20 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi if err != nil { 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) if err == nil && existingEntry != nil { recordDuplicateRequest("credit") - s.logger.Info("duplicate credit request (idempotency)", - zap.String("idempotencyKey", req.IdempotencyKey), + logger.Info("duplicate credit request (idempotency)", zap.String("existingEntryID", existingEntry.GetID().Hex())) return &ledgerv1.PostResponse{ JournalEntryRef: existingEntry.GetID().Hex(), @@ -56,7 +66,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi } if err != nil && err != storage.ErrJournalEntryNotFound { 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") } @@ -67,7 +77,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi return nil, merrors.NoData("account not found") } 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") } 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 if len(charges) == 0 { 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 { charges = computed } @@ -118,7 +128,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi if err == storage.ErrAccountNotFound { 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") } 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 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") } @@ -207,7 +217,7 @@ func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCredi } 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") } diff --git a/api/ledger/internal/service/ledger/posting_debit.go b/api/ledger/internal/service/ledger/posting_debit.go index 7faac354..3d7426ee 100644 --- a/api/ledger/internal/service/ledger/posting_debit.go +++ b/api/ledger/internal/service/ledger/posting_debit.go @@ -3,6 +3,7 @@ package ledger import ( "context" "fmt" + "strings" "time" "github.com/tech/sendico/ledger/storage" @@ -10,6 +11,7 @@ import ( storageMongo "github.com/tech/sendico/ledger/storage/mongo" "github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mutil/mzap" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" "go.mongodb.org/mongo-driver/bson/primitive" "go.uber.org/zap" @@ -39,12 +41,20 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR if err != nil { 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) if err == nil && existingEntry != nil { recordDuplicateRequest("debit") - s.logger.Info("duplicate debit request (idempotency)", - zap.String("idempotencyKey", req.IdempotencyKey), + logger.Info("duplicate debit request (idempotency)", zap.String("existingEntryID", existingEntry.GetID().Hex())) return &ledgerv1.PostResponse{ JournalEntryRef: existingEntry.GetID().Hex(), @@ -53,7 +63,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR }, nil } 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") } @@ -62,7 +72,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR if err == storage.ErrAccountNotFound { 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") } 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 if len(charges) == 0 { 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 { charges = computed } @@ -112,7 +122,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR if err == storage.ErrAccountNotFound { 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") } 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 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") } @@ -201,7 +211,7 @@ func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitR } 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") } diff --git a/api/ledger/internal/service/ledger/posting_fx.go b/api/ledger/internal/service/ledger/posting_fx.go index bd4d1d13..b0cfea25 100644 --- a/api/ledger/internal/service/ledger/posting_fx.go +++ b/api/ledger/internal/service/ledger/posting_fx.go @@ -10,6 +10,7 @@ import ( storageMongo "github.com/tech/sendico/ledger/storage/mongo" "github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mutil/mzap" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" "go.mongodb.org/mongo-driver/bson/primitive" "go.uber.org/zap" @@ -62,13 +63,21 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp if err != nil { 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 existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) if err == nil && existingEntry != nil { recordDuplicateRequest("fx") - s.logger.Info("duplicate FX request (idempotency)", - zap.String("idempotencyKey", req.IdempotencyKey), + logger.Info("duplicate FX request (idempotency)", zap.String("existingEntryID", existingEntry.GetID().Hex())) return &ledgerv1.PostResponse{ JournalEntryRef: existingEntry.GetID().Hex(), @@ -77,7 +86,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp }, nil } 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") } @@ -87,7 +96,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp if err == storage.ErrAccountNotFound { 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") } 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 { 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") } 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 { 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") } 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 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") } @@ -220,7 +229,7 @@ func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresp } 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") } diff --git a/api/ledger/internal/service/ledger/posting_support.go b/api/ledger/internal/service/ledger/posting_support.go index e5c20aee..a1e2175b 100644 --- a/api/ledger/internal/service/ledger/posting_support.go +++ b/api/ledger/internal/service/ledger/posting_support.go @@ -11,6 +11,7 @@ import ( "github.com/tech/sendico/ledger/storage" "github.com/tech/sendico/ledger/storage/model" "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mutil/mzap" "go.mongodb.org/mongo-driver/bson/primitive" "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", zap.Error(err), - zap.String("organizationRef", orgRef.Hex()), + mzap.ObjRef("organization_ref", orgRef), zap.String("currency", currency)) 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 { account := accounts[accountRef] 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") } @@ -140,7 +141,7 @@ func (s *Service) upsertBalances(ctx context.Context, lines []*model.PostingLine if err != nil && !errors.Is(err, storage.ErrBalanceNotFound) { s.logger.Warn("failed to fetch account balance", zap.Error(err), - zap.String("accountRef", accountRef.Hex())) + mzap.ObjRef("account_ref", accountRef)) 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 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") } } diff --git a/api/ledger/internal/service/ledger/posting_transfer.go b/api/ledger/internal/service/ledger/posting_transfer.go index 00534a99..56d89360 100644 --- a/api/ledger/internal/service/ledger/posting_transfer.go +++ b/api/ledger/internal/service/ledger/posting_transfer.go @@ -10,6 +10,7 @@ import ( storageMongo "github.com/tech/sendico/ledger/storage/mongo" "github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mutil/mzap" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" "go.mongodb.org/mongo-driver/bson/primitive" "go.uber.org/zap" @@ -53,13 +54,19 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq if err != nil { 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 existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) if err == nil && existingEntry != nil { recordDuplicateRequest("transfer") - s.logger.Info("duplicate transfer request (idempotency)", - zap.String("idempotencyKey", req.IdempotencyKey), + logger.Info("duplicate transfer request (idempotency)", zap.String("existingEntryID", existingEntry.GetID().Hex())) return &ledgerv1.PostResponse{ JournalEntryRef: existingEntry.GetID().Hex(), @@ -68,7 +75,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq }, nil } 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") } @@ -78,7 +85,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq if err == storage.ErrAccountNotFound { 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") } 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 { 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") } 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 { 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") } 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 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") } @@ -206,7 +213,7 @@ func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferReq } 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") } diff --git a/api/ledger/internal/service/ledger/queries.go b/api/ledger/internal/service/ledger/queries.go index bf542554..da80179c 100644 --- a/api/ledger/internal/service/ledger/queries.go +++ b/api/ledger/internal/service/ledger/queries.go @@ -10,6 +10,7 @@ import ( "github.com/tech/sendico/ledger/storage" "github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mutil/mzap" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" "go.uber.org/zap" @@ -27,6 +28,7 @@ func (s *Service) getBalanceResponder(_ context.Context, req *ledgerv1.GetBalanc if err != nil { return nil, err } + logger := s.logger.With(mzap.ObjRef("ledger_account_ref", accountRef)) // Get account to verify it exists 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 { 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") } @@ -53,7 +55,7 @@ func (s *Service) getBalanceResponder(_ context.Context, req *ledgerv1.GetBalanc LastUpdated: timestamppb.Now(), }, 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") } @@ -82,6 +84,7 @@ func (s *Service) getJournalEntryResponder(_ context.Context, req *ledgerv1.GetE if err != nil { return nil, err } + logger := s.logger.With(mzap.ObjRef("entry_ref", entryRef)) // Get journal entry 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 { 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") } // Get posting lines for this entry lines, err := s.storage.PostingLines().ListByJournalEntry(ctx, entryRef) 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") } @@ -140,6 +143,7 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat if err != nil { return nil, err } + logger := s.logger.With(mzap.ObjRef("ledger_account_ref", accountRef)) // Verify account exists _, err = s.storage.Accounts().Get(ctx, accountRef) @@ -147,7 +151,7 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat if err == storage.ErrAccountNotFound { 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") } @@ -167,11 +171,12 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat 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 postingLines, err := s.storage.PostingLines().ListByAccount(ctx, accountRef, limit+1, offset) 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") } @@ -189,18 +194,22 @@ func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStat entries := make([]*ledgerv1.JournalEntryResponse, 0) 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) 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 } // Get all lines for this entry lines, err := s.storage.PostingLines().ListByJournalEntry(ctx, entryRef) 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 } diff --git a/api/ledger/storage/mongo/store/accounts.go b/api/ledger/storage/mongo/store/accounts.go index b215a513..28051d8a 100644 --- a/api/ledger/storage/mongo/store/accounts.go +++ b/api/ledger/storage/mongo/store/accounts.go @@ -10,6 +10,7 @@ import ( ri "github.com/tech/sendico/pkg/db/repository/index" "github.com/tech/sendico/pkg/merrors" "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/mongo" "go.uber.org/zap" @@ -87,14 +88,14 @@ func (a *accountsStore) Get(ctx context.Context, accountRef primitive.ObjectID) result := &model.Account{} if err := a.repo.Get(ctx, accountRef, result); err != nil { 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 } - 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 } - 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)) return result, nil } @@ -156,11 +157,11 @@ func (a *accountsStore) GetDefaultSettlement(ctx context.Context, orgRef primiti if errors.Is(err, merrors.ErrNoData) { a.logger.Debug("default settlement account not found", zap.String("currency", currency), - zap.String("organizationRef", orgRef.Hex())) + mzap.ObjRef("organization_ref", orgRef)) return nil, storage.ErrAccountNotFound } 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)) 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) 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 } - 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))) return nil } diff --git a/api/ledger/storage/mongo/store/balances.go b/api/ledger/storage/mongo/store/balances.go index 5c5c4288..b30f4bff 100644 --- a/api/ledger/storage/mongo/store/balances.go +++ b/api/ledger/storage/mongo/store/balances.go @@ -10,6 +10,7 @@ import ( ri "github.com/tech/sendico/pkg/db/repository/index" "github.com/tech/sendico/pkg/merrors" "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/mongo" "go.uber.org/zap" @@ -55,14 +56,14 @@ func (b *balancesStore) Get(ctx context.Context, accountRef primitive.ObjectID) result := &model.AccountBalance{} if err := b.repo.FindOneByFilter(ctx, query, result); err != nil { 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 } - 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 } - 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)) return result, nil } diff --git a/api/ledger/storage/mongo/store/journal_entries.go b/api/ledger/storage/mongo/store/journal_entries.go index 8968ef20..1dcb6215 100644 --- a/api/ledger/storage/mongo/store/journal_entries.go +++ b/api/ledger/storage/mongo/store/journal_entries.go @@ -10,6 +10,7 @@ import ( ri "github.com/tech/sendico/pkg/db/repository/index" "github.com/tech/sendico/pkg/merrors" "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/mongo" "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 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 } j.logger.Warn("failed to create journal entry", zap.Error(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))) return nil } @@ -86,15 +87,15 @@ func (j *journalEntriesStore) Get(ctx context.Context, entryRef primitive.Object result := &model.JournalEntry{} if err := j.repo.Get(ctx, entryRef, result); err != nil { 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 } - 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 } - j.logger.Debug("journal entry loaded", zap.String("entryRef", entryRef.Hex()), - zap.String("idempotencyKey", result.IdempotencyKey)) + j.logger.Debug("journal entry loaded", mzap.ObjRef("entry_ref", entryRef), + zap.String("idempotency_key", result.IdempotencyKey)) return result, nil } @@ -115,15 +116,15 @@ func (j *journalEntriesStore) GetByIdempotencyKey(ctx context.Context, orgRef pr result := &model.JournalEntry{} if err := j.repo.FindOneByFilter(ctx, query, result); err != nil { 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 } 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 } - 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 } diff --git a/api/ledger/storage/mongo/store/posting_lines.go b/api/ledger/storage/mongo/store/posting_lines.go index 03c26dfa..cfe3c69b 100644 --- a/api/ledger/storage/mongo/store/posting_lines.go +++ b/api/ledger/storage/mongo/store/posting_lines.go @@ -10,6 +10,7 @@ import ( "github.com/tech/sendico/pkg/db/storable" "github.com/tech/sendico/pkg/merrors" "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/mongo" "go.uber.org/zap" @@ -97,11 +98,11 @@ func (p *postingLinesStore) ListByJournalEntry(ctx context.Context, entryRef pri return 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 } - 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 } @@ -129,10 +130,10 @@ func (p *postingLinesStore) ListByAccount(ctx context.Context, accountRef primit return 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 } - 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 } diff --git a/api/notification/go.mod b/api/notification/go.mod index 949d5742..f1fa6bd9 100644 --- a/api/notification/go.mod +++ b/api/notification/go.mod @@ -52,7 +52,7 @@ require ( golang.org/x/net v0.49.0 // indirect golang.org/x/sync v0.19.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/protobuf v1.36.11 // indirect ) diff --git a/api/notification/go.sum b/api/notification/go.sum index 5444e385..55c4e9f4 100644 --- a/api/notification/go.sum +++ b/api/notification/go.sum @@ -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= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0= -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 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/payments/orchestrator/go.mod b/api/payments/orchestrator/go.mod index 4e2d8667..3ff84f7b 100644 --- a/api/payments/orchestrator/go.mod +++ b/api/payments/orchestrator/go.mod @@ -62,5 +62,5 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.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 ) diff --git a/api/payments/orchestrator/go.sum b/api/payments/orchestrator/go.sum index b2d58631..9d8d301c 100644 --- a/api/payments/orchestrator/go.sum +++ b/api/payments/orchestrator/go.sum @@ -215,8 +215,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0= -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 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/payments/orchestrator/internal/server/internal/builders.go b/api/payments/orchestrator/internal/server/internal/builders.go index 1b5c8922..abb65aa7 100644 --- a/api/payments/orchestrator/internal/server/internal/builders.go +++ b/api/payments/orchestrator/internal/server/internal/builders.go @@ -3,13 +3,9 @@ package serverimp import ( "strings" - chainclient "github.com/tech/sendico/gateway/chain/client" "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/mlogger" - "github.com/tech/sendico/pkg/payments/rail" - "go.uber.org/zap" ) 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) } - -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 -} diff --git a/api/payments/orchestrator/internal/server/internal/clients.go b/api/payments/orchestrator/internal/server/internal/clients.go index d0ff9910..ec94130c 100644 --- a/api/payments/orchestrator/internal/server/internal/clients.go +++ b/api/payments/orchestrator/internal/server/internal/clients.go @@ -1,159 +1,5 @@ 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() { if i.discoveryClients != nil { i.discoveryClients.Close() diff --git a/api/payments/orchestrator/internal/server/internal/config.go b/api/payments/orchestrator/internal/server/internal/config.go index 121ef828..f8739aba 100644 --- a/api/payments/orchestrator/internal/server/internal/config.go +++ b/api/payments/orchestrator/internal/server/internal/config.go @@ -77,17 +77,6 @@ type limitsOverrideCfg struct { 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 { if c.CallTimeoutSecs <= 0 { return 3 * time.Second diff --git a/api/payments/orchestrator/internal/server/internal/dependencies.go b/api/payments/orchestrator/internal/server/internal/dependencies.go index 15b948b3..238ea93d 100644 --- a/api/payments/orchestrator/internal/server/internal/dependencies.go +++ b/api/payments/orchestrator/internal/server/internal/dependencies.go @@ -16,7 +16,7 @@ type orchestratorDeps struct { gatewayInvokeResolver orchestrator.GatewayInvokeResolver } -func (i *Imp) initDependencies(cfg *config) *orchestratorDeps { +func (i *Imp) initDependencies(_ *config) *orchestratorDeps { deps := &orchestratorDeps{} if i.discoveryReg == nil { if i.logger != nil { diff --git a/api/payments/orchestrator/internal/service/orchestrator/handlers_commands.go b/api/payments/orchestrator/internal/service/orchestrator/handlers_commands.go index 80ab2119..4e2b4a71 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/handlers_commands.go +++ b/api/payments/orchestrator/internal/service/orchestrator/handlers_commands.go @@ -2,7 +2,10 @@ package orchestrator import ( "context" + "crypto/sha256" + "encoding/hex" "errors" + "sort" "strings" "time" @@ -16,6 +19,7 @@ import ( orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" "go.mongodb.org/mongo-driver/bson/primitive" "go.uber.org/zap" + "google.golang.org/protobuf/proto" ) type quotePaymentCommand struct { @@ -23,55 +27,194 @@ type quotePaymentCommand struct { 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 { return gsresponse.Unavailable[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err) } if req == nil { 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()) if err != nil { - return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err) + return nil, err } 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) } - 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 { - return gsresponse.Auto[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err) + sum := sha256.Sum256([]byte("marshal_error")) + return hex.EncodeToString(sum[:]) } - if !req.GetPreviewOnly() { - quotesStore, err := ensureQuotesStore(h.engine.Repository()) - 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}) + sum := sha256.Sum256(b) + return hex.EncodeToString(sum[:]) } type quotePaymentsCommand struct { @@ -79,76 +222,98 @@ type quotePaymentsCommand struct { 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 { return gsresponse.Unavailable[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err) } if req == nil { return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request")) } - orgID, orgRef, err := validateMetaAndOrgRef(req.GetMeta()) + + qc, intents, err := h.prepare(req) if err != nil { - return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err) - } - intents := req.GetIntents() - if len(intents) == 0 { - return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("intents are required")) + return h.mapErr(err) } - baseKey := strings.TrimSpace(req.GetIdempotencyKey()) - quotes := make([]*orchestratorv1.PaymentQuote, 0, len(intents)) - expires := make([]time.Time, 0, len(intents)) - for i, intent := range intents { - if err := requireNonNilIntent(intent); err != nil { - return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err) - } - quoteReq := &orchestratorv1.QuotePaymentRequest{ - Meta: req.GetMeta(), - IdempotencyKey: perIntentIdempotencyKey(baseKey, i, len(intents)), - Intent: intent, - PreviewOnly: req.GetPreviewOnly(), - } - quote, expiresAt, err := h.engine.BuildPaymentQuote(ctx, orgID, quoteReq) + quotesStore, err := ensureQuotesStore(h.engine.Repository()) + if err != nil { + return gsresponse.Unavailable[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err) + } + + if qc.previewOnly { + quotes, expires, err := h.buildQuotes(ctx, req.GetMeta(), qc.idempotencyKey, intents, true) if err != nil { return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err) } - quotes = append(quotes, quote) - expires = append(expires, expiresAt) - } - - aggregate, err := aggregatePaymentQuotes(quotes) - if err != nil { - return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InternalWrap(err, "quote aggregation failed")) - } - expiresAt, ok := minQuoteExpiry(expires) - if !ok { - return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.Internal("quote expiry missing")) - } - - quoteRef := "" - if !req.GetPreviewOnly() { - quotesStore, err := ensureQuotesStore(h.engine.Repository()) + aggregate, expiresAt, err := h.aggregate(quotes, expires) 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) } - h.logger.Info("Stored payment quotes", - zap.String("quote_ref", quoteRef), mzap.ObjRef("org_ref", orgRef), - zap.String("idempotency_key", baseKey), zap.Int("quote_count", len(quotes)), - ) + _ = expiresAt + return gsresponse.Success(&orchestratorv1.QuotePaymentsResponse{ + 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{ QuoteRef: quoteRef, 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 { engine paymentEngine logger mlogger.Logger diff --git a/api/payments/orchestrator/internal/service/orchestrator/service_helpers_test.go b/api/payments/orchestrator/internal/service/orchestrator/service_helpers_test.go index aff60175..26dcd034 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/service_helpers_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/service_helpers_test.go @@ -429,3 +429,15 @@ func (s *helperQuotesStore) GetByRef(_ context.Context, _ primitive.ObjectID, re } 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 +} diff --git a/api/payments/orchestrator/internal/service/orchestrator/service_test.go b/api/payments/orchestrator/internal/service/orchestrator/service_test.go index 18ec0921..4c550561 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/service_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/service_test.go @@ -423,6 +423,18 @@ func (s *stubQuotesStore) GetByRef(ctx context.Context, orgRef primitive.ObjectI 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 { routes []*model.PaymentRoute } diff --git a/api/payments/orchestrator/storage/model/quote.go b/api/payments/orchestrator/storage/model/quote.go index 74022084..bdc60945 100644 --- a/api/payments/orchestrator/storage/model/quote.go +++ b/api/payments/orchestrator/storage/model/quote.go @@ -12,12 +12,14 @@ type PaymentQuoteRecord struct { storable.Base `bson:",inline" json:",inline"` model.OrganizationBoundBase `bson:",inline" json:",inline"` - QuoteRef string `bson:"quoteRef" json:"quoteRef"` - Intent PaymentIntent `bson:"intent,omitempty" json:"intent,omitempty"` - Intents []PaymentIntent `bson:"intents,omitempty" json:"intents,omitempty"` - Quote *PaymentQuoteSnapshot `bson:"quote,omitempty" json:"quote,omitempty"` - Quotes []*PaymentQuoteSnapshot `bson:"quotes,omitempty" json:"quotes,omitempty"` - ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"` + QuoteRef string `bson:"quoteRef" json:"quoteRef"` + IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"` + Intent PaymentIntent `bson:"intent,omitempty" json:"intent,omitempty"` + Intents []PaymentIntent `bson:"intents,omitempty" json:"intents,omitempty"` + Quote *PaymentQuoteSnapshot `bson:"quote,omitempty" json:"quote,omitempty"` + Quotes []*PaymentQuoteSnapshot `bson:"quotes,omitempty" json:"quotes,omitempty"` + ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"` + Hash string `bson:"hash" json:"hash"` } // Collection implements storable.Storable. diff --git a/api/payments/orchestrator/storage/mongo/store/quotes.go b/api/payments/orchestrator/storage/mongo/store/quotes.go index f49031a9..7a59551d 100644 --- a/api/payments/orchestrator/storage/mongo/store/quotes.go +++ b/api/payments/orchestrator/storage/mongo/store/quotes.go @@ -65,6 +65,9 @@ func (q *Quotes) Create(ctx context.Context, quote *model.PaymentQuoteRecord) er if quote.OrganizationRef == primitive.NilObjectID { return merrors.InvalidArgument("quotesStore: organization_ref is required") } + if quote.IdempotencyKey == "" { + return merrors.InvalidArgument("quotesStore: idempotency key is required") + } if quote.ExpiresAt.IsZero() { 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 } +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) func int32Ptr(v int32) *int32 { diff --git a/api/payments/orchestrator/storage/storage.go b/api/payments/orchestrator/storage/storage.go index f85c92fe..a1f13dbc 100644 --- a/api/payments/orchestrator/storage/storage.go +++ b/api/payments/orchestrator/storage/storage.go @@ -55,6 +55,7 @@ type PaymentsStore interface { type QuotesStore interface { Create(ctx context.Context, quote *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. diff --git a/api/pkg/go.mod b/api/pkg/go.mod index 78ca6977..d3f3d55c 100644 --- a/api/pkg/go.mod +++ b/api/pkg/go.mod @@ -93,6 +93,6 @@ require ( golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.33.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 ) diff --git a/api/pkg/go.sum b/api/pkg/go.sum index d55eaa64..634a348b 100644 --- a/api/pkg/go.sum +++ b/api/pkg/go.sum @@ -269,8 +269,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/api v0.0.0-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/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0= -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 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/proto/payments/orchestrator/v1/orchestrator.proto b/api/proto/payments/orchestrator/v1/orchestrator.proto index 80d91e7a..d7538367 100644 --- a/api/proto/payments/orchestrator/v1/orchestrator.proto +++ b/api/proto/payments/orchestrator/v1/orchestrator.proto @@ -25,7 +25,7 @@ enum PaymentKind { // SettlementMode defines how to treat fees/FX variance for payouts. enum SettlementMode { 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 } @@ -73,7 +73,7 @@ message ExternalChainEndpoint { // Card payout destination. message CardEndpoint { oneof card { - string pan = 1; // raw PAN + string pan = 1; // raw PAN string token = 2; // network or gateway-issued token } string cardholder_name = 3; @@ -234,6 +234,7 @@ message QuotePaymentRequest { message QuotePaymentResponse { PaymentQuote quote = 1; + string idempotency_key = 2; } message QuotePaymentsRequest { diff --git a/api/server/go.mod b/api/server/go.mod index 3ed8d9ec..1ff582ba 100644 --- a/api/server/go.mod +++ b/api/server/go.mod @@ -139,6 +139,6 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.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 ) diff --git a/api/server/go.sum b/api/server/go.sum index d465d072..7d00cc4f 100644 --- a/api/server/go.sum +++ b/api/server/go.sum @@ -361,8 +361,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/api v0.0.0-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/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0= -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 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +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/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/server/interface/api/sresponse/payment.go b/api/server/interface/api/sresponse/payment.go index e459d859..b859c88e 100644 --- a/api/server/interface/api/sresponse/payment.go +++ b/api/server/interface/api/sresponse/payment.go @@ -35,6 +35,7 @@ type FxQuote struct { } type PaymentQuote struct { + IdempotencyKey string `json:"idempotencyKey"` QuoteRef string `json:"quoteRef,omitempty"` DebitAmount *model.Money `json:"debitAmount,omitempty"` ExpectedSettlementAmount *model.Money `json:"expectedSettlementAmount,omitempty"` @@ -50,9 +51,10 @@ type PaymentQuoteAggregate struct { } type PaymentQuotes struct { - QuoteRef string `json:"quoteRef,omitempty"` - Aggregate *PaymentQuoteAggregate `json:"aggregate,omitempty"` - Quotes []PaymentQuote `json:"quotes,omitempty"` + IdempotencyKey string `json:"idempotencyKey"` + QuoteRef string `json:"quoteRef,omitempty"` + Aggregate *PaymentQuoteAggregate `json:"aggregate,omitempty"` + Quotes []PaymentQuote `json:"quotes,omitempty"` } type Payment struct {