+source currency pick fix +fx side propagation

This commit is contained in:
Stephan D
2026-02-26 02:39:48 +01:00
parent 008427483c
commit 70b1c2a9cc
73 changed files with 2123 additions and 656 deletions

View File

@@ -23,7 +23,7 @@ import (
func TestBuildPlan_BuildsStepsAndFundingGate(t *testing.T) {
svc := New(nil, WithFundingProfileResolver(gateway_funding_profile.NewStaticFundingProfileResolver(gateway_funding_profile.StaticFundingProfileResolverInput{
GatewayModes: map[string]model.FundingMode{
"monetix": model.FundingModeBalanceReserve,
paymenttypes.DefaultCardsGatewayID: model.FundingModeBalanceReserve,
},
})))
@@ -168,6 +168,69 @@ func TestBuildPlan_RequiresFXUsesSettlementCurrencyForDestinationStep(t *testing
}
}
func TestBuildPlan_UsesSourceAssetCurrencyForSourceStep(t *testing.T) {
svc := New(nil)
orgID := bson.NewObjectID()
intent := sampleCryptoToCardQuoteIntent()
intent.SettlementMode = transfer_intent_hydrator.QuoteSettlementModeFixReceived
intent.Amount = &paymenttypes.Money{
Amount: "5000",
Currency: "RUB",
}
intent.SettlementCurrency = "RUB"
intent.RequiresFX = false
planModel, err := svc.BuildPlan(context.Background(), ComputeInput{
OrganizationRef: orgID.Hex(),
OrganizationID: orgID,
BaseIdempotencyKey: "idem-key",
PreviewOnly: false,
Intents: []*transfer_intent_hydrator.QuoteIntent{intent},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(planModel.Items) != 1 {
t.Fatalf("expected one plan item")
}
item := planModel.Items[0]
if item == nil {
t.Fatalf("expected plan item")
}
if !item.QuoteInput.Intent.RequiresFX {
t.Fatalf("expected derived FX requirement for fix_received cross-currency flow")
}
if item.QuoteInput.Intent.FX == nil || item.QuoteInput.Intent.FX.Pair == nil {
t.Fatalf("expected derived FX pair")
}
if got, want := strings.TrimSpace(item.QuoteInput.Intent.FX.Pair.GetBase()), "USDT"; got != want {
t.Fatalf("unexpected derived FX base currency: got=%q want=%q", got, want)
}
if got, want := strings.TrimSpace(item.QuoteInput.Intent.FX.Pair.GetQuote()), "RUB"; got != want {
t.Fatalf("unexpected derived FX quote currency: got=%q want=%q", got, want)
}
steps := item.Steps
if got, want := len(steps), 4; got != want {
t.Fatalf("unexpected step count: got=%d want=%d", got, want)
}
if steps[0] == nil || steps[0].Amount == nil {
t.Fatalf("expected source step amount")
}
if got, want := strings.TrimSpace(steps[0].Amount.GetCurrency()), "USDT"; got != want {
t.Fatalf("unexpected source step currency: got=%q want=%q", got, want)
}
last := steps[len(steps)-1]
if last == nil || last.Amount == nil {
t.Fatalf("expected destination step amount")
}
if got, want := strings.TrimSpace(last.Amount.GetCurrency()), "RUB"; got != want {
t.Fatalf("unexpected destination step currency: got=%q want=%q", got, want)
}
if got, want := steps[1].Operation, model.RailOperationFXConvert; got != want {
t.Fatalf("unexpected middle operation: got=%q want=%q", got, want)
}
}
func TestBuildPlan_ResolvesIndependentEconomicsKnobs(t *testing.T) {
svc := New(nil)
orgID := bson.NewObjectID()
@@ -397,7 +460,7 @@ func TestCompute_EnrichesRouteConditionsAndTotalCost(t *testing.T) {
}
svc := New(core, WithFundingProfileResolver(gateway_funding_profile.NewStaticFundingProfileResolver(gateway_funding_profile.StaticFundingProfileResolverInput{
GatewayModes: map[string]model.FundingMode{
"monetix": model.FundingModeBalanceReserve,
paymenttypes.DefaultCardsGatewayID: model.FundingModeBalanceReserve,
},
})))
@@ -459,7 +522,7 @@ func TestCompute_EnrichesRouteConditionsAndTotalCost(t *testing.T) {
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 {
if got, want := hops[1].GetGateway(), paymenttypes.DefaultCardsGatewayID; got != want {
t.Fatalf("unexpected destination gateway in build input route: got=%q want=%q", got, want)
}
if core.lastQuoteIn.ExecutionConditions == nil {
@@ -611,7 +674,7 @@ func sampleCardQuoteIntent() *transfer_intent_hydrator.QuoteIntent {
},
SettlementCurrency: "USD",
Attributes: map[string]string{
"gateway": "monetix",
"gateway": paymenttypes.DefaultCardsGatewayID,
},
}
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/tech/sendico/payments/quotation/internal/service/quotation/transfer_intent_hydrator"
"github.com/tech/sendico/payments/storage/model"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
)
func modelIntentFromQuoteIntent(src *transfer_intent_hydrator.QuoteIntent) model.PaymentIntent {
@@ -22,6 +23,7 @@ func modelIntentFromQuoteIntent(src *transfer_intent_hydrator.QuoteIntent) model
Source: modelEndpointFromQuoteEndpoint(src.Source),
Destination: modelEndpointFromQuoteEndpoint(src.Destination),
Amount: cloneModelMoney(src.Amount),
FX: fxIntentFromHydratedIntent(src),
RequiresFX: src.RequiresFX,
Attributes: cloneStringMap(src.Attributes),
SettlementMode: modelSettlementMode(src.SettlementMode),
@@ -30,6 +32,72 @@ func modelIntentFromQuoteIntent(src *transfer_intent_hydrator.QuoteIntent) model
}
}
func fxIntentFromHydratedIntent(src *transfer_intent_hydrator.QuoteIntent) *model.FXIntent {
if src == nil {
return nil
}
if strings.TrimSpace(string(src.FXSide)) == "" || src.FXSide == paymenttypes.FXSideUnspecified {
return nil
}
return &model.FXIntent{Side: src.FXSide}
}
func ensureDerivedFXIntent(intent *model.PaymentIntent) {
if intent == nil {
return
}
amountCurrency := ""
if intent.Amount != nil {
amountCurrency = normalizeAsset(intent.Amount.GetCurrency())
}
settlementCurrency := normalizeAsset(intent.SettlementCurrency)
if settlementCurrency == "" {
settlementCurrency = amountCurrency
}
if intent.SettlementCurrency == "" && settlementCurrency != "" {
intent.SettlementCurrency = settlementCurrency
}
sourceCurrency := sourceAssetToken(intent.Source)
// For FIX_RECEIVED, destination amounts can be provided in payout currency.
// Derive FX necessity from source asset currency when available.
if !intent.RequiresFX &&
intent.SettlementMode == model.SettlementModeFixReceived &&
sourceCurrency != "" &&
settlementCurrency != "" &&
!strings.EqualFold(sourceCurrency, settlementCurrency) {
intent.RequiresFX = true
}
if !intent.RequiresFX {
return
}
baseCurrency := firstNonEmpty(sourceCurrency, amountCurrency)
quoteCurrency := settlementCurrency
if baseCurrency == "" || quoteCurrency == "" {
return
}
if intent.FX == nil {
intent.FX = &model.FXIntent{}
}
if intent.FX.Pair == nil {
intent.FX.Pair = &paymenttypes.CurrencyPair{}
}
if normalizeAsset(intent.FX.Pair.Base) == "" {
intent.FX.Pair.Base = baseCurrency
}
if normalizeAsset(intent.FX.Pair.Quote) == "" {
intent.FX.Pair.Quote = quoteCurrency
}
if strings.TrimSpace(string(intent.FX.Side)) == "" || intent.FX.Side == paymenttypes.FXSideUnspecified {
intent.FX.Side = paymenttypes.FXSideSellBaseBuyQuote
}
}
func modelEndpointFromQuoteEndpoint(src transfer_intent_hydrator.QuoteEndpoint) model.PaymentEndpoint {
result := model.PaymentEndpoint{
Type: model.EndpointTypeUnspecified,

View File

@@ -0,0 +1,71 @@
package quote_computation_service
import (
"testing"
"github.com/tech/sendico/payments/quotation/internal/service/quotation/transfer_intent_hydrator"
"github.com/tech/sendico/payments/storage/model"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
)
func TestEnsureDerivedFXIntent_DefaultsSideWhenEmpty(t *testing.T) {
intent := &model.PaymentIntent{
RequiresFX: true,
SettlementCurrency: "RUB",
Amount: &paymenttypes.Money{Amount: "100", Currency: "USDT"},
FX: &model.FXIntent{},
}
ensureDerivedFXIntent(intent)
if intent.FX == nil {
t.Fatal("expected fx intent")
}
if got, want := intent.FX.Side, paymenttypes.FXSideSellBaseBuyQuote; got != want {
t.Fatalf("unexpected side: got=%q want=%q", got, want)
}
}
func TestEnsureDerivedFXIntent_DefaultsSideWhenUnspecified(t *testing.T) {
intent := &model.PaymentIntent{
RequiresFX: true,
SettlementCurrency: "RUB",
Amount: &paymenttypes.Money{Amount: "100", Currency: "USDT"},
FX: &model.FXIntent{Side: paymenttypes.FXSideUnspecified},
}
ensureDerivedFXIntent(intent)
if intent.FX == nil {
t.Fatal("expected fx intent")
}
if got, want := intent.FX.Side, paymenttypes.FXSideSellBaseBuyQuote; got != want {
t.Fatalf("unexpected side: got=%q want=%q", got, want)
}
}
func TestEnsureDerivedFXIntent_PreservesExplicitSideFromHydratedIntent(t *testing.T) {
hydrated := &transfer_intent_hydrator.QuoteIntent{
Source: transfer_intent_hydrator.QuoteEndpoint{
Type: transfer_intent_hydrator.QuoteEndpointTypeManagedWallet,
ManagedWallet: &transfer_intent_hydrator.QuoteManagedWalletEndpoint{
ManagedWalletRef: "mw-src",
Asset: &paymenttypes.Asset{TokenSymbol: "USDT"},
},
},
Amount: &paymenttypes.Money{Amount: "100", Currency: "USDT"},
SettlementCurrency: "RUB",
RequiresFX: true,
FXSide: paymenttypes.FXSideBuyBaseSellQuote,
}
intent := modelIntentFromQuoteIntent(hydrated)
ensureDerivedFXIntent(&intent)
if intent.FX == nil {
t.Fatal("expected fx intent")
}
if got, want := intent.FX.Side, paymenttypes.FXSideBuyBaseSellQuote; got != want {
t.Fatalf("unexpected side: got=%q want=%q", got, want)
}
}

View File

@@ -10,6 +10,10 @@ import (
"go.uber.org/zap"
)
type managedWalletAssetResolver interface {
ResolveManagedWalletAsset(ctx context.Context, managedWalletRef string) (*paymenttypes.Asset, error)
}
func (s *QuoteComputationService) enrichManagedWalletEndpointNetwork(
ctx context.Context,
endpoint *model.PaymentEndpoint,
@@ -25,7 +29,30 @@ func (s *QuoteComputationService) enrichManagedWalletEndpointNetwork(
if walletRef == "" {
return merrors.InvalidArgument("managed_wallet_ref is required")
}
if endpoint.ManagedWallet.Asset != nil && strings.TrimSpace(endpoint.ManagedWallet.Asset.GetChain()) != "" {
asset := endpoint.ManagedWallet.Asset
if asset == nil {
asset = &paymenttypes.Asset{}
endpoint.ManagedWallet.Asset = asset
}
if resolver, ok := s.managedWalletNetworkResolver.(managedWalletAssetResolver); ok && resolver != nil {
if strings.TrimSpace(asset.GetChain()) == "" || strings.TrimSpace(asset.GetTokenSymbol()) == "" {
if resolved, err := resolver.ResolveManagedWalletAsset(ctx, walletRef); err == nil && resolved != nil {
if strings.TrimSpace(asset.GetChain()) == "" {
asset.Chain = strings.ToUpper(strings.TrimSpace(resolved.GetChain()))
}
if strings.TrimSpace(asset.GetTokenSymbol()) == "" {
asset.TokenSymbol = strings.ToUpper(strings.TrimSpace(resolved.GetTokenSymbol()))
}
if strings.TrimSpace(asset.GetContractAddress()) == "" {
asset.ContractAddress = strings.TrimSpace(resolved.GetContractAddress())
}
}
}
}
if strings.TrimSpace(asset.GetChain()) != "" {
asset.Chain = strings.ToUpper(strings.TrimSpace(asset.GetChain()))
asset.TokenSymbol = strings.ToUpper(strings.TrimSpace(asset.GetTokenSymbol()))
asset.ContractAddress = strings.TrimSpace(asset.GetContractAddress())
return nil
}
if s.managedWalletNetworkResolver == nil {
@@ -57,11 +84,6 @@ func (s *QuoteComputationService) enrichManagedWalletEndpointNetwork(
)
}
asset := endpoint.ManagedWallet.Asset
if asset == nil {
asset = &paymenttypes.Asset{}
endpoint.ManagedWallet.Asset = asset
}
asset.Chain = network
asset.TokenSymbol = strings.ToUpper(strings.TrimSpace(asset.TokenSymbol))
asset.ContractAddress = strings.TrimSpace(asset.ContractAddress)

View File

@@ -8,6 +8,7 @@ import (
"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"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
"go.mongodb.org/mongo-driver/v2/bson"
)
@@ -147,6 +148,93 @@ func TestBuildPlan_ManagedWalletNetworkResolverCachesByWalletRef(t *testing.T) {
}
}
func TestBuildPlan_ResolvesManagedWalletAssetTokenForSourceCurrency(t *testing.T) {
resolver := &fakeManagedWalletNetworkResolver{
networks: map[string]string{
"wallet-usdt-source": "TRON_NILE",
},
assets: map[string]*paymenttypes.Asset{
"wallet-usdt-source": {
Chain: "TRON_NILE",
TokenSymbol: "USDT",
},
},
}
svc := New(nil,
WithManagedWalletNetworkResolver(resolver),
WithGatewayRegistry(staticGatewayRegistry{
items: []*model.GatewayInstanceDescriptor{
{
ID: "crypto-tron",
InstanceID: "crypto-tron",
Rail: model.RailCrypto,
Network: "TRON_NILE",
Currencies: []string{"USDT"},
Capabilities: model.RailCapabilities{
CanPayOut: true,
},
IsEnabled: true,
},
{
ID: "fx-tron",
InstanceID: "fx-tron",
Rail: model.RailProviderSettlement,
Network: "TRON_NILE",
Currencies: []string{"USDT", "RUB"},
Operations: []model.RailOperation{model.RailOperationFXConvert},
IsEnabled: true,
},
{
ID: "card-gw",
InstanceID: "card-gw",
Rail: model.RailCardPayout,
Currencies: []string{"RUB"},
Capabilities: model.RailCapabilities{
CanPayOut: true,
},
IsEnabled: true,
},
},
}),
)
intent := sampleCryptoToCardQuoteIntent()
intent.Source.ManagedWallet.Asset = nil
intent.Amount = &paymenttypes.Money{
Amount: "5000",
Currency: "RUB",
}
intent.SettlementMode = transfer_intent_hydrator.QuoteSettlementModeFixReceived
intent.SettlementCurrency = "RUB"
intent.RequiresFX = false
orgID := bson.NewObjectID()
planModel, err := svc.BuildPlan(context.Background(), ComputeInput{
OrganizationRef: orgID.Hex(),
OrganizationID: orgID,
BaseIdempotencyKey: "idem-wallet-asset",
Intents: []*transfer_intent_hydrator.QuoteIntent{intent},
})
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 := item.Steps[0].GatewayID, "crypto-tron"; got != want {
t.Fatalf("unexpected source gateway: got=%q want=%q", got, want)
}
if item.Steps[0] == nil || item.Steps[0].Amount == nil {
t.Fatalf("expected source step amount")
}
if got, want := item.Steps[0].Amount.GetCurrency(), "USDT"; got != want {
t.Fatalf("unexpected source step currency: got=%q want=%q", got, want)
}
if got, want := resolver.assetCalls, 1; got != want {
t.Fatalf("unexpected asset resolver calls: got=%d want=%d", got, want)
}
}
func TestBuildPlan_FailsWhenManagedWalletNetworkResolutionFails(t *testing.T) {
resolver := &fakeManagedWalletNetworkResolver{
err: merrors.NoData("wallet not found"),
@@ -168,9 +256,12 @@ func TestBuildPlan_FailsWhenManagedWalletNetworkResolutionFails(t *testing.T) {
}
type fakeManagedWalletNetworkResolver struct {
networks map[string]string
err error
calls int
networks map[string]string
assets map[string]*paymenttypes.Asset
err error
assetErr error
calls int
assetCalls int
}
func (f *fakeManagedWalletNetworkResolver) ResolveManagedWalletNetwork(_ context.Context, managedWalletRef string) (string, error) {
@@ -183,3 +274,22 @@ func (f *fakeManagedWalletNetworkResolver) ResolveManagedWalletNetwork(_ context
}
return f.networks[managedWalletRef], nil
}
func (f *fakeManagedWalletNetworkResolver) ResolveManagedWalletAsset(_ context.Context, managedWalletRef string) (*paymenttypes.Asset, error) {
f.assetCalls++
if f.assetErr != nil {
return nil, f.assetErr
}
if f.assets == nil {
return nil, nil
}
src := f.assets[managedWalletRef]
if src == nil {
return nil, nil
}
return &paymenttypes.Asset{
Chain: src.GetChain(),
TokenSymbol: src.GetTokenSymbol(),
ContractAddress: src.GetContractAddress(),
}, nil
}

View File

@@ -136,6 +136,7 @@ func (s *QuoteComputationService) buildPlanItem(
}
modelIntent.Source = clonePaymentEndpoint(source)
modelIntent.Destination = clonePaymentEndpoint(destination)
ensureDerivedFXIntent(&modelIntent)
sourceRail, sourceNetwork, err := plan.RailFromEndpoint(source, modelIntent.Attributes, true)
if err != nil {

View File

@@ -5,6 +5,7 @@ import (
"strings"
"github.com/tech/sendico/payments/storage/model"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
)
@@ -20,6 +21,7 @@ func buildComputationSteps(
attrs := intent.Attributes
amount := protoMoneyFromModel(intent.Amount)
sourceAmount := sourceStepAmount(intent, amount)
destinationAmount := destinationStepAmount(intent, amount)
sourceRail := sourceRailForIntent(intent)
destinationRail := destinationRailForIntent(intent)
@@ -45,7 +47,7 @@ func buildComputationSteps(
Operation: sourceOperationForRail(rails[0]),
GatewayID: sourceGatewayID,
InstanceID: sourceInstanceID,
Amount: cloneProtoMoney(amount),
Amount: cloneProtoMoney(sourceAmount),
Optional: false,
IncludeInAggregate: false,
},
@@ -63,7 +65,7 @@ func buildComputationSteps(
Rail: model.RailProviderSettlement,
Operation: model.RailOperationFXConvert,
DependsOn: []string{sourceStepID},
Amount: cloneProtoMoney(amount),
Amount: cloneProtoMoney(sourceAmount),
Optional: false,
IncludeInAggregate: false,
})
@@ -82,12 +84,16 @@ func buildComputationSteps(
operation = model.RailOperationFXConvert
fxAssigned = true
}
stepAmount := amount
if operation == model.RailOperationFXConvert {
stepAmount = sourceAmount
}
steps = append(steps, &QuoteComputationStep{
StepID: stepID,
Rail: rail,
Operation: operation,
DependsOn: []string{lastStepID},
Amount: cloneProtoMoney(amount),
Amount: cloneProtoMoney(stepAmount),
Optional: false,
IncludeInAggregate: false,
})
@@ -209,11 +215,78 @@ func destinationStepAmount(intent model.PaymentIntent, sourceAmount *moneyv1.Mon
}
settlementCurrency := strings.ToUpper(strings.TrimSpace(intent.SettlementCurrency))
if settlementCurrency == "" && intent.FX != nil && intent.FX.Pair != nil {
settlementCurrency = strings.ToUpper(strings.TrimSpace(intent.FX.Pair.Quote))
if settlementCurrency == "" {
settlementCurrency = settlementCurrencyFromFX(intent.FX)
}
if settlementCurrency != "" {
amount.Currency = settlementCurrency
}
return amount
}
func settlementCurrencyFromFX(fx *model.FXIntent) string {
if fx == nil || fx.Pair == nil {
return ""
}
base := normalizeAsset(fx.Pair.GetBase())
quote := normalizeAsset(fx.Pair.GetQuote())
switch fx.Side {
case paymenttypes.FXSideBuyBaseSellQuote:
return firstNonEmpty(base, quote)
case paymenttypes.FXSideSellBaseBuyQuote:
return firstNonEmpty(quote, base)
default:
return firstNonEmpty(quote, base)
}
}
func sourceStepAmount(intent model.PaymentIntent, amount *moneyv1.Money) *moneyv1.Money {
result := cloneProtoMoney(amount)
if result == nil {
return nil
}
if currency := sourceStepCurrency(intent, result.GetCurrency()); currency != "" {
result.Currency = currency
}
return result
}
func sourceStepCurrency(intent model.PaymentIntent, fallback string) string {
if currency := sourceCurrencyFromFX(intent.FX); currency != "" {
return currency
}
if currency := sourceAssetToken(intent.Source); currency != "" {
return currency
}
return normalizeAsset(fallback)
}
func sourceCurrencyFromFX(fx *model.FXIntent) string {
if fx == nil || fx.Pair == nil {
return ""
}
base := normalizeAsset(fx.Pair.GetBase())
quote := normalizeAsset(fx.Pair.GetQuote())
switch fx.Side {
case paymenttypes.FXSideBuyBaseSellQuote:
return quote
case paymenttypes.FXSideSellBaseBuyQuote:
return base
default:
return firstNonEmpty(base, quote)
}
}
func sourceAssetToken(endpoint model.PaymentEndpoint) string {
if endpoint.ManagedWallet != nil && endpoint.ManagedWallet.Asset != nil {
if token := normalizeAsset(endpoint.ManagedWallet.Asset.GetTokenSymbol()); token != "" {
return token
}
}
if endpoint.ExternalChain != nil && endpoint.ExternalChain.Asset != nil {
if token := normalizeAsset(endpoint.ExternalChain.Asset.GetTokenSymbol()); token != "" {
return token
}
}
return ""
}

View File

@@ -0,0 +1,93 @@
package quote_computation_service
import (
"testing"
"github.com/tech/sendico/payments/storage/model"
paymenttypes "github.com/tech/sendico/pkg/payments/types"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
)
func TestDestinationStepAmount_UsesSideAwareCurrencyFallback(t *testing.T) {
tests := []struct {
name string
side paymenttypes.FXSide
want string
}{
{
name: "buy_base_sell_quote uses base settlement currency",
side: paymenttypes.FXSideBuyBaseSellQuote,
want: "RUB",
},
{
name: "sell_base_buy_quote uses quote settlement currency",
side: paymenttypes.FXSideSellBaseBuyQuote,
want: "USDT",
},
{
name: "unspecified defaults to quote settlement currency",
side: paymenttypes.FXSideUnspecified,
want: "USDT",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
intent := model.PaymentIntent{
RequiresFX: true,
FX: &model.FXIntent{
Pair: &paymenttypes.CurrencyPair{
Base: "RUB",
Quote: "USDT",
},
Side: tt.side,
},
}
got := destinationStepAmount(intent, &moneyv1.Money{
Amount: "100",
Currency: "EUR",
})
if got == nil {
t.Fatal("expected destination amount")
}
if got.GetCurrency() != tt.want {
t.Fatalf("unexpected destination currency: got=%q want=%q", got.GetCurrency(), tt.want)
}
})
}
}
func TestSourceCurrencyFromFX_RespectsSide(t *testing.T) {
tests := []struct {
name string
side paymenttypes.FXSide
want string
}{
{
name: "buy_base_sell_quote debits quote currency",
side: paymenttypes.FXSideBuyBaseSellQuote,
want: "USDT",
},
{
name: "sell_base_buy_quote debits base currency",
side: paymenttypes.FXSideSellBaseBuyQuote,
want: "RUB",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := sourceCurrencyFromFX(&model.FXIntent{
Pair: &paymenttypes.CurrencyPair{
Base: "RUB",
Quote: "USDT",
},
Side: tt.side,
})
if got != tt.want {
t.Fatalf("unexpected source currency: got=%q want=%q", got, tt.want)
}
})
}
}