Files
sendico/api/payments/quotation/internal/service/quotation/options.go
2026-02-25 19:25:51 +01:00

298 lines
8.0 KiB
Go

package quotation
import (
"context"
"strings"
"time"
oracleclient "github.com/tech/sendico/fx/oracle/client"
chainclient "github.com/tech/sendico/gateway/chain/client"
ledgerclient "github.com/tech/sendico/ledger/client"
"github.com/tech/sendico/payments/storage/model"
clockpkg "github.com/tech/sendico/pkg/clock"
"github.com/tech/sendico/pkg/discovery"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/payments/rail"
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
)
// Option configures service dependencies.
type Option func(*Service)
// GatewayInvokeResolver resolves gateway invoke URIs into chain gateway clients.
type GatewayInvokeResolver interface {
Resolve(ctx context.Context, invokeURI string) (chainclient.Client, error)
}
// ChainGatewayResolver resolves chain gateway clients by network.
type ChainGatewayResolver interface {
Resolve(ctx context.Context, network string) (chainclient.Client, error)
}
// GatewayRegistry exposes gateway instances for capability-based selection.
type GatewayRegistry interface {
List(ctx context.Context) ([]*model.GatewayInstanceDescriptor, error)
}
// CardGatewayRoute maps a gateway to its funding and fee destinations.
type CardGatewayRoute struct {
FundingAddress string
FeeAddress string
FeeWalletRef string
}
type feesDependency struct {
client feesv1.FeeEngineClient
timeout time.Duration
}
func (f feesDependency) available() bool {
if f.client == nil {
return false
}
if checker, ok := f.client.(interface{ Available() bool }); ok {
return checker.Available()
}
return true
}
type gatewayDependency struct {
resolver ChainGatewayResolver
}
type oracleDependency struct {
client oracleclient.Client
}
func (o oracleDependency) available() bool {
if o.client == nil {
return false
}
if checker, ok := o.client.(interface{ Available() bool }); ok {
return checker.Available()
}
return true
}
// WithFeeEngine wires the fee engine client.
func WithFeeEngine(client feesv1.FeeEngineClient, timeout time.Duration) Option {
return func(s *Service) {
s.deps.fees = feesDependency{client: client, timeout: timeout}
}
}
// WithOracleClient wires the FX oracle client.
func WithOracleClient(client oracleclient.Client) Option {
return func(s *Service) {
s.deps.oracle = oracleDependency{client: client}
}
}
// WithChainGatewayResolver wires a resolver for chain gateway clients.
func WithChainGatewayResolver(resolver ChainGatewayResolver) Option {
return func(s *Service) {
if resolver != nil {
s.deps.gateway = gatewayDependency{resolver: resolver}
}
}
}
// WithGatewayInvokeResolver wires a resolver for invoke URIs.
func WithGatewayInvokeResolver(resolver GatewayInvokeResolver) Option {
return func(s *Service) {
if resolver != nil {
s.deps.gatewayInvokeResolver = resolver
}
}
}
// WithGatewayRegistry wires gateway descriptors used by quote computation/gateway selection.
func WithGatewayRegistry(registry GatewayRegistry) Option {
return func(s *Service) {
if registry != nil {
s.deps.gatewayRegistry = registry
}
}
}
// WithCardGatewayRoutes configures funding/fee wallet routing per gateway.
func WithCardGatewayRoutes(routes map[string]CardGatewayRoute) Option {
return func(s *Service) {
if len(routes) == 0 {
return
}
s.deps.cardRoutes = make(map[string]CardGatewayRoute, len(routes))
for key, route := range routes {
normalized := strings.ToLower(strings.TrimSpace(key))
if normalized == "" {
continue
}
s.deps.cardRoutes[normalized] = route
}
}
}
// WithFeeLedgerAccounts maps gateway IDs to fee ledger accounts.
func WithFeeLedgerAccounts(accounts map[string]string) Option {
return func(s *Service) {
if len(accounts) == 0 {
return
}
s.deps.feeLedgerAccounts = make(map[string]string, len(accounts))
for key, account := range accounts {
normalized := strings.ToLower(strings.TrimSpace(key))
value := strings.TrimSpace(account)
if normalized == "" || value == "" {
continue
}
s.deps.feeLedgerAccounts[normalized] = value
}
}
}
// WithClock overrides the default clock.
func WithClock(clock clockpkg.Clock) Option {
return func(s *Service) {
if clock != nil {
s.clock = clock
}
}
}
// WithLedgerClient is retained for backward compatibility and is currently a no-op.
func WithLedgerClient(_ ledgerclient.Client) Option {
return func(*Service) {}
}
// WithProviderSettlementGatewayClient is retained for backward compatibility and is currently a no-op.
func WithProviderSettlementGatewayClient(_ chainclient.Client) Option {
return func(*Service) {}
}
// WithProviderSettlementGatewayResolver is retained for backward compatibility and is currently a no-op.
func WithProviderSettlementGatewayResolver(_ ChainGatewayResolver) Option {
return func(*Service) {}
}
// WithRailGateways is retained for backward compatibility and is currently a no-op.
func WithRailGateways(_ map[string]rail.RailGateway) Option {
return func(*Service) {}
}
type discoveryGatewayRegistry struct {
registry *discovery.Registry
}
// NewDiscoveryGatewayRegistry adapts discovery entries into gateway descriptors.
func NewDiscoveryGatewayRegistry(_ mlogger.Logger, registry *discovery.Registry) GatewayRegistry {
if registry == nil {
return nil
}
return &discoveryGatewayRegistry{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 {
railID := railFromDiscovery(entry.Rail)
if railID == model.RailUnspecified {
continue
}
operations := operationsFromDiscovery(entry.Operations)
items = append(items, &model.GatewayInstanceDescriptor{
ID: strings.TrimSpace(entry.ID),
InstanceID: strings.TrimSpace(entry.InstanceID),
Rail: railID,
Network: strings.ToUpper(strings.TrimSpace(entry.Network)),
InvokeURI: strings.TrimSpace(entry.InvokeURI),
Currencies: currenciesFromDiscovery(entry.Currencies),
Operations: operations,
Capabilities: model.RailCapabilitiesFromOperations(operations),
Limits: limitsFromDiscovery(entry.Limits),
IsEnabled: entry.Healthy,
})
}
return items, nil
}
func railFromDiscovery(value string) model.Rail {
switch discovery.NormalizeRail(value) {
case discovery.RailCrypto:
return model.RailCrypto
case discovery.RailProviderSettlement:
return model.RailProviderSettlement
case discovery.RailLedger:
return model.RailLedger
case discovery.RailCardPayout:
return model.RailCardPayout
case discovery.RailFiatOnRamp:
return model.RailFiatOnRamp
default:
return model.RailUnspecified
}
}
func operationsFromDiscovery(values []string) []model.RailOperation {
return model.NormalizeRailOperationStrings(discovery.NormalizeRailOperations(values))
}
func currenciesFromDiscovery(values []string) []string {
if len(values) == 0 {
return nil
}
result := make([]string, 0, len(values))
seen := map[string]bool{}
for _, value := range values {
currency := strings.ToUpper(strings.TrimSpace(value))
if currency == "" || seen[currency] {
continue
}
seen[currency] = true
result = append(result, currency)
}
if len(result) == 0 {
return nil
}
return result
}
func limitsFromDiscovery(src *discovery.Limits) model.Limits {
limits := model.Limits{}
if src == nil {
return limits
}
limits.MinAmount = strings.TrimSpace(src.MinAmount)
limits.MaxAmount = strings.TrimSpace(src.MaxAmount)
if len(src.VolumeLimit) > 0 {
limits.VolumeLimit = map[string]string{}
for bucket, value := range src.VolumeLimit {
key := strings.TrimSpace(bucket)
amount := strings.TrimSpace(value)
if key == "" || amount == "" {
continue
}
limits.VolumeLimit[key] = amount
}
}
if len(src.VelocityLimit) > 0 {
limits.VelocityLimit = map[string]int{}
for bucket, value := range src.VelocityLimit {
key := strings.TrimSpace(bucket)
if key == "" || value <= 0 {
continue
}
limits.VelocityLimit[key] = value
}
}
return limits
}