package model import ( "fmt" "strings" "github.com/shopspring/decimal" ) type GatewayDirection int const ( GatewayDirectionAny GatewayDirection = iota GatewayDirectionOut GatewayDirectionIn ) func (d GatewayDirection) String() string { switch d { case GatewayDirectionOut: return "out" case GatewayDirectionIn: return "in" default: return "any" } } func NoEligibleGatewayMessage(network, currency string, action RailOperation, dir GatewayDirection) string { return fmt.Sprintf( "plan builder: no eligible gateway found for %s %s %s for direction %s", strings.ToUpper(strings.TrimSpace(network)), strings.ToUpper(strings.TrimSpace(currency)), ParseRailOperation(string(action)), dir.String(), ) } func IsGatewayEligible( gw *GatewayInstanceDescriptor, rail Rail, network, currency string, action RailOperation, dir GatewayDirection, amount decimal.Decimal, ) error { if gw == nil { return gatewayIneligible(gw, "gateway instance is required") } if !gw.IsEnabled { return gatewayIneligible(gw, "gateway instance is disabled") } if gw.Rail != rail { return gatewayIneligible(gw, fmt.Sprintf("rail mismatch: want %s got %s", rail, gw.Rail)) } if network != "" && gw.Network != "" && !strings.EqualFold(gw.Network, network) { return gatewayIneligible(gw, fmt.Sprintf("network mismatch: want %s got %s", network, gw.Network)) } if currency != "" && len(gw.Currencies) > 0 { found := false for _, c := range gw.Currencies { if strings.EqualFold(c, currency) { found = true break } } if !found { return gatewayIneligible(gw, "currency not supported: "+currency) } } if !gatewayAllowsAction(gw.Operations, gw.Capabilities, action, dir) { return gatewayIneligible(gw, fmt.Sprintf("gateway does not allow action=%s dir=%s", action, dir.String())) } if currency != "" { if err := amountWithinLimits(gw, gw.Limits, currency, amount, action); err != nil { return err } } return nil } type gatewayIneligibleError struct { reason string } func (e gatewayIneligibleError) Error() string { return e.reason } func gatewayIneligible(gw *GatewayInstanceDescriptor, reason string) error { if strings.TrimSpace(reason) == "" { reason = "gateway instance is not eligible" } instanceID := "" if gw != nil { instanceID = gw.InstanceID } return gatewayIneligibleError{reason: fmt.Sprintf("gateway %s eligibility check error: %s", instanceID, reason)} } func gatewayAllowsAction(operations []RailOperation, cap RailCapabilities, action RailOperation, dir GatewayDirection) bool { normalized := NormalizeRailOperations(operations) if len(normalized) > 0 { return operationsAllowAction(normalized, action, dir) } return capabilityAllowsAction(cap, action, dir) } func capabilityAllowsAction(cap RailCapabilities, action RailOperation, dir GatewayDirection) bool { switch action { case RailOperationSend: switch dir { case GatewayDirectionOut: return cap.CanPayOut case GatewayDirectionIn: return cap.CanPayIn default: return cap.CanPayIn || cap.CanPayOut } case RailOperationExternalDebit, RailOperationExternalCredit: switch dir { case GatewayDirectionOut: return cap.CanPayOut case GatewayDirectionIn: return cap.CanPayIn default: return cap.CanPayIn || cap.CanPayOut } case RailOperationFee: return cap.CanSendFee case RailOperationObserveConfirm: return cap.RequiresObserveConfirm case RailOperationBlock: return cap.CanBlock case RailOperationRelease: return cap.CanRelease default: return true } } func operationsAllowAction(operations []RailOperation, action RailOperation, dir GatewayDirection) bool { action = ParseRailOperation(string(action)) if action == RailOperationUnspecified { return false } if HasRailOperation(operations, action) { return true } switch action { case RailOperationSend: switch dir { case GatewayDirectionIn: return HasRailOperation(operations, RailOperationExternalDebit) case GatewayDirectionOut: return HasRailOperation(operations, RailOperationExternalCredit) default: return HasRailOperation(operations, RailOperationExternalDebit) || HasRailOperation(operations, RailOperationExternalCredit) } case RailOperationExternalDebit: return HasRailOperation(operations, RailOperationSend) case RailOperationExternalCredit: return HasRailOperation(operations, RailOperationSend) default: return false } } func amountWithinLimits(gw *GatewayInstanceDescriptor, limits Limits, currency string, amount decimal.Decimal, action RailOperation) error { min := firstLimitValue(limits.MinAmount, "") max := firstLimitValue(limits.MaxAmount, "") perTxMin := firstLimitValue(limits.PerTxMinAmount, "") perTxMax := firstLimitValue(limits.PerTxMaxAmount, "") maxFee := firstLimitValue(limits.PerTxMaxFee, "") if override, ok := limits.CurrencyLimits[currency]; ok { min = firstLimitValue(override.MinAmount, min) max = firstLimitValue(override.MaxAmount, max) if action == RailOperationFee { maxFee = firstLimitValue(override.MaxFee, maxFee) } } if min != "" { if val, err := decimal.NewFromString(min); err == nil && amount.LessThan(val) { return gatewayIneligible(gw, 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 gatewayIneligible(gw, 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 gatewayIneligible(gw, 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 gatewayIneligible(gw, fmt.Sprintf("amount %s %s exceeds per-tx max limit %s", amount.String(), currency, val.String())) } } if action == RailOperationFee && maxFee != "" { if val, err := decimal.NewFromString(maxFee); err == nil && amount.GreaterThan(val) { return gatewayIneligible(gw, fmt.Sprintf("fee amount %s %s exceeds max fee limit %s", amount.String(), currency, val.String())) } } return nil } func firstLimitValue(primary, fallback string) string { val := strings.TrimSpace(primary) if val != "" { return val } return strings.TrimSpace(fallback) }