fixed linting config

This commit is contained in:
Stephan D
2026-02-12 13:00:37 +01:00
parent 97395acd8f
commit 7cbcbb4b3c
42 changed files with 1813 additions and 237 deletions

View File

@@ -51,7 +51,7 @@ func (r *discoveryGatewayRegistry) List(_ context.Context) ([]*model.GatewayInst
InvokeURI: strings.TrimSpace(entry.InvokeURI),
Currencies: normalizeCurrencies(entry.Currencies),
Capabilities: capabilitiesFromOps(entry.Operations),
Limits: limitsFromDiscovery(entry.Limits),
Limits: limitsFromDiscovery(entry.Limits, entry.CurrencyMeta),
Version: entry.Version,
IsEnabled: entry.Healthy,
})
@@ -102,36 +102,111 @@ func capabilitiesFromOps(ops []string) model.RailCapabilities {
return cap
}
func limitsFromDiscovery(src *discovery.Limits) model.Limits {
if src == nil {
return model.Limits{}
}
func limitsFromDiscovery(src *discovery.Limits, currencies []discovery.CurrencyAnnouncement) model.Limits {
limits := model.Limits{
MinAmount: strings.TrimSpace(src.MinAmount),
MaxAmount: strings.TrimSpace(src.MaxAmount),
VolumeLimit: map[string]string{},
VelocityLimit: map[string]int{},
VolumeLimit: map[string]string{},
VelocityLimit: map[string]int{},
CurrencyLimits: map[string]model.LimitsOverride{},
}
for key, value := range src.VolumeLimit {
k := strings.TrimSpace(key)
v := strings.TrimSpace(value)
if k == "" || v == "" {
continue
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
}
limits.VolumeLimit[k] = v
}
for key, value := range src.VelocityLimit {
k := strings.TrimSpace(key)
if k == "" {
continue
for key, value := range src.VelocityLimit {
k := strings.TrimSpace(key)
if k == "" {
continue
}
limits.VelocityLimit[k] = value
}
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 = firstDiscoveryLimitValue(dst.PerTxMinAmount, commonMin)
}
if commonMaxInit && commonMaxConsistent {
dst.PerTxMaxAmount = firstDiscoveryLimitValue(dst.PerTxMaxAmount, commonMax)
}
}
func firstDiscoveryLimitValue(primary, fallback string) string {
primary = strings.TrimSpace(primary)
if primary != "" {
return primary
}
return strings.TrimSpace(fallback)
}

View File

@@ -0,0 +1,62 @@
package orchestrator
import (
"testing"
"github.com/tech/sendico/pkg/discovery"
)
func TestLimitsFromDiscovery_MapsPerTxMinimumFromCurrencyMeta(t *testing.T) {
limits := limitsFromDiscovery(nil, []discovery.CurrencyAnnouncement{
{
Currency: "RUB",
Limits: &discovery.CurrencyLimits{
Amount: &discovery.CurrencyAmount{
Min: "100.00",
Max: "10000.00",
},
},
},
})
if limits.PerTxMinAmount != "100.00" {
t.Fatalf("expected per tx min 100.00, got %q", limits.PerTxMinAmount)
}
if limits.PerTxMaxAmount != "10000.00" {
t.Fatalf("expected per tx max 10000.00, got %q", limits.PerTxMaxAmount)
}
override, ok := limits.CurrencyLimits["RUB"]
if !ok {
t.Fatalf("expected RUB currency override")
}
if override.MinAmount != "100.00" {
t.Fatalf("expected RUB min override 100.00, got %q", override.MinAmount)
}
}
func TestLimitsFromDiscovery_DropsCommonPerTxMinimumWhenCurrenciesDiffer(t *testing.T) {
limits := limitsFromDiscovery(nil, []discovery.CurrencyAnnouncement{
{
Currency: "USD",
Limits: &discovery.CurrencyLimits{
Amount: &discovery.CurrencyAmount{Min: "10.00"},
},
},
{
Currency: "EUR",
Limits: &discovery.CurrencyLimits{
Amount: &discovery.CurrencyAmount{Min: "20.00"},
},
},
})
if limits.PerTxMinAmount != "" {
t.Fatalf("expected empty common per tx min, got %q", limits.PerTxMinAmount)
}
if limits.CurrencyLimits["USD"].MinAmount != "10.00" {
t.Fatalf("expected USD min override 10.00, got %q", limits.CurrencyLimits["USD"].MinAmount)
}
if limits.CurrencyLimits["EUR"].MinAmount != "20.00" {
t.Fatalf("expected EUR min override 20.00, got %q", limits.CurrencyLimits["EUR"].MinAmount)
}
}

View File

@@ -55,6 +55,9 @@ func (h *initiatePaymentsCommand) Execute(ctx context.Context, req *orchestrator
}
return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}
if note := strings.TrimSpace(record.ExecutionNote); note != "" {
return gsresponse.FailedPrecondition[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, "quote_not_executable", merrors.InvalidArgument(note))
}
intents := record.Intents
quotes := record.Quotes
@@ -209,6 +212,8 @@ func (h *initiatePaymentCommand) Execute(ctx context.Context, req *orchestratorv
return gsresponse.FailedPrecondition[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, qerr.code, qerr.err)
case "quote_expired":
return gsresponse.FailedPrecondition[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, qerr.code, qerr.err)
case "quote_not_executable":
return gsresponse.FailedPrecondition[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, qerr.code, qerr.err)
case "quote_intent_mismatch":
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, qerr.err)
default:

View File

@@ -123,6 +123,9 @@ func (s *Service) resolvePaymentQuote(ctx context.Context, in quoteResolutionInp
if !record.ExpiresAt.IsZero() && s.clock.Now().After(record.ExpiresAt) {
return nil, nil, nil, quoteResolutionError{code: "quote_expired", err: merrors.InvalidArgument("quote_ref expired")}
}
if note := strings.TrimSpace(record.ExecutionNote); note != "" {
return nil, nil, nil, quoteResolutionError{code: "quote_not_executable", err: merrors.InvalidArgument(note)}
}
intent, err := recordIntentFromQuote(record)
if err != nil {
return nil, nil, nil, err

View File

@@ -125,6 +125,38 @@ func TestResolvePaymentQuote_Expired(t *testing.T) {
}
}
func TestResolvePaymentQuote_NotExecutable(t *testing.T) {
org := bson.NewObjectID()
intent := &sharedv1.PaymentIntent{
Ref: "ref-1",
Amount: &moneyv1.Money{Currency: "USD", Amount: "1"},
SettlementCurrency: "USD",
}
record := &model.PaymentQuoteRecord{
QuoteRef: "q1",
Intent: intentFromProto(intent),
Quote: &model.PaymentQuoteSnapshot{},
ExecutionNote: "quote will not be executed: amount 1 USD below per-tx min limit 10",
ExpiresAt: time.Now().Add(time.Minute),
IdempotencyKey: "idem-1",
}
svc := &Service{
storage: stubRepo{quotes: &helperQuotesStore{records: map[string]*model.PaymentQuoteRecord{"q1": record}}},
clock: clockpkg.NewSystem(),
}
_, _, _, err := svc.resolvePaymentQuote(context.Background(), quoteResolutionInput{
OrgRef: org.Hex(),
OrgID: org,
Meta: &sharedv1.RequestMeta{OrganizationRef: org.Hex()},
Intent: intent,
QuoteRef: "q1",
})
if qerr, ok := err.(quoteResolutionError); !ok || qerr.code != "quote_not_executable" {
t.Fatalf("expected quote_not_executable, got %v", err)
}
}
func TestResolvePaymentQuote_QuoteRefUsesStoredIntent(t *testing.T) {
org := bson.NewObjectID()
intent := &sharedv1.PaymentIntent{