300 lines
8.0 KiB
Go
300 lines
8.0 KiB
Go
package quotation
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"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/merrors"
|
|
mb "github.com/tech/sendico/pkg/messaging/broker"
|
|
"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)
|
|
}
|
|
|
|
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 ledgerDependency struct {
|
|
client ledgerclient.Client
|
|
internal rail.InternalLedger
|
|
}
|
|
|
|
type gatewayDependency struct {
|
|
resolver ChainGatewayResolver
|
|
}
|
|
|
|
type railGatewayDependency struct {
|
|
byID map[string]rail.RailGateway
|
|
byRail map[model.Rail][]rail.RailGateway
|
|
registry GatewayRegistry
|
|
chainResolver GatewayInvokeResolver
|
|
providerResolver GatewayInvokeResolver
|
|
logger mlogger.Logger
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type providerGatewayDependency struct {
|
|
resolver ChainGatewayResolver
|
|
}
|
|
|
|
type staticChainGatewayResolver struct {
|
|
client chainclient.Client
|
|
}
|
|
|
|
func (r staticChainGatewayResolver) Resolve(ctx context.Context, _ string) (chainclient.Client, error) {
|
|
if r.client == nil {
|
|
return nil, merrors.InvalidArgument("chain gateway client is required")
|
|
}
|
|
return r.client, nil
|
|
}
|
|
|
|
// CardGatewayRoute maps a gateway to its funding and fee destinations.
|
|
type CardGatewayRoute struct {
|
|
FundingAddress string
|
|
FeeAddress string
|
|
FeeWalletRef string
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
}
|
|
}
|
|
|
|
func WithPaymentGatewayBroker(broker mb.Broker) Option {
|
|
return func(s *Service) {
|
|
if broker != nil {
|
|
s.gatewayBroker = broker
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithLedgerClient wires the ledger client.
|
|
func WithLedgerClient(client ledgerclient.Client) Option {
|
|
return func(s *Service) {
|
|
s.deps.ledger = ledgerDependency{
|
|
client: client,
|
|
internal: client,
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithChainGatewayClient wires the chain gateway client.
|
|
func WithChainGatewayClient(client chainclient.Client) Option {
|
|
return func(s *Service) {
|
|
s.deps.gateway = gatewayDependency{resolver: staticChainGatewayResolver{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}
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithProviderSettlementGatewayClient wires the provider settlement gateway client.
|
|
func WithProviderSettlementGatewayClient(client chainclient.Client) Option {
|
|
return func(s *Service) {
|
|
s.deps.providerGateway = providerGatewayDependency{resolver: staticChainGatewayResolver{client: client}}
|
|
}
|
|
}
|
|
|
|
// WithProviderSettlementGatewayResolver wires a resolver for provider settlement gateway clients.
|
|
func WithProviderSettlementGatewayResolver(resolver ChainGatewayResolver) Option {
|
|
return func(s *Service) {
|
|
if resolver != nil {
|
|
s.deps.providerGateway = providerGatewayDependency{resolver: resolver}
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithGatewayInvokeResolver wires a resolver for gateway invoke URIs.
|
|
func WithGatewayInvokeResolver(resolver GatewayInvokeResolver) Option {
|
|
return func(s *Service) {
|
|
if resolver == nil {
|
|
return
|
|
}
|
|
s.deps.gatewayInvokeResolver = resolver
|
|
s.deps.railGateways.chainResolver = resolver
|
|
s.deps.railGateways.providerResolver = resolver
|
|
}
|
|
}
|
|
|
|
// WithRailGateways wires rail gateway adapters by instance ID.
|
|
func WithRailGateways(gateways map[string]rail.RailGateway) Option {
|
|
return func(s *Service) {
|
|
if len(gateways) == 0 {
|
|
return
|
|
}
|
|
s.deps.railGateways = buildRailGatewayDependency(gateways, s.deps.gatewayRegistry, s.deps.gatewayInvokeResolver, s.deps.gatewayInvokeResolver, s.logger)
|
|
}
|
|
}
|
|
|
|
// WithOracleClient wires the FX oracle client.
|
|
func WithOracleClient(client oracleclient.Client) Option {
|
|
return func(s *Service) {
|
|
s.deps.oracle = oracleDependency{client: client}
|
|
}
|
|
}
|
|
|
|
// 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 k, v := range routes {
|
|
s.deps.cardRoutes[strings.ToLower(strings.TrimSpace(k))] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithFeeLedgerAccounts maps gateway identifiers to ledger accounts used for fees.
|
|
func WithFeeLedgerAccounts(routes map[string]string) Option {
|
|
return func(s *Service) {
|
|
if len(routes) == 0 {
|
|
return
|
|
}
|
|
s.deps.feeLedgerAccounts = make(map[string]string, len(routes))
|
|
for k, v := range routes {
|
|
key := strings.ToLower(strings.TrimSpace(k))
|
|
val := strings.TrimSpace(v)
|
|
if key == "" || val == "" {
|
|
continue
|
|
}
|
|
s.deps.feeLedgerAccounts[key] = val
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithPlanBuilder wires a payment plan builder implementation.
|
|
func WithPlanBuilder(builder PlanBuilder) Option {
|
|
return func(s *Service) {
|
|
if builder != nil {
|
|
s.deps.planBuilder = builder
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithGatewayRegistry wires a registry of gateway instances for routing.
|
|
func WithGatewayRegistry(registry GatewayRegistry) Option {
|
|
return func(s *Service) {
|
|
if registry != nil {
|
|
s.deps.gatewayRegistry = registry
|
|
s.deps.railGateways.registry = registry
|
|
s.deps.railGateways.chainResolver = s.deps.gatewayInvokeResolver
|
|
s.deps.railGateways.providerResolver = s.deps.gatewayInvokeResolver
|
|
s.deps.railGateways.logger = s.logger.Named("rail_gateways")
|
|
if s.deps.planBuilder == nil {
|
|
s.deps.planBuilder = newDefaultPlanBuilder(s.logger)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithClock overrides the default clock.
|
|
func WithClock(clock clockpkg.Clock) Option {
|
|
return func(s *Service) {
|
|
if clock != nil {
|
|
s.clock = clock
|
|
}
|
|
}
|
|
}
|
|
|
|
func buildRailGatewayDependency(gateways map[string]rail.RailGateway, registry GatewayRegistry, chainResolver GatewayInvokeResolver, providerResolver GatewayInvokeResolver, logger mlogger.Logger) railGatewayDependency {
|
|
result := railGatewayDependency{
|
|
byID: map[string]rail.RailGateway{},
|
|
byRail: map[model.Rail][]rail.RailGateway{},
|
|
registry: registry,
|
|
chainResolver: chainResolver,
|
|
providerResolver: providerResolver,
|
|
logger: logger,
|
|
}
|
|
if len(gateways) == 0 {
|
|
return result
|
|
}
|
|
|
|
type item struct {
|
|
id string
|
|
gw rail.RailGateway
|
|
}
|
|
itemsByRail := map[model.Rail][]item{}
|
|
|
|
for id, gw := range gateways {
|
|
cleanID := strings.TrimSpace(id)
|
|
if cleanID == "" || gw == nil {
|
|
continue
|
|
}
|
|
result.byID[cleanID] = gw
|
|
railID := parseRailValue(gw.Rail())
|
|
if railID == model.RailUnspecified {
|
|
continue
|
|
}
|
|
itemsByRail[railID] = append(itemsByRail[railID], item{id: cleanID, gw: gw})
|
|
}
|
|
|
|
for railID, items := range itemsByRail {
|
|
sort.Slice(items, func(i, j int) bool {
|
|
return items[i].id < items[j].id
|
|
})
|
|
for _, entry := range items {
|
|
result.byRail[railID] = append(result.byRail[railID], entry.gw)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|