From f578278205fe34aeb13a68fdb9e63f651422423c Mon Sep 17 00:00:00 2001 From: Stephan D Date: Wed, 11 Mar 2026 20:04:10 +0100 Subject: [PATCH] Orchestrator refactoring + planned amounts --- api/billing/documents/go.mod | 4 +- api/billing/documents/go.sum | 8 +- api/billing/fees/go.mod | 4 +- api/billing/fees/go.sum | 8 +- api/discovery/go.mod | 4 +- api/discovery/go.sum | 8 +- api/edge/bff/go.mod | 4 +- api/edge/bff/go.sum | 8 +- .../bff/interface/api/sresponse/payment.go | 137 +++++-- .../interface/api/sresponse/payment_test.go | 224 +++++++++-- api/edge/callbacks/go.mod | 4 +- api/edge/callbacks/go.sum | 8 +- api/fx/ingestor/go.mod | 4 +- api/fx/ingestor/go.sum | 8 +- api/fx/oracle/go.mod | 4 +- api/fx/oracle/go.sum | 8 +- api/fx/storage/go.mod | 2 +- api/fx/storage/go.sum | 4 +- api/gateway/aurora/go.mod | 4 +- api/gateway/aurora/go.sum | 8 +- api/gateway/chain/go.mod | 8 +- api/gateway/chain/go.sum | 16 +- api/gateway/chsettle/go.mod | 4 +- api/gateway/chsettle/go.sum | 8 +- api/gateway/common/go.mod | 2 +- api/gateway/common/go.sum | 4 +- api/gateway/mntx/go.mod | 4 +- api/gateway/mntx/go.sum | 8 +- api/gateway/tgsettle/go.mod | 4 +- api/gateway/tgsettle/go.sum | 8 +- api/gateway/tron/go.mod | 10 +- api/gateway/tron/go.sum | 20 +- api/ledger/go.mod | 4 +- api/ledger/go.sum | 8 +- api/notification/go.mod | 4 +- api/notification/go.sum | 8 +- api/payments/methods/go.mod | 4 +- api/payments/methods/go.sum | 8 +- api/payments/orchestrator/config.dev.yml | 3 +- api/payments/orchestrator/config.yml | 3 +- api/payments/orchestrator/go.mod | 6 +- api/payments/orchestrator/go.sum | 8 +- .../internal/server/internal/builders.go | 1 + .../internal/server/internal/config.go | 1 + .../service/execution/execution_plan.go | 164 -------- .../internal/service/execution/export.go | 123 ------ .../execution/payment_plan_analyzer.go | 123 ------ .../service/execution/payment_plan_helpers.go | 220 ----------- .../service/execution/payment_plan_order.go | 227 ----------- .../service/orchestrationv2/agg/module.go | 50 +-- .../service/orchestrationv2/agg/service.go | 21 +- .../service/orchestrationv2/erecon/event.go | 31 +- .../orchestrationv2/opagg/aggregate_test.go | 54 +++ .../service/orchestrationv2/opagg/clone.go | 27 ++ .../orchestrationv2/opagg/merge_core.go | 2 - .../opagg/merge_quote_parts.go | 8 +- .../service/orchestrationv2/opagg/module.go | 9 + .../service/orchestrationv2/opagg/service.go | 57 ++- .../service/orchestrationv2/oshared/clone.go | 31 ++ .../service/orchestrationv2/prepo/document.go | 54 +-- .../orchestrationv2/prmap/service_test.go | 35 +- .../orchestrationv2/prmap/step_mapping.go | 37 +- .../orchestrationv2/psvc/batch_optimizer.go | 368 ++++++++++++++++-- .../psvc/batch_optimizer_test.go | 48 +++ .../orchestrationv2/psvc/default_executors.go | 9 +- .../service/orchestrationv2/psvc/execute.go | 22 +- .../orchestrationv2/psvc/execute_batch.go | 86 ++-- .../psvc/execute_batch_test.go | 41 ++ .../psvc/optimization_policy.go | 104 +++-- .../psvc/optimization_policy_test.go | 35 ++ .../orchestrationv2/psvc/planned_money.go | 234 +++++++++++ .../service/orchestrationv2/psvc/runtime.go | 13 +- .../orchestrationv2/psvc/step_money.go | 22 +- .../orchestrationv2/sexec/service_test.go | 13 + .../service/orchestrationv2/ssched/input.go | 69 ++-- .../xplan/compile_flow_test.go | 59 ++- .../orchestrationv2/xplan/expansion.go | 16 +- .../orchestrationv2/xplan/fee_planning.go | 45 +-- .../service/orchestrationv2/xplan/service.go | 14 +- .../xplan/service_boundaries.go | 28 +- .../orchestrator/batch_merge_key_resolver.go | 119 ++++++ .../orchestrator/card_payout_executor.go | 11 +- .../orchestrator/card_payout_executor_test.go | 80 ++++ .../service/orchestrator/crypto_executor.go | 44 +-- .../orchestrator/crypto_executor_test.go | 107 ++++- .../service/orchestrator/external_runtime.go | 3 +- .../service/orchestrator/ledger_executor.go | 6 +- .../orchestrator/ledger_executor_test.go | 43 ++ .../service/orchestrator/service_v2.go | 17 +- .../orchestrator/settlement_executor.go | 23 +- .../orchestrator/settlement_executor_test.go | 90 +++++ .../service/orchestrator/step_money.go | 37 +- .../internal/service/shared/money.go | 24 ++ api/payments/quotation/go.mod | 4 +- api/payments/quotation/go.sum | 8 +- .../internal/service/plan/helpers.go | 11 +- .../funding_gate_builder.go | 8 +- .../internal/service/quotation/helpers.go | 9 +- .../quotation/quotation_service_v2/helpers.go | 9 +- .../quote_computation_service/helpers.go | 17 +- .../quote_response_mapper_v2/helpers.go | 9 +- .../quotation/internal/shared/money.go | 58 +++ api/payments/storage/go.mod | 2 +- api/payments/storage/go.sum | 4 +- api/pkg/go.mod | 10 +- api/pkg/go.sum | 16 +- .../orchestration/v2/orchestration.proto | 26 +- ci/scripts/common/run_backend_lint.sh | 2 +- .../lib/data/dto/payment/operation.dart | 29 ++ .../lib/data/mapper/payment/operation.dart | 36 +- .../models/payment/execution_operation.dart | 24 ++ 111 files changed, 2485 insertions(+), 1517 deletions(-) delete mode 100644 api/payments/orchestrator/internal/service/execution/execution_plan.go delete mode 100644 api/payments/orchestrator/internal/service/execution/export.go delete mode 100644 api/payments/orchestrator/internal/service/execution/payment_plan_analyzer.go delete mode 100644 api/payments/orchestrator/internal/service/execution/payment_plan_helpers.go delete mode 100644 api/payments/orchestrator/internal/service/execution/payment_plan_order.go create mode 100644 api/payments/orchestrator/internal/service/orchestrationv2/oshared/clone.go create mode 100644 api/payments/orchestrator/internal/service/orchestrationv2/psvc/batch_optimizer_test.go create mode 100644 api/payments/orchestrator/internal/service/orchestrationv2/psvc/planned_money.go create mode 100644 api/payments/orchestrator/internal/service/orchestrator/batch_merge_key_resolver.go create mode 100644 api/payments/orchestrator/internal/service/shared/money.go create mode 100644 api/payments/quotation/internal/shared/money.go diff --git a/api/billing/documents/go.mod b/api/billing/documents/go.mod index f110851a..6027dc3a 100644 --- a/api/billing/documents/go.mod +++ b/api/billing/documents/go.mod @@ -64,7 +64,7 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/api/billing/documents/go.sum b/api/billing/documents/go.sum index 571ef75c..69b4be16 100644 --- a/api/billing/documents/go.sum +++ b/api/billing/documents/go.sum @@ -249,8 +249,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -258,8 +258,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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/billing/fees/go.mod b/api/billing/fees/go.mod index 22797596..f7b13ef7 100644 --- a/api/billing/fees/go.mod +++ b/api/billing/fees/go.mod @@ -49,7 +49,7 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect google.golang.org/protobuf v1.36.11 ) diff --git a/api/billing/fees/go.sum b/api/billing/fees/go.sum index 70696e0c..a55b4d54 100644 --- a/api/billing/fees/go.sum +++ b/api/billing/fees/go.sum @@ -199,8 +199,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -208,8 +208,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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/discovery/go.mod b/api/discovery/go.mod index 76626629..0a5803ea 100644 --- a/api/discovery/go.mod +++ b/api/discovery/go.mod @@ -42,8 +42,8 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect google.golang.org/grpc v1.79.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/api/discovery/go.sum b/api/discovery/go.sum index 70696e0c..a55b4d54 100644 --- a/api/discovery/go.sum +++ b/api/discovery/go.sum @@ -199,8 +199,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -208,8 +208,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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/edge/bff/go.mod b/api/edge/bff/go.mod index 6e07bc73..0f6ee05f 100644 --- a/api/edge/bff/go.mod +++ b/api/edge/bff/go.mod @@ -157,7 +157,7 @@ require ( golang.org/x/crypto v0.48.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect ) diff --git a/api/edge/bff/go.sum b/api/edge/bff/go.sum index f90a66ff..55627e56 100644 --- a/api/edge/bff/go.sum +++ b/api/edge/bff/go.sum @@ -380,8 +380,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -401,8 +401,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-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/edge/bff/interface/api/sresponse/payment.go b/api/edge/bff/interface/api/sresponse/payment.go index a30af970..05199335 100644 --- a/api/edge/bff/interface/api/sresponse/payment.go +++ b/api/edge/bff/interface/api/sresponse/payment.go @@ -91,18 +91,27 @@ type PaymentEndpoint struct { } type PaymentOperation struct { - StepRef string `json:"stepRef,omitempty"` - Code string `json:"code,omitempty"` - State string `json:"state,omitempty"` - Label string `json:"label,omitempty"` + StepRef string `json:"stepRef,omitempty"` + Code string `json:"code,omitempty"` + State string `json:"state,omitempty"` + Label string `json:"label,omitempty"` + Money *PaymentOperationMoney `json:"money,omitempty"` + OperationRef string `json:"operationRef,omitempty"` + Gateway string `json:"gateway,omitempty"` + FailureCode string `json:"failureCode,omitempty"` + FailureReason string `json:"failureReason,omitempty"` + StartedAt time.Time `json:"startedAt,omitempty"` + CompletedAt time.Time `json:"completedAt,omitempty"` +} + +type PaymentOperationMoney struct { + Planned *PaymentOperationMoneySnapshot `json:"planned,omitempty"` + Executed *PaymentOperationMoneySnapshot `json:"executed,omitempty"` +} + +type PaymentOperationMoneySnapshot struct { Amount *paymenttypes.Money `json:"amount,omitempty"` ConvertedAmount *paymenttypes.Money `json:"convertedAmount,omitempty"` - OperationRef string `json:"operationRef,omitempty"` - Gateway string `json:"gateway,omitempty"` - FailureCode string `json:"failureCode,omitempty"` - FailureReason string `json:"failureReason,omitempty"` - StartedAt time.Time `json:"startedAt,omitempty"` - CompletedAt time.Time `json:"completedAt,omitempty"` } type paymentQuoteResponse struct { @@ -581,19 +590,20 @@ func toUserVisibleOperations(steps []*orchestrationv2.StepExecution) []PaymentOp func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation { operationRef, gateway := operationRefAndGateway(step.GetStepCode(), step.GetRefs()) - amount := normalizeOperationMoney(toMoney(step.GetExecutedMoney())) - convertedAmount := normalizeOperationMoney(toMoney(step.GetConvertedMoney())) + plannedAmount := stepPlannedAmount(step) + plannedConvertedAmount := stepPlannedConvertedAmount(step) + executedAmount := stepExecutedAmount(step) + executedConvertedAmount := stepExecutedConvertedAmount(step) op := PaymentOperation{ - StepRef: step.GetStepRef(), - Code: step.GetStepCode(), - State: enumJSONName(step.GetState().String()), - Label: strings.TrimSpace(step.GetUserLabel()), - Amount: amount, - ConvertedAmount: convertedAmount, - OperationRef: operationRef, - Gateway: gateway, - StartedAt: timestampAsTime(step.GetStartedAt()), - CompletedAt: timestampAsTime(step.GetCompletedAt()), + StepRef: step.GetStepRef(), + Code: step.GetStepCode(), + State: enumJSONName(step.GetState().String()), + Label: strings.TrimSpace(step.GetUserLabel()), + Money: toOperationMoney(plannedAmount, plannedConvertedAmount, executedAmount, executedConvertedAmount), + OperationRef: operationRef, + Gateway: gateway, + StartedAt: timestampAsTime(step.GetStartedAt()), + CompletedAt: timestampAsTime(step.GetCompletedAt()), } failure := step.GetFailure() if failure == nil { @@ -607,6 +617,89 @@ func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation { return op } +func stepPlannedAmount(step *orchestrationv2.StepExecution) *paymenttypes.Money { + if step == nil { + return nil + } + if money := step.GetMoney(); money != nil { + if planned := money.GetPlanned(); planned != nil { + if normalized := normalizeOperationMoney(toMoney(planned.GetAmount())); normalized != nil { + return normalized + } + } + } + return nil +} + +func stepPlannedConvertedAmount(step *orchestrationv2.StepExecution) *paymenttypes.Money { + if step == nil { + return nil + } + if money := step.GetMoney(); money != nil { + if planned := money.GetPlanned(); planned != nil { + if normalized := normalizeOperationMoney(toMoney(planned.GetConvertedAmount())); normalized != nil { + return normalized + } + } + } + return nil +} + +func stepExecutedAmount(step *orchestrationv2.StepExecution) *paymenttypes.Money { + if step == nil { + return nil + } + if money := step.GetMoney(); money != nil { + if executed := money.GetExecuted(); executed != nil { + if normalized := normalizeOperationMoney(toMoney(executed.GetAmount())); normalized != nil { + return normalized + } + } + } + return nil +} + +func stepExecutedConvertedAmount(step *orchestrationv2.StepExecution) *paymenttypes.Money { + if step == nil { + return nil + } + if money := step.GetMoney(); money != nil { + if executed := money.GetExecuted(); executed != nil { + if normalized := normalizeOperationMoney(toMoney(executed.GetConvertedAmount())); normalized != nil { + return normalized + } + } + } + return nil +} + +func toOperationMoney( + plannedAmount *paymenttypes.Money, + plannedConvertedAmount *paymenttypes.Money, + executedAmount *paymenttypes.Money, + executedConvertedAmount *paymenttypes.Money, +) *PaymentOperationMoney { + planned := toOperationMoneySnapshot(plannedAmount, plannedConvertedAmount) + executed := toOperationMoneySnapshot(executedAmount, executedConvertedAmount) + if planned == nil && executed == nil { + return nil + } + return &PaymentOperationMoney{ + Planned: planned, + Executed: executed, + } +} + +func toOperationMoneySnapshot(amount *paymenttypes.Money, convertedAmount *paymenttypes.Money) *PaymentOperationMoneySnapshot { + if amount == nil && convertedAmount == nil { + return nil + } + return &PaymentOperationMoneySnapshot{ + Amount: amount, + ConvertedAmount: convertedAmount, + } +} + func normalizeOperationMoney(value *paymenttypes.Money) *paymenttypes.Money { if value == nil { return nil diff --git a/api/edge/bff/interface/api/sresponse/payment_test.go b/api/edge/bff/interface/api/sresponse/payment_test.go index fd3e6a01..2a3c77d6 100644 --- a/api/edge/bff/interface/api/sresponse/payment_test.go +++ b/api/edge/bff/interface/api/sresponse/payment_test.go @@ -343,83 +343,235 @@ func TestToPaymentOperation_MapsAmount(t *testing.T) { State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED, }) - if got := op.Amount; got != nil { - t.Fatalf("expected nil amount without executed_money, got=%+v", got) - } - if got := op.ConvertedAmount; got != nil { - t.Fatalf("expected no converted_amount for non-fx operation, got=%+v", got) + if got := op.Money; got != nil { + t.Fatalf("expected nil money payload without step money, got=%+v", got) } } func TestToPaymentOperation_PrefersExecutedMoney(t *testing.T) { op := toPaymentOperation(&orchestrationv2.StepExecution{ - StepRef: "step-4b", - StepCode: "hop.4.card_payout.send", - State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED, - ExecutedMoney: &moneyv1.Money{Amount: "99.95", Currency: "EUR"}, + StepRef: "step-4b", + StepCode: "hop.4.card_payout.send", + State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED, + Money: &orchestrationv2.StepExecutionMoney{ + Planned: &orchestrationv2.StepExecutionMoneySnapshot{ + Amount: &moneyv1.Money{Amount: "88.00", Currency: "EUR"}, + }, + Executed: &orchestrationv2.StepExecutionMoneySnapshot{ + Amount: &moneyv1.Money{Amount: "99.95", Currency: "EUR"}, + }, + }, }) - if op.Amount == nil { - t.Fatal("expected amount to be mapped") + if op.Money == nil || op.Money.Executed == nil || op.Money.Executed.Amount == nil { + t.Fatal("expected executed amount to be mapped") } - if got, want := op.Amount.Amount, "99.95"; got != want { - t.Fatalf("amount.value mismatch: got=%q want=%q", got, want) + if got, want := op.Money.Executed.Amount.Amount, "99.95"; got != want { + t.Fatalf("executed amount.value mismatch: got=%q want=%q", got, want) } - if got, want := op.Amount.Currency, "EUR"; got != want { - t.Fatalf("amount.currency mismatch: got=%q want=%q", got, want) + if got, want := op.Money.Executed.Amount.Currency, "EUR"; got != want { + t.Fatalf("executed amount.currency mismatch: got=%q want=%q", got, want) } - if got := op.ConvertedAmount; got != nil { - t.Fatalf("expected no converted_amount for non-fx operation, got=%+v", got) + if op.Money.Planned == nil || op.Money.Planned.Amount == nil { + t.Fatal("expected planned amount to be exposed") + } + if got, want := op.Money.Planned.Amount.Amount, "88.00"; got != want { + t.Fatalf("planned amount.value mismatch: got=%q want=%q", got, want) + } + if got, want := op.Money.Planned.Amount.Currency, "EUR"; got != want { + t.Fatalf("planned amount.currency mismatch: got=%q want=%q", got, want) + } + if got := op.Money.Executed.ConvertedAmount; got != nil { + t.Fatalf("expected no executed converted_amount for non-fx operation, got=%+v", got) + } +} + +func TestToPaymentOperation_UsesPlannedMoneyBeforeExecution(t *testing.T) { + op := toPaymentOperation(&orchestrationv2.StepExecution{ + StepRef: "step-4c", + StepCode: "hop.4.card_payout.send", + State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_PENDING, + Money: &orchestrationv2.StepExecutionMoney{ + Planned: &orchestrationv2.StepExecutionMoneySnapshot{ + Amount: &moneyv1.Money{Amount: "77.10", Currency: "USD"}, + }, + }, + }) + + if op.Money == nil || op.Money.Planned == nil || op.Money.Planned.Amount == nil { + t.Fatal("expected planned amount from structured planned money") + } + if got, want := op.Money.Planned.Amount.Amount, "77.10"; got != want { + t.Fatalf("planned amount.value mismatch: got=%q want=%q", got, want) + } + if got, want := op.Money.Planned.Amount.Currency, "USD"; got != want { + t.Fatalf("planned amount.currency mismatch: got=%q want=%q", got, want) + } + if got := op.Money.Executed; got != nil { + t.Fatalf("expected no executed snapshot before execution, got=%+v", got) + } +} + +func TestToPaymentOperation_UsesStructuredMoneyEnvelope(t *testing.T) { + op := toPaymentOperation(&orchestrationv2.StepExecution{ + StepRef: "step-4d", + StepCode: "hop.4.card_payout.send", + State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_PENDING, + Money: &orchestrationv2.StepExecutionMoney{ + Planned: &orchestrationv2.StepExecutionMoneySnapshot{ + Amount: &moneyv1.Money{Amount: "66.00", Currency: "USD"}, + }, + Executed: &orchestrationv2.StepExecutionMoneySnapshot{ + Amount: &moneyv1.Money{Amount: "67.00", Currency: "USD"}, + }, + }, + }) + + if op.Money == nil || op.Money.Executed == nil || op.Money.Executed.Amount == nil { + t.Fatal("expected amount from structured executed money") + } + if got, want := op.Money.Executed.Amount.Amount, "67.00"; got != want { + t.Fatalf("executed amount.value mismatch: got=%q want=%q", got, want) + } + if op.Money.Planned == nil || op.Money.Planned.Amount == nil { + t.Fatal("expected planned amount from structured money") + } + if got, want := op.Money.Planned.Amount.Amount, "66.00"; got != want { + t.Fatalf("planned amount.value mismatch: got=%q want=%q", got, want) } } func TestToPaymentOperation_MapsFxTwoAmounts(t *testing.T) { op := toPaymentOperation(&orchestrationv2.StepExecution{ - StepRef: "step-5", - StepCode: "hop.2.settlement.fx_convert", - State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED, - ConvertedMoney: &moneyv1.Money{Amount: "100.00", Currency: "EUR"}, + StepRef: "step-5", + StepCode: "hop.2.settlement.fx_convert", + State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED, + Money: &orchestrationv2.StepExecutionMoney{ + Executed: &orchestrationv2.StepExecutionMoneySnapshot{ + ConvertedAmount: &moneyv1.Money{Amount: "100.00", Currency: "EUR"}, + }, + }, }) - if got := op.Amount; got != nil { + if op.Money == nil || op.Money.Executed == nil { + t.Fatal("expected executed snapshot to be mapped") + } + if got := op.Money.Executed.Amount; got != nil { t.Fatalf("expected nil base amount without executed_money, got=%+v", got) } - if op.ConvertedAmount == nil { + if op.Money.Executed.ConvertedAmount == nil { t.Fatal("expected fx converted amount to be mapped") } - if got, want := op.ConvertedAmount.Amount, "100.00"; got != want { + if got, want := op.Money.Executed.ConvertedAmount.Amount, "100.00"; got != want { t.Fatalf("converted amount.value mismatch: got=%q want=%q", got, want) } - if got, want := op.ConvertedAmount.Currency, "EUR"; got != want { + if got, want := op.Money.Executed.ConvertedAmount.Currency, "EUR"; got != want { t.Fatalf("converted amount.currency mismatch: got=%q want=%q", got, want) } } +func TestToPaymentOperation_UsesPlannedFxAmountsBeforeExecution(t *testing.T) { + op := toPaymentOperation(&orchestrationv2.StepExecution{ + StepRef: "step-5b", + StepCode: "hop.2.settlement.fx_convert", + State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_PENDING, + Money: &orchestrationv2.StepExecutionMoney{ + Planned: &orchestrationv2.StepExecutionMoneySnapshot{ + Amount: &moneyv1.Money{Amount: "109.50", Currency: "USDT"}, + ConvertedAmount: &moneyv1.Money{Amount: "100.00", Currency: "EUR"}, + }, + }, + }) + + if op.Money == nil || op.Money.Planned == nil { + t.Fatal("expected planned snapshot from structured money") + } + if got, want := op.Money.Planned.Amount.Amount, "109.50"; got != want { + t.Fatalf("base amount.value mismatch: got=%q want=%q", got, want) + } + if got, want := op.Money.Planned.Amount.Currency, "USDT"; got != want { + t.Fatalf("base amount.currency mismatch: got=%q want=%q", got, want) + } + if op.Money.Planned.ConvertedAmount == nil { + t.Fatal("expected fx converted amount from structured planned money") + } + if got, want := op.Money.Planned.ConvertedAmount.Amount, "100.00"; got != want { + t.Fatalf("converted amount.value mismatch: got=%q want=%q", got, want) + } + if got, want := op.Money.Planned.ConvertedAmount.Currency, "EUR"; got != want { + t.Fatalf("converted amount.currency mismatch: got=%q want=%q", got, want) + } + if got := op.Money.Executed; got != nil { + t.Fatalf("expected nil executed snapshot before execution, got=%+v", got) + } +} + +func TestToPaymentOperation_UsesStructuredFxMoneyEnvelope(t *testing.T) { + op := toPaymentOperation(&orchestrationv2.StepExecution{ + StepRef: "step-5c", + StepCode: "hop.2.settlement.fx_convert", + State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED, + Money: &orchestrationv2.StepExecutionMoney{ + Planned: &orchestrationv2.StepExecutionMoneySnapshot{ + Amount: &moneyv1.Money{Amount: "109.50", Currency: "USDT"}, + ConvertedAmount: &moneyv1.Money{Amount: "100.00", Currency: "EUR"}, + }, + Executed: &orchestrationv2.StepExecutionMoneySnapshot{ + Amount: &moneyv1.Money{Amount: "110.00", Currency: "USDT"}, + ConvertedAmount: &moneyv1.Money{Amount: "101.00", Currency: "EUR"}, + }, + }, + }) + + if op.Money == nil || op.Money.Executed == nil || op.Money.Planned == nil { + t.Fatal("expected snapshots from structured money") + } + if got, want := op.Money.Executed.Amount.Amount, "110.00"; got != want { + t.Fatalf("amount.value mismatch: got=%q want=%q", got, want) + } + if got, want := op.Money.Executed.ConvertedAmount.Amount, "101.00"; got != want { + t.Fatalf("converted amount.value mismatch: got=%q want=%q", got, want) + } + if op.Money.Planned.Amount == nil || op.Money.Planned.ConvertedAmount == nil { + t.Fatal("expected planned amounts from structured money") + } + if got, want := op.Money.Planned.Amount.Amount, "109.50"; got != want { + t.Fatalf("planned amount.value mismatch: got=%q want=%q", got, want) + } + if got, want := op.Money.Planned.ConvertedAmount.Amount, "100.00"; got != want { + t.Fatalf("planned converted amount.value mismatch: got=%q want=%q", got, want) + } +} + func TestToPaymentOperation_FxWithExecutedMoney_StillProvidesTwoAmounts(t *testing.T) { op := toPaymentOperation(&orchestrationv2.StepExecution{ - StepRef: "step-6", - StepCode: "hop.2.settlement.fx_convert", - State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED, - ExecutedMoney: &moneyv1.Money{Amount: "109.50", Currency: "USDT"}, - ConvertedMoney: &moneyv1.Money{Amount: "100.00", Currency: "EUR"}, + StepRef: "step-6", + StepCode: "hop.2.settlement.fx_convert", + State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED, + Money: &orchestrationv2.StepExecutionMoney{ + Executed: &orchestrationv2.StepExecutionMoneySnapshot{ + Amount: &moneyv1.Money{Amount: "109.50", Currency: "USDT"}, + ConvertedAmount: &moneyv1.Money{Amount: "100.00", Currency: "EUR"}, + }, + }, }) - if op.Amount == nil { + if op.Money == nil || op.Money.Executed == nil || op.Money.Executed.Amount == nil { t.Fatal("expected fx base amount to be mapped") } - if got, want := op.Amount.Amount, "109.50"; got != want { + if got, want := op.Money.Executed.Amount.Amount, "109.50"; got != want { t.Fatalf("base amount.value mismatch: got=%q want=%q", got, want) } - if got, want := op.Amount.Currency, "USDT"; got != want { + if got, want := op.Money.Executed.Amount.Currency, "USDT"; got != want { t.Fatalf("base amount.currency mismatch: got=%q want=%q", got, want) } - if op.ConvertedAmount == nil { + if op.Money.Executed.ConvertedAmount == nil { t.Fatal("expected fx quote amount to be mapped") } - if got, want := op.ConvertedAmount.Amount, "100.00"; got != want { + if got, want := op.Money.Executed.ConvertedAmount.Amount, "100.00"; got != want { t.Fatalf("converted amount.value mismatch: got=%q want=%q", got, want) } - if got, want := op.ConvertedAmount.Currency, "EUR"; got != want { + if got, want := op.Money.Executed.ConvertedAmount.Currency, "EUR"; got != want { t.Fatalf("converted amount.currency mismatch: got=%q want=%q", got, want) } } diff --git a/api/edge/callbacks/go.mod b/api/edge/callbacks/go.mod index b42b33f1..4f367a21 100644 --- a/api/edge/callbacks/go.mod +++ b/api/edge/callbacks/go.mod @@ -56,9 +56,9 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect google.golang.org/grpc v1.79.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/api/edge/callbacks/go.sum b/api/edge/callbacks/go.sum index 553564f6..bb3bc27d 100644 --- a/api/edge/callbacks/go.sum +++ b/api/edge/callbacks/go.sum @@ -232,8 +232,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -243,8 +243,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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= 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 280bb4ba..af5fada1 100644 --- a/api/fx/ingestor/go.mod +++ b/api/fx/ingestor/go.mod @@ -46,8 +46,8 @@ require ( golang.org/x/crypto v0.48.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect google.golang.org/grpc v1.79.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/api/fx/ingestor/go.sum b/api/fx/ingestor/go.sum index 70696e0c..a55b4d54 100644 --- a/api/fx/ingestor/go.sum +++ b/api/fx/ingestor/go.sum @@ -199,8 +199,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -208,8 +208,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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= 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 8a87be3a..5563fcfd 100644 --- a/api/fx/oracle/go.mod +++ b/api/fx/oracle/go.mod @@ -47,6 +47,6 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect ) diff --git a/api/fx/oracle/go.sum b/api/fx/oracle/go.sum index 70696e0c..a55b4d54 100644 --- a/api/fx/oracle/go.sum +++ b/api/fx/oracle/go.sum @@ -199,8 +199,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -208,8 +208,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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/fx/storage/go.mod b/api/fx/storage/go.mod index c4a3c9ea..1cdb4657 100644 --- a/api/fx/storage/go.mod +++ b/api/fx/storage/go.mod @@ -25,6 +25,6 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/text v0.35.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/api/fx/storage/go.sum b/api/fx/storage/go.sum index 0ec8db8a..e709ef1a 100644 --- a/api/fx/storage/go.sum +++ b/api/fx/storage/go.sum @@ -160,8 +160,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/api/gateway/aurora/go.mod b/api/gateway/aurora/go.mod index 216ba7f5..29d24777 100644 --- a/api/gateway/aurora/go.mod +++ b/api/gateway/aurora/go.mod @@ -50,6 +50,6 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect ) diff --git a/api/gateway/aurora/go.sum b/api/gateway/aurora/go.sum index fb54e96e..cbdf5d65 100644 --- a/api/gateway/aurora/go.sum +++ b/api/gateway/aurora/go.sum @@ -201,8 +201,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -210,8 +210,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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= 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 b1683b3e..f38aa945 100644 --- a/api/gateway/chain/go.mod +++ b/api/gateway/chain/go.mod @@ -32,12 +32,12 @@ require ( github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/consensys/gnark-crypto v0.19.2 // indirect + github.com/consensys/gnark-crypto v0.20.0 // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect - github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect github.com/go-chi/chi/v5 v5.2.5 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -89,7 +89,7 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect ) diff --git a/api/gateway/chain/go.sum b/api/gateway/chain/go.sum index 9af57a19..2b65605c 100644 --- a/api/gateway/chain/go.sum +++ b/api/gateway/chain/go.sum @@ -40,8 +40,8 @@ github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwP github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= -github.com/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80= -github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0= +github.com/consensys/gnark-crypto v0.20.0 h1:dJmv2sC9KWV/cNRjMjy2S0h7emfyyX8eSsJzwk0DQzw= +github.com/consensys/gnark-crypto v0.20.0/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= @@ -72,8 +72,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= -github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= -github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= +github.com/ethereum/c-kzg-4844/v2 v2.1.7 h1:aat3CuITdDbPC6pmEGRT0zJ5eOxzrZj8TJT5z7Xk//M= +github.com/ethereum/c-kzg-4844/v2 v2.1.7/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= github.com/ethereum/go-ethereum v1.17.1 h1:IjlQDjgxg2uL+GzPRkygGULPMLzcYWncEI7wbaizvho= @@ -344,8 +344,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -355,8 +355,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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/gateway/chsettle/go.mod b/api/gateway/chsettle/go.mod index 308c9960..8df5d4c4 100644 --- a/api/gateway/chsettle/go.mod +++ b/api/gateway/chsettle/go.mod @@ -47,6 +47,6 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect ) diff --git a/api/gateway/chsettle/go.sum b/api/gateway/chsettle/go.sum index 70696e0c..a55b4d54 100644 --- a/api/gateway/chsettle/go.sum +++ b/api/gateway/chsettle/go.sum @@ -199,8 +199,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -208,8 +208,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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/gateway/common/go.mod b/api/gateway/common/go.mod index 80142487..7407a441 100644 --- a/api/gateway/common/go.mod +++ b/api/gateway/common/go.mod @@ -25,6 +25,6 @@ require ( golang.org/x/crypto v0.48.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/text v0.35.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/api/gateway/common/go.sum b/api/gateway/common/go.sum index f0dea89e..4605c955 100644 --- a/api/gateway/common/go.sum +++ b/api/gateway/common/go.sum @@ -146,8 +146,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/api/gateway/mntx/go.mod b/api/gateway/mntx/go.mod index 8ebca97b..6eaf780b 100644 --- a/api/gateway/mntx/go.mod +++ b/api/gateway/mntx/go.mod @@ -50,6 +50,6 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect ) diff --git a/api/gateway/mntx/go.sum b/api/gateway/mntx/go.sum index fb54e96e..cbdf5d65 100644 --- a/api/gateway/mntx/go.sum +++ b/api/gateway/mntx/go.sum @@ -201,8 +201,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -210,8 +210,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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= 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 81498073..27146df5 100644 --- a/api/gateway/tgsettle/go.mod +++ b/api/gateway/tgsettle/go.mod @@ -47,6 +47,6 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect ) diff --git a/api/gateway/tgsettle/go.sum b/api/gateway/tgsettle/go.sum index 70696e0c..a55b4d54 100644 --- a/api/gateway/tgsettle/go.sum +++ b/api/gateway/tgsettle/go.sum @@ -199,8 +199,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -208,8 +208,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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/gateway/tron/go.mod b/api/gateway/tron/go.mod index 2cd27e2e..3638c2c3 100644 --- a/api/gateway/tron/go.mod +++ b/api/gateway/tron/go.mod @@ -36,12 +36,12 @@ require ( github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/consensys/gnark-crypto v0.19.2 // indirect + github.com/consensys/gnark-crypto v0.20.0 // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/deckarep/golang-set v1.8.0 // indirect github.com/deckarep/golang-set/v2 v2.8.0 // indirect - github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect github.com/fbsobreira/go-bip39 v1.2.0 // indirect github.com/go-chi/chi/v5 v5.2.5 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect @@ -97,8 +97,8 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect ) diff --git a/api/gateway/tron/go.sum b/api/gateway/tron/go.sum index 7855059d..100b70b8 100644 --- a/api/gateway/tron/go.sum +++ b/api/gateway/tron/go.sum @@ -42,8 +42,8 @@ github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwP github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= -github.com/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80= -github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0= +github.com/consensys/gnark-crypto v0.20.0 h1:dJmv2sC9KWV/cNRjMjy2S0h7emfyyX8eSsJzwk0DQzw= +github.com/consensys/gnark-crypto v0.20.0/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= @@ -76,8 +76,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= -github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= -github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= +github.com/ethereum/c-kzg-4844/v2 v2.1.7 h1:aat3CuITdDbPC6pmEGRT0zJ5eOxzrZj8TJT5z7Xk//M= +github.com/ethereum/c-kzg-4844/v2 v2.1.7/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= github.com/ethereum/go-ethereum v1.17.1 h1:IjlQDjgxg2uL+GzPRkygGULPMLzcYWncEI7wbaizvho= @@ -360,8 +360,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -371,10 +371,10 @@ 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/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= -google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c h1:OyQPd6I3pN/9gDxz6L13kYGJgqkpdrAohJRBeXyxlgI= +google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/ledger/go.mod b/api/ledger/go.mod index 3f6abc92..d034111e 100644 --- a/api/ledger/go.mod +++ b/api/ledger/go.mod @@ -48,6 +48,6 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect ) diff --git a/api/ledger/go.sum b/api/ledger/go.sum index 66951826..57c08698 100644 --- a/api/ledger/go.sum +++ b/api/ledger/go.sum @@ -201,8 +201,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -210,8 +210,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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/notification/go.mod b/api/notification/go.mod index bbb4e93e..94b344c1 100644 --- a/api/notification/go.mod +++ b/api/notification/go.mod @@ -14,7 +14,7 @@ require ( github.com/xhit/go-simple-mail/v2 v2.16.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - golang.org/x/text v0.34.0 + golang.org/x/text v0.35.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -50,7 +50,7 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect google.golang.org/grpc v1.79.2 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/api/notification/go.sum b/api/notification/go.sum index 918e20a4..8a40c794 100644 --- a/api/notification/go.sum +++ b/api/notification/go.sum @@ -216,8 +216,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -225,8 +225,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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/payments/methods/go.mod b/api/payments/methods/go.mod index 9ea3ad67..e8f45922 100644 --- a/api/payments/methods/go.mod +++ b/api/payments/methods/go.mod @@ -48,6 +48,6 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect ) diff --git a/api/payments/methods/go.sum b/api/payments/methods/go.sum index 66951826..57c08698 100644 --- a/api/payments/methods/go.sum +++ b/api/payments/methods/go.sum @@ -201,8 +201,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -210,8 +210,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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/payments/orchestrator/config.dev.yml b/api/payments/orchestrator/config.dev.yml index 6f270a77..4714b942 100644 --- a/api/payments/orchestrator/config.dev.yml +++ b/api/payments/orchestrator/config.dev.yml @@ -46,7 +46,7 @@ card_gateways: # Batch optimizer settings: # - default_mode disables aggregation for unmatched traffic -# - crypto rule enables destination aggregation for crypto rail +# - crypto rule enables aggregation by operation destination for crypto operations optimizer: aggregation: default_mode: "no_optimization" @@ -54,6 +54,7 @@ optimizer: - id: "crypto_by_destination" priority: 100 mode: "merge_by_destination" + group_by: "rail_target" match: rail: "CRYPTO" diff --git a/api/payments/orchestrator/config.yml b/api/payments/orchestrator/config.yml index 820a2115..66d264b6 100644 --- a/api/payments/orchestrator/config.yml +++ b/api/payments/orchestrator/config.yml @@ -46,7 +46,7 @@ card_gateways: # Batch optimizer settings: # - default_mode disables aggregation for unmatched traffic -# - crypto rule enables destination aggregation for crypto rail +# - crypto rule enables aggregation by operation destination for crypto operations optimizer: aggregation: default_mode: "no_optimization" @@ -54,6 +54,7 @@ optimizer: - id: "crypto_by_destination" priority: 100 mode: "merge_by_destination" + group_by: "rail_target" match: rail: "CRYPTO" diff --git a/api/payments/orchestrator/go.mod b/api/payments/orchestrator/go.mod index dd29c1f6..065092d0 100644 --- a/api/payments/orchestrator/go.mod +++ b/api/payments/orchestrator/go.mod @@ -17,7 +17,6 @@ replace github.com/tech/sendico/ledger => ../../ledger replace github.com/tech/sendico/payments/storage => ../storage require ( - github.com/google/uuid v1.6.0 github.com/shopspring/decimal v1.4.0 github.com/tech/sendico/fx/oracle v0.0.0-00010101000000-000000000000 github.com/tech/sendico/gateway/chain v0.0.0-00010101000000-000000000000 @@ -40,6 +39,7 @@ require ( github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-chi/chi/v5 v5.2.5 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.4 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -63,6 +63,6 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect ) diff --git a/api/payments/orchestrator/go.sum b/api/payments/orchestrator/go.sum index 4c41d3ef..bc53f93f 100644 --- a/api/payments/orchestrator/go.sum +++ b/api/payments/orchestrator/go.sum @@ -202,8 +202,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -211,8 +211,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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= 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 efa2aeae..9406048e 100644 --- a/api/payments/orchestrator/internal/server/internal/builders.go +++ b/api/payments/orchestrator/internal/server/internal/builders.go @@ -54,6 +54,7 @@ func buildBatchOptimizationPolicy(cfg optimizerConfig) psvc.BatchOptimizationPol Enabled: ruleCfg.Enabled, Priority: ruleCfg.Priority, Mode: psvc.BatchOptimizationMode(strings.TrimSpace(ruleCfg.Mode)), + GroupBy: psvc.BatchOptimizationGrouping(strings.TrimSpace(ruleCfg.GroupBy)), Match: psvc.BatchOptimizationMatch{ Rail: model.ParseRail(ruleCfg.Match.Rail), Providers: cloneTrimmedSlice(ruleCfg.Match.Providers), diff --git a/api/payments/orchestrator/internal/server/internal/config.go b/api/payments/orchestrator/internal/server/internal/config.go index c1cd08aa..1c7b72bf 100644 --- a/api/payments/orchestrator/internal/server/internal/config.go +++ b/api/payments/orchestrator/internal/server/internal/config.go @@ -33,6 +33,7 @@ type aggregationRuleConfig struct { Enabled *bool `yaml:"enabled"` Priority int `yaml:"priority"` Mode string `yaml:"mode"` + GroupBy string `yaml:"group_by"` Match aggregationMatchConfig `yaml:"match"` } diff --git a/api/payments/orchestrator/internal/service/execution/execution_plan.go b/api/payments/orchestrator/internal/service/execution/execution_plan.go deleted file mode 100644 index 158bcc6a..00000000 --- a/api/payments/orchestrator/internal/service/execution/execution_plan.go +++ /dev/null @@ -1,164 +0,0 @@ -package execution - -import ( - "strings" - - "github.com/tech/sendico/payments/storage/model" - chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" -) - -const ( - executionStepMetadataRole = "role" - executionStepMetadataStatus = "status" - - executionStepRoleSource = "source" - executionStepRoleConsumer = "consumer" - executionStepCodeCardPayout = "card_payout" -) - -func setExecutionStepRole(step *model.ExecutionStep, role string) { - role = strings.ToLower(strings.TrimSpace(role)) - setExecutionStepMetadata(step, executionStepMetadataRole, role) -} - -func setExecutionStepStatus(step *model.ExecutionStep, state model.OperationState) { - step.State = state - setExecutionStepMetadata(step, executionStepMetadataStatus, string(state)) -} - -func executionStepRole(step *model.ExecutionStep) string { - if step == nil { - return "" - } - if role := strings.TrimSpace(step.Metadata[executionStepMetadataRole]); role != "" { - return strings.ToLower(role) - } - if strings.EqualFold(step.Code, executionStepCodeCardPayout) { - return executionStepRoleConsumer - } - return executionStepRoleSource -} - -func isSourceExecutionStep(step *model.ExecutionStep) bool { - return executionStepRole(step) == executionStepRoleSource -} - -func sourceStepsConfirmed(plan *model.ExecutionPlan) bool { - if plan == nil || len(plan.Steps) == 0 { - return false - } - hasSource := false - for _, step := range plan.Steps { - if step == nil || !isSourceExecutionStep(step) { - continue - } - if step.State == model.OperationStateSkipped { - continue - } - hasSource = true - if step.State != model.OperationStateSuccess { - return false - } - } - return hasSource -} - -func findExecutionStepByTransferRef(plan *model.ExecutionPlan, transferRef string) *model.ExecutionStep { - if plan == nil { - return nil - } - transferRef = strings.TrimSpace(transferRef) - if transferRef == "" { - return nil - } - for _, step := range plan.Steps { - if step == nil { - continue - } - if strings.EqualFold(strings.TrimSpace(step.TransferRef), transferRef) { - return step - } - } - return nil -} - -func updateExecutionStepFromTransfer(plan *model.ExecutionPlan, event *chainv1.TransferStatusChangedEvent) *model.ExecutionStep { - if plan == nil || event == nil || event.GetTransfer() == nil { - return nil - } - transfer := event.GetTransfer() - transferRef := strings.TrimSpace(transfer.GetTransferRef()) - if transferRef == "" { - return nil - } - if status := executionStepStatusFromTransferStatus(transfer.GetStatus()); status != "" { - var updated *model.ExecutionStep - for _, step := range plan.Steps { - if step == nil { - continue - } - if !strings.EqualFold(strings.TrimSpace(step.TransferRef), transferRef) { - continue - } - if step.TransferRef == "" { - step.TransferRef = transferRef - } - setExecutionStepStatus(step, status) - if updated == nil { - updated = step - } - } - return updated - } - return nil -} - -func executionStepStatusFromTransferStatus(status chainv1.TransferStatus) model.OperationState { - switch status { - - case chainv1.TransferStatus_TRANSFER_CREATED: - return model.OperationStatePlanned - - case chainv1.TransferStatus_TRANSFER_PROCESSING: - return model.OperationStateProcessing - - case chainv1.TransferStatus_TRANSFER_WAITING: - return model.OperationStateWaiting - - case chainv1.TransferStatus_TRANSFER_SUCCESS: - return model.OperationStateSuccess - - case chainv1.TransferStatus_TRANSFER_FAILED: - return model.OperationStateFailed - - case chainv1.TransferStatus_TRANSFER_CANCELLED: - return model.OperationStateCancelled - - default: - return model.OperationStatePlanned - } -} - -func setExecutionStepMetadata(step *model.ExecutionStep, key, value string) { - if step == nil { - return - } - key = strings.TrimSpace(key) - if key == "" { - return - } - value = strings.TrimSpace(value) - if value == "" { - if step.Metadata != nil { - delete(step.Metadata, key) - if len(step.Metadata) == 0 { - step.Metadata = nil - } - } - return - } - if step.Metadata == nil { - step.Metadata = map[string]string{} - } - step.Metadata[key] = value -} diff --git a/api/payments/orchestrator/internal/service/execution/export.go b/api/payments/orchestrator/internal/service/execution/export.go deleted file mode 100644 index 5eeee1b6..00000000 --- a/api/payments/orchestrator/internal/service/execution/export.go +++ /dev/null @@ -1,123 +0,0 @@ -package execution - -import ( - "github.com/tech/sendico/payments/storage/model" - "github.com/tech/sendico/pkg/mlogger" - "github.com/tech/sendico/pkg/model/account_role" - chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" - sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1" -) - -const ( - ExecutionStepRoleSource = executionStepRoleSource - ExecutionStepRoleConsumer = executionStepRoleConsumer -) - -func SetExecutionStepRole(step *model.ExecutionStep, role string) { - setExecutionStepRole(step, role) -} - -func SetExecutionStepStatus(step *model.ExecutionStep, state model.OperationState) { - setExecutionStepStatus(step, state) -} - -func ExecutionStepRole(step *model.ExecutionStep) string { - return executionStepRole(step) -} - -func IsSourceExecutionStep(step *model.ExecutionStep) bool { - return isSourceExecutionStep(step) -} - -func SourceStepsConfirmed(plan *model.ExecutionPlan) bool { - return sourceStepsConfirmed(plan) -} - -func FindExecutionStepByTransferRef(plan *model.ExecutionPlan, transferRef string) *model.ExecutionStep { - return findExecutionStepByTransferRef(plan, transferRef) -} - -func UpdateExecutionStepFromTransfer(plan *model.ExecutionPlan, event *chainv1.TransferStatusChangedEvent) *model.ExecutionStep { - return updateExecutionStepFromTransfer(plan, event) -} - -func ExecutionStepStatusFromTransferStatus(status chainv1.TransferStatus) model.OperationState { - return executionStepStatusFromTransferStatus(status) -} - -func SetExecutionStepMetadata(step *model.ExecutionStep, key, value string) { - setExecutionStepMetadata(step, key, value) -} - -func EnsureExecutionRefs(payment *model.Payment) *model.ExecutionRefs { - return ensureExecutionRefs(payment) -} - -func ExecutionQuote( - payment *model.Payment, - quote *sharedv1.PaymentQuote, - quoteFromSnapshot func(*model.PaymentQuoteSnapshot) *sharedv1.PaymentQuote, -) *sharedv1.PaymentQuote { - return executionQuote(payment, quote, quoteFromSnapshot) -} - -func EnsureExecutionPlanForPlan(payment *model.Payment, plan *model.PaymentPlan) *model.ExecutionPlan { - return ensureExecutionPlanForPlan(payment, plan) -} - -func ExecutionPlanComplete(plan *model.ExecutionPlan) bool { - return executionPlanComplete(plan) -} - -func BlockStepConfirmed(plan *model.PaymentPlan, execPlan *model.ExecutionPlan) bool { - return blockStepConfirmed(plan, execPlan) -} - -func RoleHintsForStep(plan *model.PaymentPlan, idx int) (*account_role.AccountRole, *account_role.AccountRole) { - return roleHintsForStep(plan, idx) -} - -func LinkRailObservation(payment *model.Payment, rail model.Rail, referenceID, dependsOn string) { - linkRailObservation(payment, rail, referenceID, dependsOn) -} - -func PlanStepID(step *model.PaymentStep, idx int) string { - return planStepID(step, idx) -} - -func DescribePlanStep(step *model.PaymentStep) string { - return describePlanStep(step) -} - -func PlanStepIdempotencyKey(payment *model.Payment, idx int, step *model.PaymentStep) string { - return planStepIdempotencyKey(payment, idx, step) -} - -func FailureCodeForStep(step *model.PaymentStep) model.PaymentFailureCode { - return failureCodeForStep(step) -} - -func ExecutionStepsByCode(plan *model.ExecutionPlan) map[string]*model.ExecutionStep { - return executionStepsByCode(plan) -} - -func PlanStepsByID(plan *model.PaymentPlan) map[string]*model.PaymentStep { - return planStepsByID(plan) -} - -func StepDependenciesReady( - step *model.PaymentStep, - execSteps map[string]*model.ExecutionStep, - planSteps map[string]*model.PaymentStep, - requireSuccess bool, -) (ready bool, waiting bool, blocked bool, err error) { - return stepDependenciesReady(step, execSteps, planSteps, requireSuccess) -} - -func CardPayoutDependenciesConfirmed(plan *model.PaymentPlan, execPlan *model.ExecutionPlan) bool { - return cardPayoutDependenciesConfirmed(plan, execPlan) -} - -func AnalyzeExecutionPlan(logger mlogger.Logger, payment *model.Payment) (bool, bool, error) { - return analyzeExecutionPlan(logger, payment) -} diff --git a/api/payments/orchestrator/internal/service/execution/payment_plan_analyzer.go b/api/payments/orchestrator/internal/service/execution/payment_plan_analyzer.go deleted file mode 100644 index ea667542..00000000 --- a/api/payments/orchestrator/internal/service/execution/payment_plan_analyzer.go +++ /dev/null @@ -1,123 +0,0 @@ -package execution - -import ( - "errors" - - "github.com/tech/sendico/payments/storage/model" - "github.com/tech/sendico/pkg/mlogger" - "go.uber.org/zap" -) - -type Liveness string - -const ( - StepFinal Liveness = "final" - StepRunnable Liveness = "runnable" - StepBlocked Liveness = "blocked" - StepDead Liveness = "dead" -) - -func buildPaymentStepIndex(plan *model.PaymentPlan) map[string]*model.PaymentStep { - idx := make(map[string]*model.PaymentStep, len(plan.Steps)) - for _, s := range plan.Steps { - idx[s.StepID] = s - } - return idx -} - -func buildExecutionStepIndex(plan *model.ExecutionPlan) map[string]*model.ExecutionStep { - index := make(map[string]*model.ExecutionStep, len(plan.Steps)) - for _, s := range plan.Steps { - if s == nil { - continue - } - index[s.Code] = s - } - return index -} - -func stepLiveness( - logger mlogger.Logger, - step *model.ExecutionStep, - pStepIdx map[string]*model.PaymentStep, - eStepIdx map[string]*model.ExecutionStep, -) Liveness { - - if step.IsTerminal() { - return StepFinal - } - - pStep, ok := pStepIdx[step.Code] - if !ok { - logger.Error("Step missing in payment plan", - zap.String("step_id", step.Code), - ) - return StepDead - } - - for _, depID := range pStep.DependsOn { - dep := eStepIdx[depID] - if dep == nil { - logger.Warn("Dependency missing in execution plan", - zap.String("step_id", step.Code), - zap.String("dep_id", depID), - ) - continue - } - - switch dep.State { - case model.OperationStateFailed: - return StepDead - } - } - - allSuccess := true - for _, depID := range pStep.DependsOn { - dep := eStepIdx[depID] - if dep == nil || dep.State != model.OperationStateSuccess { - allSuccess = false - break - } - } - - if allSuccess { - return StepRunnable - } - - return StepBlocked -} - -func analyzeExecutionPlan( - logger mlogger.Logger, - payment *model.Payment, -) (bool, bool, error) { - - if payment == nil || payment.ExecutionPlan == nil { - return true, false, nil - } - - eIdx := buildExecutionStepIndex(payment.ExecutionPlan) - pIdx := buildPaymentStepIndex(payment.PaymentPlan) - - hasRunnable := false - hasFailed := false - var rootErr error - - for _, s := range payment.ExecutionPlan.Steps { - live := stepLiveness(logger, s, pIdx, eIdx) - - if live == StepRunnable { - hasRunnable = true - } - - if s.State == model.OperationStateFailed { - hasFailed = true - if rootErr == nil && s.Error != "" { - rootErr = errors.New(s.Error) - } - } - } - - done := !hasRunnable - return done, hasFailed, rootErr -} diff --git a/api/payments/orchestrator/internal/service/execution/payment_plan_helpers.go b/api/payments/orchestrator/internal/service/execution/payment_plan_helpers.go deleted file mode 100644 index 4a0b1471..00000000 --- a/api/payments/orchestrator/internal/service/execution/payment_plan_helpers.go +++ /dev/null @@ -1,220 +0,0 @@ -package execution - -import ( - "fmt" - "github.com/tech/sendico/pkg/discovery" - "strings" - - "github.com/google/uuid" - "github.com/tech/sendico/payments/storage/model" - "github.com/tech/sendico/pkg/model/account_role" - sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1" -) - -func ensureExecutionRefs(payment *model.Payment) *model.ExecutionRefs { - if payment.Execution == nil { - payment.Execution = &model.ExecutionRefs{} - } - return payment.Execution -} - -func executionQuote( - payment *model.Payment, - quote *sharedv1.PaymentQuote, - quoteFromSnapshot func(*model.PaymentQuoteSnapshot) *sharedv1.PaymentQuote, -) *sharedv1.PaymentQuote { - if quote != nil { - return quote - } - if payment != nil && payment.LastQuote != nil && quoteFromSnapshot != nil { - return quoteFromSnapshot(payment.LastQuote) - } - return &sharedv1.PaymentQuote{} -} - -func ensureExecutionPlanForPlan( - payment *model.Payment, - plan *model.PaymentPlan, -) *model.ExecutionPlan { - - if payment.ExecutionPlan != nil { - return payment.ExecutionPlan - } - - exec := &model.ExecutionPlan{ - Steps: make([]*model.ExecutionStep, 0, len(plan.Steps)), - } - - for _, step := range plan.Steps { - if step == nil { - continue - } - - exec.Steps = append(exec.Steps, &model.ExecutionStep{ - Code: step.StepID, - State: model.OperationStatePlanned, - OperationRef: uuid.New().String(), - }) - } - - return exec -} - -func executionPlanComplete(plan *model.ExecutionPlan) bool { - if plan == nil || len(plan.Steps) == 0 { - return false - } - for _, step := range plan.Steps { - if step == nil { - continue - } - if step.State == model.OperationStateSkipped { - continue - } - if step.State != model.OperationStateSuccess { - return false - } - } - return true -} - -func blockStepConfirmed(plan *model.PaymentPlan, execPlan *model.ExecutionPlan) bool { - if plan == nil || execPlan == nil || len(plan.Steps) == 0 { - return false - } - execSteps := executionStepsByCode(execPlan) - for idx, step := range plan.Steps { - if step == nil || step.Action != discovery.RailOperationBlock { - continue - } - execStep := execSteps[planStepID(step, idx)] - if execStep == nil { - continue - } - if execStep.State == model.OperationStateSuccess { - return true - } - } - return false -} - -func roleHintsForStep(plan *model.PaymentPlan, idx int) (*account_role.AccountRole, *account_role.AccountRole) { - if plan == nil || idx <= 0 { - return nil, nil - } - for i := idx - 1; i >= 0; i-- { - step := plan.Steps[i] - if step == nil { - continue - } - if step.Rail != discovery.RailLedger || step.Action != discovery.RailOperationMove { - continue - } - if step.ToRole != nil && strings.TrimSpace(string(*step.ToRole)) != "" { - role := *step.ToRole - return &role, nil - } - } - return nil, nil -} - -func linkRailObservation(payment *model.Payment, rail model.Rail, referenceID, dependsOn string) { - if payment == nil || payment.PaymentPlan == nil { - return - } - ref := strings.TrimSpace(referenceID) - if ref == "" { - return - } - plan := payment.PaymentPlan - execPlan := ensureExecutionPlanForPlan(payment, plan) - if execPlan == nil { - return - } - dep := strings.TrimSpace(dependsOn) - for idx, planStep := range plan.Steps { - if planStep == nil { - continue - } - if planStep.Rail != rail || planStep.Action != discovery.RailOperationObserveConfirm { - continue - } - if dep != "" { - matched := false - for _, entry := range planStep.DependsOn { - if strings.EqualFold(strings.TrimSpace(entry), dep) { - matched = true - break - } - } - if !matched { - continue - } - } - if idx >= len(execPlan.Steps) { - continue - } - execStep := execPlan.Steps[idx] - if execStep == nil { - execStep = &model.ExecutionStep{Code: planStepID(planStep, idx), Description: describePlanStep(planStep)} - execPlan.Steps[idx] = execStep - } - if execStep.TransferRef == "" { - execStep.TransferRef = ref - } - } -} - -func planStepID(step *model.PaymentStep, idx int) string { - if step != nil { - if val := strings.TrimSpace(step.StepID); val != "" { - return val - } - } - return fmt.Sprintf("plan_step_%d", idx) -} - -func describePlanStep(step *model.PaymentStep) string { - if step == nil { - return "" - } - return strings.TrimSpace(fmt.Sprintf("%s %s", step.Rail, step.Action)) -} - -func planStepIdempotencyKey(payment *model.Payment, idx int, step *model.PaymentStep) string { - base := "" - if payment != nil { - base = strings.TrimSpace(payment.IdempotencyKey) - if base == "" { - base = strings.TrimSpace(payment.PaymentRef) - } - } - if base == "" { - base = "payment" - } - if step == nil { - return fmt.Sprintf("%s:plan:%d", base, idx) - } - stepID := strings.TrimSpace(step.StepID) - if stepID == "" { - stepID = fmt.Sprintf("%d", idx) - } - return fmt.Sprintf("%s:plan:%s:%s:%s", base, stepID, strings.ToLower(string(step.Rail)), strings.ToLower(string(step.Action))) -} - -func failureCodeForStep(step *model.PaymentStep) model.PaymentFailureCode { - if step == nil { - return model.PaymentFailureCodePolicy - } - switch step.Rail { - case discovery.RailLedger: - if step.Action == discovery.RailOperationFXConvert { - return model.PaymentFailureCodeFX - } - return model.PaymentFailureCodeLedger - case discovery.RailCrypto: - return model.PaymentFailureCodeChain - default: - return model.PaymentFailureCodePolicy - } -} diff --git a/api/payments/orchestrator/internal/service/execution/payment_plan_order.go b/api/payments/orchestrator/internal/service/execution/payment_plan_order.go deleted file mode 100644 index 9a0c6d59..00000000 --- a/api/payments/orchestrator/internal/service/execution/payment_plan_order.go +++ /dev/null @@ -1,227 +0,0 @@ -package execution - -import ( - "github.com/tech/sendico/pkg/discovery" - "strings" - - "github.com/tech/sendico/payments/storage/model" - "github.com/tech/sendico/pkg/merrors" -) - -func executionStepsByCode(plan *model.ExecutionPlan) map[string]*model.ExecutionStep { - result := map[string]*model.ExecutionStep{} - if plan == nil { - return result - } - for _, step := range plan.Steps { - if step == nil { - continue - } - if code := strings.TrimSpace(step.Code); code != "" { - result[code] = step - } - } - return result -} - -func planStepsByID(plan *model.PaymentPlan) map[string]*model.PaymentStep { - result := map[string]*model.PaymentStep{} - if plan == nil { - return result - } - for idx, step := range plan.Steps { - if step == nil { - continue - } - id := planStepID(step, idx) - if id == "" { - continue - } - result[id] = step - } - return result -} - -func stepDependenciesReady( - step *model.PaymentStep, - execSteps map[string]*model.ExecutionStep, - planSteps map[string]*model.PaymentStep, - requireSuccess bool, -) (ready bool, waiting bool, blocked bool, err error) { - - if step == nil { - return false, false, false, - merrors.InvalidArgument("payment plan: step is required") - } - - for _, dep := range step.DependsOn { - key := strings.TrimSpace(dep) - if key == "" { - continue - } - - execStep := execSteps[key] - if execStep == nil { - // step has not been started - return false, true, false, nil - } - - if execStep.State == model.OperationStateFailed || - execStep.State == model.OperationStateCancelled { - // dependency dead, step is impossible - return false, false, true, nil - } - - if !execStep.ReadyForNext() { - // step is processed - return false, true, false, nil - } - } - - // ------------------------------------------------------------ - // Commit policies - // ------------------------------------------------------------ - switch step.CommitPolicy { - - case model.CommitPolicyImmediate, model.CommitPolicyUnspecified: - return true, false, false, nil - - case model.CommitPolicyAfterSuccess: - commitAfter := step.CommitAfter - if len(commitAfter) == 0 { - commitAfter = step.DependsOn - } - - for _, dep := range commitAfter { - key := strings.TrimSpace(dep) - if key == "" { - continue - } - - execStep := execSteps[key] - if execStep == nil { - return false, true, false, - merrors.InvalidArgument("commit dependency missing") - } - - if execStep.State == model.OperationStateFailed || - execStep.State == model.OperationStateCancelled { - return false, false, true, nil - } - - if !execStep.IsSuccess() { - return false, true, false, nil - } - } - - return true, false, false, nil - - case model.CommitPolicyAfterFailure: - commitAfter := step.CommitAfter - if len(commitAfter) == 0 { - commitAfter = step.DependsOn - } - - for _, dep := range commitAfter { - key := strings.TrimSpace(dep) - if key == "" { - continue - } - - execStep := execSteps[key] - if execStep == nil { - return false, true, false, - merrors.InvalidArgument("commit dependency missing") - } - - if execStep.State == model.OperationStateFailed { - continue - } - - if execStep.IsTerminal() { - // complete with fail, block - return false, false, true, nil - } - - // still exexuting, wait - return false, true, false, nil - } - - return true, false, false, nil - - case model.CommitPolicyAfterCanceled: - commitAfter := step.CommitAfter - if len(commitAfter) == 0 { - commitAfter = step.DependsOn - } - - for _, dep := range commitAfter { - key := strings.TrimSpace(dep) - if key == "" { - continue - } - - execStep := execSteps[key] - if execStep == nil { - return false, true, false, - merrors.InvalidArgument("commit dependency missing") - } - - if !execStep.IsTerminal() { - return false, true, false, nil - } - } - - return true, false, false, nil - - default: - return true, false, false, nil - } -} - -func cardPayoutDependenciesConfirmed( - plan *model.PaymentPlan, - execPlan *model.ExecutionPlan, -) bool { - - if execPlan == nil { - return false - } - - if plan == nil || len(plan.Steps) == 0 { - return sourceStepsConfirmed(execPlan) - } - - execSteps := executionStepsByCode(execPlan) - planSteps := planStepsByID(plan) - - for _, step := range plan.Steps { - if step == nil { - continue - } - - if step.Rail != discovery.RailCardPayout || - step.Action != discovery.RailOperationSend { - continue - } - - ready, waiting, blocked, err := - stepDependenciesReady(step, execSteps, planSteps, true) - - if err != nil || blocked { - // payout definitely cannot run - return false - } - - if waiting { - // dependencies exist but are not finished yet - // payout must NOT run - return false - } - - // only true when dependencies are REALLY satisfied - return ready - } - - return false -} diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/agg/module.go b/api/payments/orchestrator/internal/service/orchestrationv2/agg/module.go index 17488544..c2688be4 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/agg/module.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/agg/module.go @@ -44,33 +44,37 @@ const ( // StepShell defines one initial step telemetry item. type StepShell struct { - StepRef string `bson:"stepRef" json:"stepRef"` - StepCode string `bson:"stepCode" json:"stepCode"` - Rail model.Rail `bson:"rail,omitempty" json:"rail,omitempty"` - Gateway string `bson:"gateway,omitempty" json:"gateway,omitempty"` - InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"` - ReportVisibility model.ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"` - UserLabel string `bson:"userLabel,omitempty" json:"userLabel,omitempty"` + StepRef string `bson:"stepRef" json:"stepRef"` + StepCode string `bson:"stepCode" json:"stepCode"` + Rail model.Rail `bson:"rail,omitempty" json:"rail,omitempty"` + Gateway string `bson:"gateway,omitempty" json:"gateway,omitempty"` + InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"` + ReportVisibility model.ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"` + UserLabel string `bson:"userLabel,omitempty" json:"userLabel,omitempty"` + PlannedMoney *paymenttypes.Money `bson:"plannedMoney,omitempty" json:"plannedMoney,omitempty"` + PlannedConvertedMoney *paymenttypes.Money `bson:"plannedConvertedMoney,omitempty" json:"plannedConvertedMoney,omitempty"` } // StepExecution is runtime telemetry for one step. type StepExecution struct { - StepRef string `bson:"stepRef" json:"stepRef"` - StepCode string `bson:"stepCode" json:"stepCode"` - Rail model.Rail `bson:"rail,omitempty" json:"rail,omitempty"` - Gateway string `bson:"gateway,omitempty" json:"gateway,omitempty"` - InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"` - ReportVisibility model.ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"` - UserLabel string `bson:"userLabel,omitempty" json:"userLabel,omitempty"` - State StepState `bson:"state" json:"state"` - Attempt uint32 `bson:"attempt" json:"attempt"` - StartedAt *time.Time `bson:"startedAt,omitempty" json:"startedAt,omitempty"` - CompletedAt *time.Time `bson:"completedAt,omitempty" json:"completedAt,omitempty"` - FailureCode string `bson:"failureCode,omitempty" json:"failureCode,omitempty"` - FailureMsg string `bson:"failureMsg,omitempty" json:"failureMsg,omitempty"` - ExternalRefs []ExternalRef `bson:"externalRefs,omitempty" json:"externalRefs,omitempty"` - ExecutedMoney *paymenttypes.Money `bson:"executedMoney,omitempty" json:"executedMoney,omitempty"` - ConvertedMoney *paymenttypes.Money `bson:"convertedMoney,omitempty" json:"convertedMoney,omitempty"` + StepRef string `bson:"stepRef" json:"stepRef"` + StepCode string `bson:"stepCode" json:"stepCode"` + Rail model.Rail `bson:"rail,omitempty" json:"rail,omitempty"` + Gateway string `bson:"gateway,omitempty" json:"gateway,omitempty"` + InstanceID string `bson:"instanceId,omitempty" json:"instanceId,omitempty"` + ReportVisibility model.ReportVisibility `bson:"reportVisibility,omitempty" json:"reportVisibility,omitempty"` + UserLabel string `bson:"userLabel,omitempty" json:"userLabel,omitempty"` + State StepState `bson:"state" json:"state"` + Attempt uint32 `bson:"attempt" json:"attempt"` + StartedAt *time.Time `bson:"startedAt,omitempty" json:"startedAt,omitempty"` + CompletedAt *time.Time `bson:"completedAt,omitempty" json:"completedAt,omitempty"` + FailureCode string `bson:"failureCode,omitempty" json:"failureCode,omitempty"` + FailureMsg string `bson:"failureMsg,omitempty" json:"failureMsg,omitempty"` + ExternalRefs []ExternalRef `bson:"externalRefs,omitempty" json:"externalRefs,omitempty"` + ExecutedMoney *paymenttypes.Money `bson:"executedMoney,omitempty" json:"executedMoney,omitempty"` + ConvertedMoney *paymenttypes.Money `bson:"convertedMoney,omitempty" json:"convertedMoney,omitempty"` + PlannedMoney *paymenttypes.Money `bson:"plannedMoney,omitempty" json:"plannedMoney,omitempty"` + PlannedConvertedMoney *paymenttypes.Money `bson:"plannedConvertedMoney,omitempty" json:"plannedConvertedMoney,omitempty"` } // ExternalRef links step execution to an external operation. diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/agg/service.go b/api/payments/orchestrator/internal/service/orchestrationv2/agg/service.go index 186423a6..97e51d1c 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/agg/service.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/agg/service.go @@ -4,6 +4,7 @@ import ( "strings" "time" + svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared" "github.com/tech/sendico/payments/storage/model" "github.com/tech/sendico/pkg/db/storable" "github.com/tech/sendico/pkg/merrors" @@ -148,15 +149,17 @@ func buildInitialStepTelemetry(shell []StepShell) ([]StepExecution, error) { instanceID := strings.TrimSpace(shell[i].InstanceID) out = append(out, StepExecution{ - StepRef: stepRef, - StepCode: stepCode, - Rail: railValue, - Gateway: gatewayID, - InstanceID: instanceID, - ReportVisibility: visibility, - UserLabel: userLabel, - State: StepStatePending, - Attempt: 1, + StepRef: stepRef, + StepCode: stepCode, + Rail: railValue, + Gateway: gatewayID, + InstanceID: instanceID, + ReportVisibility: visibility, + UserLabel: userLabel, + State: StepStatePending, + Attempt: 1, + PlannedMoney: svcshared.CloneMoneyTrimNonEmpty(shell[i].PlannedMoney), + PlannedConvertedMoney: svcshared.CloneMoneyTrimNonEmpty(shell[i].PlannedConvertedMoney), }) } return out, nil diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/erecon/event.go b/api/payments/orchestrator/internal/service/orchestrationv2/erecon/event.go index e94d7d2d..2f648a13 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/erecon/event.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/erecon/event.go @@ -5,6 +5,8 @@ import ( "time" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/oshared" + svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared" "github.com/tech/sendico/pkg/merrors" paymenttypes "github.com/tech/sendico/pkg/payments/types" ) @@ -123,9 +125,9 @@ func normalizeGatewayEvent(src GatewayEvent) (*normalizedEvent, error) { ev := &normalizedEvent{ stepRef: strings.TrimSpace(src.StepRef), targetState: target, - failureInfo: buildFailureInfo(failureCode, failureMsg, normalizeTimePtr(src.OccurredAt)), + failureInfo: buildFailureInfo(failureCode, failureMsg, oshared.CloneTimeUTC(src.OccurredAt)), forceAggregate: buildForceAggregate(src.TerminalFailure, needsAttention), - executedMoney: normalizeEventMoney(src.ExecutedMoney), + executedMoney: svcshared.CloneMoneyTrimNonEmpty(src.ExecutedMoney), } ev.matchRefs = normalizeRefList([]agg.ExternalRef{ { @@ -162,7 +164,7 @@ func normalizeLedgerEvent(src LedgerEvent) (*normalizedEvent, error) { ev := &normalizedEvent{ stepRef: strings.TrimSpace(src.StepRef), targetState: target, - failureInfo: buildFailureInfo(failureCode, failureMsg, normalizeTimePtr(src.OccurredAt)), + failureInfo: buildFailureInfo(failureCode, failureMsg, oshared.CloneTimeUTC(src.OccurredAt)), forceAggregate: buildForceAggregate(src.TerminalFailure, needsAttention), } ev.matchRefs = normalizeRefList([]agg.ExternalRef{ @@ -194,7 +196,7 @@ func normalizeCardEvent(src CardEvent) (*normalizedEvent, error) { ev := &normalizedEvent{ stepRef: strings.TrimSpace(src.StepRef), targetState: target, - failureInfo: buildFailureInfo(failureCode, failureMsg, normalizeTimePtr(src.OccurredAt)), + failureInfo: buildFailureInfo(failureCode, failureMsg, oshared.CloneTimeUTC(src.OccurredAt)), forceAggregate: buildForceAggregate(src.TerminalFailure, needsAttention), } ev.matchRefs = normalizeRefList([]agg.ExternalRef{ @@ -268,18 +270,7 @@ func normalizeCardStatus(status CardStatus) (CardStatus, bool) { } func normalizeEventMoney(money *paymenttypes.Money) *paymenttypes.Money { - if money == nil { - return nil - } - amount := strings.TrimSpace(money.GetAmount()) - currency := strings.TrimSpace(money.GetCurrency()) - if amount == "" || currency == "" { - return nil - } - return &paymenttypes.Money{ - Amount: amount, - Currency: currency, - } + return svcshared.CloneMoneyTrimNonEmpty(money) } func mapFailureTarget(status any, retryable *bool) (agg.StepState, bool) { @@ -302,14 +293,6 @@ func mapFailureTarget(status any, retryable *bool) (agg.StepState, bool) { } } -func normalizeTimePtr(ts *time.Time) *time.Time { - if ts == nil { - return nil - } - val := ts.UTC() - return &val -} - func normalizeRefList(refs []agg.ExternalRef) []agg.ExternalRef { if len(refs) == 0 { return nil diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/opagg/aggregate_test.go b/api/payments/orchestrator/internal/service/orchestrationv2/opagg/aggregate_test.go index 553a23aa..0e94a0e5 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/opagg/aggregate_test.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/opagg/aggregate_test.go @@ -197,3 +197,57 @@ func TestAggregate_UserBatchQuoteSampleCompactsToSingleRecipientOperation(t *tes t.Fatalf("aggregated items mismatch: got=%q want=%q", got, want) } } + +func TestAggregate_MergeByRecipient_IgnoresSourceAndQuoteRefs(t *testing.T) { + aggregator := New() + + firstIntent := sampleIntent("intent-a", "card-1", "100") + secondIntent := sampleIntent("intent-b", "card-1", "125") + secondIntent.Source.ManagedWallet.ManagedWalletRef = "src-wallet-2" + + firstQuote := sampleQuote("quote-a", "100", "9150", "1.8") + secondQuote := sampleQuote("quote-b", "125", "11437.5", "1.8") + secondQuote.Route.RouteRef = "route-b" + secondQuote.FXQuote.QuoteRef = "fx-b" + secondQuote.FXQuote.RateRef = "rate-b" + secondQuote.FXQuote.Price.Value = "92.7" + + in := Input{ + Items: []Item{ + { + IntentSnapshot: firstIntent, + QuoteSnapshot: firstQuote, + MergeMode: MergeModeByRecipient, + }, + { + IntentSnapshot: secondIntent, + QuoteSnapshot: secondQuote, + MergeMode: MergeModeByRecipient, + }, + }, + } + + out, err := aggregator.Aggregate(in) + if err != nil { + t.Fatalf("Aggregate returned error: %v", err) + } + if out == nil { + t.Fatal("expected output") + } + if got, want := len(out.Groups), 1; got != want { + t.Fatalf("groups count mismatch: got=%d want=%d", got, want) + } + group := out.Groups[0] + if got, want := group.IntentSnapshot.Amount.Amount, "225"; got != want { + t.Fatalf("intent amount mismatch: got=%q want=%q", got, want) + } + if group.QuoteSnapshot == nil { + t.Fatal("expected quote snapshot") + } + if got, want := group.QuoteSnapshot.DebitAmount.Amount, "225"; got != want { + t.Fatalf("debit amount mismatch: got=%q want=%q", got, want) + } + if got, want := group.QuoteSnapshot.ExpectedSettlementAmount.Amount, "20587.5"; got != want { + t.Fatalf("settlement amount mismatch: got=%q want=%q", got, want) + } +} diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/opagg/clone.go b/api/payments/orchestrator/internal/service/orchestrationv2/opagg/clone.go index 0f48cce9..b2c86e39 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/opagg/clone.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/opagg/clone.go @@ -219,6 +219,33 @@ func cloneStringSlice(src []string) []string { return out } +func cloneGroupMembers(src []GroupMember) []GroupMember { + if len(src) == 0 { + return nil + } + out := make([]GroupMember, 0, len(src)) + for i := range src { + member := src[i] + intentSnapshot, err := cloneIntentSnapshot(member.IntentSnapshot) + if err != nil { + continue + } + quoteSnapshot, err := cloneQuoteSnapshot(member.QuoteSnapshot) + if err != nil { + continue + } + out = append(out, GroupMember{ + IntentRef: strings.TrimSpace(member.IntentRef), + IntentSnapshot: intentSnapshot, + QuoteSnapshot: quoteSnapshot, + }) + } + if len(out) == 0 { + return nil + } + return out +} + func cloneIntentSnapshot(src model.PaymentIntent) (model.PaymentIntent, error) { var dst model.PaymentIntent if err := bsonClone(src, &dst); err != nil { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/opagg/merge_core.go b/api/payments/orchestrator/internal/service/orchestrationv2/opagg/merge_core.go index 4f5c1621..80141339 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/opagg/merge_core.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/opagg/merge_core.go @@ -95,8 +95,6 @@ func mergeQuoteSnapshot(dst *model.PaymentQuoteSnapshot, src *model.PaymentQuote if strings.TrimSpace(dst.QuoteRef) == "" { dst.QuoteRef = strings.TrimSpace(src.QuoteRef) - } else if srcRef := strings.TrimSpace(src.QuoteRef); srcRef != "" && dst.QuoteRef != srcRef { - return merrors.InvalidArgument("quote_snapshot.quote_ref mismatch") } return nil diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/opagg/merge_quote_parts.go b/api/payments/orchestrator/internal/service/orchestrationv2/opagg/merge_quote_parts.go index 630a685f..04007a4e 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/opagg/merge_quote_parts.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/opagg/merge_quote_parts.go @@ -3,7 +3,6 @@ package opagg import ( "strings" - "github.com/tech/sendico/pkg/merrors" paymenttypes "github.com/tech/sendico/pkg/payments/types" ) @@ -14,9 +13,7 @@ func mergeRoute(dst, src *paymenttypes.QuoteRouteSpecification) (*paymenttypes.Q if src == nil { return dst, nil } - if routeSignature(dst) != routeSignature(src) { - return nil, merrors.InvalidArgument("quote_snapshot.route mismatch") - } + // Aggregation is destination-driven; keep the first route as execution template. return dst, nil } @@ -27,9 +24,6 @@ func mergeFXQuote(dst, src *paymenttypes.FXQuote) (*paymenttypes.FXQuote, error) if src == nil { return dst, nil } - if fxQuoteSignature(dst) != fxQuoteSignature(src) { - return nil, merrors.InvalidArgument("quote_snapshot.fx_quote mismatch") - } var err error dst.BaseAmount, err = mergeMoney(dst.BaseAmount, src.BaseAmount, "quote_snapshot.fx_quote.base_amount") if err != nil { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/opagg/module.go b/api/payments/orchestrator/internal/service/orchestrationv2/opagg/module.go index 74ba68ba..a9721a05 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/opagg/module.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/opagg/module.go @@ -31,15 +31,24 @@ type Item struct { IntentSnapshot model.PaymentIntent QuoteSnapshot *model.PaymentQuoteSnapshot MergeMode MergeMode + MergeKey string PolicyTag string } +// GroupMember preserves one original batch member inside an aggregated group. +type GroupMember struct { + IntentRef string + IntentSnapshot model.PaymentIntent + QuoteSnapshot *model.PaymentQuoteSnapshot +} + // Group is one aggregated recipient operation group. type Group struct { RecipientKey string IntentRefs []string IntentSnapshot model.PaymentIntent QuoteSnapshot *model.PaymentQuoteSnapshot + Members []GroupMember } // Output is the aggregation result. diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/opagg/service.go b/api/payments/orchestrator/internal/service/orchestrationv2/opagg/service.go index 3a7fde8e..f637afa3 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/opagg/service.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/opagg/service.go @@ -27,6 +27,7 @@ type groupAccumulator struct { intentRefs []string intent model.PaymentIntent quote *model.PaymentQuoteSnapshot + members []GroupMember } func (s *svc) Aggregate(in Input) (out *Output, err error) { @@ -90,12 +91,23 @@ func (s *svc) Aggregate(in Input) (out *Output, err error) { if quoteSnapshot == nil { return nil, merrors.InvalidArgument("items[" + itoa(i) + "].quote_snapshot is required") } + member, memberErr := makeGroupMember(intentRef, item.IntentSnapshot, item.QuoteSnapshot) + if memberErr != nil { + return nil, memberErr + } + groupRecipientKey := recipientKey + if normalizeMergeMode(item.MergeMode) == MergeModeByRecipient { + if override := strings.TrimSpace(item.MergeKey); override != "" { + groupRecipientKey = override + } + } groups[key] = &groupAccumulator{ - recipientKey: recipientKey, + recipientKey: groupRecipientKey, intentRefs: []string{intentRef}, intent: intentSnapshot, quote: quoteSnapshot, + members: []GroupMember{member}, } order = append(order, key) continue @@ -108,6 +120,11 @@ func (s *svc) Aggregate(in Input) (out *Output, err error) { return nil, merrors.InvalidArgument("items[" + itoa(i) + "]: " + err.Error()) } acc.intentRefs = append(acc.intentRefs, intentRef) + member, memberErr := makeGroupMember(intentRef, item.IntentSnapshot, item.QuoteSnapshot) + if memberErr != nil { + return nil, memberErr + } + acc.members = append(acc.members, member) } out = &Output{ @@ -139,6 +156,7 @@ func (s *svc) Aggregate(in Input) (out *Output, err error) { IntentRefs: cloneStringSlice(acc.intentRefs), IntentSnapshot: finalIntent, QuoteSnapshot: finalQuote, + Members: cloneGroupMembers(acc.members), }) } @@ -165,15 +183,28 @@ func validateItem(item Item) error { } func groupingKey(item Item) (string, string, error) { - sourceKey, err := endpointKey(item.IntentSnapshot.Source) - if err != nil { - return "", "", merrors.InvalidArgument("intent_snapshot.source: " + err.Error()) - } recipientKey, err := endpointKey(item.IntentSnapshot.Destination) if err != nil { return "", "", merrors.InvalidArgument("intent_snapshot.destination: " + err.Error()) } + if normalizeMergeMode(item.MergeMode) == MergeModeByRecipient { + mergeKey := strings.TrimSpace(item.MergeKey) + if mergeKey == "" { + mergeKey = recipientKey + } + key := strings.Join([]string{ + "kind=" + strings.ToLower(strings.TrimSpace(string(item.IntentSnapshot.Kind))), + "merge_key=" + mergeKey, + }, keySep) + return key, recipientKey, nil + } + + sourceKey, err := endpointKey(item.IntentSnapshot.Source) + if err != nil { + return "", "", merrors.InvalidArgument("intent_snapshot.source: " + err.Error()) + } + quote := item.QuoteSnapshot key := strings.Join([]string{ "kind=" + strings.ToLower(strings.TrimSpace(string(item.IntentSnapshot.Kind))), @@ -190,6 +221,22 @@ func groupingKey(item Item) (string, string, error) { return key, recipientKey, nil } +func makeGroupMember(intentRef string, intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot) (GroupMember, error) { + intentSnapshot, err := cloneIntentSnapshot(intent) + if err != nil { + return GroupMember{}, err + } + quoteSnapshot, err := cloneQuoteSnapshot(quote) + if err != nil { + return GroupMember{}, err + } + return GroupMember{ + IntentRef: strings.TrimSpace(intentRef), + IntentSnapshot: intentSnapshot, + QuoteSnapshot: quoteSnapshot, + }, nil +} + func isBatchingEligible(quote *model.PaymentQuoteSnapshot) bool { if quote == nil || quote.ExecutionConditions == nil { return true diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/oshared/clone.go b/api/payments/orchestrator/internal/service/orchestrationv2/oshared/clone.go new file mode 100644 index 00000000..ea31f6f2 --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrationv2/oshared/clone.go @@ -0,0 +1,31 @@ +package oshared + +import ( + "strings" + "time" + + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg" +) + +func CloneExternalRefs(refs []agg.ExternalRef) []agg.ExternalRef { + if len(refs) == 0 { + return nil + } + out := make([]agg.ExternalRef, 0, len(refs)) + for i := range refs { + ref := refs[i] + ref.GatewayInstanceID = strings.TrimSpace(ref.GatewayInstanceID) + ref.Kind = strings.TrimSpace(ref.Kind) + ref.Ref = strings.TrimSpace(ref.Ref) + out = append(out, ref) + } + return out +} + +func CloneTimeUTC(ts *time.Time) *time.Time { + if ts == nil { + return nil + } + val := ts.UTC() + return &val +} diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/prepo/document.go b/api/payments/orchestrator/internal/service/orchestrationv2/prepo/document.go index 4d0ed6d2..02c5a6fc 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/prepo/document.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/prepo/document.go @@ -2,14 +2,14 @@ package prepo import ( "strings" - "time" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/oshared" + svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared" "github.com/tech/sendico/payments/storage/model" "github.com/tech/sendico/pkg/db/storable" pm "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" - paymenttypes "github.com/tech/sendico/pkg/payments/types" "go.mongodb.org/mongo-driver/v2/bson" ) @@ -108,50 +108,14 @@ func cloneStepExecutions(src []agg.StepExecution) []agg.StepExecution { if step.Attempt == 0 { step.Attempt = 1 } - step.ExternalRefs = cloneExternalRefs(step.ExternalRefs) - step.ExecutedMoney = cloneStepMoney(step.ExecutedMoney) - step.ConvertedMoney = cloneStepMoney(step.ConvertedMoney) - step.StartedAt = cloneTime(step.StartedAt) - step.CompletedAt = cloneTime(step.CompletedAt) + step.ExternalRefs = oshared.CloneExternalRefs(step.ExternalRefs) + step.ExecutedMoney = svcshared.CloneMoneyTrimNonEmpty(step.ExecutedMoney) + step.ConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(step.ConvertedMoney) + step.PlannedMoney = svcshared.CloneMoneyTrimNonEmpty(step.PlannedMoney) + step.PlannedConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(step.PlannedConvertedMoney) + step.StartedAt = oshared.CloneTimeUTC(step.StartedAt) + step.CompletedAt = oshared.CloneTimeUTC(step.CompletedAt) out = append(out, step) } return out } - -func cloneStepMoney(money *paymenttypes.Money) *paymenttypes.Money { - if money == nil { - return nil - } - amount := strings.TrimSpace(money.GetAmount()) - currency := strings.TrimSpace(money.GetCurrency()) - if amount == "" || currency == "" { - return nil - } - return &paymenttypes.Money{ - Amount: amount, - Currency: currency, - } -} - -func cloneExternalRefs(refs []agg.ExternalRef) []agg.ExternalRef { - if len(refs) == 0 { - return nil - } - out := make([]agg.ExternalRef, 0, len(refs)) - for i := range refs { - ref := refs[i] - ref.GatewayInstanceID = strings.TrimSpace(ref.GatewayInstanceID) - ref.Kind = strings.TrimSpace(ref.Kind) - ref.Ref = strings.TrimSpace(ref.Ref) - out = append(out, ref) - } - return out -} - -func cloneTime(ts *time.Time) *time.Time { - if ts == nil { - return nil - } - val := ts.UTC() - return &val -} diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/prmap/service_test.go b/api/payments/orchestrator/internal/service/orchestrationv2/prmap/service_test.go index fd6f16ee..165f201e 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/prmap/service_test.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/prmap/service_test.go @@ -118,23 +118,26 @@ func TestMap_Success(t *testing.T) { if got, want := steps[0].GetUserLabel(), "Card payout"; got != want { t.Fatalf("user_label mismatch: got=%q want=%q", got, want) } - if steps[0].GetExecutedMoney() == nil { - t.Fatal("expected executed_money to be mapped") + if steps[0].GetMoney() == nil { + t.Fatal("expected structured money envelope to be mapped") } - if got, want := steps[0].GetExecutedMoney().GetAmount(), "95"; got != want { - t.Fatalf("executed_money.amount mismatch: got=%q want=%q", got, want) + if steps[0].GetMoney().GetPlanned() == nil { + t.Fatal("expected structured planned money snapshot") } - if got, want := steps[0].GetExecutedMoney().GetCurrency(), "USD"; got != want { - t.Fatalf("executed_money.currency mismatch: got=%q want=%q", got, want) + if got, want := steps[0].GetMoney().GetPlanned().GetAmount().GetAmount(), "96"; got != want { + t.Fatalf("money.planned.amount.amount mismatch: got=%q want=%q", got, want) } - if steps[0].GetConvertedMoney() == nil { - t.Fatal("expected converted_money to be mapped") + if got, want := steps[0].GetMoney().GetPlanned().GetConvertedAmount().GetAmount(), "91"; got != want { + t.Fatalf("money.planned.converted_amount.amount mismatch: got=%q want=%q", got, want) } - if got, want := steps[0].GetConvertedMoney().GetAmount(), "90"; got != want { - t.Fatalf("converted_money.amount mismatch: got=%q want=%q", got, want) + if steps[0].GetMoney().GetExecuted() == nil { + t.Fatal("expected structured executed money snapshot") } - if got, want := steps[0].GetConvertedMoney().GetCurrency(), "EUR"; got != want { - t.Fatalf("converted_money.currency mismatch: got=%q want=%q", got, want) + if got, want := steps[0].GetMoney().GetExecuted().GetAmount().GetAmount(), "95"; got != want { + t.Fatalf("money.executed.amount.amount mismatch: got=%q want=%q", got, want) + } + if got, want := steps[0].GetMoney().GetExecuted().GetConvertedAmount().GetAmount(), "90"; got != want { + t.Fatalf("money.executed.converted_amount.amount mismatch: got=%q want=%q", got, want) } if got, want := steps[1].GetReportVisibility(), orchestrationv2.ReportVisibility_REPORT_VISIBILITY_HIDDEN; got != want { t.Fatalf("report_visibility mismatch: got=%s want=%s", got.String(), want.String()) @@ -389,6 +392,14 @@ func newPaymentFixture() *agg.Payment { Amount: "90", Currency: "EUR", }, + PlannedMoney: &paymenttypes.Money{ + Amount: "96", + Currency: "USD", + }, + PlannedConvertedMoney: &paymenttypes.Money{ + Amount: "91", + Currency: "EUR", + }, StartedAt: &startedAt, }, { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/prmap/step_mapping.go b/api/payments/orchestrator/internal/service/orchestrationv2/prmap/step_mapping.go index af17feb4..0e593a87 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/prmap/step_mapping.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/prmap/step_mapping.go @@ -6,6 +6,7 @@ import ( "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg" "github.com/tech/sendico/pkg/merrors" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2" ) @@ -35,6 +36,12 @@ func mapStepExecution(step agg.StepExecution, index int) (*orchestrationv2.StepE attempt = 1 } + plannedAmount := moneyToProto(step.PlannedMoney) + plannedConverted := moneyToProto(step.PlannedConvertedMoney) + executedAmount := moneyToProto(step.ExecutedMoney) + executedConverted := moneyToProto(step.ConvertedMoney) + moneyEnvelope := mapStepMoneyEnvelope(plannedAmount, plannedConverted, executedAmount, executedConverted) + return &orchestrationv2.StepExecution{ StepRef: strings.TrimSpace(step.StepRef), StepCode: strings.TrimSpace(step.StepCode), @@ -46,11 +53,37 @@ func mapStepExecution(step agg.StepExecution, index int) (*orchestrationv2.StepE Refs: mapExternalRefs(step.StepCode, step.ExternalRefs), ReportVisibility: mapReportVisibility(step.ReportVisibility), UserLabel: strings.TrimSpace(step.UserLabel), - ExecutedMoney: moneyToProto(step.ExecutedMoney), - ConvertedMoney: moneyToProto(step.ConvertedMoney), + Money: moneyEnvelope, }, nil } +func mapStepMoneyEnvelope( + plannedAmount *moneyv1.Money, + plannedConverted *moneyv1.Money, + executedAmount *moneyv1.Money, + executedConverted *moneyv1.Money, +) *orchestrationv2.StepExecutionMoney { + planned := mapStepMoneySnapshot(plannedAmount, plannedConverted) + executed := mapStepMoneySnapshot(executedAmount, executedConverted) + if planned == nil && executed == nil { + return nil + } + return &orchestrationv2.StepExecutionMoney{ + Planned: planned, + Executed: executed, + } +} + +func mapStepMoneySnapshot(amount *moneyv1.Money, converted *moneyv1.Money) *orchestrationv2.StepExecutionMoneySnapshot { + if amount == nil && converted == nil { + return nil + } + return &orchestrationv2.StepExecutionMoneySnapshot{ + Amount: amount, + ConvertedAmount: converted, + } +} + func mapStepFailure(step agg.StepExecution, state agg.StepState) *orchestrationv2.Failure { if state != agg.StepStateFailed && state != agg.StepStateNeedsAttention { return nil diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/batch_optimizer.go b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/batch_optimizer.go index 60c0e769..bb57f516 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/batch_optimizer.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/batch_optimizer.go @@ -30,17 +30,24 @@ type BatchOptimizer interface { Optimize(in BatchOptimizeInput) (*BatchOptimizeOutput, error) } +// BatchOptimizationMergeKeyResolver allows external packages to resolve custom merge keys. +type BatchOptimizationMergeKeyResolver interface { + ResolveMergeKey(item qsnap.ResolvedItem, selection BatchOptimizationSelection) (string, bool) +} + // PolicyBatchOptimizerDependencies configures policy-based optimizer implementation. type PolicyBatchOptimizerDependencies struct { - Logger mlogger.Logger - Aggregator opagg.Aggregator - Policy BatchOptimizationPolicy + Logger mlogger.Logger + Aggregator opagg.Aggregator + Policy BatchOptimizationPolicy + MergeKeyResolver BatchOptimizationMergeKeyResolver } type policyBatchOptimizer struct { - logger mlogger.Logger - aggregator opagg.Aggregator - policy BatchOptimizationPolicy + logger mlogger.Logger + aggregator opagg.Aggregator + policy BatchOptimizationPolicy + mergeKeyResolver BatchOptimizationMergeKeyResolver } // NewPolicyBatchOptimizer creates default rule-based optimizer implementation. @@ -54,9 +61,10 @@ func NewPolicyBatchOptimizer(deps PolicyBatchOptimizerDependencies) BatchOptimiz aggregator = opagg.New(opagg.Dependencies{Logger: logger}) } return &policyBatchOptimizer{ - logger: logger.Named("batch_optimizer"), - aggregator: aggregator, - policy: normalizeBatchOptimizationPolicy(deps.Policy), + logger: logger.Named("batch_optimizer"), + aggregator: aggregator, + policy: normalizeBatchOptimizationPolicy(deps.Policy), + mergeKeyResolver: deps.MergeKeyResolver, } } @@ -68,13 +76,15 @@ func (o *policyBatchOptimizer) Optimize(in BatchOptimizeInput) (*BatchOptimizeOu aggItems := make([]opagg.Item, 0, len(in.Items)) for i := range in.Items { item := in.Items[i] - selection := o.policy.Select(batchOptimizationContextFromItem(item)) + selection := o.policy.SelectMany(batchOptimizationContextsFromItem(item)) intentSnapshot := applyBatchOptimizationSelection(item.IntentSnapshot, selection) + mergeKey := batchOptimizationMergeKey(item, selection, o.mergeKeyResolver) aggItems = append(aggItems, opagg.Item{ IntentRef: item.IntentRef, IntentSnapshot: intentSnapshot, QuoteSnapshot: item.QuoteSnapshot, MergeMode: mergeModeFromBatchOptimization(selection.Mode), + MergeKey: mergeKey, PolicyTag: batchOptimizationSelectionTag(selection), }) } @@ -89,13 +99,211 @@ func (o *policyBatchOptimizer) Optimize(in BatchOptimizeInput) (*BatchOptimizeOu return &BatchOptimizeOutput{Groups: aggOutput.Groups}, nil } +func batchOptimizationContextsFromItem(item qsnap.ResolvedItem) []BatchOptimizationContext { + out := make([]BatchOptimizationContext, 0, 6) + add := func(base BatchOptimizationContext) { + if base.Rail == discovery.RailUnspecified { + return + } + money := selectSettlementMoney(item) + if money != nil { + base.Amount = strings.TrimSpace(money.Amount) + base.Currency = strings.TrimSpace(money.Currency) + } + out = append(out, base) + } + + add(batchOptimizationContextFromItem(item)) + add(mergeBatchOptimizationContext( + endpointOptimizationContext(item.IntentSnapshot.Source), + sourceHopContext(item.QuoteSnapshot), + )) + route := item.QuoteSnapshot + if route != nil && route.Route != nil { + if parsed := model.ParseRail(route.Route.Rail); parsed != discovery.RailUnspecified { + add(BatchOptimizationContext{ + Rail: parsed, + Provider: strings.TrimSpace(route.Route.Provider), + Network: strings.TrimSpace(route.Route.Network), + }) + } + for i := range route.Route.Hops { + hop := route.Route.Hops[i] + if hop == nil { + continue + } + parsed := model.ParseRail(hop.Rail) + if parsed == discovery.RailUnspecified { + continue + } + add(BatchOptimizationContext{ + Rail: parsed, + Provider: strings.TrimSpace(hop.Gateway), + Network: strings.TrimSpace(hop.Network), + }) + } + } + if len(out) == 0 { + add(batchOptimizationContextFromItem(item)) + } + return out +} + +func batchOptimizationMergeKey( + item qsnap.ResolvedItem, + sel BatchOptimizationSelection, + resolver BatchOptimizationMergeKeyResolver, +) string { + if mergeModeFromBatchOptimization(sel.Mode) != opagg.MergeModeByRecipient { + return "" + } + + groupBy := normalizeBatchOptimizationGrouping(sel.GroupBy) + if groupBy == BatchOptimizationGroupingUnspecified { + groupBy = BatchOptimizationGroupingOperationDestination + } + switch groupBy { + case BatchOptimizationGroupingOperationSource: + return mergeEndpointKey(item.IntentSnapshot.Source) + case BatchOptimizationGroupingRailTarget: + if resolver != nil { + if key, ok := resolver.ResolveMergeKey(item, sel); ok { + return strings.TrimSpace(key) + } + } + return railTargetMergeKey(item, sel.MatchRail) + default: + return mergeEndpointKey(item.IntentSnapshot.Destination) + } +} + +func railTargetMergeKey(item qsnap.ResolvedItem, rail model.Rail) string { + matchRail := model.ParseRail(string(rail)) + if matchRail == discovery.RailUnspecified { + ctx := batchOptimizationContextFromItem(item) + matchRail = ctx.Rail + } + + switch matchRail { + case discovery.RailCardPayout: + return mergeEndpointKey(item.IntentSnapshot.Destination) + case discovery.RailCrypto: + if endpointRail(item.IntentSnapshot.Destination) == discovery.RailCrypto { + return mergeEndpointKey(item.IntentSnapshot.Destination) + } + if endpointRail(item.IntentSnapshot.Source) == discovery.RailCrypto { + return mergeEndpointKey(item.IntentSnapshot.Source) + } + return routeRailTargetKey(item.QuoteSnapshot, matchRail) + default: + return routeRailTargetKey(item.QuoteSnapshot, matchRail) + } +} + +func routeRailTargetKey(snapshot *model.PaymentQuoteSnapshot, rail model.Rail) string { + if snapshot == nil || snapshot.Route == nil { + return "" + } + route := snapshot.Route + target := "" + fallback := "" + for i := range route.Hops { + hop := route.Hops[i] + if hop == nil || model.ParseRail(hop.Rail) != rail { + continue + } + key := strings.Join([]string{ + "rail=" + strings.ToUpper(strings.TrimSpace(hop.Rail)), + "gateway=" + strings.TrimSpace(hop.Gateway), + "instance=" + strings.TrimSpace(hop.InstanceID), + "network=" + strings.TrimSpace(hop.Network), + }, "|") + if fallback == "" { + fallback = key + } + if hop.Role == paymenttypes.QuoteRouteHopRoleDestination { + target = key + break + } + } + if target != "" { + return target + } + if fallback != "" { + return fallback + } + return "" +} + +func mergeEndpointKey(ep model.PaymentEndpoint) string { + switch normalizeEndpointType(ep) { + case model.EndpointTypeLedger: + if ep.Ledger == nil { + return "" + } + return strings.Join([]string{ + "type=ledger", + "account=" + strings.TrimSpace(ep.Ledger.LedgerAccountRef), + "contra=" + strings.TrimSpace(ep.Ledger.ContraLedgerAccountRef), + }, "|") + case model.EndpointTypeManagedWallet: + if ep.ManagedWallet == nil { + return "" + } + return strings.Join([]string{ + "type=managed_wallet", + "wallet=" + strings.TrimSpace(ep.ManagedWallet.ManagedWalletRef), + "asset=" + mergeAssetKey(ep.ManagedWallet.Asset), + }, "|") + case model.EndpointTypeExternalChain: + if ep.ExternalChain == nil { + return "" + } + return strings.Join([]string{ + "type=external_chain", + "address=" + strings.TrimSpace(ep.ExternalChain.Address), + "memo=" + strings.TrimSpace(ep.ExternalChain.Memo), + "asset=" + mergeAssetKey(ep.ExternalChain.Asset), + }, "|") + case model.EndpointTypeCard: + if ep.Card == nil { + return "" + } + return strings.Join([]string{ + "type=card", + "token=" + strings.TrimSpace(ep.Card.Token), + "pan=" + strings.TrimSpace(ep.Card.Pan), + "masked=" + strings.TrimSpace(ep.Card.MaskedPan), + "country=" + strings.TrimSpace(ep.Card.Country), + "exp=" + strconv.FormatUint(uint64(ep.Card.ExpMonth), 10) + "-" + strconv.FormatUint(uint64(ep.Card.ExpYear), 10), + }, "|") + default: + return "" + } +} + +func mergeAssetKey(asset *paymenttypes.Asset) string { + if asset == nil { + return "" + } + return strings.Join([]string{ + strings.ToUpper(strings.TrimSpace(asset.Chain)), + strings.ToUpper(strings.TrimSpace(asset.TokenSymbol)), + strings.TrimSpace(asset.ContractAddress), + }, ":") +} + func batchOptimizationContextFromItem(item qsnap.ResolvedItem) BatchOptimizationContext { - rail, provider, network := destinationHopSignature(item.QuoteSnapshot) + // Match policy against operation target (intent destination), not route leg semantics. + target := mergeBatchOptimizationContext( + endpointOptimizationContext(item.IntentSnapshot.Destination), + destinationHopContext(item.QuoteSnapshot), + ) money := selectSettlementMoney(item) ctx := BatchOptimizationContext{ - Rail: rail, - Provider: provider, - Network: network, + Rail: target.Rail, + Provider: target.Provider, + Network: target.Network, } if money != nil { ctx.Amount = strings.TrimSpace(money.Amount) @@ -104,6 +312,70 @@ func batchOptimizationContextFromItem(item qsnap.ResolvedItem) BatchOptimization return ctx } +func mergeBatchOptimizationContext(primary, fallback BatchOptimizationContext) BatchOptimizationContext { + out := primary + if out.Rail == "" || out.Rail == discovery.RailUnspecified { + out.Rail = fallback.Rail + } + if strings.TrimSpace(out.Provider) == "" { + out.Provider = fallback.Provider + } + if strings.TrimSpace(out.Network) == "" { + out.Network = fallback.Network + } + return out +} + +func endpointOptimizationContext(endpoint model.PaymentEndpoint) BatchOptimizationContext { + out := BatchOptimizationContext{ + Rail: endpointRail(endpoint), + } + switch normalizeEndpointType(endpoint) { + case model.EndpointTypeManagedWallet: + if endpoint.ManagedWallet != nil && endpoint.ManagedWallet.Asset != nil { + out.Network = strings.TrimSpace(endpoint.ManagedWallet.Asset.Chain) + } + case model.EndpointTypeExternalChain: + if endpoint.ExternalChain != nil { + if endpoint.ExternalChain.Asset != nil { + out.Network = strings.TrimSpace(endpoint.ExternalChain.Asset.Chain) + } + } + } + return out +} + +func endpointRail(endpoint model.PaymentEndpoint) model.Rail { + switch normalizeEndpointType(endpoint) { + case model.EndpointTypeManagedWallet, model.EndpointTypeExternalChain: + return discovery.RailCrypto + case model.EndpointTypeCard: + return discovery.RailCardPayout + case model.EndpointTypeLedger: + return discovery.RailLedger + default: + return discovery.RailUnspecified + } +} + +func normalizeEndpointType(endpoint model.PaymentEndpoint) model.PaymentEndpointType { + if endpoint.Type != model.EndpointTypeUnspecified { + return endpoint.Type + } + switch { + case endpoint.Ledger != nil: + return model.EndpointTypeLedger + case endpoint.ManagedWallet != nil: + return model.EndpointTypeManagedWallet + case endpoint.ExternalChain != nil: + return model.EndpointTypeExternalChain + case endpoint.Card != nil: + return model.EndpointTypeCard + default: + return model.EndpointTypeUnspecified + } +} + func selectSettlementMoney(item qsnap.ResolvedItem) *paymenttypes.Money { if item.QuoteSnapshot != nil && item.QuoteSnapshot.ExpectedSettlementAmount != nil { return item.QuoteSnapshot.ExpectedSettlementAmount @@ -111,14 +383,16 @@ func selectSettlementMoney(item qsnap.ResolvedItem) *paymenttypes.Money { return item.IntentSnapshot.Amount } -func destinationHopSignature(snapshot *model.PaymentQuoteSnapshot) (model.Rail, string, string) { +func destinationHopContext(snapshot *model.PaymentQuoteSnapshot) BatchOptimizationContext { if snapshot == nil || snapshot.Route == nil { - return discovery.RailUnspecified, "", "" + return BatchOptimizationContext{} } route := snapshot.Route - rail := model.ParseRail(route.Rail) - provider := strings.TrimSpace(route.Provider) - network := strings.TrimSpace(route.Network) + ctx := BatchOptimizationContext{ + Rail: model.ParseRail(route.Rail), + Provider: strings.TrimSpace(route.Provider), + Network: strings.TrimSpace(route.Network), + } var destinationHop *paymenttypes.QuoteRouteHop for i := range route.Hops { @@ -133,16 +407,54 @@ func destinationHopSignature(snapshot *model.PaymentQuoteSnapshot) (model.Rail, } if destinationHop != nil { if parsed := model.ParseRail(destinationHop.Rail); parsed != discovery.RailUnspecified { - rail = parsed + ctx.Rail = parsed } - if provider == "" { - provider = strings.TrimSpace(destinationHop.Gateway) + if ctx.Provider == "" { + ctx.Provider = strings.TrimSpace(destinationHop.Gateway) } - if network == "" { - network = strings.TrimSpace(destinationHop.Network) + if ctx.Network == "" { + ctx.Network = strings.TrimSpace(destinationHop.Network) } } - return rail, provider, network + return ctx +} + +func sourceHopContext(snapshot *model.PaymentQuoteSnapshot) BatchOptimizationContext { + if snapshot == nil || snapshot.Route == nil { + return BatchOptimizationContext{} + } + route := snapshot.Route + ctx := BatchOptimizationContext{ + Rail: model.ParseRail(route.Rail), + Provider: strings.TrimSpace(route.Provider), + Network: strings.TrimSpace(route.Network), + } + var sourceHop *paymenttypes.QuoteRouteHop + for i := range route.Hops { + hop := route.Hops[i] + if hop == nil { + continue + } + if sourceHop == nil { + sourceHop = hop + } + if hop.Role == paymenttypes.QuoteRouteHopRoleSource { + sourceHop = hop + break + } + } + if sourceHop != nil { + if parsed := model.ParseRail(sourceHop.Rail); parsed != discovery.RailUnspecified { + ctx.Rail = parsed + } + if ctx.Provider == "" { + ctx.Provider = strings.TrimSpace(sourceHop.Gateway) + } + if ctx.Network == "" { + ctx.Network = strings.TrimSpace(sourceHop.Network) + } + } + return ctx } func mergeModeFromBatchOptimization(mode BatchOptimizationMode) opagg.MergeMode { @@ -174,6 +486,12 @@ func applyBatchOptimizationSelection(intent model.PaymentIntent, sel BatchOptimi } intent.Attributes[attrBatchOptimizerMode] = string(mode) intent.Attributes[attrBatchOptimizerRuleMatch] = strconv.FormatBool(sel.Matched) + groupBy := normalizeBatchOptimizationGrouping(sel.GroupBy) + if groupBy == BatchOptimizationGroupingUnspecified { + delete(intent.Attributes, attrBatchOptimizerGrouping) + } else { + intent.Attributes[attrBatchOptimizerGrouping] = string(groupBy) + } ruleID := strings.TrimSpace(sel.RuleID) if ruleID == "" { delete(intent.Attributes, attrBatchOptimizerRuleID) diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/batch_optimizer_test.go b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/batch_optimizer_test.go new file mode 100644 index 00000000..05af061f --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/batch_optimizer_test.go @@ -0,0 +1,48 @@ +package psvc + +import ( + "testing" + + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/qsnap" + "github.com/tech/sendico/payments/storage/model" + "github.com/tech/sendico/pkg/discovery" + paymenttypes "github.com/tech/sendico/pkg/payments/types" +) + +func TestBatchOptimizationContextFromItem_UsesOperationTargetContext(t *testing.T) { + item := qsnap.ResolvedItem{ + IntentRef: "intent-1", + IntentSnapshot: model.PaymentIntent{ + Ref: "intent-1", + Destination: model.PaymentEndpoint{ + Type: model.EndpointTypeCard, + Card: &model.CardEndpoint{ + Pan: "2200001100000001", + ExpMonth: 1, + ExpYear: 2030, + }, + }, + Amount: &paymenttypes.Money{Amount: "100", Currency: "RUB"}, + SettlementCurrency: "RUB", + }, + QuoteSnapshot: &model.PaymentQuoteSnapshot{ + Route: &paymenttypes.QuoteRouteSpecification{ + Hops: []*paymenttypes.QuoteRouteHop{ + {Index: 1, Rail: "CRYPTO", Gateway: "gw-crypto", Network: "TRON_NILE", Role: paymenttypes.QuoteRouteHopRoleSource}, + {Index: 2, Rail: "CARD", Gateway: "mcards", Network: "TRON_NILE", Role: paymenttypes.QuoteRouteHopRoleDestination}, + }, + }, + }, + } + + ctx := batchOptimizationContextFromItem(item) + if got, want := ctx.Rail, model.ParseRail(string(discovery.RailCardPayout)); got != want { + t.Fatalf("operation rail mismatch: got=%q want=%q", got, want) + } + if got, want := ctx.Provider, "mcards"; got != want { + t.Fatalf("provider mismatch: got=%q want=%q", got, want) + } + if got, want := ctx.Network, "TRON_NILE"; got != want { + t.Fatalf("network mismatch: got=%q want=%q", got, want) + } +} diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/default_executors.go b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/default_executors.go index 6a62d3b8..38bebdf3 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/default_executors.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/default_executors.go @@ -7,6 +7,7 @@ import ( "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan" + svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared" "github.com/tech/sendico/pkg/mlogger" paymenttypes "github.com/tech/sendico/pkg/payments/types" ) @@ -201,7 +202,7 @@ func inheritedExternalRefs(payment *agg.Payment, step xplan.Step, current agg.St } func inheritedExecutedMoney(payment *agg.Payment, step xplan.Step, current agg.StepExecution) *paymenttypes.Money { - if money := cloneStepMoney(current.ExecutedMoney); money != nil { + if money := svcshared.CloneMoneyTrimNonEmpty(current.ExecutedMoney); money != nil { return money } if payment == nil || len(step.DependsOn) == 0 { @@ -213,7 +214,7 @@ func inheritedExecutedMoney(payment *agg.Payment, step xplan.Step, current agg.S if !ok || idx < 0 || idx >= len(payment.StepExecutions) { continue } - if money := cloneStepMoney(payment.StepExecutions[idx].ExecutedMoney); money != nil { + if money := svcshared.CloneMoneyTrimNonEmpty(payment.StepExecutions[idx].ExecutedMoney); money != nil { return money } } @@ -221,7 +222,7 @@ func inheritedExecutedMoney(payment *agg.Payment, step xplan.Step, current agg.S } func inheritedConvertedMoney(payment *agg.Payment, step xplan.Step, current agg.StepExecution) *paymenttypes.Money { - if money := cloneStepMoney(current.ConvertedMoney); money != nil { + if money := svcshared.CloneMoneyTrimNonEmpty(current.ConvertedMoney); money != nil { return money } if payment == nil || len(step.DependsOn) == 0 { @@ -233,7 +234,7 @@ func inheritedConvertedMoney(payment *agg.Payment, step xplan.Step, current agg. if !ok || idx < 0 || idx >= len(payment.StepExecutions) { continue } - if money := cloneStepMoney(payment.StepExecutions[idx].ConvertedMoney); money != nil { + if money := svcshared.CloneMoneyTrimNonEmpty(payment.StepExecutions[idx].ConvertedMoney); money != nil { return money } } diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/execute.go b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/execute.go index 0d834db6..e32fda86 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/execute.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/execute.go @@ -14,6 +14,7 @@ import ( "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/qsnap" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/reqval" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan" + "github.com/tech/sendico/payments/storage/model" "github.com/tech/sendico/pkg/merrors" orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2" "go.uber.org/zap" @@ -168,7 +169,7 @@ func (s *svc) createNewPayment(ctx context.Context, requestCtx *reqval.Ctx) (*ag ClientPaymentRef: requestCtx.ClientPaymentRef, IntentSnapshot: resolved.IntentSnapshot, QuoteSnapshot: resolved.QuoteSnapshot, - Steps: toStepShells(graph), + Steps: toStepShells(graph, resolved.IntentSnapshot, resolved.QuoteSnapshot), }) if err != nil { return nil, err @@ -212,20 +213,23 @@ func (s *svc) resolveAndPlan(ctx context.Context, requestCtx *reqval.Ctx) (*qsna return resolved, graph, nil } -func toStepShells(graph *xplan.Graph) []agg.StepShell { +func toStepShells(graph *xplan.Graph, intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot) []agg.StepShell { if graph == nil || len(graph.Steps) == 0 { return nil } out := make([]agg.StepShell, 0, len(graph.Steps)) for i := range graph.Steps { + plannedMoney, plannedConverted := plannedMoneyForStep(graph.Steps[i], intent, quote) out = append(out, agg.StepShell{ - StepRef: graph.Steps[i].StepRef, - StepCode: graph.Steps[i].StepCode, - Rail: graph.Steps[i].Rail, - Gateway: graph.Steps[i].Gateway, - InstanceID: graph.Steps[i].InstanceID, - ReportVisibility: graph.Steps[i].Visibility, - UserLabel: graph.Steps[i].UserLabel, + StepRef: graph.Steps[i].StepRef, + StepCode: graph.Steps[i].StepCode, + Rail: graph.Steps[i].Rail, + Gateway: graph.Steps[i].Gateway, + InstanceID: graph.Steps[i].InstanceID, + ReportVisibility: graph.Steps[i].Visibility, + UserLabel: graph.Steps[i].UserLabel, + PlannedMoney: plannedMoney, + PlannedConvertedMoney: plannedConverted, }) } return out diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/execute_batch.go b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/execute_batch.go index 312d391d..e93ae308 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/execute_batch.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/execute_batch.go @@ -31,6 +31,7 @@ const ( attrAggregatedByRecipient = "orchestrator.v2.aggregated_by_recipient" attrAggregatedItems = "orchestrator.v2.aggregated_items" attrBatchOptimizerMode = "orchestrator.v2.batch_optimizer_mode" + attrBatchOptimizerGrouping = "orchestrator.v2.batch_optimizer_group_by" attrBatchOptimizerRuleID = "orchestrator.v2.batch_optimizer_rule_id" attrBatchOptimizerRuleMatch = "orchestrator.v2.batch_optimizer_rule_matched" ) @@ -198,7 +199,7 @@ func (s *svc) createGroupPayment( ClientPaymentRef: requestCtx.ClientPaymentRef, IntentSnapshot: intentSnapshot, QuoteSnapshot: group.QuoteSnapshot, - Steps: toStepShells(graph), + Steps: toStepShells(graph, intentSnapshot, group.QuoteSnapshot), }) if err != nil { return nil, err @@ -291,7 +292,7 @@ func (s *svc) buildBatchOperationGroups(groups []opagg.Group) ([]opagg.Group, er } group.IntentSnapshot.Attributes[attrAggregatedItems] = strconv.Itoa(len(group.IntentRefs)) - targets := buildBatchPayoutTargets([]opagg.Group{group}) + targets := buildBatchPayoutTargets(group) if routeContainsCardPayout(group.QuoteSnapshot) && len(targets) > 0 { raw, err := batchmeta.EncodePayoutTargets(targets) if err != nil { @@ -311,32 +312,49 @@ func (s *svc) buildBatchOperationGroups(groups []opagg.Group) ([]opagg.Group, er return out, nil } -func buildBatchPayoutTargets(groups []opagg.Group) []batchmeta.PayoutTarget { - if len(groups) == 0 { +func buildBatchPayoutTargets(group opagg.Group) []batchmeta.PayoutTarget { + if len(group.Members) > 0 { + targets := make([]batchmeta.PayoutTarget, 0, len(group.Members)) + for i := range group.Members { + member := group.Members[i] + target := batchmeta.PayoutTarget{ + TargetRef: firstNonEmpty(strings.TrimSpace(member.IntentRef), "recipient-"+strconv.Itoa(i+1)), + IntentRef: normalizeIntentRefs([]string{member.IntentRef}), + Amount: batchPayoutAmountFromMember(member), + } + if member.IntentSnapshot.Destination.Type == model.EndpointTypeCard && member.IntentSnapshot.Destination.Card != nil { + card := *member.IntentSnapshot.Destination.Card + target.Card = &card + } + if member.IntentSnapshot.Customer != nil { + customer := *member.IntentSnapshot.Customer + target.Customer = &customer + } + targets = append(targets, target) + } + if len(targets) == 0 { + return nil + } + return targets + } + + if isEmptyIntentSnapshot(group.IntentSnapshot) { return nil } - out := make([]batchmeta.PayoutTarget, 0, len(groups)) - for i := range groups { - group := groups[i] - target := batchmeta.PayoutTarget{ - TargetRef: firstNonEmpty(strings.TrimSpace(group.RecipientKey), "recipient-"+strconv.Itoa(i+1)), - IntentRef: normalizeIntentRefs(group.IntentRefs), - Amount: batchPayoutAmount(group), - } - if group.IntentSnapshot.Destination.Type == model.EndpointTypeCard && group.IntentSnapshot.Destination.Card != nil { - card := *group.IntentSnapshot.Destination.Card - target.Card = &card - } - if group.IntentSnapshot.Customer != nil { - customer := *group.IntentSnapshot.Customer - target.Customer = &customer - } - out = append(out, target) + target := batchmeta.PayoutTarget{ + TargetRef: firstNonEmpty(strings.TrimSpace(group.RecipientKey), "recipient-1"), + IntentRef: normalizeIntentRefs(group.IntentRefs), + Amount: batchPayoutAmount(group), } - if len(out) == 0 { - return nil + if group.IntentSnapshot.Destination.Type == model.EndpointTypeCard && group.IntentSnapshot.Destination.Card != nil { + card := *group.IntentSnapshot.Destination.Card + target.Card = &card } - return out + if group.IntentSnapshot.Customer != nil { + customer := *group.IntentSnapshot.Customer + target.Customer = &customer + } + return []batchmeta.PayoutTarget{target} } func batchPayoutAmount(group opagg.Group) *paymenttypes.Money { @@ -355,6 +373,26 @@ func batchPayoutAmount(group opagg.Group) *paymenttypes.Money { } } +func batchPayoutAmountFromMember(member opagg.GroupMember) *paymenttypes.Money { + if member.QuoteSnapshot != nil && member.QuoteSnapshot.ExpectedSettlementAmount != nil { + return &paymenttypes.Money{ + Amount: strings.TrimSpace(member.QuoteSnapshot.ExpectedSettlementAmount.Amount), + Currency: strings.TrimSpace(member.QuoteSnapshot.ExpectedSettlementAmount.Currency), + } + } + if member.IntentSnapshot.Amount == nil { + return nil + } + return &paymenttypes.Money{ + Amount: strings.TrimSpace(member.IntentSnapshot.Amount.Amount), + Currency: strings.TrimSpace(member.IntentSnapshot.Amount.Currency), + } +} + +func isEmptyIntentSnapshot(intent model.PaymentIntent) bool { + return intent.Amount == nil && (intent.Kind == "" || intent.Kind == model.PaymentKindUnspecified) +} + func routeContainsCardPayout(snapshot *model.PaymentQuoteSnapshot) bool { if snapshot == nil || snapshot.Route == nil { return false diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/execute_batch_test.go b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/execute_batch_test.go index 12496423..d2798b07 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/execute_batch_test.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/execute_batch_test.go @@ -162,6 +162,47 @@ func TestExecuteBatchPayment_CryptoPolicyMergesByDestination(t *testing.T) { } } +func TestExecuteBatchPayment_CryptoPolicyMergesSameRecipientEvenWithDifferentSourceAndQuoteRefs(t *testing.T) { + env := newTestEnvWithPolicy(t, BatchOptimizationPolicy{ + DefaultMode: BatchOptimizationModeNoOptimization, + Rules: []BatchOptimizationRule{ + { + ID: "crypto-merge", + Priority: 100, + Mode: BatchOptimizationModeMergeByDestination, + Match: BatchOptimizationMatch{ + Rail: discovery.RailCrypto, + }, + }, + }, + }, func(_ string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) { + step := req.StepExecution + step.State = agg.StepStateCompleted + return &sexec.ExecuteOutput{StepExecution: step}, nil + }) + + quote := newExecutableBatchCryptoQuoteSameDestination(env.orgID, "quote-batch-crypto-merge-mixed") + if len(quote.Items) != 2 { + t.Fatalf("expected 2 quote items, got=%d", len(quote.Items)) + } + quote.Items[0].Quote.Route.RouteRef = "route-a" + quote.Items[1].Intent.Source = testLedgerEndpoint("ledger-src-2") + quote.Items[1].Quote.Route.RouteRef = "route-b" + env.quotes.Put(quote) + + resp, err := env.svc.ExecuteBatchPayment(context.Background(), &orchestrationv2.ExecuteBatchPaymentRequest{ + Meta: testMeta(env.orgID, "idem-batch-crypto-merge-mixed"), + QuotationRef: "quote-batch-crypto-merge-mixed", + ClientPaymentRef: "client-batch-crypto-merge-mixed", + }) + if err != nil { + t.Fatalf("ExecuteBatchPayment returned error: %v", err) + } + if got, want := len(resp.GetPayments()), 1; got != want { + t.Fatalf("expected %d merged payment for crypto policy, got=%d", want, got) + } +} + func TestExecuteBatchPayment_IdempotentRetry(t *testing.T) { env := newTestEnv(t, func(_ string, req sexec.StepRequest) (*sexec.ExecuteOutput, error) { step := req.StepExecution diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/optimization_policy.go b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/optimization_policy.go index dded7f9f..f4778b4c 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/optimization_policy.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/optimization_policy.go @@ -17,11 +17,21 @@ const ( BatchOptimizationModeNoOptimization BatchOptimizationMode = "no_optimization" ) +// BatchOptimizationGrouping defines which target key is used for merge grouping. +type BatchOptimizationGrouping string + +const ( + BatchOptimizationGroupingUnspecified BatchOptimizationGrouping = "" + BatchOptimizationGroupingOperationDestination BatchOptimizationGrouping = "operation_destination" + BatchOptimizationGroupingOperationSource BatchOptimizationGrouping = "operation_source" + BatchOptimizationGroupingRailTarget BatchOptimizationGrouping = "rail_target" +) + // BatchOptimizationPolicy configures batch operation compaction behavior. // // Rules are evaluated by "best match": // 1. all specified match fields must match the item context; -// 2. winner is highest specificity (number of optional selectors present); +// 2. winner is highest specificity (number of optional match fields present); // 3. ties are resolved by larger Priority; // 4. remaining ties keep the first rule order from config. // @@ -37,6 +47,7 @@ type BatchOptimizationRule struct { Enabled *bool Priority int Mode BatchOptimizationMode + GroupBy BatchOptimizationGrouping Match BatchOptimizationMatch } @@ -67,9 +78,11 @@ type BatchOptimizationContext struct { // BatchOptimizationSelection is the selected optimization decision. type BatchOptimizationSelection struct { - Mode BatchOptimizationMode - RuleID string - Matched bool + Mode BatchOptimizationMode + RuleID string + Matched bool + GroupBy BatchOptimizationGrouping + MatchRail model.Rail } func defaultBatchOptimizationPolicy() BatchOptimizationPolicy { @@ -107,10 +120,14 @@ func normalizeBatchOptimizationRule(in BatchOptimizationRule) (BatchOptimization Enabled: in.Enabled, Priority: in.Priority, Mode: normalizeBatchOptimizationMode(in.Mode), + GroupBy: normalizeBatchOptimizationGrouping(in.GroupBy), } if out.Mode == BatchOptimizationModeUnspecified { return BatchOptimizationRule{}, false } + if out.GroupBy == BatchOptimizationGroupingUnspecified { + out.GroupBy = BatchOptimizationGroupingOperationDestination + } match, ok := normalizeBatchOptimizationMatch(in.Match) if !ok { @@ -174,41 +191,78 @@ func normalizeBatchOptimizationAmountRange(in BatchOptimizationAmountRange) (Bat } func (p BatchOptimizationPolicy) Select(in BatchOptimizationContext) BatchOptimizationSelection { + return p.SelectMany([]BatchOptimizationContext{in}) +} + +func (p BatchOptimizationPolicy) SelectMany(inputs []BatchOptimizationContext) BatchOptimizationSelection { policy := normalizeBatchOptimizationPolicy(p) - ctx := normalizeBatchOptimizationContext(in) + contexts := make([]BatchOptimizationContext, 0, len(inputs)) + for i := range inputs { + ctx := normalizeBatchOptimizationContext(inputs[i]) + if ctx.Rail == discovery.RailUnspecified { + continue + } + contexts = append(contexts, ctx) + } selected := BatchOptimizationSelection{ - Mode: policy.DefaultMode, + Mode: policy.DefaultMode, + GroupBy: BatchOptimizationGroupingOperationDestination, + MatchRail: discovery.RailUnspecified, + } + if len(contexts) == 0 { + return selected } bestSpecificity := -1 bestPriority := 0 bestOrder := len(policy.Rules) + 1 - for i := range policy.Rules { - rule := policy.Rules[i] - if rule.Enabled != nil && !*rule.Enabled { - continue - } - specificity, ok := rule.matches(ctx) - if !ok { - continue - } - if specificity > bestSpecificity || - (specificity == bestSpecificity && rule.Priority > bestPriority) || - (specificity == bestSpecificity && rule.Priority == bestPriority && i < bestOrder) { - selected = BatchOptimizationSelection{ - Mode: rule.Mode, - RuleID: rule.ID, - Matched: true, + bestContext := len(contexts) + 1 + for cIdx := range contexts { + ctx := contexts[cIdx] + for rIdx := range policy.Rules { + rule := policy.Rules[rIdx] + if rule.Enabled != nil && !*rule.Enabled { + continue + } + specificity, ok := rule.matches(ctx) + if !ok { + continue + } + if specificity > bestSpecificity || + (specificity == bestSpecificity && rule.Priority > bestPriority) || + (specificity == bestSpecificity && rule.Priority == bestPriority && rIdx < bestOrder) || + (specificity == bestSpecificity && rule.Priority == bestPriority && rIdx == bestOrder && cIdx < bestContext) { + selected = BatchOptimizationSelection{ + Mode: rule.Mode, + RuleID: rule.ID, + Matched: true, + GroupBy: rule.GroupBy, + MatchRail: rule.Match.Rail, + } + bestSpecificity = specificity + bestPriority = rule.Priority + bestOrder = rIdx + bestContext = cIdx } - bestSpecificity = specificity - bestPriority = rule.Priority - bestOrder = i } } return selected } +func normalizeBatchOptimizationGrouping(raw BatchOptimizationGrouping) BatchOptimizationGrouping { + switch strings.ToLower(strings.TrimSpace(string(raw))) { + case "operation_destination", "operation-destination", "destination", "intent_destination", "intent-destination": + return BatchOptimizationGroupingOperationDestination + case "operation_source", "operation-source", "source", "intent_source", "intent-source": + return BatchOptimizationGroupingOperationSource + case "rail_target", "rail-target", "by_rail_target", "by-rail-target": + return BatchOptimizationGroupingRailTarget + default: + return BatchOptimizationGroupingUnspecified + } +} + func (r BatchOptimizationRule) matches(ctx BatchOptimizationContext) (int, bool) { match := r.Match if match.Rail == discovery.RailUnspecified || ctx.Rail == discovery.RailUnspecified { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/optimization_policy_test.go b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/optimization_policy_test.go index 11bef0d0..497e23d5 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/optimization_policy_test.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/optimization_policy_test.go @@ -53,6 +53,9 @@ func TestNormalizeBatchOptimizationPolicy_DropsInvalidRules(t *testing.T) { if got.Rules[0].ID != "valid" { t.Fatalf("rule id mismatch: got=%q want=%q", got.Rules[0].ID, "valid") } + if got, want := got.Rules[0].GroupBy, BatchOptimizationGroupingOperationDestination; got != want { + t.Fatalf("group_by mismatch: got=%q want=%q", got, want) + } } func TestBatchOptimizationPolicy_Select_UsesBestSpecificityThenPriority(t *testing.T) { @@ -101,6 +104,12 @@ func TestBatchOptimizationPolicy_Select_UsesBestSpecificityThenPriority(t *testi if out.Mode != BatchOptimizationModeMergeByDestination { t.Fatalf("mode mismatch: got=%q want=%q", out.Mode, BatchOptimizationModeMergeByDestination) } + if got, want := out.GroupBy, BatchOptimizationGroupingOperationDestination; got != want { + t.Fatalf("group_by mismatch: got=%q want=%q", got, want) + } + if got, want := string(out.MatchRail), string(discovery.RailCrypto); got != want { + t.Fatalf("match rail mismatch: got=%q want=%q", got, want) + } } func TestBatchOptimizationPolicy_Select_FallsBackToDefault(t *testing.T) { @@ -124,6 +133,9 @@ func TestBatchOptimizationPolicy_Select_FallsBackToDefault(t *testing.T) { if out.Mode != BatchOptimizationModeNoOptimization { t.Fatalf("mode mismatch: got=%q want=%q", out.Mode, BatchOptimizationModeNoOptimization) } + if got, want := out.GroupBy, BatchOptimizationGroupingOperationDestination; got != want { + t.Fatalf("default group_by mismatch: got=%q want=%q", got, want) + } } func TestBatchOptimizationPolicy_Select_MatchesAmountRange(t *testing.T) { @@ -165,3 +177,26 @@ func TestBatchOptimizationPolicy_Select_MatchesAmountRange(t *testing.T) { t.Fatalf("default mode mismatch: got=%q want=%q", notMatched.Mode, BatchOptimizationModeNoOptimization) } } + +func TestBatchOptimizationPolicy_Select_ReturnsConfiguredGroupBy(t *testing.T) { + policy := BatchOptimizationPolicy{ + DefaultMode: BatchOptimizationModeNoOptimization, + Rules: []BatchOptimizationRule{ + { + ID: "crypto-by-rail-target", + Mode: BatchOptimizationModeMergeByDestination, + GroupBy: BatchOptimizationGroupingRailTarget, + Match: BatchOptimizationMatch{ + Rail: discovery.RailCrypto, + }, + }, + }, + } + out := policy.Select(BatchOptimizationContext{Rail: discovery.RailCrypto}) + if !out.Matched { + t.Fatal("expected matched rule") + } + if got, want := out.GroupBy, BatchOptimizationGroupingRailTarget; got != want { + t.Fatalf("group_by mismatch: got=%q want=%q", got, want) + } +} diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/planned_money.go b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/planned_money.go new file mode 100644 index 00000000..3ce477ad --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/planned_money.go @@ -0,0 +1,234 @@ +package psvc + +import ( + "fmt" + "strings" + + "github.com/shopspring/decimal" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/batchmeta" + svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared" + "github.com/tech/sendico/payments/storage/model" + "github.com/tech/sendico/pkg/discovery" + paymenttypes "github.com/tech/sendico/pkg/payments/types" + + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan" +) + +func plannedMoneyForStep( + step xplan.Step, + intent model.PaymentIntent, + quote *model.PaymentQuoteSnapshot, +) (*paymenttypes.Money, *paymenttypes.Money) { + action := model.ParseRailOperation(string(step.Action)) + + switch step.Rail { + case discovery.RailLedger: + return plannedLedgerMoney(step, intent, quote, action), nil + case discovery.RailCrypto: + switch action { + case discovery.RailOperationSend: + return plannedSourceMoney(intent, quote), nil + case discovery.RailOperationFee: + return plannedFeeMoney(intent, quote), nil + } + case discovery.RailProviderSettlement: + if action == discovery.RailOperationFXConvert { + return plannedSourceMoney(intent, quote), plannedSettlementMoney(intent, quote) + } + case discovery.RailCardPayout: + if action == discovery.RailOperationSend { + return plannedCardPayoutMoney(step, intent, quote), nil + } + } + + if override, ok := batchmeta.AmountFromMetadata(step.Metadata); ok { + return svcshared.CloneMoneyTrimNonEmpty(override), nil + } + return nil, nil +} + +func plannedLedgerMoney( + step xplan.Step, + intent model.PaymentIntent, + quote *model.PaymentQuoteSnapshot, + action model.RailOperation, +) *paymenttypes.Money { + if override, ok := batchmeta.AmountFromMetadata(step.Metadata); ok { + return svcshared.CloneMoneyTrimNonEmpty(override) + } + + sourceMoney := plannedSourceMoney(intent, quote) + settlementMoney := plannedSettlementMoney(intent, quote) + payoutMoney := settlementMoney + + if fromRail, toRail, ok := plannedLedgerBoundaryRails(step.StepCode, quote); ok { + switch { + case isLedgerExternalRail(fromRail) && isLedgerExternalRail(toRail): + return sourceMoney + case isLedgerExternalRail(fromRail) && isLedgerInternalRail(toRail): + return settlementMoney + case isLedgerInternalRail(fromRail) && isLedgerExternalRail(toRail): + if toRail == discovery.RailCardPayout { + return payoutMoney + } + return settlementMoney + case isLedgerInternalRail(fromRail) && isLedgerInternalRail(toRail): + return settlementMoney + } + } + + switch action { + case discovery.RailOperationCredit, discovery.RailOperationExternalCredit: + return settlementMoney + case discovery.RailOperationDebit, discovery.RailOperationExternalDebit: + if sourceMoney != nil { + return sourceMoney + } + return settlementMoney + case discovery.RailOperationMove, discovery.RailOperationBlock, discovery.RailOperationRelease: + if settlementMoney != nil { + return settlementMoney + } + return sourceMoney + default: + return nil + } +} + +func plannedSourceMoney(intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot) *paymenttypes.Money { + if quote != nil && quote.DebitAmount != nil { + return svcshared.CloneMoneyTrimNonEmpty(quote.DebitAmount) + } + return svcshared.CloneMoneyTrimNonEmpty(intent.Amount) +} + +func plannedSettlementMoney(intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot) *paymenttypes.Money { + if quote != nil && quote.ExpectedSettlementAmount != nil { + return svcshared.CloneMoneyTrimNonEmpty(quote.ExpectedSettlementAmount) + } + return plannedSourceMoney(intent, quote) +} + +func plannedCardPayoutMoney(step xplan.Step, intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot) *paymenttypes.Money { + if override, ok := batchmeta.AmountFromMetadata(step.Metadata); ok { + return svcshared.CloneMoneyTrimNonEmpty(override) + } + if quote != nil && quote.ExpectedSettlementAmount != nil { + return svcshared.CloneMoneyTrimNonEmpty(quote.ExpectedSettlementAmount) + } + return svcshared.CloneMoneyTrimNonEmpty(intent.Amount) +} + +func plannedFeeMoney(intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot) *paymenttypes.Money { + if quote == nil || len(quote.FeeLines) == 0 { + return nil + } + + sourceCurrency := "" + if source := plannedSourceMoney(intent, quote); source != nil { + sourceCurrency = strings.TrimSpace(source.Currency) + } + + total := decimal.Zero + currency := "" + for i := range quote.FeeLines { + line := quote.FeeLines[i] + if line == nil || line.GetMoney() == nil { + continue + } + + lineCurrency := strings.TrimSpace(line.GetMoney().GetCurrency()) + if lineCurrency == "" { + continue + } + if sourceCurrency != "" && !strings.EqualFold(sourceCurrency, lineCurrency) { + continue + } + if currency == "" { + currency = strings.ToUpper(lineCurrency) + } else if !strings.EqualFold(currency, lineCurrency) { + return nil + } + + amountRaw := strings.TrimSpace(line.GetMoney().GetAmount()) + amount, err := decimal.NewFromString(amountRaw) + if err != nil { + continue + } + amount = amount.Abs() + if amount.IsZero() { + continue + } + if line.GetSide() == paymenttypes.EntrySideCredit { + total = total.Sub(amount) + } else { + total = total.Add(amount) + } + } + + if total.Sign() <= 0 || strings.TrimSpace(currency) == "" { + return nil + } + return &paymenttypes.Money{ + Amount: total.String(), + Currency: strings.ToUpper(strings.TrimSpace(currency)), + } +} + +func plannedLedgerBoundaryRails(stepCode string, quote *model.PaymentQuoteSnapshot) (model.Rail, model.Rail, bool) { + fromIndex, toIndex, ok := parseLedgerEdgeStepCode(stepCode) + if !ok || quote == nil || quote.Route == nil { + return discovery.RailUnspecified, discovery.RailUnspecified, false + } + + var fromRail model.Rail = discovery.RailUnspecified + var toRail model.Rail = discovery.RailUnspecified + for i := range quote.Route.Hops { + hop := quote.Route.Hops[i] + if hop == nil { + continue + } + if hop.Index == fromIndex { + fromRail = model.ParseRail(hop.Rail) + } + if hop.Index == toIndex { + toRail = model.ParseRail(hop.Rail) + } + } + if fromRail == discovery.RailUnspecified || toRail == discovery.RailUnspecified { + return discovery.RailUnspecified, discovery.RailUnspecified, false + } + return fromRail, toRail, true +} + +func parseLedgerEdgeStepCode(stepCode string) (uint32, uint32, bool) { + code := strings.ToLower(strings.TrimSpace(stepCode)) + if !strings.HasPrefix(code, "edge.") || !strings.Contains(code, ".ledger.") { + return 0, 0, false + } + var ( + from uint32 + to uint32 + op string + ) + if _, err := fmt.Sscanf(code, "edge.%d_%d.ledger.%s", &from, &to, &op); err != nil { + return 0, 0, false + } + if strings.TrimSpace(op) == "" { + return 0, 0, false + } + return from, to, true +} + +func isLedgerInternalRail(rail model.Rail) bool { + return rail == discovery.RailLedger +} + +func isLedgerExternalRail(rail model.Rail) bool { + switch rail { + case discovery.RailCrypto, discovery.RailProviderSettlement, discovery.RailCardPayout, discovery.RailFiatOnRamp: + return true + default: + return false + } +} diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/runtime.go b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/runtime.go index 6b85e613..a8d4dbd1 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/runtime.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/runtime.go @@ -13,6 +13,7 @@ import ( "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/ssched" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan" + svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared" "github.com/tech/sendico/pkg/merrors" "go.uber.org/zap" ) @@ -325,8 +326,10 @@ func normalizeExecutorOutput(current agg.StepExecution, out *sexec.ExecuteOutput next.Attempt = out.StepExecution.Attempt } next.ExternalRefs = out.StepExecution.ExternalRefs - next.ExecutedMoney = cloneStepMoney(out.StepExecution.ExecutedMoney) - next.ConvertedMoney = cloneStepMoney(out.StepExecution.ConvertedMoney) + next.ExecutedMoney = svcshared.CloneMoneyTrimNonEmpty(out.StepExecution.ExecutedMoney) + next.ConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(out.StepExecution.ConvertedMoney) + next.PlannedMoney = svcshared.CloneMoneyTrimNonEmpty(out.StepExecution.PlannedMoney) + next.PlannedConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(out.StepExecution.PlannedConvertedMoney) next.FailureCode = strings.TrimSpace(out.StepExecution.FailureCode) next.FailureMsg = strings.TrimSpace(out.StepExecution.FailureMsg) @@ -445,6 +448,12 @@ func stepExecutionEqual(left, right agg.StepExecution) bool { if !stepMoneyEqual(left.ConvertedMoney, right.ConvertedMoney) { return false } + if !stepMoneyEqual(left.PlannedMoney, right.PlannedMoney) { + return false + } + if !stepMoneyEqual(left.PlannedConvertedMoney, right.PlannedConvertedMoney) { + return false + } return true } diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/step_money.go b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/step_money.go index 6aed2204..1f776feb 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/psvc/step_money.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/psvc/step_money.go @@ -1,29 +1,13 @@ package psvc import ( - "strings" - + svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared" paymenttypes "github.com/tech/sendico/pkg/payments/types" ) -func cloneStepMoney(src *paymenttypes.Money) *paymenttypes.Money { - if src == nil { - return nil - } - amount := strings.TrimSpace(src.GetAmount()) - currency := strings.TrimSpace(src.GetCurrency()) - if amount == "" || currency == "" { - return nil - } - return &paymenttypes.Money{ - Amount: amount, - Currency: currency, - } -} - func stepMoneyEqual(left, right *paymenttypes.Money) bool { - left = cloneStepMoney(left) - right = cloneStepMoney(right) + left = svcshared.CloneMoneyTrimNonEmpty(left) + right = svcshared.CloneMoneyTrimNonEmpty(right) switch { case left == nil && right == nil: return true diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/sexec/service_test.go b/api/payments/orchestrator/internal/service/orchestrationv2/sexec/service_test.go index 50134cc3..8d125475 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/sexec/service_test.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/sexec/service_test.go @@ -207,6 +207,19 @@ func TestExecute_UnsupportedProviderSettlementSend(t *testing.T) { } } +func TestExecute_UnsupportedLedgerFee(t *testing.T) { + registry := New(Dependencies{}) + + _, err := registry.Execute(context.Background(), ExecuteInput{ + Payment: &agg.Payment{PaymentRef: "p1"}, + Step: xplan.Step{StepRef: "s1", StepCode: "ledger.fee", Action: discovery.RailOperationFee, Rail: discovery.RailLedger}, + StepExecution: agg.StepExecution{StepRef: "s1", StepCode: "ledger.fee", Attempt: 1}, + }) + if !errors.Is(err, ErrUnsupportedStep) { + t.Fatalf("expected ErrUnsupportedStep, got %v", err) + } +} + func TestExecute_MissingExecutor(t *testing.T) { registry := New(Dependencies{}) diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/ssched/input.go b/api/payments/orchestrator/internal/service/orchestrationv2/ssched/input.go index 5ee3e030..c6162de6 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/ssched/input.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/ssched/input.go @@ -4,11 +4,12 @@ import ( "strings" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/oshared" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan" + svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared" "github.com/tech/sendico/payments/storage/model" "github.com/tech/sendico/pkg/discovery" "github.com/tech/sendico/pkg/merrors" - paymenttypes "github.com/tech/sendico/pkg/payments/types" ) func (s *svc) prepareInput(in Input) (*preparedInput, error) { @@ -174,9 +175,11 @@ func (s *svc) normalizeStepExecution(exec agg.StepExecution, index int) (agg.Ste exec.InstanceID = strings.TrimSpace(exec.InstanceID) exec.Rail = model.ParseRail(string(exec.Rail)) exec.ReportVisibility = model.NormalizeReportVisibility(exec.ReportVisibility) - exec.ExternalRefs = cloneExternalRefs(exec.ExternalRefs) - exec.ExecutedMoney = cloneStepMoney(exec.ExecutedMoney) - exec.ConvertedMoney = cloneStepMoney(exec.ConvertedMoney) + exec.ExternalRefs = oshared.CloneExternalRefs(exec.ExternalRefs) + exec.ExecutedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.ExecutedMoney) + exec.ConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.ConvertedMoney) + exec.PlannedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.PlannedMoney) + exec.PlannedConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.PlannedConvertedMoney) if exec.StepRef == "" { return agg.StepExecution{}, merrors.InvalidArgument("stepExecutions[" + itoa(index) + "].step_ref is required") } @@ -212,15 +215,17 @@ func seedMissingExecutions( maxAttemptsByRef[stepRef] = 1 } executionsByRef[stepRef] = &agg.StepExecution{ - StepRef: step.StepRef, - StepCode: step.StepCode, - Rail: step.Rail, - Gateway: strings.TrimSpace(step.Gateway), - InstanceID: strings.TrimSpace(step.InstanceID), - ReportVisibility: effectiveStepVisibility(model.ReportVisibilityUnspecified, step.Visibility), - UserLabel: strings.TrimSpace(step.UserLabel), - State: agg.StepStatePending, - Attempt: attempt, + StepRef: step.StepRef, + StepCode: step.StepCode, + Rail: step.Rail, + Gateway: strings.TrimSpace(step.Gateway), + InstanceID: strings.TrimSpace(step.InstanceID), + ReportVisibility: effectiveStepVisibility(model.ReportVisibilityUnspecified, step.Visibility), + UserLabel: strings.TrimSpace(step.UserLabel), + State: agg.StepStatePending, + Attempt: attempt, + PlannedMoney: nil, + PlannedConvertedMoney: nil, } } } @@ -259,39 +264,11 @@ func normalizeStepState(state agg.StepState) (agg.StepState, bool) { func cloneStepExecution(exec agg.StepExecution) agg.StepExecution { out := exec - out.ExternalRefs = cloneExternalRefs(exec.ExternalRefs) - out.ExecutedMoney = cloneStepMoney(exec.ExecutedMoney) - out.ConvertedMoney = cloneStepMoney(exec.ConvertedMoney) - return out -} - -func cloneStepMoney(money *paymenttypes.Money) *paymenttypes.Money { - if money == nil { - return nil - } - amount := strings.TrimSpace(money.GetAmount()) - currency := strings.TrimSpace(money.GetCurrency()) - if amount == "" || currency == "" { - return nil - } - return &paymenttypes.Money{ - Amount: amount, - Currency: currency, - } -} - -func cloneExternalRefs(refs []agg.ExternalRef) []agg.ExternalRef { - if len(refs) == 0 { - return nil - } - out := make([]agg.ExternalRef, 0, len(refs)) - for i := range refs { - ref := refs[i] - ref.GatewayInstanceID = strings.TrimSpace(ref.GatewayInstanceID) - ref.Kind = strings.TrimSpace(ref.Kind) - ref.Ref = strings.TrimSpace(ref.Ref) - out = append(out, ref) - } + out.ExternalRefs = oshared.CloneExternalRefs(exec.ExternalRefs) + out.ExecutedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.ExecutedMoney) + out.ConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.ConvertedMoney) + out.PlannedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.PlannedMoney) + out.PlannedConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(exec.PlannedConvertedMoney) return out } diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/compile_flow_test.go b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/compile_flow_test.go index 37f276a6..8050009b 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/compile_flow_test.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/compile_flow_test.go @@ -170,7 +170,7 @@ func TestCompile_ExternalToExternal_WithWalletFee_InsertsFeeStep(t *testing.T) { } } -func TestCompile_ExternalToExternal_IgnoresNonWalletFeeLines(t *testing.T) { +func TestCompile_ExternalToExternal_IncludesNonWalletFeeLines(t *testing.T) { compiler := New() graph, err := compiler.Compile(Input{ @@ -195,14 +195,18 @@ func TestCompile_ExternalToExternal_IgnoresNonWalletFeeLines(t *testing.T) { if err != nil { t.Fatalf("Compile returned error: %v", err) } - if got, want := len(graph.Steps), 9; got != want { - t.Fatalf("expected 9 steps, got %d", got) + if got, want := len(graph.Steps), 10; got != want { + t.Fatalf("expected 10 steps, got %d", got) } + feeCount := 0 for i := range graph.Steps { - if got := graph.Steps[i].Action; got == discovery.RailOperationFee { - t.Fatalf("unexpected fee step at index %d: %+v", i, graph.Steps[i]) + if graph.Steps[i].Action == discovery.RailOperationFee { + feeCount++ } } + if got, want := feeCount, 1; got != want { + t.Fatalf("fee steps mismatch: got=%d want=%d", got, want) + } } func TestCompile_InternalToExternal_UsesHoldAndSettlementBranches(t *testing.T) { @@ -239,6 +243,51 @@ func TestCompile_InternalToExternal_UsesHoldAndSettlementBranches(t *testing.T) } } +func TestCompile_InternalToExternal_WithWalletFee_InsertsFeeStep(t *testing.T) { + compiler := New() + + graph, err := compiler.Compile(Input{ + IntentSnapshot: testIntent(model.PaymentKindPayout), + QuoteSnapshot: &model.PaymentQuoteSnapshot{ + DebitAmount: &paymenttypes.Money{Amount: "100", Currency: "USDT"}, + FeeLines: []*paymenttypes.FeeLine{ + { + Money: &paymenttypes.Money{Amount: "1", Currency: "USDT"}, + Side: paymenttypes.EntrySideDebit, + Meta: map[string]string{"fee_target": "wallet"}, + }, + }, + Route: &paymenttypes.QuoteRouteSpecification{ + Hops: []*paymenttypes.QuoteRouteHop{ + {Index: 10, Rail: "LEDGER", Role: paymenttypes.QuoteRouteHopRoleSource}, + {Index: 20, Rail: "CARD", Gateway: "gw-card", Role: paymenttypes.QuoteRouteHopRoleDestination}, + }, + }, + }, + }) + if err != nil { + t.Fatalf("Compile returned error: %v", err) + } + if len(graph.Steps) != 7 { + t.Fatalf("expected 7 steps, got %d", len(graph.Steps)) + } + + assertStep(t, graph.Steps[0], "edge.10_20.ledger.block", discovery.RailOperationBlock, discovery.RailLedger, model.ReportVisibilityHidden) + assertStep(t, graph.Steps[1], "hop.10.ledger.fee", discovery.RailOperationFee, discovery.RailLedger, model.ReportVisibilityHidden) + assertStep(t, graph.Steps[2], "hop.20.card_payout.send", discovery.RailOperationSend, discovery.RailCardPayout, model.ReportVisibilityUser) + assertStep(t, graph.Steps[3], "hop.20.card_payout.observe", discovery.RailOperationObserveConfirm, discovery.RailCardPayout, model.ReportVisibilityUser) + assertStep(t, graph.Steps[4], "edge.10_20.ledger.debit", discovery.RailOperationExternalDebit, discovery.RailLedger, model.ReportVisibilityHidden) + assertStep(t, graph.Steps[5], "edge.10_20.ledger.release", discovery.RailOperationRelease, discovery.RailLedger, model.ReportVisibilityHidden) + assertStep(t, graph.Steps[6], "edge.10_20.ledger.release", discovery.RailOperationRelease, discovery.RailLedger, model.ReportVisibilityHidden) + + if got, want := graph.Steps[1].DependsOn, []string{graph.Steps[0].StepRef}; !equalStringSlice(got, want) { + t.Fatalf("fee deps mismatch: got=%v want=%v", got, want) + } + if got, want := graph.Steps[2].DependsOn, []string{graph.Steps[1].StepRef}; !equalStringSlice(got, want) { + t.Fatalf("send deps mismatch: got=%v want=%v", got, want) + } +} + func TestCompile_ExternalToInternal_UsesCreditAfterObserve(t *testing.T) { compiler := New() diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/expansion.go b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/expansion.go index 66606bce..7a50ee2c 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/expansion.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/expansion.go @@ -51,28 +51,28 @@ func (e *expansion) nextRef(base string) string { return token + "_" + itoa(count+1) } -func (e *expansion) needsWalletFeeStep(hop normalizedHop) bool { - if e == nil || len(e.walletFeeHops) == 0 { +func (e *expansion) needsFeeStep(hop normalizedHop) bool { + if e == nil || len(e.feeHops) == 0 { return false } key := observedKey(hop) - if _, ok := e.walletFeeHops[key]; !ok { + if _, ok := e.feeHops[key]; !ok { return false } - if _, emitted := e.walletFeeEmitted[key]; emitted { + if _, emitted := e.feeEmitted[key]; emitted { return false } return true } -func (e *expansion) markWalletFeeEmitted(hop normalizedHop) { +func (e *expansion) markFeeEmitted(hop normalizedHop) { if e == nil { return } - if e.walletFeeEmitted == nil { - e.walletFeeEmitted = map[string]struct{}{} + if e.feeEmitted == nil { + e.feeEmitted = map[string]struct{}{} } - e.walletFeeEmitted[observedKey(hop)] = struct{}{} + e.feeEmitted[observedKey(hop)] = struct{}{} } func normalizeStep(step Step) Step { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/fee_planning.go b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/fee_planning.go index 617fb4ff..8afefe75 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/fee_planning.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/fee_planning.go @@ -5,27 +5,26 @@ import ( "strings" "github.com/shopspring/decimal" - "github.com/tech/sendico/pkg/discovery" "github.com/tech/sendico/payments/storage/model" "github.com/tech/sendico/pkg/merrors" paymenttypes "github.com/tech/sendico/pkg/payments/types" ) -func planWalletFeeHops( +func planFeeHops( hops []normalizedHop, intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot, ) (map[string]struct{}, error) { - _, hasWalletFee, err := walletFeeAmountFromSnapshots(intent, quote) + _, hasFee, err := feeAmountFromSnapshots(intent, quote) if err != nil { return nil, err } - if !hasWalletFee { + if !hasFee { return nil, nil } - sourceHop, ok := sourceCryptoHop(hops) + sourceHop, ok := sourceFeeHop(hops) if !ok { return nil, nil } @@ -34,19 +33,19 @@ func planWalletFeeHops( }, nil } -func sourceCryptoHop(hops []normalizedHop) (normalizedHop, bool) { +func sourceFeeHop(hops []normalizedHop) (normalizedHop, bool) { for i := range hops { - if hops[i].rail == discovery.RailCrypto && hops[i].role == paymenttypes.QuoteRouteHopRoleSource { + if hops[i].role == paymenttypes.QuoteRouteHopRoleSource { return hops[i], true } } - if len(hops) > 0 && hops[0].rail == discovery.RailCrypto { + if len(hops) > 0 { return hops[0], true } return normalizedHop{}, false } -func walletFeeAmountFromSnapshots( +func feeAmountFromSnapshots( intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot, ) (*paymenttypes.Money, bool, error) { @@ -62,10 +61,7 @@ func walletFeeAmountFromSnapshots( total := decimal.Zero currency := "" for i, line := range quote.FeeLines { - if !isWalletDebitFeeLine(line) { - continue - } - money := line.GetMoney() + money := feeLineMoney(line) if money == nil { continue } @@ -80,7 +76,7 @@ func walletFeeAmountFromSnapshots( if currency == "" { currency = lineCurrency } else if !strings.EqualFold(currency, lineCurrency) { - return nil, false, merrors.InvalidArgument("quote_snapshot.fee_lines wallet fee currency mismatch") + return nil, false, merrors.InvalidArgument("quote_snapshot.fee_lines fee currency mismatch") } amountRaw := strings.TrimSpace(money.GetAmount()) @@ -88,10 +84,12 @@ func walletFeeAmountFromSnapshots( if err != nil { return nil, false, merrors.InvalidArgument(fmt.Sprintf("quote_snapshot.fee_lines[%d].money.amount is invalid", i)) } - if amount.Sign() < 0 { - amount = amount.Neg() + amount = amount.Abs() + if amount.IsZero() { + continue } - if amount.Sign() == 0 { + if line.GetSide() == paymenttypes.EntrySideCredit { + total = total.Sub(amount) continue } total = total.Add(amount) @@ -113,16 +111,9 @@ func feePlanningSourceAmount(intent model.PaymentIntent, quote *model.PaymentQuo return intent.Amount } -func isWalletDebitFeeLine(line *paymenttypes.FeeLine) bool { +func feeLineMoney(line *paymenttypes.FeeLine) *paymenttypes.Money { if line == nil { - return false + return nil } - if line.GetSide() != paymenttypes.EntrySideDebit { - return false - } - meta := line.Meta - if len(meta) == 0 { - return false - } - return strings.EqualFold(strings.TrimSpace(meta["fee_target"]), "wallet") + return line.GetMoney() } diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service.go b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service.go index 1d87372d..8c98a9c7 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service.go @@ -30,16 +30,16 @@ type expansion struct { lastMainRef string refSeq map[string]int externalObserved map[string]string - walletFeeHops map[string]struct{} - walletFeeEmitted map[string]struct{} + feeHops map[string]struct{} + feeEmitted map[string]struct{} } -func newExpansion(walletFeeHops map[string]struct{}) *expansion { +func newExpansion(feeHops map[string]struct{}) *expansion { return &expansion{ refSeq: map[string]int{}, externalObserved: map[string]string{}, - walletFeeHops: walletFeeHops, - walletFeeEmitted: map[string]struct{}{}, + feeHops: feeHops, + feeEmitted: map[string]struct{}{}, } } @@ -90,12 +90,12 @@ func (s *svc) Compile(in Input) (graph *Graph, err error) { return nil, err } - walletFeeHops, err := planWalletFeeHops(hops, in.IntentSnapshot, in.QuoteSnapshot) + feeHops, err := planFeeHops(hops, in.IntentSnapshot, in.QuoteSnapshot) if err != nil { return nil, err } - ex := newExpansion(walletFeeHops) + ex := newExpansion(feeHops) appendGuards(ex, conditions) if len(hops) == 1 { diff --git a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service_boundaries.go b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service_boundaries.go index cd6e7b56..304abafe 100644 --- a/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service_boundaries.go +++ b/api/payments/orchestrator/internal/service/orchestrationv2/xplan/service_boundaries.go @@ -18,7 +18,7 @@ func (s *svc) expandSingleHop(ex *expansion, hop normalizedHop, intent model.Pay switch hop.role { case paymenttypes.QuoteRouteHopRoleSource: - ex.appendMain(Step{ + appendMainWithOptionalFee(ex, Step{ StepCode: singleHopCode(hop, "debit"), Kind: StepKindFundsDebit, Action: discovery.RailOperationDebit, @@ -26,7 +26,7 @@ func (s *svc) expandSingleHop(ex *expansion, hop normalizedHop, intent model.Pay HopIndex: hop.index, HopRole: hop.role, Visibility: model.ReportVisibilityHidden, - }) + }, hop) case paymenttypes.QuoteRouteHopRoleDestination: ex.appendMain(Step{ StepCode: singleHopCode(hop, "credit"), @@ -74,7 +74,7 @@ func (s *svc) applyDefaultBoundary( if to.rail == discovery.RailCardPayout && len(targets) > 0 { return s.applyBatchCardPayoutBoundary(ex, from, to, internalRail, intent, targets) } - ex.appendMain(makeFundsBlockStep(from, to, internalRail)) + appendMainWithOptionalFee(ex, makeFundsBlockStep(from, to, internalRail), from) observeRef, err := s.ensureExternalObserved(ex, to, intent) if err != nil { return err @@ -97,7 +97,7 @@ func (s *svc) applyDefaultBoundary( return nil case isInternalRail(from.rail) && isInternalRail(to.rail): - ex.appendMain(makeFundsMoveStep(from, to, internalRailForBoundary(from, to))) + appendMainWithOptionalFee(ex, makeFundsMoveStep(from, to, internalRailForBoundary(from, to)), from) return nil default: @@ -113,7 +113,7 @@ func (s *svc) applyBatchCardPayoutBoundary( intent model.PaymentIntent, targets []batchmeta.PayoutTarget, ) error { - blockRef := ex.appendMain(makeFundsBlockStep(from, to, internalRail)) + blockRef := appendMainWithOptionalFee(ex, makeFundsBlockStep(from, to, internalRail), from) for i := range targets { target := targets[i] if target.Amount == nil { @@ -148,13 +148,13 @@ func (s *svc) ensureExternalObserved(ex *expansion, hop normalizedHop, intent mo sendStep := makeRailSendStep(hop, intent) sendRef := ex.appendMain(sendStep) observeDependency := sendRef - if ex.needsWalletFeeStep(hop) { + if ex.needsFeeStep(hop) { feeStep := makeRailFeeStep(hop) if observeDependency != "" { feeStep.DependsOn = []string{observeDependency} } observeDependency = ex.appendMain(feeStep) - ex.markWalletFeeEmitted(hop) + ex.markFeeEmitted(hop) } observeStep := makeRailObserveStep(hop, intent) @@ -167,6 +167,20 @@ func (s *svc) ensureExternalObserved(ex *expansion, hop normalizedHop, intent mo return observeRef, nil } +func appendMainWithOptionalFee(ex *expansion, step Step, hop normalizedHop) string { + mainRef := ex.appendMain(step) + if !ex.needsFeeStep(hop) { + return mainRef + } + feeStep := makeRailFeeStep(hop) + if strings.TrimSpace(mainRef) != "" { + feeStep.DependsOn = []string{mainRef} + } + feeRef := ex.appendMain(feeStep) + ex.markFeeEmitted(hop) + return feeRef +} + func makeRailFeeStep(hop normalizedHop) Step { return Step{ StepCode: singleHopCode(hop, "fee"), diff --git a/api/payments/orchestrator/internal/service/orchestrator/batch_merge_key_resolver.go b/api/payments/orchestrator/internal/service/orchestrator/batch_merge_key_resolver.go new file mode 100644 index 00000000..4aa0dc34 --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrator/batch_merge_key_resolver.go @@ -0,0 +1,119 @@ +package orchestrator + +import ( + "strings" + + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/psvc" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/qsnap" + "github.com/tech/sendico/payments/storage/model" + "github.com/tech/sendico/pkg/discovery" + paymenttypes "github.com/tech/sendico/pkg/payments/types" +) + +type orchestratorBatchMergeKeyResolver struct { + cardGatewayRoutes map[string]CardGatewayRoute +} + +func newOrchestratorBatchMergeKeyResolver(routes map[string]CardGatewayRoute) psvc.BatchOptimizationMergeKeyResolver { + return &orchestratorBatchMergeKeyResolver{ + cardGatewayRoutes: cloneCardGatewayRoutes(routes), + } +} + +func (r *orchestratorBatchMergeKeyResolver) ResolveMergeKey( + item qsnap.ResolvedItem, + selection psvc.BatchOptimizationSelection, +) (string, bool) { + if psvc.BatchOptimizationGrouping(strings.TrimSpace(string(selection.GroupBy))) != psvc.BatchOptimizationGroupingRailTarget { + return "", false + } + if model.ParseRail(string(selection.MatchRail)) != discovery.RailCrypto { + return "", false + } + return r.cryptoRailTargetKey(item) +} + +func (r *orchestratorBatchMergeKeyResolver) cryptoRailTargetKey(item qsnap.ResolvedItem) (string, bool) { + destination := item.IntentSnapshot.Destination + if destination.Type == model.EndpointTypeExternalChain && destination.ExternalChain != nil { + return strings.Join([]string{ + "type=external_chain", + "address=" + strings.TrimSpace(destination.ExternalChain.Address), + "memo=" + strings.TrimSpace(destination.ExternalChain.Memo), + }, "|"), true + } + if destination.Type == model.EndpointTypeManagedWallet && destination.ManagedWallet != nil { + return strings.Join([]string{ + "type=managed_wallet", + "wallet=" + strings.TrimSpace(destination.ManagedWallet.ManagedWalletRef), + }, "|"), true + } + if destination.Type == model.EndpointTypeCard { + address := r.resolveCardFundingAddress(item.QuoteSnapshot) + if strings.TrimSpace(address) == "" { + return "", false + } + return strings.Join([]string{ + "type=external_chain", + "address=" + strings.TrimSpace(address), + "network=" + cryptoNetwork(item.QuoteSnapshot), + }, "|"), true + } + return "", false +} + +func (r *orchestratorBatchMergeKeyResolver) resolveCardFundingAddress(snapshot *model.PaymentQuoteSnapshot) string { + gatewayKey := destinationCardGatewayKeyFromQuote(snapshot) + if gatewayKey == "" { + return "" + } + route, ok := lookupCardGatewayRoute(r.cardGatewayRoutes, gatewayKey) + if !ok { + return "" + } + return strings.TrimSpace(route.FundingAddress) +} + +func destinationCardGatewayKeyFromQuote(snapshot *model.PaymentQuoteSnapshot) string { + if snapshot == nil || snapshot.Route == nil { + return "" + } + hops := snapshot.Route.Hops + fallback := "" + for i := range hops { + hop := hops[i] + if hop == nil || model.ParseRail(hop.Rail) != discovery.RailCardPayout { + continue + } + key := firstNonEmpty(strings.TrimSpace(hop.Gateway), strings.TrimSpace(hop.InstanceID)) + if key == "" { + continue + } + if hop.Role != paymenttypes.QuoteRouteHopRoleDestination { + fallback = key + continue + } + return key + } + return fallback +} + +func cryptoNetwork(snapshot *model.PaymentQuoteSnapshot) string { + if snapshot == nil || snapshot.Route == nil { + return "" + } + for i := range snapshot.Route.Hops { + hop := snapshot.Route.Hops[i] + if hop == nil || model.ParseRail(hop.Rail) != discovery.RailCrypto { + continue + } + network := strings.TrimSpace(hop.Network) + if network != "" { + return strings.ToUpper(network) + } + } + if strings.TrimSpace(snapshot.Route.Network) != "" { + return strings.ToUpper(strings.TrimSpace(snapshot.Route.Network)) + } + return "" +} diff --git a/api/payments/orchestrator/internal/service/orchestrator/card_payout_executor.go b/api/payments/orchestrator/internal/service/orchestrator/card_payout_executor.go index e1711d35..b16e66b8 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/card_payout_executor.go +++ b/api/payments/orchestrator/internal/service/orchestrator/card_payout_executor.go @@ -67,7 +67,7 @@ func (e *gatewayCardPayoutExecutor) ExecuteCardPayout(ctx context.Context, req s if err != nil { return nil, err } - amountMinor, currency, err := cardPayoutAmountMinor(req.Payment, req.Step.Metadata) + amountMinor, currency, err := cardPayoutAmountMinor(req.Payment, req.StepExecution, req.Step.Metadata) if err != nil { return nil, err } @@ -310,7 +310,10 @@ func payoutDestinationCard(payment *agg.Payment, metadata map[string]string) (*m return destination.Card, nil } -func cardPayoutMoney(payment *agg.Payment, metadata map[string]string) *paymenttypes.Money { +func cardPayoutMoney(payment *agg.Payment, stepExecution agg.StepExecution, metadata map[string]string) *paymenttypes.Money { + if planned := plannedStepMoney(stepExecution); planned != nil { + return planned + } if override, ok := batchmeta.AmountFromMetadata(metadata); ok && override != nil { return override } @@ -323,8 +326,8 @@ func cardPayoutMoney(payment *agg.Payment, metadata map[string]string) *paymentt return payment.IntentSnapshot.Amount } -func cardPayoutAmountMinor(payment *agg.Payment, metadata map[string]string) (int64, string, error) { - money := cardPayoutMoney(payment, metadata) +func cardPayoutAmountMinor(payment *agg.Payment, stepExecution agg.StepExecution, metadata map[string]string) (int64, string, error) { + money := cardPayoutMoney(payment, stepExecution, metadata) if money == nil { return 0, "", merrors.InvalidArgument("card payout send: payout amount is required") } diff --git a/api/payments/orchestrator/internal/service/orchestrator/card_payout_executor_test.go b/api/payments/orchestrator/internal/service/orchestrator/card_payout_executor_test.go index c9578f3c..b2866c3a 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/card_payout_executor_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/card_payout_executor_test.go @@ -176,6 +176,86 @@ func TestGatewayCardPayoutExecutor_ExecuteCardPayout_SubmitsCardPayout(t *testin } } +func TestGatewayCardPayoutExecutor_ExecuteCardPayout_UsesPlannedMoney(t *testing.T) { + orgID := bson.NewObjectID() + + var payoutReq *mntxv1.CardPayoutRequest + executor := &gatewayCardPayoutExecutor{ + gatewayRegistry: &fakeGatewayRegistry{ + items: []*model.GatewayInstanceDescriptor{ + { + ID: paymenttypes.DefaultCardsGatewayID, + InstanceID: paymenttypes.DefaultCardsGatewayID, + Rail: discovery.RailCardPayout, + InvokeURI: "grpc://mntx-gateway:50051", + IsEnabled: true, + }, + }, + }, + dialClient: func(_ context.Context, _ string) (mntxclient.Client, error) { + return &mntxclient.Fake{ + CreateCardPayoutFn: func(_ context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) { + payoutReq = req + return &mntxv1.CardPayoutResponse{ + Payout: &mntxv1.CardPayoutState{ + PayoutId: "payout-planned", + }, + }, nil + }, + }, nil + }, + } + + _, err := executor.ExecuteCardPayout(context.Background(), sexec.StepRequest{ + Payment: &agg.Payment{ + OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID}, + PaymentRef: "payment-planned", + IdempotencyKey: "idem-planned", + IntentSnapshot: model.PaymentIntent{ + Ref: "intent-planned", + Destination: model.PaymentEndpoint{ + Type: model.EndpointTypeCard, + Card: &model.CardEndpoint{ + Pan: "2200700142860161", + ExpMonth: 3, + ExpYear: 2030, + }, + }, + Amount: &paymenttypes.Money{Amount: "1.000000", Currency: "USDT"}, + }, + QuoteSnapshot: &model.PaymentQuoteSnapshot{ + ExpectedSettlementAmount: &paymenttypes.Money{Amount: "76.50", Currency: "RUB"}, + }, + }, + Step: xplan.Step{ + StepRef: "hop_4_card_payout_send", + StepCode: "hop.4.card_payout.send", + Action: discovery.RailOperationSend, + Rail: discovery.RailCardPayout, + Gateway: paymenttypes.DefaultCardsGatewayID, + InstanceID: paymenttypes.DefaultCardsGatewayID, + }, + StepExecution: agg.StepExecution{ + StepRef: "hop_4_card_payout_send", + StepCode: "hop.4.card_payout.send", + Attempt: 1, + PlannedMoney: &paymenttypes.Money{Amount: "100.00", Currency: "RUB"}, + }, + }) + if err != nil { + t.Fatalf("ExecuteCardPayout returned error: %v", err) + } + if payoutReq == nil { + t.Fatal("expected payout request to be submitted") + } + if got, want := payoutReq.GetAmountMinor(), int64(10000); got != want { + t.Fatalf("amount_minor mismatch: got=%d want=%d", got, want) + } + if got, want := payoutReq.GetCurrency(), "RUB"; got != want { + t.Fatalf("currency mismatch: got=%q want=%q", got, want) + } +} + func TestGatewayCardPayoutExecutor_ExecuteCardPayout_UsesStepMetadataOverrides(t *testing.T) { orgID := bson.NewObjectID() diff --git a/api/payments/orchestrator/internal/service/orchestrator/crypto_executor.go b/api/payments/orchestrator/internal/service/orchestrator/crypto_executor.go index c7a9c6d9..099126ff 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/crypto_executor.go +++ b/api/payments/orchestrator/internal/service/orchestrator/crypto_executor.go @@ -51,7 +51,7 @@ func (e *gatewayCryptoExecutor) ExecuteCrypto(ctx context.Context, req sexec.Ste if err != nil { return nil, err } - amount, err := sourceAmount(req.Payment, action) + amount, err := sourceAmount(req.Payment, req.StepExecution, action) if err != nil { return nil, err } @@ -170,19 +170,26 @@ func sourceManagedWalletRef(payment *agg.Payment) (string, error) { return ref, nil } -func sourceAmount(payment *agg.Payment, action model.RailOperation) (*moneyv1.Money, error) { +func sourceAmount(payment *agg.Payment, stepExecution agg.StepExecution, action model.RailOperation) (*moneyv1.Money, error) { + if planned := plannedStepMoney(stepExecution); planned != nil { + proto := paymentMoneyToProto(planned) + if proto == nil { + return nil, merrors.InvalidArgument("crypto send: planned amount is invalid") + } + return proto, nil + } if payment == nil { return nil, merrors.InvalidArgument("crypto send: payment is required") } var money *paymenttypes.Money switch action { case discovery.RailOperationFee: - resolved, ok, err := walletFeeAmount(payment) + resolved, ok, err := feeAmount(payment) if err != nil { return nil, err } if !ok { - return nil, merrors.InvalidArgument("crypto send: wallet fee amount is required") + return nil, merrors.InvalidArgument("crypto send: fee amount is required") } money = resolved default: @@ -212,7 +219,7 @@ func effectiveSourceAmount(payment *agg.Payment) *paymenttypes.Money { return payment.IntentSnapshot.Amount } -func walletFeeAmount(payment *agg.Payment) (*paymenttypes.Money, bool, error) { +func feeAmount(payment *agg.Payment) (*paymenttypes.Money, bool, error) { if payment == nil || payment.QuoteSnapshot == nil || len(payment.QuoteSnapshot.FeeLines) == 0 { return nil, false, nil } @@ -225,9 +232,6 @@ func walletFeeAmount(payment *agg.Payment) (*paymenttypes.Money, bool, error) { total := decimal.Zero currency := "" for i, line := range payment.QuoteSnapshot.FeeLines { - if !isWalletDebitFeeLine(line) { - continue - } money := line.GetMoney() if money == nil { continue @@ -243,7 +247,7 @@ func walletFeeAmount(payment *agg.Payment) (*paymenttypes.Money, bool, error) { if currency == "" { currency = lineCurrency } else if !strings.EqualFold(currency, lineCurrency) { - return nil, false, merrors.InvalidArgument("crypto send: wallet fee currency mismatch") + return nil, false, merrors.InvalidArgument("crypto send: fee currency mismatch") } amountRaw := strings.TrimSpace(money.GetAmount()) @@ -251,10 +255,12 @@ func walletFeeAmount(payment *agg.Payment) (*paymenttypes.Money, bool, error) { if err != nil { return nil, false, merrors.InvalidArgument(fmt.Sprintf("crypto send: fee_lines[%d].money.amount is invalid", i)) } - if amount.Sign() < 0 { - amount = amount.Neg() + amount = amount.Abs() + if amount.IsZero() { + continue } - if amount.Sign() == 0 { + if line.GetSide() == paymenttypes.EntrySideCredit { + total = total.Sub(amount) continue } total = total.Add(amount) @@ -269,20 +275,6 @@ func walletFeeAmount(payment *agg.Payment) (*paymenttypes.Money, bool, error) { }, true, nil } -func isWalletDebitFeeLine(line *paymenttypes.FeeLine) bool { - if line == nil { - return false - } - if line.GetSide() != paymenttypes.EntrySideDebit { - return false - } - meta := line.Meta - if len(meta) == 0 { - return false - } - return strings.EqualFold(strings.TrimSpace(meta["fee_target"]), "wallet") -} - func (e *gatewayCryptoExecutor) resolveDestination( ctx context.Context, client chainclient.Client, diff --git a/api/payments/orchestrator/internal/service/orchestrator/crypto_executor_test.go b/api/payments/orchestrator/internal/service/orchestrator/crypto_executor_test.go index cb893055..3b9cf16b 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/crypto_executor_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/crypto_executor_test.go @@ -138,6 +138,97 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_SubmitsTransfer(t *testing.T) { } } +func TestGatewayCryptoExecutor_ExecuteCrypto_UsesPlannedMoney(t *testing.T) { + orgID := bson.NewObjectID() + + var submitReq *chainv1.SubmitTransferRequest + client := &chainclient.Fake{ + SubmitTransferFn: func(_ context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) { + submitReq = req + return &chainv1.SubmitTransferResponse{ + Transfer: &chainv1.Transfer{ + TransferRef: "trf-planned", + OperationRef: "op-planned", + }, + }, nil + }, + } + executor := &gatewayCryptoExecutor{ + gatewayInvokeResolver: &fakeGatewayInvokeResolver{client: client}, + gatewayRegistry: &fakeGatewayRegistry{ + items: []*model.GatewayInstanceDescriptor{ + { + ID: "crypto_rail_gateway_arbitrum_sepolia", + InstanceID: "crypto_rail_gateway_arbitrum_sepolia", + Rail: discovery.RailCrypto, + InvokeURI: "grpc://crypto-gateway", + IsEnabled: true, + }, + }, + }, + cardGatewayRoutes: map[string]CardGatewayRoute{ + paymenttypes.DefaultCardsGatewayID: {FundingAddress: "TUA_DEST"}, + }, + } + + _, err := executor.ExecuteCrypto(context.Background(), sexec.StepRequest{ + Payment: &agg.Payment{ + OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID}, + PaymentRef: "payment-planned", + IdempotencyKey: "idem-planned", + IntentSnapshot: model.PaymentIntent{ + Ref: "intent-planned", + Source: model.PaymentEndpoint{ + Type: model.EndpointTypeManagedWallet, + ManagedWallet: &model.ManagedWalletEndpoint{ + ManagedWalletRef: "wallet-src", + }, + }, + Destination: model.PaymentEndpoint{ + Type: model.EndpointTypeCard, + Card: &model.CardEndpoint{Pan: "4111111111111111"}, + }, + Amount: &paymenttypes.Money{Amount: "1", Currency: "USDT"}, + }, + QuoteSnapshot: &model.PaymentQuoteSnapshot{ + DebitAmount: &paymenttypes.Money{Amount: "1.000000", Currency: "USDT"}, + Route: &paymenttypes.QuoteRouteSpecification{ + Hops: []*paymenttypes.QuoteRouteHop{ + {Index: 1, Rail: "CRYPTO", Gateway: "crypto_rail_gateway_arbitrum_sepolia", InstanceID: "crypto_rail_gateway_arbitrum_sepolia", Role: paymenttypes.QuoteRouteHopRoleSource}, + {Index: 4, Rail: "CARD", Gateway: paymenttypes.DefaultCardsGatewayID, InstanceID: paymenttypes.DefaultCardsGatewayID, Role: paymenttypes.QuoteRouteHopRoleDestination}, + }, + }, + }, + }, + Step: xplan.Step{ + StepRef: "hop_1_crypto_send", + StepCode: "hop.1.crypto.send", + Action: discovery.RailOperationSend, + Rail: discovery.RailCrypto, + Gateway: "crypto_rail_gateway_arbitrum_sepolia", + InstanceID: "crypto_rail_gateway_arbitrum_sepolia", + }, + StepExecution: agg.StepExecution{ + StepRef: "hop_1_crypto_send", + StepCode: "hop.1.crypto.send", + Attempt: 1, + PlannedMoney: &paymenttypes.Money{Amount: "5.50", Currency: "USDT"}, + }, + }) + if err != nil { + t.Fatalf("ExecuteCrypto returned error: %v", err) + } + if submitReq == nil { + t.Fatal("expected transfer submission request") + } + if got, want := submitReq.GetAmount().GetAmount(), "5.50"; got != want { + t.Fatalf("amount mismatch: got=%q want=%q", got, want) + } + if got, want := submitReq.GetAmount().GetCurrency(), "USDT"; got != want { + t.Fatalf("currency mismatch: got=%q want=%q", got, want) + } +} + func TestGatewayCryptoExecutor_ExecuteCrypto_MissingCardRoute(t *testing.T) { orgID := bson.NewObjectID() executor := &gatewayCryptoExecutor{ @@ -430,7 +521,7 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_FeeActionResolvesFeeAddressFromFeeW } } -func TestGatewayCryptoExecutor_ExecuteCrypto_FeeActionUsesWalletFeeAmount(t *testing.T) { +func TestGatewayCryptoExecutor_ExecuteCrypto_FeeActionUsesAllFeeLines(t *testing.T) { orgID := bson.NewObjectID() var submitReq *chainv1.SubmitTransferRequest @@ -493,6 +584,18 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_FeeActionUsesWalletFeeAmount(t *tes Side: paymenttypes.EntrySideDebit, Meta: map[string]string{"fee_target": "wallet"}, }, + { + Money: &paymenttypes.Money{Amount: "0.30", Currency: "USDT"}, + LineType: paymenttypes.PostingLineTypeTax, + Side: paymenttypes.EntrySideDebit, + Meta: map[string]string{"fee_target": "ledger"}, + }, + { + Money: &paymenttypes.Money{Amount: "0.10", Currency: "USDT"}, + LineType: paymenttypes.PostingLineTypeReversal, + Side: paymenttypes.EntrySideCredit, + Meta: map[string]string{"fee_target": "rebate"}, + }, }, Route: &paymenttypes.QuoteRouteSpecification{ Hops: []*paymenttypes.QuoteRouteHop{ @@ -524,7 +627,7 @@ func TestGatewayCryptoExecutor_ExecuteCrypto_FeeActionUsesWalletFeeAmount(t *tes if submitReq == nil { t.Fatal("expected transfer submission") } - if got, want := submitReq.GetAmount().GetAmount(), "0.7"; got != want { + if got, want := submitReq.GetAmount().GetAmount(), "0.9"; got != want { t.Fatalf("fee amount mismatch: got=%q want=%q", got, want) } if got, want := submitReq.GetDestination().GetExternalAddress(), "TUA_FEE"; got != want { diff --git a/api/payments/orchestrator/internal/service/orchestrator/external_runtime.go b/api/payments/orchestrator/internal/service/orchestrator/external_runtime.go index a264e670..0472c75f 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/external_runtime.go +++ b/api/payments/orchestrator/internal/service/orchestrator/external_runtime.go @@ -13,6 +13,7 @@ import ( "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/erecon" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/prepo" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/psvc" + svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared" "github.com/tech/sendico/payments/storage/model" cons "github.com/tech/sendico/pkg/messaging/consumer" paymentgatewaynotifications "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway" @@ -159,7 +160,7 @@ func buildGatewayExecutionEvent(payment *agg.Payment, msg *pmodel.PaymentGateway TransferRef: transferRef, GatewayInstanceID: gatewayInstanceID, Status: status, - ExecutedMoney: clonePaymentMoney(msg.ExecutedMoney), + ExecutedMoney: svcshared.CloneMoneyTrimNonEmpty(msg.ExecutedMoney), } switch status { diff --git a/api/payments/orchestrator/internal/service/orchestrator/ledger_executor.go b/api/payments/orchestrator/internal/service/orchestrator/ledger_executor.go index 73aff9ba..e7175de6 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/ledger_executor.go +++ b/api/payments/orchestrator/internal/service/orchestrator/ledger_executor.go @@ -56,7 +56,7 @@ func (e *gatewayLedgerExecutor) ExecuteLedger(ctx context.Context, req sexec.Ste return nil, merrors.InvalidArgument("ledger step: unsupported action") } - amount, err := ledgerAmountForStep(req.Payment, req.Step, action) + amount, err := ledgerAmountForStep(req.Payment, req.Step, req.StepExecution, action) if err != nil { return nil, err } @@ -132,8 +132,12 @@ func (e *gatewayLedgerExecutor) ExecuteLedger(ctx context.Context, req sexec.Ste func ledgerAmountForStep( payment *agg.Payment, step xplan.Step, + stepExecution agg.StepExecution, action model.RailOperation, ) (*moneyv1.Money, error) { + if planned := plannedStepMoney(stepExecution); planned != nil { + return protoMoneyRequired(planned, "ledger step: planned amount is invalid") + } if override, ok := batchmeta.AmountFromMetadata(step.Metadata); ok && override != nil { return protoMoneyRequired(override, "ledger step: override amount is invalid") } diff --git a/api/payments/orchestrator/internal/service/orchestrator/ledger_executor_test.go b/api/payments/orchestrator/internal/service/orchestrator/ledger_executor_test.go index e4c62a4a..3391d96a 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/ledger_executor_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/ledger_executor_test.go @@ -95,6 +95,49 @@ func TestGatewayLedgerExecutor_ExecuteLedger_CreditUsesSourceAmountAndDefaultRol } } +func TestGatewayLedgerExecutor_ExecuteLedger_UsesPlannedMoney(t *testing.T) { + orgID := bson.NewObjectID() + payment := testLedgerExecutorPayment(orgID) + + var transferReq *ledgerv1.TransferRequest + executor := &gatewayLedgerExecutor{ + ledgerClient: &ledgerclient.Fake{ + TransferInternalFn: func(_ context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) { + transferReq = req + return &ledgerv1.PostResponse{JournalEntryRef: "entry-planned"}, nil + }, + }, + } + + _, err := executor.ExecuteLedger(context.Background(), sexec.StepRequest{ + Payment: payment, + Step: xplan.Step{ + StepRef: "edge_1_2_ledger_credit", + StepCode: "edge.1_2.ledger.credit", + Action: discovery.RailOperationCredit, + Rail: discovery.RailLedger, + }, + StepExecution: agg.StepExecution{ + StepRef: "edge_1_2_ledger_credit", + StepCode: "edge.1_2.ledger.credit", + Attempt: 1, + PlannedMoney: &paymenttypes.Money{Amount: "12.34", Currency: "EUR"}, + }, + }) + if err != nil { + t.Fatalf("ExecuteLedger returned error: %v", err) + } + if transferReq == nil { + t.Fatal("expected ledger transfer request") + } + if got, want := transferReq.GetMoney().GetAmount(), "12.34"; got != want { + t.Fatalf("money.amount mismatch: got=%q want=%q", got, want) + } + if got, want := transferReq.GetMoney().GetCurrency(), "EUR"; got != want { + t.Fatalf("money.currency mismatch: got=%q want=%q", got, want) + } +} + func TestGatewayLedgerExecutor_ExecuteLedger_ExternalCreditUsesPostCreditWithCharges(t *testing.T) { orgID := bson.NewObjectID() payment := testLedgerExecutorPayment(orgID) diff --git a/api/payments/orchestrator/internal/service/orchestrator/service_v2.go b/api/payments/orchestrator/internal/service/orchestrator/service_v2.go index 766c3334..77db1498 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/service_v2.go +++ b/api/payments/orchestrator/internal/service/orchestrator/service_v2.go @@ -71,12 +71,17 @@ func newOrchestrationV2Service(logger mlogger.Logger, repo storage.Repository, r executors := buildOrchestrationV2Executors(logger, runtimeDeps) svc, err := psvc.New(psvc.Dependencies{ - Logger: logger.Named("v2"), - QuoteStore: repo.Quotes(), - Repository: paymentRepo, - Query: query, - Observer: observer, - Executors: executors, + Logger: logger.Named("v2"), + QuoteStore: repo.Quotes(), + Repository: paymentRepo, + Query: query, + Observer: observer, + Executors: executors, + BatchOptimizer: psvc.NewPolicyBatchOptimizer(psvc.PolicyBatchOptimizerDependencies{ + Logger: logger.Named("v2"), + Policy: runtimeDeps.BatchOptimizationPolicy, + MergeKeyResolver: newOrchestratorBatchMergeKeyResolver(runtimeDeps.CardGatewayRoutes), + }), BatchOptimizationPolicy: runtimeDeps.BatchOptimizationPolicy, Producer: runtimeDeps.Producer, }) diff --git a/api/payments/orchestrator/internal/service/orchestrator/settlement_executor.go b/api/payments/orchestrator/internal/service/orchestrator/settlement_executor.go index 8608e6d4..7ed054f7 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/settlement_executor.go +++ b/api/payments/orchestrator/internal/service/orchestrator/settlement_executor.go @@ -2,13 +2,14 @@ package orchestrator import ( "context" - "github.com/tech/sendico/pkg/discovery" "strings" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec" "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan" + svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared" "github.com/tech/sendico/payments/storage/model" + "github.com/tech/sendico/pkg/discovery" "github.com/tech/sendico/pkg/merrors" paymenttypes "github.com/tech/sendico/pkg/payments/types" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" @@ -52,11 +53,11 @@ func (e *gatewayProviderSettlementExecutor) ExecuteProviderSettlement(ctx contex }, } - amount, err := settlementAmount(req.Payment) + amount, err := settlementAmount(req.Payment, req.StepExecution) if err != nil { return nil, err } - convertedAmount := settlementConvertedMoney(req.Payment) + convertedAmount := settlementConvertedMoney(req.Payment, req.StepExecution) if convertedAmount == nil { return nil, merrors.InvalidArgument("settlement fx_convert: converted amount is required") } @@ -106,11 +107,14 @@ func (e *gatewayProviderSettlementExecutor) ExecuteProviderSettlement(ctx contex return &sexec.ExecuteOutput{StepExecution: step}, nil } -func settlementConvertedMoney(payment *agg.Payment) *paymenttypes.Money { +func settlementConvertedMoney(payment *agg.Payment, stepExecution agg.StepExecution) *paymenttypes.Money { + if planned := plannedStepConvertedMoney(stepExecution); planned != nil { + return planned + } if payment == nil || payment.QuoteSnapshot == nil { return nil } - return clonePaymentMoney(payment.QuoteSnapshot.ExpectedSettlementAmount) + return svcshared.CloneMoneyTrimNonEmpty(payment.QuoteSnapshot.ExpectedSettlementAmount) } func (e *gatewayProviderSettlementExecutor) resolveGateway(ctx context.Context, step xplan.Step) (*model.GatewayInstanceDescriptor, error) { @@ -165,7 +169,14 @@ func (e *gatewayProviderSettlementExecutor) resolveGateway(ctx context.Context, } } -func settlementAmount(payment *agg.Payment) (*moneyv1.Money, error) { +func settlementAmount(payment *agg.Payment, stepExecution agg.StepExecution) (*moneyv1.Money, error) { + if planned := plannedStepMoney(stepExecution); planned != nil { + amount := paymentMoneyToProto(planned) + if amount == nil { + return nil, merrors.InvalidArgument("settlement fx_convert: planned amount is invalid") + } + return amount, nil + } if payment == nil { return nil, merrors.InvalidArgument("settlement fx_convert: payment is required") } diff --git a/api/payments/orchestrator/internal/service/orchestrator/settlement_executor_test.go b/api/payments/orchestrator/internal/service/orchestrator/settlement_executor_test.go index be3fbe4e..352718df 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/settlement_executor_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/settlement_executor_test.go @@ -151,6 +151,96 @@ func TestGatewayProviderSettlementExecutor_ExecuteProviderSettlement_SubmitsTran } } +func TestGatewayProviderSettlementExecutor_ExecuteProviderSettlement_UsesPlannedMoney(t *testing.T) { + orgID := bson.NewObjectID() + + var submitReq *chainv1.SubmitTransferRequest + executor := &gatewayProviderSettlementExecutor{ + gatewayInvokeResolver: &fakeGatewayInvokeResolver{ + client: &chainclient.Fake{ + SubmitTransferFn: func(_ context.Context, req *chainv1.SubmitTransferRequest) (*chainv1.SubmitTransferResponse, error) { + submitReq = req + return &chainv1.SubmitTransferResponse{ + Transfer: &chainv1.Transfer{ + TransferRef: "trf-planned", + OperationRef: "op-planned", + }, + }, nil + }, + }, + }, + gatewayRegistry: &fakeGatewayRegistry{ + items: []*model.GatewayInstanceDescriptor{ + { + ID: "payment_gateway_settlement", + InstanceID: "payment_gateway_settlement", + Rail: discovery.RailProviderSettlement, + InvokeURI: "grpc://tgsettle-gateway", + IsEnabled: true, + }, + }, + }, + } + + out, err := executor.ExecuteProviderSettlement(context.Background(), sexec.StepRequest{ + Payment: &agg.Payment{ + OrganizationBoundBase: pm.OrganizationBoundBase{OrganizationRef: orgID}, + PaymentRef: "payment-planned", + IdempotencyKey: "idem-planned", + IntentSnapshot: model.PaymentIntent{ + Ref: "intent-planned", + Source: model.PaymentEndpoint{ + Type: model.EndpointTypeManagedWallet, + ManagedWallet: &model.ManagedWalletEndpoint{ + ManagedWalletRef: "wallet-src", + }, + }, + Amount: &paymenttypes.Money{Amount: "1.000000", Currency: "USDT"}, + }, + QuoteSnapshot: &model.PaymentQuoteSnapshot{ + DebitAmount: &paymenttypes.Money{Amount: "1.000000", Currency: "USDT"}, + ExpectedSettlementAmount: &paymenttypes.Money{Amount: "76.63", Currency: "RUB"}, + }, + }, + Step: xplan.Step{ + StepRef: "hop_2_settlement_fx_convert", + StepCode: "hop.2.settlement.fx_convert", + Action: discovery.RailOperationFXConvert, + Rail: discovery.RailProviderSettlement, + Gateway: "payment_gateway_settlement", + InstanceID: "payment_gateway_settlement", + }, + StepExecution: agg.StepExecution{ + StepRef: "hop_2_settlement_fx_convert", + StepCode: "hop.2.settlement.fx_convert", + Attempt: 1, + PlannedMoney: &paymenttypes.Money{Amount: "2.00", Currency: "USDT"}, + PlannedConvertedMoney: &paymenttypes.Money{Amount: "150.00", Currency: "RUB"}, + }, + }) + if err != nil { + t.Fatalf("ExecuteProviderSettlement returned error: %v", err) + } + if submitReq == nil { + t.Fatal("expected transfer submission request") + } + if got, want := submitReq.GetAmount().GetAmount(), "2.00"; got != want { + t.Fatalf("amount mismatch: got=%q want=%q", got, want) + } + if got, want := submitReq.GetAmount().GetCurrency(), "USDT"; got != want { + t.Fatalf("currency mismatch: got=%q want=%q", got, want) + } + if out == nil || out.StepExecution.ConvertedMoney == nil { + t.Fatal("expected converted money in executor output") + } + if got, want := out.StepExecution.ConvertedMoney.Amount, "150.00"; got != want { + t.Fatalf("converted amount mismatch: got=%q want=%q", got, want) + } + if got, want := out.StepExecution.ConvertedMoney.Currency, "RUB"; got != want { + t.Fatalf("converted currency mismatch: got=%q want=%q", got, want) + } +} + func TestGatewayProviderSettlementExecutor_ExecuteProviderSettlement_MissingSettlementAmount(t *testing.T) { orgID := bson.NewObjectID() diff --git a/api/payments/orchestrator/internal/service/orchestrator/step_money.go b/api/payments/orchestrator/internal/service/orchestrator/step_money.go index eb8fef39..16be5ee8 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/step_money.go +++ b/api/payments/orchestrator/internal/service/orchestrator/step_money.go @@ -4,6 +4,8 @@ import ( "strings" "github.com/shopspring/decimal" + "github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg" + svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared" paymenttypes "github.com/tech/sendico/pkg/payments/types" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" @@ -11,21 +13,6 @@ import ( var cardMinorUnitScale = decimal.NewFromInt(100) -func clonePaymentMoney(src *paymenttypes.Money) *paymenttypes.Money { - if src == nil { - return nil - } - amount := strings.TrimSpace(src.GetAmount()) - currency := strings.TrimSpace(src.GetCurrency()) - if amount == "" || currency == "" { - return nil - } - return &paymenttypes.Money{ - Amount: amount, - Currency: currency, - } -} - func protoMoneyToPaymentMoney(src *moneyv1.Money) *paymenttypes.Money { if src == nil { return nil @@ -38,6 +25,26 @@ func protoMoneyToPaymentMoney(src *moneyv1.Money) *paymenttypes.Money { return &paymenttypes.Money{Amount: amount, Currency: currency} } +func paymentMoneyToProto(src *paymenttypes.Money) *moneyv1.Money { + if src == nil { + return nil + } + amount := strings.TrimSpace(src.GetAmount()) + currency := strings.TrimSpace(src.GetCurrency()) + if amount == "" || currency == "" { + return nil + } + return &moneyv1.Money{Amount: amount, Currency: currency} +} + +func plannedStepMoney(step agg.StepExecution) *paymenttypes.Money { + return svcshared.CloneMoneyTrimNonEmpty(step.PlannedMoney) +} + +func plannedStepConvertedMoney(step agg.StepExecution) *paymenttypes.Money { + return svcshared.CloneMoneyTrimNonEmpty(step.PlannedConvertedMoney) +} + func transferExecutedMoney(transfer *chainv1.Transfer) *paymenttypes.Money { if transfer == nil { return nil diff --git a/api/payments/orchestrator/internal/service/shared/money.go b/api/payments/orchestrator/internal/service/shared/money.go new file mode 100644 index 00000000..bf961b6c --- /dev/null +++ b/api/payments/orchestrator/internal/service/shared/money.go @@ -0,0 +1,24 @@ +package shared + +import ( + "strings" + + paymenttypes "github.com/tech/sendico/pkg/payments/types" +) + +// CloneMoneyTrimNonEmpty clones a money value after trimming fields. +// It returns nil when amount or currency is missing. +func CloneMoneyTrimNonEmpty(src *paymenttypes.Money) *paymenttypes.Money { + if src == nil { + return nil + } + amount := strings.TrimSpace(src.GetAmount()) + currency := strings.TrimSpace(src.GetCurrency()) + if amount == "" || currency == "" { + return nil + } + return &paymenttypes.Money{ + Amount: amount, + Currency: currency, + } +} diff --git a/api/payments/quotation/go.mod b/api/payments/quotation/go.mod index d9ef0187..a8afd5bd 100644 --- a/api/payments/quotation/go.mod +++ b/api/payments/quotation/go.mod @@ -62,6 +62,6 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect ) diff --git a/api/payments/quotation/go.sum b/api/payments/quotation/go.sum index f1aefe34..9527fc9a 100644 --- a/api/payments/quotation/go.sum +++ b/api/payments/quotation/go.sum @@ -202,8 +202,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -211,8 +211,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-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/payments/quotation/internal/service/plan/helpers.go b/api/payments/quotation/internal/service/plan/helpers.go index 59bdbfae..4b5eec4d 100644 --- a/api/payments/quotation/internal/service/plan/helpers.go +++ b/api/payments/quotation/internal/service/plan/helpers.go @@ -5,6 +5,7 @@ import ( "time" "github.com/shopspring/decimal" + "github.com/tech/sendico/payments/quotation/internal/shared" "github.com/tech/sendico/payments/storage/model" "github.com/tech/sendico/pkg/merrors" paymenttypes "github.com/tech/sendico/pkg/payments/types" @@ -23,10 +24,7 @@ type moneyGetter interface { } func cloneMoney(input *paymenttypes.Money) *paymenttypes.Money { - if input == nil { - return nil - } - return &paymenttypes.Money{Currency: input.GetCurrency(), Amount: input.GetAmount()} + return shared.CloneModelMoneyRaw(input) } func cloneStringList(values []string) []string { @@ -100,10 +98,7 @@ func protoMoney(m *paymenttypes.Money) *moneyv1.Money { } func cloneProtoMoney(input *moneyv1.Money) *moneyv1.Money { - if input == nil { - return nil - } - return &moneyv1.Money{Currency: input.GetCurrency(), Amount: input.GetAmount()} + return shared.CloneProtoMoneyRaw(input) } func decimalFromProto(value *moneyv1.Decimal) *paymenttypes.Decimal { diff --git a/api/payments/quotation/internal/service/quotation/gateway_funding_profile/funding_gate_builder.go b/api/payments/quotation/internal/service/quotation/gateway_funding_profile/funding_gate_builder.go index e96d209a..c60ada34 100644 --- a/api/payments/quotation/internal/service/quotation/gateway_funding_profile/funding_gate_builder.go +++ b/api/payments/quotation/internal/service/quotation/gateway_funding_profile/funding_gate_builder.go @@ -67,13 +67,7 @@ func BuildFundingGateFromProfile( } func cloneProtoMoney(src *moneyv1.Money) *moneyv1.Money { - if src == nil { - return nil - } - return &moneyv1.Money{ - Amount: strings.TrimSpace(src.GetAmount()), - Currency: strings.TrimSpace(src.GetCurrency()), - } + return shared.CloneProtoMoneyTrim(src) } func clonePaymentEndpoint(src *model.PaymentEndpoint) *model.PaymentEndpoint { diff --git a/api/payments/quotation/internal/service/quotation/helpers.go b/api/payments/quotation/internal/service/quotation/helpers.go index 9860f864..86608454 100644 --- a/api/payments/quotation/internal/service/quotation/helpers.go +++ b/api/payments/quotation/internal/service/quotation/helpers.go @@ -6,6 +6,7 @@ import ( "github.com/shopspring/decimal" oracleclient "github.com/tech/sendico/fx/oracle/client" + qshared "github.com/tech/sendico/payments/quotation/internal/shared" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" "google.golang.org/protobuf/proto" @@ -31,13 +32,7 @@ const ( ) func cloneProtoMoney(input *moneyv1.Money) *moneyv1.Money { - if input == nil { - return nil - } - return &moneyv1.Money{ - Currency: input.GetCurrency(), - Amount: input.GetAmount(), - } + return qshared.CloneProtoMoneyRaw(input) } func cloneMetadata(input map[string]string) map[string]string { diff --git a/api/payments/quotation/internal/service/quotation/quotation_service_v2/helpers.go b/api/payments/quotation/internal/service/quotation/quotation_service_v2/helpers.go index 4bebc500..42b7111d 100644 --- a/api/payments/quotation/internal/service/quotation/quotation_service_v2/helpers.go +++ b/api/payments/quotation/internal/service/quotation/quotation_service_v2/helpers.go @@ -4,6 +4,7 @@ import ( "strings" "time" + qshared "github.com/tech/sendico/payments/quotation/internal/shared" "github.com/tech/sendico/payments/storage/model" payecon "github.com/tech/sendico/pkg/payments/economics" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" @@ -38,13 +39,7 @@ func firstNonEmpty(values ...string) string { } func cloneProtoMoney(src *moneyv1.Money) *moneyv1.Money { - if src == nil { - return nil - } - return &moneyv1.Money{ - Amount: strings.TrimSpace(src.GetAmount()), - Currency: strings.ToUpper(strings.TrimSpace(src.GetCurrency())), - } + return qshared.CloneProtoMoneyTrimUpperCurrency(src) } func resolvedSettlementModeFromModel(mode model.SettlementMode) paymentv1.SettlementMode { diff --git a/api/payments/quotation/internal/service/quotation/quote_computation_service/helpers.go b/api/payments/quotation/internal/service/quotation/quote_computation_service/helpers.go index 318ea6cf..61187a5c 100644 --- a/api/payments/quotation/internal/service/quotation/quote_computation_service/helpers.go +++ b/api/payments/quotation/internal/service/quotation/quote_computation_service/helpers.go @@ -3,19 +3,14 @@ package quote_computation_service import ( "strings" + qshared "github.com/tech/sendico/payments/quotation/internal/shared" "github.com/tech/sendico/payments/storage/model" paymenttypes "github.com/tech/sendico/pkg/payments/types" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" ) func cloneProtoMoney(src *moneyv1.Money) *moneyv1.Money { - if src == nil { - return nil - } - return &moneyv1.Money{ - Amount: strings.TrimSpace(src.GetAmount()), - Currency: strings.TrimSpace(src.GetCurrency()), - } + return qshared.CloneProtoMoneyTrim(src) } func protoMoneyFromModel(src *paymenttypes.Money) *moneyv1.Money { @@ -58,13 +53,7 @@ func cloneAsset(src *paymenttypes.Asset) *paymenttypes.Asset { } func cloneModelMoney(src *paymenttypes.Money) *paymenttypes.Money { - if src == nil { - return nil - } - return &paymenttypes.Money{ - Amount: strings.TrimSpace(src.GetAmount()), - Currency: strings.ToUpper(strings.TrimSpace(src.GetCurrency())), - } + return qshared.CloneModelMoneyTrimUpperCurrency(src) } func clonePaymentIntent(src model.PaymentIntent) model.PaymentIntent { diff --git a/api/payments/quotation/internal/service/quotation/quote_response_mapper_v2/helpers.go b/api/payments/quotation/internal/service/quotation/quote_response_mapper_v2/helpers.go index 912f2896..04097f73 100644 --- a/api/payments/quotation/internal/service/quotation/quote_response_mapper_v2/helpers.go +++ b/api/payments/quotation/internal/service/quotation/quote_response_mapper_v2/helpers.go @@ -3,6 +3,7 @@ package quote_response_mapper_v2 import ( "strings" + qshared "github.com/tech/sendico/payments/quotation/internal/shared" feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1" @@ -12,13 +13,7 @@ import ( ) func cloneMoney(src *moneyv1.Money) *moneyv1.Money { - if src == nil { - return nil - } - return &moneyv1.Money{ - Amount: strings.TrimSpace(src.GetAmount()), - Currency: strings.TrimSpace(src.GetCurrency()), - } + return qshared.CloneProtoMoneyTrim(src) } func cloneFeeLines(src []*feesv1.DerivedPostingLine) []*feesv1.DerivedPostingLine { diff --git a/api/payments/quotation/internal/shared/money.go b/api/payments/quotation/internal/shared/money.go new file mode 100644 index 00000000..ac848617 --- /dev/null +++ b/api/payments/quotation/internal/shared/money.go @@ -0,0 +1,58 @@ +package shared + +import ( + "strings" + + paymenttypes "github.com/tech/sendico/pkg/payments/types" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" +) + +func CloneProtoMoneyRaw(src *moneyv1.Money) *moneyv1.Money { + if src == nil { + return nil + } + return &moneyv1.Money{ + Amount: src.GetAmount(), + Currency: src.GetCurrency(), + } +} + +func CloneProtoMoneyTrim(src *moneyv1.Money) *moneyv1.Money { + if src == nil { + return nil + } + return &moneyv1.Money{ + Amount: strings.TrimSpace(src.GetAmount()), + Currency: strings.TrimSpace(src.GetCurrency()), + } +} + +func CloneProtoMoneyTrimUpperCurrency(src *moneyv1.Money) *moneyv1.Money { + if src == nil { + return nil + } + return &moneyv1.Money{ + Amount: strings.TrimSpace(src.GetAmount()), + Currency: strings.ToUpper(strings.TrimSpace(src.GetCurrency())), + } +} + +func CloneModelMoneyRaw(src *paymenttypes.Money) *paymenttypes.Money { + if src == nil { + return nil + } + return &paymenttypes.Money{ + Amount: src.GetAmount(), + Currency: src.GetCurrency(), + } +} + +func CloneModelMoneyTrimUpperCurrency(src *paymenttypes.Money) *paymenttypes.Money { + if src == nil { + return nil + } + return &paymenttypes.Money{ + Amount: strings.TrimSpace(src.GetAmount()), + Currency: strings.ToUpper(strings.TrimSpace(src.GetCurrency())), + } +} diff --git a/api/payments/storage/go.mod b/api/payments/storage/go.mod index bef983a6..13ec7773 100644 --- a/api/payments/storage/go.mod +++ b/api/payments/storage/go.mod @@ -38,6 +38,6 @@ require ( golang.org/x/crypto v0.48.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/text v0.35.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/api/payments/storage/go.sum b/api/payments/storage/go.sum index f54c8239..bff89996 100644 --- a/api/payments/storage/go.sum +++ b/api/payments/storage/go.sum @@ -182,8 +182,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/api/pkg/go.mod b/api/pkg/go.mod index f341dfbd..fe80db8d 100644 --- a/api/pkg/go.mod +++ b/api/pkg/go.mod @@ -1,6 +1,6 @@ module github.com/tech/sendico/pkg -go 1.25.0 +go 1.25.7 require ( github.com/casbin/casbin/v2 v2.135.0 @@ -35,7 +35,7 @@ require ( github.com/casbin/govaluate v1.10.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/consensys/gnark-crypto v0.19.2 // indirect + github.com/consensys/gnark-crypto v0.20.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect @@ -46,7 +46,7 @@ require ( github.com/docker/docker v27.3.1+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.7 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -111,8 +111,8 @@ require ( golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.15.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/api/pkg/go.sum b/api/pkg/go.sum index 85572e8a..37e7c434 100644 --- a/api/pkg/go.sum +++ b/api/pkg/go.sum @@ -30,8 +30,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80= -github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0= +github.com/consensys/gnark-crypto v0.20.0 h1:dJmv2sC9KWV/cNRjMjy2S0h7emfyyX8eSsJzwk0DQzw= +github.com/consensys/gnark-crypto v0.20.0/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= @@ -59,8 +59,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= -github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= -github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= +github.com/ethereum/c-kzg-4844/v2 v2.1.7 h1:aat3CuITdDbPC6pmEGRT0zJ5eOxzrZj8TJT5z7Xk//M= +github.com/ethereum/c-kzg-4844/v2 v2.1.7/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-ethereum v1.17.1 h1:IjlQDjgxg2uL+GzPRkygGULPMLzcYWncEI7wbaizvho= github.com/ethereum/go-ethereum v1.17.1/go.mod h1:7UWOVHL7K3b8RfVRea022btnzLCaanwHtBuH1jUCH/I= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -323,8 +323,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -342,8 +342,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-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E= google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= diff --git a/api/proto/payments/orchestration/v2/orchestration.proto b/api/proto/payments/orchestration/v2/orchestration.proto index 27e6722c..0b806c64 100644 --- a/api/proto/payments/orchestration/v2/orchestration.proto +++ b/api/proto/payments/orchestration/v2/orchestration.proto @@ -176,10 +176,28 @@ message StepExecution { ReportVisibility report_visibility = 9; // Optional user-facing operation label. string user_label = 10; - // Final amount processed by this step (if known). - common.money.v1.Money executed_money = 11; - // Converted amount produced by this step (for FX operations). - common.money.v1.Money converted_money = 12; + // Structured money envelope for this step lifecycle. + StepExecutionMoney money = 11; + + // Former flat money fields intentionally removed. + reserved 12, 13, 14, 15; + reserved "executed_money", "converted_money", "planned_money", "planned_converted_money"; +} + +// StepExecutionMoney groups planned and executed monetary snapshots. +message StepExecutionMoney { + // Monetary instruction bound by planner before execution. + StepExecutionMoneySnapshot planned = 1; + // Monetary outcome observed/confirmed after execution. + StepExecutionMoneySnapshot executed = 2; +} + +// StepExecutionMoneySnapshot captures base and converted amounts. +message StepExecutionMoneySnapshot { + // Base amount for the step. + common.money.v1.Money amount = 1; + // Converted amount for FX-like steps. + common.money.v1.Money converted_amount = 2; } // Kept local on purpose: no shared enum exists for orchestration step runtime. diff --git a/ci/scripts/common/run_backend_lint.sh b/ci/scripts/common/run_backend_lint.sh index 9e16a57d..48e1fabf 100755 --- a/ci/scripts/common/run_backend_lint.sh +++ b/ci/scripts/common/run_backend_lint.sh @@ -47,7 +47,7 @@ run_go_lint() { echo "[backend-lint] running golangci-lint in ${module}" ( cd "${module}" - golangci-lint run --timeout=10m ./... + golangci-lint run --timeout=30m ./... ) } diff --git a/frontend/pshared/lib/data/dto/payment/operation.dart b/frontend/pshared/lib/data/dto/payment/operation.dart index f10db2c2..98cfbc44 100644 --- a/frontend/pshared/lib/data/dto/payment/operation.dart +++ b/frontend/pshared/lib/data/dto/payment/operation.dart @@ -1,4 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; +import 'package:pshared/data/dto/money.dart'; part 'operation.g.dart'; @@ -10,6 +11,7 @@ class PaymentOperationDTO { final String? code; final String? state; final String? label; + final PaymentOperationMoneyDTO? money; final String? failureCode; final String? failureReason; final String? startedAt; @@ -22,6 +24,7 @@ class PaymentOperationDTO { this.code, this.state, this.label, + this.money, this.failureCode, this.failureReason, this.startedAt, @@ -32,3 +35,29 @@ class PaymentOperationDTO { _$PaymentOperationDTOFromJson(json); Map toJson() => _$PaymentOperationDTOToJson(this); } + +@JsonSerializable() +class PaymentOperationMoneyDTO { + final PaymentOperationMoneySnapshotDTO? planned; + final PaymentOperationMoneySnapshotDTO? executed; + + const PaymentOperationMoneyDTO({this.planned, this.executed}); + + factory PaymentOperationMoneyDTO.fromJson(Map json) => + _$PaymentOperationMoneyDTOFromJson(json); + Map toJson() => _$PaymentOperationMoneyDTOToJson(this); +} + +@JsonSerializable() +class PaymentOperationMoneySnapshotDTO { + final MoneyDTO? amount; + final MoneyDTO? convertedAmount; + + const PaymentOperationMoneySnapshotDTO({this.amount, this.convertedAmount}); + + factory PaymentOperationMoneySnapshotDTO.fromJson( + Map json, + ) => _$PaymentOperationMoneySnapshotDTOFromJson(json); + Map toJson() => + _$PaymentOperationMoneySnapshotDTOToJson(this); +} diff --git a/frontend/pshared/lib/data/mapper/payment/operation.dart b/frontend/pshared/lib/data/mapper/payment/operation.dart index 0b086d6f..0e486bba 100644 --- a/frontend/pshared/lib/data/mapper/payment/operation.dart +++ b/frontend/pshared/lib/data/mapper/payment/operation.dart @@ -1,7 +1,7 @@ import 'package:pshared/data/dto/payment/operation.dart'; +import 'package:pshared/data/mapper/money.dart'; import 'package:pshared/models/payment/execution_operation.dart'; - extension PaymentOperationDTOMapper on PaymentOperationDTO { PaymentExecutionOperation toDomain() => PaymentExecutionOperation( stepRef: stepRef, @@ -10,6 +10,7 @@ extension PaymentOperationDTOMapper on PaymentOperationDTO { code: code, state: state, label: label, + money: money?.toDomain(), failureCode: failureCode, failureReason: failureReason, startedAt: _parseDateTime(startedAt), @@ -25,6 +26,7 @@ extension PaymentExecutionOperationMapper on PaymentExecutionOperation { code: code, state: state, label: label, + money: money?.toDTO(), failureCode: failureCode, failureReason: failureReason, startedAt: startedAt?.toUtc().toIso8601String(), @@ -32,6 +34,38 @@ extension PaymentExecutionOperationMapper on PaymentExecutionOperation { ); } +extension PaymentOperationMoneyDTOMapper on PaymentOperationMoneyDTO { + PaymentExecutionOperationMoney toDomain() => PaymentExecutionOperationMoney( + planned: planned?.toDomain(), + executed: executed?.toDomain(), + ); +} + +extension PaymentExecutionOperationMoneyMapper + on PaymentExecutionOperationMoney { + PaymentOperationMoneyDTO toDTO() => PaymentOperationMoneyDTO( + planned: planned?.toDTO(), + executed: executed?.toDTO(), + ); +} + +extension PaymentOperationMoneySnapshotDTOMapper + on PaymentOperationMoneySnapshotDTO { + PaymentExecutionOperationMoneySnapshot toDomain() => + PaymentExecutionOperationMoneySnapshot( + amount: amount?.toDomain(), + convertedAmount: convertedAmount?.toDomain(), + ); +} + +extension PaymentExecutionOperationMoneySnapshotMapper + on PaymentExecutionOperationMoneySnapshot { + PaymentOperationMoneySnapshotDTO toDTO() => PaymentOperationMoneySnapshotDTO( + amount: amount?.toDTO(), + convertedAmount: convertedAmount?.toDTO(), + ); +} + DateTime? _parseDateTime(String? value) { final normalized = value?.trim(); if (normalized == null || normalized.isEmpty) return null; diff --git a/frontend/pshared/lib/models/payment/execution_operation.dart b/frontend/pshared/lib/models/payment/execution_operation.dart index 427f93da..9f7f3785 100644 --- a/frontend/pshared/lib/models/payment/execution_operation.dart +++ b/frontend/pshared/lib/models/payment/execution_operation.dart @@ -1,3 +1,5 @@ +import 'package:pshared/models/money.dart'; + class PaymentExecutionOperation { final String? stepRef; final String? operationRef; @@ -5,6 +7,7 @@ class PaymentExecutionOperation { final String? code; final String? state; final String? label; + final PaymentExecutionOperationMoney? money; final String? failureCode; final String? failureReason; final DateTime? startedAt; @@ -17,9 +20,30 @@ class PaymentExecutionOperation { required this.code, required this.state, required this.label, + required this.money, required this.failureCode, required this.failureReason, required this.startedAt, required this.completedAt, }); } + +class PaymentExecutionOperationMoney { + final PaymentExecutionOperationMoneySnapshot? planned; + final PaymentExecutionOperationMoneySnapshot? executed; + + const PaymentExecutionOperationMoney({ + required this.planned, + required this.executed, + }); +} + +class PaymentExecutionOperationMoneySnapshot { + final Money? amount; + final Money? convertedAmount; + + const PaymentExecutionOperationMoneySnapshot({ + required this.amount, + required this.convertedAmount, + }); +}