package plan import ( "context" "github.com/tech/sendico/pkg/discovery" "sort" "strings" "github.com/shopspring/decimal" "github.com/tech/sendico/payments/storage/model" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" paymenttypes "github.com/tech/sendico/pkg/payments/types" "go.uber.org/zap" ) func ensureGatewayForAction(ctx context.Context, logger mlogger.Logger, registry GatewayRegistry, cache map[model.Rail]*model.GatewayInstanceDescriptor, rail model.Rail, network string, amount *paymenttypes.Money, action model.RailOperation, instanceID string, dir sendDirection) (*model.GatewayInstanceDescriptor, error) { if registry == nil { return nil, merrors.InvalidArgument("plan builder: gateway registry is required") } if gw, ok := cache[rail]; ok && gw != nil { if instanceID == "" || strings.EqualFold(gw.InstanceID, instanceID) { if err := validateGatewayAction(gw, network, amount, action, dir); err != nil { logger.Warn("Failed to validate gateway", zap.Error(err), zap.String("instance_id", instanceID), zap.String("rail", string(rail)), zap.String("network", network), zap.String("action", string(action)), zap.String("direction", sendDirectionLabel(dir)), zap.Int("rails_qty", len(cache)), ) return nil, err } return gw, nil } } gw, err := selectGateway(ctx, registry, rail, network, amount, action, instanceID, dir) if err != nil { logger.Warn("Failed to select gateway", zap.Error(err), zap.String("instance_id", instanceID), zap.String("rail", string(rail)), zap.String("network", network), zap.String("action", string(action)), zap.String("direction", sendDirectionLabel(dir)), zap.Int("rails_qty", len(cache)), ) return nil, err } cache[rail] = gw return gw, nil } func validateGatewayAction(gw *model.GatewayInstanceDescriptor, network string, amount *paymenttypes.Money, action model.RailOperation, dir sendDirection) error { if gw == nil { return merrors.InvalidArgument("plan builder: gateway instance is required") } currency := "" amt := decimal.Zero if amount != nil && strings.TrimSpace(amount.GetAmount()) != "" { value, err := decimalFromMoney(amount) if err != nil { return err } amt = value currency = strings.ToUpper(strings.TrimSpace(amount.GetCurrency())) } if err := model.IsGatewayEligible(gw, gw.Rail, network, currency, action, toGatewayDirection(dir), amt); err != nil { return merrors.NoData(model.NoEligibleGatewayMessage(network, currency, action, toGatewayDirection(dir))) } return nil } type sendDirection int const ( sendDirectionAny sendDirection = iota sendDirectionOut sendDirectionIn ) func sendDirectionForRail(rail model.Rail) sendDirection { switch rail { case discovery.RailFiatOnRamp: return sendDirectionIn default: return sendDirectionOut } } func selectGateway(ctx context.Context, registry GatewayRegistry, rail model.Rail, network string, amount *paymenttypes.Money, action model.RailOperation, instanceID string, dir sendDirection) (*model.GatewayInstanceDescriptor, error) { if registry == nil { return nil, merrors.InvalidArgument("plan builder: gateway registry is required") } all, err := registry.List(ctx) if err != nil { return nil, err } if len(all) == 0 { return nil, merrors.InvalidArgument("plan builder: no gateway instances available") } currency := "" amt := decimal.Zero if amount != nil && strings.TrimSpace(amount.GetAmount()) != "" { amt, err = decimalFromMoney(amount) if err != nil { return nil, err } currency = strings.ToUpper(strings.TrimSpace(amount.GetCurrency())) } network = strings.ToUpper(strings.TrimSpace(network)) eligible := make([]*model.GatewayInstanceDescriptor, 0) for _, gw := range all { if err := model.IsGatewayEligible(gw, rail, network, currency, action, toGatewayDirection(dir), amt); err != nil { continue } eligible = append(eligible, gw) } if len(eligible) == 0 { return nil, merrors.NoData(model.NoEligibleGatewayMessage(network, currency, action, toGatewayDirection(dir))) } sort.Slice(eligible, func(i, j int) bool { return eligible[i].ID < eligible[j].ID }) if instanceID != "" { for _, gw := range eligible { if strings.EqualFold(strings.TrimSpace(gw.InstanceID), instanceID) { return gw, nil } } } return eligible[0], nil } func sendDirectionLabel(dir sendDirection) string { return toGatewayDirection(dir).String() } func toGatewayDirection(dir sendDirection) model.GatewayDirection { switch dir { case sendDirectionOut: return model.GatewayDirectionOut case sendDirectionIn: return model.GatewayDirectionIn default: return model.GatewayDirectionAny } }