From 54f2f96e76fb1b21b4690eb6c12a2284e4ba3628 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Mon, 26 Jan 2026 00:54:03 +0100 Subject: [PATCH] better error tracing --- .../orchestrator/gateway_resolution.go | 7 +- .../internal/service/orchestrator/options.go | 7 +- .../orchestrator/plan_builder_gateways.go | 75 ++++++++++++++----- 3 files changed, 67 insertions(+), 22 deletions(-) diff --git a/api/payments/orchestrator/internal/service/orchestrator/gateway_resolution.go b/api/payments/orchestrator/internal/service/orchestrator/gateway_resolution.go index 2527a0b2..cbeabc16 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/gateway_resolution.go +++ b/api/payments/orchestrator/internal/service/orchestrator/gateway_resolution.go @@ -82,6 +82,7 @@ func selectGatewayForActions(ctx context.Context, registry GatewayRegistry, rail network = strings.ToUpper(strings.TrimSpace(network)) eligible := make([]*model.GatewayInstanceDescriptor, 0) + var lastErr error for _, entry := range all { if entry == nil || !entry.IsEnabled { continue @@ -94,7 +95,8 @@ func selectGatewayForActions(ctx context.Context, registry GatewayRegistry, rail } ok := true for _, action := range actions { - if !isGatewayEligible(entry, rail, network, currency, action, dir, amt) { + if err := isGatewayEligible(entry, rail, network, currency, action, dir, amt); err != nil { + lastErr = err ok = false break } @@ -106,6 +108,9 @@ func selectGatewayForActions(ctx context.Context, registry GatewayRegistry, rail } if len(eligible) == 0 { + if lastErr != nil { + return nil, merrors.NoData("no eligible gateway instance found: " + lastErr.Error()) + } return nil, merrors.NoData("no eligible gateway instance found") } sort.Slice(eligible, func(i, j int) bool { diff --git a/api/payments/orchestrator/internal/service/orchestrator/options.go b/api/payments/orchestrator/internal/service/orchestrator/options.go index f7f192a5..505309dc 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/options.go +++ b/api/payments/orchestrator/internal/service/orchestrator/options.go @@ -118,6 +118,7 @@ func (g railGatewayDependency) resolveDynamic(ctx context.Context, step *model.P } candidates := make([]*model.GatewayInstanceDescriptor, 0) + var lastErr error for _, entry := range items { if entry == nil || !entry.IsEnabled { continue @@ -132,13 +133,17 @@ func (g railGatewayDependency) resolveDynamic(ctx context.Context, step *model.P continue } if step.Action != model.RailOperationUnspecified { - if !isGatewayEligible(entry, step.Rail, "", currency, step.Action, sendDirectionForRail(step.Rail), amount) { + if err := isGatewayEligible(entry, step.Rail, "", currency, step.Action, sendDirectionForRail(step.Rail), amount); err != nil { + lastErr = err continue } } candidates = append(candidates, entry) } if len(candidates) == 0 { + if lastErr != nil { + return nil, merrors.InvalidArgument("rail gateway: missing gateway for rail: " + lastErr.Error()) + } return nil, merrors.InvalidArgument("rail gateway: missing gateway for rail") } sort.Slice(candidates, func(i, j int) bool { diff --git a/api/payments/orchestrator/internal/service/orchestrator/plan_builder_gateways.go b/api/payments/orchestrator/internal/service/orchestrator/plan_builder_gateways.go index 45118a08..06cd82d1 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/plan_builder_gateways.go +++ b/api/payments/orchestrator/internal/service/orchestrator/plan_builder_gateways.go @@ -2,6 +2,7 @@ package orchestrator import ( "context" + "fmt" "sort" "strings" @@ -45,8 +46,8 @@ func validateGatewayAction(gw *model.GatewayInstanceDescriptor, network string, amt = value currency = strings.ToUpper(strings.TrimSpace(amount.GetCurrency())) } - if !isGatewayEligible(gw, gw.Rail, network, currency, action, dir, amt) { - return merrors.InvalidArgument("plan builder: gateway instance is not eligible") + if err := isGatewayEligible(gw, gw.Rail, network, currency, action, dir, amt); err != nil { + return merrors.InvalidArgument("plan builder: gateway instance is not eligible: " + err.Error()) } return nil } @@ -92,16 +93,21 @@ func selectGateway(ctx context.Context, registry GatewayRegistry, rail model.Rai network = strings.ToUpper(strings.TrimSpace(network)) eligible := make([]*model.GatewayInstanceDescriptor, 0) + var lastErr error for _, gw := range all { if instanceID != "" && !strings.EqualFold(strings.TrimSpace(gw.InstanceID), instanceID) { continue } - if !isGatewayEligible(gw, rail, network, currency, action, dir, amt) { + if err := isGatewayEligible(gw, rail, network, currency, action, dir, amt); err != nil { + lastErr = err continue } eligible = append(eligible, gw) } if len(eligible) == 0 { + if lastErr != nil { + return nil, merrors.InvalidArgument("plan builder: no eligible gateway instance found: " + lastErr.Error()) + } return nil, merrors.InvalidArgument("plan builder: no eligible gateway instance found") } sort.Slice(eligible, func(i, j int) bool { @@ -110,15 +116,44 @@ func selectGateway(ctx context.Context, registry GatewayRegistry, rail model.Rai return eligible[0], nil } -func isGatewayEligible(gw *model.GatewayInstanceDescriptor, rail model.Rail, network, currency string, action model.RailOperation, dir sendDirection, amount decimal.Decimal) bool { - if gw == nil || !gw.IsEnabled { - return false +type gatewayIneligibleError struct { + reason string +} + +func (e gatewayIneligibleError) Error() string { + return e.reason +} + +func gatewayIneligible(reason string) error { + if strings.TrimSpace(reason) == "" { + reason = "gateway instance is not eligible" + } + return gatewayIneligibleError{reason: reason} +} + +func sendDirectionLabel(dir sendDirection) string { + switch dir { + case sendDirectionOut: + return "out" + case sendDirectionIn: + return "in" + default: + return "any" + } +} + +func isGatewayEligible(gw *model.GatewayInstanceDescriptor, rail model.Rail, network, currency string, action model.RailOperation, dir sendDirection, amount decimal.Decimal) error { + if gw == nil { + return gatewayIneligible("gateway instance is required") + } + if !gw.IsEnabled { + return gatewayIneligible("gateway instance is disabled") } if gw.Rail != rail { - return false + return gatewayIneligible(fmt.Sprintf("rail mismatch: want %s got %s", rail, gw.Rail)) } if network != "" && gw.Network != "" && !strings.EqualFold(gw.Network, network) { - return false + return gatewayIneligible(fmt.Sprintf("network mismatch: want %s got %s", network, gw.Network)) } if currency != "" && len(gw.Currencies) > 0 { found := false @@ -129,20 +164,20 @@ func isGatewayEligible(gw *model.GatewayInstanceDescriptor, rail model.Rail, net } } if !found { - return false + return gatewayIneligible("currency not supported: " + currency) } } if !capabilityAllowsAction(gw.Capabilities, action, dir) { - return false + return gatewayIneligible(fmt.Sprintf("capability does not allow action=%s dir=%s", action, sendDirectionLabel(dir))) } if currency != "" { - if !amountWithinLimits(gw.Limits, currency, amount, action) { - return false + if err := amountWithinLimits(gw.Limits, currency, amount, action); err != nil { + return err } } - return true + return nil } func capabilityAllowsAction(cap model.RailCapabilities, action model.RailOperation, dir sendDirection) bool { @@ -169,7 +204,7 @@ func capabilityAllowsAction(cap model.RailCapabilities, action model.RailOperati } } -func amountWithinLimits(limits model.Limits, currency string, amount decimal.Decimal, action model.RailOperation) bool { +func amountWithinLimits(limits model.Limits, currency string, amount decimal.Decimal, action model.RailOperation) error { min := firstLimitValue(limits.MinAmount, "") max := firstLimitValue(limits.MaxAmount, "") perTxMin := firstLimitValue(limits.PerTxMinAmount, "") @@ -186,31 +221,31 @@ func amountWithinLimits(limits model.Limits, currency string, amount decimal.Dec if min != "" { if val, err := decimal.NewFromString(min); err == nil && amount.LessThan(val) { - return false + return gatewayIneligible(fmt.Sprintf("amount %s %s below min limit %s", amount.String(), currency, val.String())) } } if perTxMin != "" { if val, err := decimal.NewFromString(perTxMin); err == nil && amount.LessThan(val) { - return false + return gatewayIneligible(fmt.Sprintf("amount %s %s below per-tx min limit %s", amount.String(), currency, val.String())) } } if max != "" { if val, err := decimal.NewFromString(max); err == nil && amount.GreaterThan(val) { - return false + return gatewayIneligible(fmt.Sprintf("amount %s %s exceeds max limit %s", amount.String(), currency, val.String())) } } if perTxMax != "" { if val, err := decimal.NewFromString(perTxMax); err == nil && amount.GreaterThan(val) { - return false + return gatewayIneligible(fmt.Sprintf("amount %s %s exceeds per-tx max limit %s", amount.String(), currency, val.String())) } } if action == model.RailOperationFee && maxFee != "" { if val, err := decimal.NewFromString(maxFee); err == nil && amount.GreaterThan(val) { - return false + return gatewayIneligible(fmt.Sprintf("fee amount %s %s exceeds max fee limit %s", amount.String(), currency, val.String())) } } - return true + return nil } func firstLimitValue(primary, fallback string) string {