Fixes + stable gateway ids
This commit is contained in:
@@ -3,6 +3,7 @@ package quote_computation_service
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -71,8 +72,8 @@ func TestBuildPlan_BuildsStepsAndFundingGate(t *testing.T) {
|
||||
if item.Route == nil {
|
||||
t.Fatalf("expected route specification")
|
||||
}
|
||||
if got, want := item.Route.GetRail(), "CARD_PAYOUT"; got != want {
|
||||
t.Fatalf("unexpected route rail: got=%q want=%q", got, want)
|
||||
if got := strings.TrimSpace(item.Route.GetRail()); got != "" {
|
||||
t.Fatalf("expected route rail header to be empty, got %q", got)
|
||||
}
|
||||
if got := item.Route.GetRouteRef(); got == "" {
|
||||
t.Fatalf("expected route_ref")
|
||||
@@ -83,6 +84,12 @@ func TestBuildPlan_BuildsStepsAndFundingGate(t *testing.T) {
|
||||
if got, want := len(item.Route.GetHops()), 2; got != want {
|
||||
t.Fatalf("unexpected route hops count: got=%d want=%d", got, want)
|
||||
}
|
||||
if item.Route.GetSettlement() == nil || item.Route.GetSettlement().GetAsset() == nil {
|
||||
t.Fatalf("expected route settlement asset")
|
||||
}
|
||||
if got, want := item.Route.GetSettlement().GetAsset().GetKey().GetTokenSymbol(), "USD"; got != want {
|
||||
t.Fatalf("unexpected settlement asset token: got=%q want=%q", got, want)
|
||||
}
|
||||
if item.ExecutionConditions == nil {
|
||||
t.Fatalf("expected execution conditions")
|
||||
}
|
||||
@@ -246,8 +253,8 @@ func TestBuildPlan_SelectsGatewaysAndIgnoresIrrelevant(t *testing.T) {
|
||||
if item.Route == nil {
|
||||
t.Fatalf("expected route")
|
||||
}
|
||||
if got, want := item.Route.GetProvider(), "payout-gw-1"; got != want {
|
||||
t.Fatalf("unexpected selected provider: got=%q want=%q", got, want)
|
||||
if got := strings.TrimSpace(item.Route.GetProvider()); got != "" {
|
||||
t.Fatalf("expected route provider header to be empty, got %q", got)
|
||||
}
|
||||
if got, want := len(item.Route.GetHops()), 3; got != want {
|
||||
t.Fatalf("unexpected route hop count: got=%d want=%d", got, want)
|
||||
@@ -258,6 +265,15 @@ func TestBuildPlan_SelectsGatewaysAndIgnoresIrrelevant(t *testing.T) {
|
||||
if got, want := item.Route.GetHops()[1].GetRole(), quotationv2.RouteHopRole_ROUTE_HOP_ROLE_TRANSIT; got != want {
|
||||
t.Fatalf("unexpected bridge role: got=%s want=%s", got.String(), want.String())
|
||||
}
|
||||
if item.Route.GetSettlement() == nil || item.Route.GetSettlement().GetAsset() == nil {
|
||||
t.Fatalf("expected route settlement asset")
|
||||
}
|
||||
if got, want := item.Route.GetSettlement().GetAsset().GetKey().GetChain(), "TRON"; got != want {
|
||||
t.Fatalf("unexpected settlement asset chain: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := item.Route.GetSettlement().GetAsset().GetKey().GetTokenSymbol(), "USDT"; got != want {
|
||||
t.Fatalf("unexpected settlement asset token: got=%q want=%q", got, want)
|
||||
}
|
||||
if got := item.Route.GetRouteRef(); got == "" {
|
||||
t.Fatalf("expected route_ref")
|
||||
}
|
||||
@@ -307,8 +323,8 @@ func TestCompute_EnrichesRouteConditionsAndTotalCost(t *testing.T) {
|
||||
if result.Quote.Route == nil {
|
||||
t.Fatalf("expected route specification")
|
||||
}
|
||||
if got, want := result.Quote.Route.GetPayoutMethod(), "CARD"; got != want {
|
||||
t.Fatalf("unexpected payout method: got=%q want=%q", got, want)
|
||||
if got := strings.TrimSpace(result.Quote.Route.GetPayoutMethod()); got != "" {
|
||||
t.Fatalf("expected payout method header to be empty, got %q", got)
|
||||
}
|
||||
if got := result.Quote.Route.GetRouteRef(); got == "" {
|
||||
t.Fatalf("expected route_ref")
|
||||
@@ -319,6 +335,12 @@ func TestCompute_EnrichesRouteConditionsAndTotalCost(t *testing.T) {
|
||||
if got, want := len(result.Quote.Route.GetHops()), 2; got != want {
|
||||
t.Fatalf("unexpected route hops count: got=%d want=%d", got, want)
|
||||
}
|
||||
if result.Quote.Route.GetSettlement() == nil || result.Quote.Route.GetSettlement().GetAsset() == nil {
|
||||
t.Fatalf("expected route settlement asset")
|
||||
}
|
||||
if got, want := result.Quote.Route.GetSettlement().GetAsset().GetKey().GetTokenSymbol(), "USD"; got != want {
|
||||
t.Fatalf("unexpected route settlement token: got=%q want=%q", got, want)
|
||||
}
|
||||
if result.Quote.ExecutionConditions == nil {
|
||||
t.Fatalf("expected execution conditions")
|
||||
}
|
||||
@@ -334,8 +356,12 @@ func TestCompute_EnrichesRouteConditionsAndTotalCost(t *testing.T) {
|
||||
if core.lastQuoteIn.Route == nil {
|
||||
t.Fatalf("expected selected route to be passed into build quote input")
|
||||
}
|
||||
if got, want := core.lastQuoteIn.Route.GetProvider(), "monetix"; got != want {
|
||||
t.Fatalf("unexpected selected route provider in build input: got=%q want=%q", got, want)
|
||||
hops := core.lastQuoteIn.Route.GetHops()
|
||||
if got, want := len(hops), 2; got != want {
|
||||
t.Fatalf("unexpected route hops in build input: got=%d want=%d", got, want)
|
||||
}
|
||||
if got, want := hops[1].GetGateway(), "monetix"; got != want {
|
||||
t.Fatalf("unexpected destination gateway in build input route: got=%q want=%q", got, want)
|
||||
}
|
||||
if core.lastQuoteIn.ExecutionConditions == nil {
|
||||
t.Fatalf("expected execution conditions to be passed into build quote input")
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
|
||||
accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
|
||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||
)
|
||||
|
||||
@@ -76,11 +77,10 @@ func cloneRouteSpecification(src *quotationv2.RouteSpecification) *quotationv2.R
|
||||
Rail: strings.TrimSpace(src.GetRail()),
|
||||
Provider: strings.TrimSpace(src.GetProvider()),
|
||||
PayoutMethod: strings.TrimSpace(src.GetPayoutMethod()),
|
||||
SettlementAsset: strings.ToUpper(strings.TrimSpace(src.GetSettlementAsset())),
|
||||
SettlementModel: strings.TrimSpace(src.GetSettlementModel()),
|
||||
Network: strings.TrimSpace(src.GetNetwork()),
|
||||
RouteRef: strings.TrimSpace(src.GetRouteRef()),
|
||||
PricingProfileRef: strings.TrimSpace(src.GetPricingProfileRef()),
|
||||
Settlement: cloneRouteSettlement(src.GetSettlement()),
|
||||
}
|
||||
if hops := src.GetHops(); len(hops) > 0 {
|
||||
result.Hops = make([]*quotationv2.RouteHop, 0, len(hops))
|
||||
@@ -96,6 +96,31 @@ func cloneRouteSpecification(src *quotationv2.RouteSpecification) *quotationv2.R
|
||||
return result
|
||||
}
|
||||
|
||||
func cloneRouteSettlement(src *quotationv2.RouteSettlement) *quotationv2.RouteSettlement {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
result := "ationv2.RouteSettlement{
|
||||
Model: strings.TrimSpace(src.GetModel()),
|
||||
}
|
||||
if asset := src.GetAsset(); asset != nil {
|
||||
key := asset.GetKey()
|
||||
result.Asset = &paymentv1.ChainAsset{
|
||||
Key: &paymentv1.ChainAssetKey{
|
||||
Chain: strings.ToUpper(strings.TrimSpace(key.GetChain())),
|
||||
TokenSymbol: strings.ToUpper(strings.TrimSpace(key.GetTokenSymbol())),
|
||||
},
|
||||
}
|
||||
if contract := strings.TrimSpace(asset.GetContractAddress()); contract != "" {
|
||||
result.Asset.ContractAddress = &contract
|
||||
}
|
||||
}
|
||||
if result.Asset == nil && result.Model == "" {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func cloneExecutionConditions(src *quotationv2.ExecutionConditions) *quotationv2.ExecutionConditions {
|
||||
if src == nil {
|
||||
return nil
|
||||
|
||||
@@ -37,18 +37,16 @@ func (s *QuoteComputationService) resolveStepGateways(
|
||||
}
|
||||
}
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return strings.TrimSpace(sorted[i].ID) < strings.TrimSpace(sorted[j].ID)
|
||||
return model.LessGatewayDescriptor(sorted[i], sorted[j])
|
||||
})
|
||||
|
||||
for idx, step := range steps {
|
||||
if step == nil {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(step.GatewayID) != "" {
|
||||
continue
|
||||
}
|
||||
if step.Rail == model.RailLedger {
|
||||
step.GatewayID = "internal"
|
||||
step.GatewayInvokeURI = ""
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -57,9 +55,8 @@ func (s *QuoteComputationService) resolveStepGateways(
|
||||
return fmt.Errorf("Step[%d] %s: %w", idx, strings.TrimSpace(step.StepID), selectErr)
|
||||
}
|
||||
step.GatewayID = strings.TrimSpace(selected.ID)
|
||||
if strings.TrimSpace(step.InstanceID) == "" {
|
||||
step.InstanceID = strings.TrimSpace(selected.InstanceID)
|
||||
}
|
||||
step.InstanceID = strings.TrimSpace(selected.InstanceID)
|
||||
step.GatewayInvokeURI = strings.TrimSpace(selected.InvokeURI)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -89,20 +86,29 @@ func selectGatewayForStep(
|
||||
direction := plan.SendDirectionForRail(step.Rail)
|
||||
network := networkForGatewaySelection(step.Rail, routeNetwork)
|
||||
|
||||
eligible := make([]*model.GatewayInstanceDescriptor, 0, len(gateways))
|
||||
var lastErr error
|
||||
for _, gw := range gateways {
|
||||
if gw == nil {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(step.InstanceID) != "" &&
|
||||
!strings.EqualFold(strings.TrimSpace(gw.InstanceID), strings.TrimSpace(step.InstanceID)) {
|
||||
continue
|
||||
}
|
||||
if err := plan.IsGatewayEligible(gw, step.Rail, network, currency, action, direction, amount); err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return gw, nil
|
||||
eligible = append(eligible, gw)
|
||||
}
|
||||
|
||||
if selected, _ := model.SelectGatewayByPreference(
|
||||
eligible,
|
||||
step.GatewayID,
|
||||
step.InstanceID,
|
||||
step.GatewayInvokeURI,
|
||||
); selected != nil {
|
||||
return selected, nil
|
||||
}
|
||||
if len(eligible) > 0 {
|
||||
return eligible[0], nil
|
||||
}
|
||||
|
||||
if lastErr != nil {
|
||||
@@ -160,6 +166,7 @@ func clearImplicitDestinationGateway(steps []*QuoteComputationStep) {
|
||||
return
|
||||
}
|
||||
last.GatewayID = ""
|
||||
last.GatewayInvokeURI = ""
|
||||
}
|
||||
|
||||
func destinationGatewayFromSteps(steps []*QuoteComputationStep) string {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package quote_computation_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
)
|
||||
|
||||
type selectorGatewayRegistry struct {
|
||||
items []*model.GatewayInstanceDescriptor
|
||||
}
|
||||
|
||||
func (s selectorGatewayRegistry) List(context.Context) ([]*model.GatewayInstanceDescriptor, error) {
|
||||
return s.items, nil
|
||||
}
|
||||
|
||||
func TestResolveStepGateways_FallsBackToInvokeURI(t *testing.T) {
|
||||
svc := New(nil, WithGatewayRegistry(selectorGatewayRegistry{
|
||||
items: []*model.GatewayInstanceDescriptor{
|
||||
{
|
||||
ID: "aaa",
|
||||
InstanceID: "inst-a",
|
||||
InvokeURI: "grpc://gw-a:50051",
|
||||
Rail: model.RailCrypto,
|
||||
Network: "TRON",
|
||||
Currencies: []string{"USDT"},
|
||||
Capabilities: model.RailCapabilities{
|
||||
CanPayOut: true,
|
||||
},
|
||||
IsEnabled: true,
|
||||
},
|
||||
{
|
||||
ID: "bbb",
|
||||
InstanceID: "inst-b",
|
||||
InvokeURI: "grpc://gw-b:50051",
|
||||
Rail: model.RailCrypto,
|
||||
Network: "TRON",
|
||||
Currencies: []string{"USDT"},
|
||||
Capabilities: model.RailCapabilities{
|
||||
CanPayOut: true,
|
||||
},
|
||||
IsEnabled: true,
|
||||
},
|
||||
},
|
||||
}))
|
||||
steps := []*QuoteComputationStep{
|
||||
{
|
||||
StepID: "i0.destination",
|
||||
Rail: model.RailCrypto,
|
||||
Operation: model.RailOperationExternalCredit,
|
||||
GatewayID: "legacy-id",
|
||||
InstanceID: "legacy-instance",
|
||||
GatewayInvokeURI: "grpc://gw-b:50051",
|
||||
Amount: &moneyv1.Money{Currency: "USDT", Amount: "10"},
|
||||
},
|
||||
}
|
||||
|
||||
if err := svc.resolveStepGateways(context.Background(), steps, "TRON"); err != nil {
|
||||
t.Fatalf("resolveStepGateways returned error: %v", err)
|
||||
}
|
||||
if got, want := steps[0].GatewayID, "bbb"; got != want {
|
||||
t.Fatalf("unexpected gateway_id: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := steps[0].InstanceID, "inst-b"; got != want {
|
||||
t.Fatalf("unexpected instance_id: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := steps[0].GatewayInvokeURI, "grpc://gw-b:50051"; got != want {
|
||||
t.Fatalf("unexpected gateway_invoke_uri: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveStepGateways_FallsBackToGatewayIDWhenInstanceChanges(t *testing.T) {
|
||||
svc := New(nil, WithGatewayRegistry(selectorGatewayRegistry{
|
||||
items: []*model.GatewayInstanceDescriptor{
|
||||
{
|
||||
ID: "aaa",
|
||||
InstanceID: "inst-a",
|
||||
InvokeURI: "grpc://gw-a:50051",
|
||||
Rail: model.RailCrypto,
|
||||
Network: "TRON",
|
||||
Currencies: []string{"USDT"},
|
||||
Capabilities: model.RailCapabilities{
|
||||
CanPayOut: true,
|
||||
},
|
||||
IsEnabled: true,
|
||||
},
|
||||
{
|
||||
ID: "crypto_rail_gateway_tron",
|
||||
InstanceID: "inst-new",
|
||||
InvokeURI: "grpc://gw-tron:50051",
|
||||
Rail: model.RailCrypto,
|
||||
Network: "TRON",
|
||||
Currencies: []string{"USDT"},
|
||||
Capabilities: model.RailCapabilities{
|
||||
CanPayOut: true,
|
||||
},
|
||||
IsEnabled: true,
|
||||
},
|
||||
},
|
||||
}))
|
||||
steps := []*QuoteComputationStep{
|
||||
{
|
||||
StepID: "i0.destination",
|
||||
Rail: model.RailCrypto,
|
||||
Operation: model.RailOperationExternalCredit,
|
||||
GatewayID: "crypto_rail_gateway_tron",
|
||||
InstanceID: "inst-old",
|
||||
Amount: &moneyv1.Money{Currency: "USDT", Amount: "10"},
|
||||
},
|
||||
}
|
||||
|
||||
if err := svc.resolveStepGateways(context.Background(), steps, "TRON"); err != nil {
|
||||
t.Fatalf("resolveStepGateways returned error: %v", err)
|
||||
}
|
||||
if got, want := steps[0].GatewayID, "crypto_rail_gateway_tron"; got != want {
|
||||
t.Fatalf("unexpected gateway_id: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := steps[0].InstanceID, "inst-new"; got != want {
|
||||
t.Fatalf("unexpected instance_id: got=%q want=%q", got, want)
|
||||
}
|
||||
if got, want := steps[0].GatewayInvokeURI, "grpc://gw-tron:50051"; got != want {
|
||||
t.Fatalf("unexpected gateway_invoke_uri: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,7 @@ type QuoteComputationStep struct {
|
||||
Operation model.RailOperation
|
||||
GatewayID string
|
||||
InstanceID string
|
||||
GatewayInvokeURI string
|
||||
DependsOn []string
|
||||
Amount *moneyv1.Money
|
||||
FromRole *account_role.AccountRole
|
||||
|
||||
@@ -78,7 +78,7 @@ func (s *QuoteComputationService) buildPlanItem(
|
||||
|
||||
source := clonePaymentEndpoint(modelIntent.Source)
|
||||
destination := clonePaymentEndpoint(modelIntent.Destination)
|
||||
_, sourceNetwork, err := plan.RailFromEndpoint(source, modelIntent.Attributes, true)
|
||||
sourceRail, sourceNetwork, err := plan.RailFromEndpoint(source, modelIntent.Attributes, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -91,7 +91,12 @@ func (s *QuoteComputationService) buildPlanItem(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
steps := buildComputationSteps(index, modelIntent, destination)
|
||||
routeRails, err := s.resolveRouteRails(ctx, sourceRail, destRail, firstNonEmpty(routeNetwork, destNetwork, sourceNetwork))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
steps := buildComputationSteps(index, modelIntent, destination, routeRails)
|
||||
if modelIntent.Destination.Type == model.EndpointTypeCard &&
|
||||
s.gatewayRegistry != nil &&
|
||||
!hasExplicitDestinationGateway(modelIntent.Attributes) {
|
||||
@@ -132,14 +137,11 @@ func (s *QuoteComputationService) buildPlanItem(
|
||||
}
|
||||
route := buildRouteSpecification(
|
||||
modelIntent,
|
||||
destination,
|
||||
destRail,
|
||||
firstNonEmpty(routeNetwork, destNetwork, sourceNetwork),
|
||||
provider,
|
||||
steps,
|
||||
)
|
||||
conditions, blockReason := buildExecutionConditions(in.PreviewOnly, steps, funding)
|
||||
if route == nil || strings.TrimSpace(route.GetRail()) == "" || route.GetRail() == string(model.RailUnspecified) {
|
||||
if route == nil || len(route.GetHops()) == 0 {
|
||||
blockReason = quotationv2.QuoteBlockReason_QUOTE_BLOCK_REASON_ROUTE_UNAVAILABLE
|
||||
}
|
||||
quoteInput := BuildQuoteInput{
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package quote_computation_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/payments/quotation/internal/service/quotation/graph_path_finder"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
)
|
||||
|
||||
func (s *QuoteComputationService) resolveRouteRails(
|
||||
ctx context.Context,
|
||||
sourceRail model.Rail,
|
||||
destinationRail model.Rail,
|
||||
network string,
|
||||
) ([]model.Rail, error) {
|
||||
if sourceRail == model.RailUnspecified {
|
||||
return nil, merrors.InvalidArgument("source rail is required")
|
||||
}
|
||||
if destinationRail == model.RailUnspecified {
|
||||
return nil, merrors.InvalidArgument("destination rail is required")
|
||||
}
|
||||
if sourceRail == destinationRail {
|
||||
return []model.Rail{sourceRail}, nil
|
||||
}
|
||||
|
||||
strictGraph := s != nil && s.routeStore != nil
|
||||
edges, err := s.routeGraphEdges(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(edges) == 0 {
|
||||
if strictGraph {
|
||||
return nil, merrors.InvalidArgument("route graph has no edges")
|
||||
}
|
||||
return fallbackRouteRails(sourceRail, destinationRail), nil
|
||||
}
|
||||
|
||||
pathFinder := s.pathFinder
|
||||
if pathFinder == nil {
|
||||
pathFinder = graph_path_finder.New()
|
||||
}
|
||||
|
||||
path, findErr := pathFinder.Find(graph_path_finder.FindInput{
|
||||
SourceRail: sourceRail,
|
||||
DestinationRail: destinationRail,
|
||||
Network: strings.ToUpper(strings.TrimSpace(network)),
|
||||
Edges: edges,
|
||||
})
|
||||
if findErr != nil {
|
||||
if strictGraph {
|
||||
return nil, findErr
|
||||
}
|
||||
return fallbackRouteRails(sourceRail, destinationRail), nil
|
||||
}
|
||||
|
||||
if path == nil || len(path.Rails) == 0 {
|
||||
if strictGraph {
|
||||
return nil, merrors.InvalidArgument("route path is empty")
|
||||
}
|
||||
return fallbackRouteRails(sourceRail, destinationRail), nil
|
||||
}
|
||||
return append([]model.Rail(nil), path.Rails...), nil
|
||||
}
|
||||
|
||||
func (s *QuoteComputationService) routeGraphEdges(ctx context.Context) ([]graph_path_finder.Edge, error) {
|
||||
if s == nil || s.routeStore == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
enabled := true
|
||||
routes, err := s.routeStore.List(ctx, &model.PaymentRouteFilter{IsEnabled: &enabled})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if routes == nil || len(routes.Items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
edges := make([]graph_path_finder.Edge, 0, len(routes.Items))
|
||||
for _, route := range routes.Items {
|
||||
if route == nil || !route.IsEnabled {
|
||||
continue
|
||||
}
|
||||
from := model.Rail(strings.ToUpper(strings.TrimSpace(string(route.FromRail))))
|
||||
to := model.Rail(strings.ToUpper(strings.TrimSpace(string(route.ToRail))))
|
||||
if from == model.RailUnspecified || to == model.RailUnspecified {
|
||||
continue
|
||||
}
|
||||
edges = append(edges, graph_path_finder.Edge{
|
||||
FromRail: from,
|
||||
ToRail: to,
|
||||
Network: strings.ToUpper(strings.TrimSpace(route.Network)),
|
||||
})
|
||||
}
|
||||
return edges, nil
|
||||
}
|
||||
|
||||
func fallbackRouteRails(sourceRail, destinationRail model.Rail) []model.Rail {
|
||||
if sourceRail == destinationRail {
|
||||
return []model.Rail{sourceRail}
|
||||
}
|
||||
if requiresTransitBridgeStep(sourceRail, destinationRail) {
|
||||
return []model.Rail{sourceRail, model.RailLedger, destinationRail}
|
||||
}
|
||||
return []model.Rail{sourceRail, destinationRail}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package quote_computation_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/payments/quotation/internal/service/quotation/transfer_intent_hydrator"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestBuildPlan_UsesRouteGraphPath(t *testing.T) {
|
||||
svc := New(nil,
|
||||
WithRouteStore(staticRouteStore{items: []*model.PaymentRoute{
|
||||
{FromRail: model.RailCrypto, ToRail: model.RailProviderSettlement, Network: "TRON", IsEnabled: true},
|
||||
{FromRail: model.RailProviderSettlement, ToRail: model.RailCardPayout, Network: "TRON", IsEnabled: true},
|
||||
}}),
|
||||
WithGatewayRegistry(staticGatewayRegistry{
|
||||
items: []*model.GatewayInstanceDescriptor{
|
||||
{
|
||||
ID: "crypto-gw",
|
||||
InstanceID: "crypto-gw",
|
||||
Rail: model.RailCrypto,
|
||||
Network: "TRON",
|
||||
Currencies: []string{"USDT"},
|
||||
Capabilities: model.RailCapabilities{
|
||||
CanPayOut: true,
|
||||
},
|
||||
IsEnabled: true,
|
||||
},
|
||||
{
|
||||
ID: "provider-gw",
|
||||
InstanceID: "provider-gw",
|
||||
Rail: model.RailProviderSettlement,
|
||||
Network: "TRON",
|
||||
Currencies: []string{"USDT"},
|
||||
IsEnabled: true,
|
||||
},
|
||||
{
|
||||
ID: "card-gw",
|
||||
InstanceID: "card-gw",
|
||||
Rail: model.RailCardPayout,
|
||||
Currencies: []string{"USDT"},
|
||||
Capabilities: model.RailCapabilities{
|
||||
CanPayOut: true,
|
||||
},
|
||||
IsEnabled: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
orgID := bson.NewObjectID()
|
||||
planModel, err := svc.BuildPlan(context.Background(), ComputeInput{
|
||||
OrganizationRef: orgID.Hex(),
|
||||
OrganizationID: orgID,
|
||||
BaseIdempotencyKey: "idem-graph",
|
||||
Intents: []*transfer_intent_hydrator.QuoteIntent{sampleCryptoToCardQuoteIntent()},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if planModel == nil || len(planModel.Items) != 1 || planModel.Items[0] == nil {
|
||||
t.Fatalf("expected one plan item")
|
||||
}
|
||||
|
||||
item := planModel.Items[0]
|
||||
if got, want := len(item.Steps), 3; got != want {
|
||||
t.Fatalf("unexpected step count: got=%d want=%d", got, want)
|
||||
}
|
||||
if got, want := string(item.Steps[1].Rail), string(model.RailProviderSettlement); got != want {
|
||||
t.Fatalf("unexpected transit rail: got=%q want=%q", got, want)
|
||||
}
|
||||
if got := strings.ToUpper(strings.TrimSpace(item.Route.GetHops()[1].GetRail())); got != "PROVIDER_SETTLEMENT" {
|
||||
t.Fatalf("unexpected route transit hop rail: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPlan_RouteGraphNoPathReturnsError(t *testing.T) {
|
||||
svc := New(nil, WithRouteStore(staticRouteStore{items: []*model.PaymentRoute{
|
||||
{FromRail: model.RailCrypto, ToRail: model.RailLedger, Network: "TRON", IsEnabled: true},
|
||||
}}))
|
||||
|
||||
orgID := bson.NewObjectID()
|
||||
_, err := svc.BuildPlan(context.Background(), ComputeInput{
|
||||
OrganizationRef: orgID.Hex(),
|
||||
OrganizationID: orgID,
|
||||
BaseIdempotencyKey: "idem-graph-no-path",
|
||||
Intents: []*transfer_intent_hydrator.QuoteIntent{sampleCryptoToCardQuoteIntent()},
|
||||
})
|
||||
if !errors.Is(err, merrors.ErrInvalidArg) {
|
||||
t.Fatalf("expected invalid argument when no path exists in route graph, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPlan_RouteGraphPrefersDirectPath(t *testing.T) {
|
||||
svc := New(nil,
|
||||
WithRouteStore(staticRouteStore{items: []*model.PaymentRoute{
|
||||
{FromRail: model.RailCrypto, ToRail: model.RailCardPayout, Network: "TRON", IsEnabled: true},
|
||||
{FromRail: model.RailCrypto, ToRail: model.RailLedger, Network: "TRON", IsEnabled: true},
|
||||
{FromRail: model.RailLedger, ToRail: model.RailCardPayout, Network: "TRON", IsEnabled: true},
|
||||
}}),
|
||||
WithGatewayRegistry(staticGatewayRegistry{
|
||||
items: []*model.GatewayInstanceDescriptor{
|
||||
{
|
||||
ID: "crypto-gw",
|
||||
InstanceID: "crypto-gw",
|
||||
Rail: model.RailCrypto,
|
||||
Network: "TRON",
|
||||
Currencies: []string{"USDT"},
|
||||
Capabilities: model.RailCapabilities{
|
||||
CanPayOut: true,
|
||||
},
|
||||
IsEnabled: true,
|
||||
},
|
||||
{
|
||||
ID: "card-gw",
|
||||
InstanceID: "card-gw",
|
||||
Rail: model.RailCardPayout,
|
||||
Currencies: []string{"USDT"},
|
||||
Capabilities: model.RailCapabilities{
|
||||
CanPayOut: true,
|
||||
},
|
||||
IsEnabled: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
orgID := bson.NewObjectID()
|
||||
planModel, err := svc.BuildPlan(context.Background(), ComputeInput{
|
||||
OrganizationRef: orgID.Hex(),
|
||||
OrganizationID: orgID,
|
||||
BaseIdempotencyKey: "idem-graph-direct",
|
||||
Intents: []*transfer_intent_hydrator.QuoteIntent{sampleCryptoToCardQuoteIntent()},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if planModel == nil || len(planModel.Items) != 1 || planModel.Items[0] == nil {
|
||||
t.Fatalf("expected one plan item")
|
||||
}
|
||||
if got, want := len(planModel.Items[0].Steps), 2; got != want {
|
||||
t.Fatalf("expected direct two-step path, got %d steps", got)
|
||||
}
|
||||
}
|
||||
|
||||
type staticRouteStore struct {
|
||||
items []*model.PaymentRoute
|
||||
}
|
||||
|
||||
func (s staticRouteStore) List(context.Context, *model.PaymentRouteFilter) (*model.PaymentRouteList, error) {
|
||||
out := make([]*model.PaymentRoute, 0, len(s.items))
|
||||
for _, item := range s.items {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
cloned := *item
|
||||
out = append(out, &cloned)
|
||||
}
|
||||
return &model.PaymentRouteList{Items: out}, nil
|
||||
}
|
||||
@@ -11,6 +11,7 @@ func buildComputationSteps(
|
||||
index int,
|
||||
intent model.PaymentIntent,
|
||||
destination model.PaymentEndpoint,
|
||||
routeRails []model.Rail,
|
||||
) []*QuoteComputationStep {
|
||||
if intent.Amount == nil {
|
||||
return nil
|
||||
@@ -20,6 +21,7 @@ func buildComputationSteps(
|
||||
amount := protoMoneyFromModel(intent.Amount)
|
||||
sourceRail := sourceRailForIntent(intent)
|
||||
destinationRail := destinationRailForIntent(intent)
|
||||
rails := normalizeRouteRails(sourceRail, destinationRail, routeRails)
|
||||
sourceGatewayID := strings.TrimSpace(lookupAttr(attrs,
|
||||
"source_gateway",
|
||||
"sourceGateway",
|
||||
@@ -37,8 +39,8 @@ func buildComputationSteps(
|
||||
steps := []*QuoteComputationStep{
|
||||
{
|
||||
StepID: sourceStepID,
|
||||
Rail: sourceRail,
|
||||
Operation: sourceOperationForRail(sourceRail),
|
||||
Rail: rails[0],
|
||||
Operation: sourceOperationForRail(rails[0]),
|
||||
GatewayID: sourceGatewayID,
|
||||
InstanceID: sourceInstanceID,
|
||||
Amount: cloneProtoMoney(amount),
|
||||
@@ -48,39 +50,54 @@ func buildComputationSteps(
|
||||
}
|
||||
|
||||
lastStepID := sourceStepID
|
||||
fxAssigned := false
|
||||
if intent.RequiresFX {
|
||||
fxStepID := fmt.Sprintf("i%d.fx", index)
|
||||
steps = append(steps, &QuoteComputationStep{
|
||||
StepID: fxStepID,
|
||||
Rail: model.RailProviderSettlement,
|
||||
Operation: model.RailOperationFXConvert,
|
||||
DependsOn: []string{sourceStepID},
|
||||
Amount: cloneProtoMoney(amount),
|
||||
Optional: false,
|
||||
IncludeInAggregate: false,
|
||||
})
|
||||
lastStepID = fxStepID
|
||||
if len(rails) > 1 && rails[1] == model.RailProviderSettlement {
|
||||
fxAssigned = true
|
||||
} else {
|
||||
fxStepID := fmt.Sprintf("i%d.fx", index)
|
||||
steps = append(steps, &QuoteComputationStep{
|
||||
StepID: fxStepID,
|
||||
Rail: model.RailProviderSettlement,
|
||||
Operation: model.RailOperationFXConvert,
|
||||
DependsOn: []string{sourceStepID},
|
||||
Amount: cloneProtoMoney(amount),
|
||||
Optional: false,
|
||||
IncludeInAggregate: false,
|
||||
})
|
||||
lastStepID = fxStepID
|
||||
fxAssigned = true
|
||||
}
|
||||
}
|
||||
|
||||
if requiresTransitBridgeStep(sourceRail, destinationRail) {
|
||||
bridgeStepID := fmt.Sprintf("i%d.bridge", index)
|
||||
transitIndex := 1
|
||||
for i := 1; i < len(rails)-1; i++ {
|
||||
rail := rails[i]
|
||||
stepID := fmt.Sprintf("i%d.transit%d", index, transitIndex)
|
||||
operation := model.RailOperationMove
|
||||
if intent.RequiresFX && !fxAssigned && rail == model.RailProviderSettlement {
|
||||
stepID = fmt.Sprintf("i%d.fx", index)
|
||||
operation = model.RailOperationFXConvert
|
||||
fxAssigned = true
|
||||
}
|
||||
steps = append(steps, &QuoteComputationStep{
|
||||
StepID: bridgeStepID,
|
||||
Rail: model.RailLedger,
|
||||
Operation: model.RailOperationMove,
|
||||
StepID: stepID,
|
||||
Rail: rail,
|
||||
Operation: operation,
|
||||
DependsOn: []string{lastStepID},
|
||||
Amount: cloneProtoMoney(amount),
|
||||
Optional: false,
|
||||
IncludeInAggregate: false,
|
||||
})
|
||||
lastStepID = bridgeStepID
|
||||
lastStepID = stepID
|
||||
transitIndex++
|
||||
}
|
||||
|
||||
destinationStepID := fmt.Sprintf("i%d.destination", index)
|
||||
steps = append(steps, &QuoteComputationStep{
|
||||
StepID: destinationStepID,
|
||||
Rail: destinationRail,
|
||||
Operation: destinationOperationForRail(destinationRail),
|
||||
Rail: rails[len(rails)-1],
|
||||
Operation: destinationOperationForRail(rails[len(rails)-1]),
|
||||
GatewayID: destinationGatewayID,
|
||||
InstanceID: destinationInstanceID,
|
||||
DependsOn: []string{lastStepID},
|
||||
@@ -92,6 +109,40 @@ func buildComputationSteps(
|
||||
return steps
|
||||
}
|
||||
|
||||
func normalizeRouteRails(sourceRail, destinationRail model.Rail, routeRails []model.Rail) []model.Rail {
|
||||
if len(routeRails) == 0 {
|
||||
if requiresTransitBridgeStep(sourceRail, destinationRail) {
|
||||
return []model.Rail{sourceRail, model.RailLedger, destinationRail}
|
||||
}
|
||||
return []model.Rail{sourceRail, destinationRail}
|
||||
}
|
||||
|
||||
result := make([]model.Rail, 0, len(routeRails))
|
||||
for _, rail := range routeRails {
|
||||
if rail == model.RailUnspecified {
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 && result[len(result)-1] == rail {
|
||||
continue
|
||||
}
|
||||
result = append(result, rail)
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return []model.Rail{sourceRail, destinationRail}
|
||||
}
|
||||
if result[0] != sourceRail {
|
||||
result = append([]model.Rail{sourceRail}, result...)
|
||||
}
|
||||
if result[len(result)-1] != destinationRail {
|
||||
result = append(result, destinationRail)
|
||||
}
|
||||
if len(result) == 1 {
|
||||
result = append(result, destinationRail)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func requiresTransitBridgeStep(sourceRail, destinationRail model.Rail) bool {
|
||||
if sourceRail == model.RailUnspecified || destinationRail == model.RailUnspecified {
|
||||
return false
|
||||
|
||||
@@ -27,17 +27,37 @@ func sameRouteSpecification(left, right *quotationv2.RouteSpecification) bool {
|
||||
if left == nil || right == nil {
|
||||
return left == right
|
||||
}
|
||||
return normalizeRail(left.GetRail()) == normalizeRail(right.GetRail()) &&
|
||||
normalizeProvider(left.GetProvider()) == normalizeProvider(right.GetProvider()) &&
|
||||
normalizePayoutMethod(left.GetPayoutMethod()) == normalizePayoutMethod(right.GetPayoutMethod()) &&
|
||||
normalizeAsset(left.GetSettlementAsset()) == normalizeAsset(right.GetSettlementAsset()) &&
|
||||
normalizeSettlementModel(left.GetSettlementModel()) == normalizeSettlementModel(right.GetSettlementModel()) &&
|
||||
normalizeNetwork(left.GetNetwork()) == normalizeNetwork(right.GetNetwork()) &&
|
||||
return sameRouteSettlement(left.GetSettlement(), right.GetSettlement()) &&
|
||||
sameRouteReference(left.GetRouteRef(), right.GetRouteRef()) &&
|
||||
samePricingProfileReference(left.GetPricingProfileRef(), right.GetPricingProfileRef()) &&
|
||||
sameRouteHops(left.GetHops(), right.GetHops())
|
||||
}
|
||||
|
||||
func sameRouteSettlement(
|
||||
left *quotationv2.RouteSettlement,
|
||||
right *quotationv2.RouteSettlement,
|
||||
) bool {
|
||||
leftChain, leftToken, leftContract, leftModel := normalizeSettlementParts(left)
|
||||
rightChain, rightToken, rightContract, rightModel := normalizeSettlementParts(right)
|
||||
return leftChain == rightChain &&
|
||||
leftToken == rightToken &&
|
||||
leftContract == rightContract &&
|
||||
leftModel == rightModel
|
||||
}
|
||||
|
||||
func normalizeSettlementParts(src *quotationv2.RouteSettlement) (chain, token, contract, model string) {
|
||||
if src != nil {
|
||||
if asset := src.GetAsset(); asset != nil {
|
||||
key := asset.GetKey()
|
||||
chain = normalizeAsset(key.GetChain())
|
||||
token = normalizeAsset(key.GetTokenSymbol())
|
||||
contract = strings.TrimSpace(asset.GetContractAddress())
|
||||
}
|
||||
model = normalizeSettlementModel(src.GetModel())
|
||||
}
|
||||
return chain, token, contract, model
|
||||
}
|
||||
|
||||
func normalizeRail(value string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package quote_computation_service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1"
|
||||
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
|
||||
)
|
||||
|
||||
func buildRouteSettlement(
|
||||
intent model.PaymentIntent,
|
||||
network string,
|
||||
hops []*quotationv2.RouteHop,
|
||||
) *quotationv2.RouteSettlement {
|
||||
modelValue := normalizeSettlementModel(settlementModelString(intent.SettlementMode))
|
||||
asset := buildRouteSettlementAsset(intent, network, hops)
|
||||
if asset == nil && modelValue == "" {
|
||||
return nil
|
||||
}
|
||||
return "ationv2.RouteSettlement{
|
||||
Asset: asset,
|
||||
Model: modelValue,
|
||||
}
|
||||
}
|
||||
|
||||
func buildRouteSettlementAsset(
|
||||
intent model.PaymentIntent,
|
||||
network string,
|
||||
hops []*quotationv2.RouteHop,
|
||||
) *paymentv1.ChainAsset {
|
||||
chain, token, contract := settlementAssetFromIntent(intent)
|
||||
|
||||
if token == "" {
|
||||
token = normalizeAsset(intent.SettlementCurrency)
|
||||
}
|
||||
if token == "" && intent.Amount != nil {
|
||||
token = normalizeAsset(intent.Amount.GetCurrency())
|
||||
}
|
||||
if chain == "" {
|
||||
chain = normalizeAsset(firstNonEmpty(network, routeNetworkFromHops(hops)))
|
||||
}
|
||||
if chain == "" && token == "" && contract == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
asset := &paymentv1.ChainAsset{
|
||||
Key: &paymentv1.ChainAssetKey{
|
||||
Chain: chain,
|
||||
TokenSymbol: token,
|
||||
},
|
||||
}
|
||||
if contract != "" {
|
||||
asset.ContractAddress = &contract
|
||||
}
|
||||
return asset
|
||||
}
|
||||
|
||||
func settlementAssetFromIntent(intent model.PaymentIntent) (chain, token, contract string) {
|
||||
candidates := []*model.PaymentEndpoint{
|
||||
&intent.Source,
|
||||
&intent.Destination,
|
||||
}
|
||||
for _, endpoint := range candidates {
|
||||
if endpoint == nil {
|
||||
continue
|
||||
}
|
||||
if endpoint.ManagedWallet != nil && endpoint.ManagedWallet.Asset != nil {
|
||||
return normalizedAssetFields(endpoint.ManagedWallet.Asset.Chain, endpoint.ManagedWallet.Asset.TokenSymbol, endpoint.ManagedWallet.Asset.ContractAddress)
|
||||
}
|
||||
if endpoint.ExternalChain != nil && endpoint.ExternalChain.Asset != nil {
|
||||
return normalizedAssetFields(endpoint.ExternalChain.Asset.Chain, endpoint.ExternalChain.Asset.TokenSymbol, endpoint.ExternalChain.Asset.ContractAddress)
|
||||
}
|
||||
}
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
func normalizedAssetFields(chain, token, contract string) (string, string, string) {
|
||||
return normalizeAsset(chain), normalizeAsset(token), strings.TrimSpace(contract)
|
||||
}
|
||||
|
||||
func routeNetworkFromHops(hops []*quotationv2.RouteHop) string {
|
||||
for _, hop := range hops {
|
||||
if hop == nil {
|
||||
continue
|
||||
}
|
||||
network := strings.TrimSpace(hop.GetNetwork())
|
||||
if network != "" {
|
||||
return network
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -14,27 +14,13 @@ import (
|
||||
|
||||
func buildRouteSpecification(
|
||||
intent model.PaymentIntent,
|
||||
destination model.PaymentEndpoint,
|
||||
destinationRail model.Rail,
|
||||
network string,
|
||||
provider string,
|
||||
steps []*QuoteComputationStep,
|
||||
) *quotationv2.RouteSpecification {
|
||||
hops := buildRouteHops(steps, network)
|
||||
if strings.TrimSpace(provider) == "" {
|
||||
provider = providerFromHops(hops)
|
||||
}
|
||||
route := "ationv2.RouteSpecification{
|
||||
Rail: normalizeRail(string(destinationRail)),
|
||||
Provider: normalizeProvider(provider),
|
||||
PayoutMethod: normalizePayoutMethod(payoutMethodFromEndpoint(destination)),
|
||||
SettlementAsset: normalizeAsset(intent.SettlementCurrency),
|
||||
SettlementModel: normalizeSettlementModel(settlementModelString(intent.SettlementMode)),
|
||||
Network: normalizeNetwork(network),
|
||||
Hops: hops,
|
||||
}
|
||||
if route.SettlementAsset == "" && intent.Amount != nil {
|
||||
route.SettlementAsset = normalizeAsset(intent.Amount.GetCurrency())
|
||||
Settlement: buildRouteSettlement(intent, network, hops),
|
||||
Hops: hops,
|
||||
}
|
||||
route.RouteRef = buildRouteReference(route)
|
||||
route.PricingProfileRef = buildPricingProfileReference(route)
|
||||
@@ -88,21 +74,6 @@ func buildExecutionConditions(
|
||||
return conditions, blockReason
|
||||
}
|
||||
|
||||
func payoutMethodFromEndpoint(endpoint model.PaymentEndpoint) string {
|
||||
switch endpoint.Type {
|
||||
case model.EndpointTypeCard:
|
||||
return "CARD"
|
||||
case model.EndpointTypeExternalChain:
|
||||
return "CRYPTO_ADDRESS"
|
||||
case model.EndpointTypeManagedWallet:
|
||||
return "MANAGED_WALLET"
|
||||
case model.EndpointTypeLedger:
|
||||
return "LEDGER"
|
||||
default:
|
||||
return "UNSPECIFIED"
|
||||
}
|
||||
}
|
||||
|
||||
func settlementModelString(mode model.SettlementMode) string {
|
||||
switch mode {
|
||||
case model.SettlementModeFixSource:
|
||||
@@ -164,18 +135,6 @@ func roleForHopIndex(index, last int) quotationv2.RouteHopRole {
|
||||
}
|
||||
}
|
||||
|
||||
func providerFromHops(hops []*quotationv2.RouteHop) string {
|
||||
for i := len(hops) - 1; i >= 0; i-- {
|
||||
if hops[i] == nil {
|
||||
continue
|
||||
}
|
||||
if gateway := normalizeProvider(hops[i].GetGateway()); gateway != "" {
|
||||
return gateway
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func buildRouteReference(route *quotationv2.RouteSpecification) string {
|
||||
signature := routeTopologySignature(route, true)
|
||||
if signature == "" {
|
||||
@@ -198,13 +157,23 @@ func routeTopologySignature(route *quotationv2.RouteSpecification, includeInstan
|
||||
if route == nil {
|
||||
return ""
|
||||
}
|
||||
parts := []string{
|
||||
normalizeRail(route.GetRail()),
|
||||
normalizeProvider(route.GetProvider()),
|
||||
normalizePayoutMethod(route.GetPayoutMethod()),
|
||||
normalizeAsset(route.GetSettlementAsset()),
|
||||
normalizeSettlementModel(route.GetSettlementModel()),
|
||||
normalizeNetwork(route.GetNetwork()),
|
||||
parts := make([]string, 0, 8)
|
||||
if settlement := route.GetSettlement(); settlement != nil {
|
||||
if asset := settlement.GetAsset(); asset != nil {
|
||||
key := asset.GetKey()
|
||||
if chain := normalizeAsset(key.GetChain()); chain != "" {
|
||||
parts = append(parts, chain)
|
||||
}
|
||||
if token := normalizeAsset(key.GetTokenSymbol()); token != "" {
|
||||
parts = append(parts, token)
|
||||
}
|
||||
if contract := strings.TrimSpace(asset.GetContractAddress()); contract != "" {
|
||||
parts = append(parts, strings.ToLower(contract))
|
||||
}
|
||||
}
|
||||
if model := normalizeSettlementModel(settlement.GetModel()); model != "" {
|
||||
parts = append(parts, model)
|
||||
}
|
||||
}
|
||||
|
||||
hops := route.GetHops()
|
||||
@@ -232,5 +201,8 @@ func routeTopologySignature(route *quotationv2.RouteSpecification, includeInstan
|
||||
parts = append(parts, strings.Join(hopParts, ":"))
|
||||
}
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(parts, "|")
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/tech/sendico/payments/quotation/internal/service/plan"
|
||||
"github.com/tech/sendico/payments/quotation/internal/service/quotation/gateway_funding_profile"
|
||||
"github.com/tech/sendico/payments/quotation/internal/service/quotation/graph_path_finder"
|
||||
)
|
||||
|
||||
type Core interface {
|
||||
@@ -18,11 +19,14 @@ type QuoteComputationService struct {
|
||||
core Core
|
||||
fundingResolver gateway_funding_profile.FundingProfileResolver
|
||||
gatewayRegistry plan.GatewayRegistry
|
||||
routeStore plan.RouteStore
|
||||
pathFinder *graph_path_finder.GraphPathFinder
|
||||
}
|
||||
|
||||
func New(core Core, opts ...Option) *QuoteComputationService {
|
||||
svc := &QuoteComputationService{
|
||||
core: core,
|
||||
core: core,
|
||||
pathFinder: graph_path_finder.New(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
if opt != nil {
|
||||
@@ -47,3 +51,19 @@ func WithGatewayRegistry(registry plan.GatewayRegistry) Option {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithRouteStore(store plan.RouteStore) Option {
|
||||
return func(svc *QuoteComputationService) {
|
||||
if svc != nil {
|
||||
svc.routeStore = store
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithPathFinder(pathFinder *graph_path_finder.GraphPathFinder) Option {
|
||||
return func(svc *QuoteComputationService) {
|
||||
if svc != nil && pathFinder != nil {
|
||||
svc.pathFinder = pathFinder
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user