Files
sendico/api/payments/quotation/internal/service/quotation/discovery_gateway_registry.go
2026-02-18 20:38:08 +01:00

213 lines
5.6 KiB
Go

package quotation
import (
"context"
"sort"
"strings"
"time"
"github.com/tech/sendico/payments/storage/model"
"github.com/tech/sendico/pkg/discovery"
"github.com/tech/sendico/pkg/mlogger"
)
type discoveryGatewayRegistry struct {
logger mlogger.Logger
registry *discovery.Registry
}
func NewDiscoveryGatewayRegistry(logger mlogger.Logger, registry *discovery.Registry) GatewayRegistry {
if registry == nil {
return nil
}
if logger != nil {
logger = logger.Named("discovery_gateway_registry")
}
return &discoveryGatewayRegistry{
logger: logger,
registry: registry,
}
}
func (r *discoveryGatewayRegistry) List(_ context.Context) ([]*model.GatewayInstanceDescriptor, error) {
if r == nil || r.registry == nil {
return nil, nil
}
entries := r.registry.List(time.Now(), true)
items := make([]*model.GatewayInstanceDescriptor, 0, len(entries))
for _, entry := range entries {
if entry.Rail == "" {
continue
}
rail := railFromDiscovery(entry.Rail)
if rail == model.RailUnspecified {
continue
}
items = append(items, &model.GatewayInstanceDescriptor{
ID: entry.ID,
InstanceID: entry.InstanceID,
Rail: rail,
Network: entry.Network,
InvokeURI: strings.TrimSpace(entry.InvokeURI),
Currencies: normalizeCurrencies(entry.Currencies),
Capabilities: capabilitiesFromOps(entry.Operations),
Limits: limitsFromDiscovery(entry.Limits, entry.CurrencyMeta),
Version: entry.Version,
IsEnabled: entry.Healthy,
})
}
sort.Slice(items, func(i, j int) bool {
return model.LessGatewayDescriptor(items[i], items[j])
})
return items, nil
}
func railFromDiscovery(value string) model.Rail {
switch strings.ToUpper(strings.TrimSpace(value)) {
case string(model.RailCrypto):
return model.RailCrypto
case string(model.RailProviderSettlement):
return model.RailProviderSettlement
case string(model.RailLedger):
return model.RailLedger
case string(model.RailCardPayout):
return model.RailCardPayout
case string(model.RailFiatOnRamp):
return model.RailFiatOnRamp
default:
return model.RailUnspecified
}
}
func capabilitiesFromOps(ops []string) model.RailCapabilities {
var cap model.RailCapabilities
for _, op := range ops {
switch strings.ToLower(strings.TrimSpace(op)) {
case "payin.crypto", "payin.card", "payin.fiat":
cap.CanPayIn = true
case "payout.crypto", "payout.card", "payout.fiat":
cap.CanPayOut = true
case "balance.read":
cap.CanReadBalance = true
case "fee.send":
cap.CanSendFee = true
case "observe.confirm", "observe.confirmation":
cap.RequiresObserveConfirm = true
case "block", "funds.block", "balance.block", "ledger.block":
cap.CanBlock = true
case "release", "funds.release", "balance.release", "ledger.release":
cap.CanRelease = true
}
}
return cap
}
func limitsFromDiscovery(src *discovery.Limits, currencies []discovery.CurrencyAnnouncement) model.Limits {
limits := model.Limits{
VolumeLimit: map[string]string{},
VelocityLimit: map[string]int{},
CurrencyLimits: map[string]model.LimitsOverride{},
}
if src != nil {
limits.MinAmount = strings.TrimSpace(src.MinAmount)
limits.MaxAmount = strings.TrimSpace(src.MaxAmount)
for key, value := range src.VolumeLimit {
k := strings.TrimSpace(key)
v := strings.TrimSpace(value)
if k == "" || v == "" {
continue
}
limits.VolumeLimit[k] = v
}
for key, value := range src.VelocityLimit {
k := strings.TrimSpace(key)
if k == "" {
continue
}
limits.VelocityLimit[k] = value
}
}
applyCurrencyTransferLimits(&limits, currencies)
if len(limits.VolumeLimit) == 0 {
limits.VolumeLimit = nil
}
if len(limits.VelocityLimit) == 0 {
limits.VelocityLimit = nil
}
if len(limits.CurrencyLimits) == 0 {
limits.CurrencyLimits = nil
}
return limits
}
func applyCurrencyTransferLimits(dst *model.Limits, currencies []discovery.CurrencyAnnouncement) {
if dst == nil || len(currencies) == 0 {
return
}
var (
commonMin string
commonMax string
commonMinInit bool
commonMaxInit bool
commonMinConsistent = true
commonMaxConsistent = true
)
for _, currency := range currencies {
code := strings.ToUpper(strings.TrimSpace(currency.Currency))
if code == "" || currency.Limits == nil || currency.Limits.Amount == nil {
commonMinConsistent = false
commonMaxConsistent = false
continue
}
min := strings.TrimSpace(currency.Limits.Amount.Min)
max := strings.TrimSpace(currency.Limits.Amount.Max)
if min != "" || max != "" {
override := dst.CurrencyLimits[code]
if min != "" {
override.MinAmount = min
}
if max != "" {
override.MaxAmount = max
}
if override.MinAmount != "" || override.MaxAmount != "" || override.MaxFee != "" || override.MaxOps > 0 || override.MaxVolume != "" {
dst.CurrencyLimits[code] = override
}
}
if min == "" {
commonMinConsistent = false
} else if !commonMinInit {
commonMin = min
commonMinInit = true
} else if commonMin != min {
commonMinConsistent = false
}
if max == "" {
commonMaxConsistent = false
} else if !commonMaxInit {
commonMax = max
commonMaxInit = true
} else if commonMax != max {
commonMaxConsistent = false
}
}
if commonMinInit && commonMinConsistent {
dst.PerTxMinAmount = firstLimitValue(dst.PerTxMinAmount, commonMin)
}
if commonMaxInit && commonMaxConsistent {
dst.PerTxMaxAmount = firstLimitValue(dst.PerTxMaxAmount, commonMax)
}
}
func firstLimitValue(primary, fallback string) string {
primary = strings.TrimSpace(primary)
if primary != "" {
return primary
}
return strings.TrimSpace(fallback)
}