fixed linting config
This commit is contained in:
@@ -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 = firstLimitValue(dst.PerTxMinAmount, commonMin)
|
||||
}
|
||||
if commonMaxInit && commonMaxConsistent {
|
||||
dst.PerTxMaxAmount = firstLimitValue(dst.PerTxMaxAmount, commonMax)
|
||||
}
|
||||
}
|
||||
|
||||
func firstLimitValue(primary, fallback string) string {
|
||||
primary = strings.TrimSpace(primary)
|
||||
if primary != "" {
|
||||
return primary
|
||||
}
|
||||
return strings.TrimSpace(fallback)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package quotation
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,11 @@ type quoteCtx struct {
|
||||
hash string
|
||||
}
|
||||
|
||||
type quotePaymentResult struct {
|
||||
quote *sharedv1.PaymentQuote
|
||||
executionNote string
|
||||
}
|
||||
|
||||
func (h *quotePaymentCommand) Execute(
|
||||
ctx context.Context,
|
||||
req *quotationv1.QuotePaymentRequest,
|
||||
@@ -65,14 +70,15 @@ func (h *quotePaymentCommand) Execute(
|
||||
return gsresponse.Unavailable[quotationv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||
}
|
||||
|
||||
quoteProto, err := h.quotePayment(ctx, quotesStore, qc, req)
|
||||
result, err := h.quotePayment(ctx, quotesStore, qc, req)
|
||||
if err != nil {
|
||||
return h.mapQuoteErr(err)
|
||||
}
|
||||
|
||||
return gsresponse.Success("ationv1.QuotePaymentResponse{
|
||||
IdempotencyKey: req.GetIdempotencyKey(),
|
||||
Quote: quoteProto,
|
||||
Quote: result.quote,
|
||||
ExecutionNote: result.executionNote,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -111,7 +117,7 @@ func (h *quotePaymentCommand) quotePayment(
|
||||
quotesStore storage.QuotesStore,
|
||||
qc *quoteCtx,
|
||||
req *quotationv1.QuotePaymentRequest,
|
||||
) (*sharedv1.PaymentQuote, error) {
|
||||
) (*quotePaymentResult, error) {
|
||||
|
||||
if qc.previewOnly {
|
||||
quote, _, err := h.engine.BuildPaymentQuote(ctx, qc.orgID, req)
|
||||
@@ -120,7 +126,7 @@ func (h *quotePaymentCommand) quotePayment(
|
||||
return nil, err
|
||||
}
|
||||
quote.QuoteRef = bson.NewObjectID().Hex()
|
||||
return quote, nil
|
||||
return "ePaymentResult{quote: quote}, nil
|
||||
}
|
||||
|
||||
existing, err := quotesStore.GetByIdempotencyKey(ctx, qc.orgRef, qc.idempotencyKey)
|
||||
@@ -140,7 +146,10 @@ func (h *quotePaymentCommand) quotePayment(
|
||||
zap.String("idempotency_key", qc.idempotencyKey),
|
||||
zap.String("quote_ref", existing.QuoteRef),
|
||||
)
|
||||
return modelQuoteToProto(existing.Quote), nil
|
||||
return "ePaymentResult{
|
||||
quote: modelQuoteToProto(existing.Quote),
|
||||
executionNote: strings.TrimSpace(existing.ExecutionNote),
|
||||
}, nil
|
||||
}
|
||||
|
||||
quote, expiresAt, err := h.engine.BuildPaymentQuote(ctx, qc.orgID, req)
|
||||
@@ -157,17 +166,28 @@ func (h *quotePaymentCommand) quotePayment(
|
||||
quoteRef := bson.NewObjectID().Hex()
|
||||
quote.QuoteRef = quoteRef
|
||||
|
||||
executionNote := ""
|
||||
plan, err := h.engine.BuildPaymentPlan(ctx, qc.orgRef, qc.intent, qc.idempotencyKey, quote)
|
||||
if err != nil {
|
||||
h.logger.Warn(
|
||||
"Failed to build payment plan",
|
||||
zap.Error(err),
|
||||
mzap.ObjRef("org_ref", qc.orgRef),
|
||||
zap.String("idempotency_key", qc.idempotencyKey),
|
||||
)
|
||||
return nil, err
|
||||
if errors.Is(err, merrors.ErrInvalidArg) {
|
||||
executionNote = quoteNonExecutableNote(err)
|
||||
h.logger.Info(
|
||||
"Payment quote marked as non-executable",
|
||||
mzap.ObjRef("org_ref", qc.orgRef),
|
||||
zap.String("idempotency_key", qc.idempotencyKey),
|
||||
zap.String("quote_ref", quoteRef),
|
||||
zap.String("execution_note", executionNote),
|
||||
)
|
||||
} else {
|
||||
h.logger.Warn(
|
||||
"Failed to build payment plan",
|
||||
zap.Error(err),
|
||||
mzap.ObjRef("org_ref", qc.orgRef),
|
||||
zap.String("idempotency_key", qc.idempotencyKey),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
record := &model.PaymentQuoteRecord{
|
||||
QuoteRef: quoteRef,
|
||||
IdempotencyKey: qc.idempotencyKey,
|
||||
@@ -175,6 +195,7 @@ func (h *quotePaymentCommand) quotePayment(
|
||||
Intent: intentFromProto(qc.intent),
|
||||
Quote: quoteSnapshotToModel(quote),
|
||||
Plan: cloneStoredPaymentPlan(plan),
|
||||
ExecutionNote: executionNote,
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
record.SetID(bson.NewObjectID())
|
||||
@@ -187,7 +208,10 @@ func (h *quotePaymentCommand) quotePayment(
|
||||
if existing.Hash != qc.hash {
|
||||
return nil, errIdempotencyParamMismatch
|
||||
}
|
||||
return modelQuoteToProto(existing.Quote), nil
|
||||
return "ePaymentResult{
|
||||
quote: modelQuoteToProto(existing.Quote),
|
||||
executionNote: strings.TrimSpace(existing.ExecutionNote),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
@@ -201,7 +225,10 @@ func (h *quotePaymentCommand) quotePayment(
|
||||
zap.String("kind", qc.intent.GetKind().String()),
|
||||
)
|
||||
|
||||
return quote, nil
|
||||
return "ePaymentResult{
|
||||
quote: quote,
|
||||
executionNote: executionNote,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *quotePaymentCommand) mapQuoteErr(err error) gsresponse.Responder[quotationv1.QuotePaymentResponse] {
|
||||
@@ -213,6 +240,16 @@ func (h *quotePaymentCommand) mapQuoteErr(err error) gsresponse.Responder[quotat
|
||||
return gsresponse.Auto[quotationv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
|
||||
}
|
||||
|
||||
func quoteNonExecutableNote(err error) string {
|
||||
reason := strings.TrimSpace(err.Error())
|
||||
reason = strings.TrimPrefix(reason, merrors.ErrInvalidArg.Error()+":")
|
||||
reason = strings.TrimSpace(reason)
|
||||
if reason == "" {
|
||||
return "quote will not be executed"
|
||||
}
|
||||
return "quote will not be executed: " + reason
|
||||
}
|
||||
|
||||
// TODO: temprorarary hashing function, replace with a proper solution later
|
||||
func hashQuoteRequest(req *quotationv1.QuotePaymentRequest) string {
|
||||
cloned := proto.Clone(req).(*quotationv1.QuotePaymentRequest)
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
package quotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/payments/storage"
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
mloggerfactory "github.com/tech/sendico/pkg/mlogger/factory"
|
||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||
quotationv1 "github.com/tech/sendico/pkg/proto/payments/quotation/v1"
|
||||
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestQuotePaymentStoresNonExecutableQuoteWhenPlanInvalid(t *testing.T) {
|
||||
org := bson.NewObjectID()
|
||||
req := "ationv1.QuotePaymentRequest{
|
||||
Meta: &sharedv1.RequestMeta{OrganizationRef: org.Hex()},
|
||||
IdempotencyKey: "idem-1",
|
||||
Intent: &sharedv1.PaymentIntent{
|
||||
Kind: sharedv1.PaymentKind_PAYMENT_KIND_PAYOUT,
|
||||
Amount: &moneyv1.Money{Currency: "USD", Amount: "1"},
|
||||
SettlementCurrency: "USD",
|
||||
},
|
||||
}
|
||||
|
||||
quotesStore := "eCommandTestQuotesStore{
|
||||
byID: make(map[string]*model.PaymentQuoteRecord),
|
||||
}
|
||||
engine := "eCommandTestEngine{
|
||||
repo: quoteCommandTestRepo{quotes: quotesStore},
|
||||
buildQuoteFn: func(context.Context, string, *quotationv1.QuotePaymentRequest) (*sharedv1.PaymentQuote, time.Time, error) {
|
||||
return &sharedv1.PaymentQuote{
|
||||
DebitAmount: &moneyv1.Money{Currency: "USD", Amount: "1"},
|
||||
}, time.Now().Add(time.Hour), nil
|
||||
},
|
||||
buildPlanFn: func(context.Context, bson.ObjectID, *sharedv1.PaymentIntent, string, *sharedv1.PaymentQuote) (*model.PaymentPlan, error) {
|
||||
return nil, merrors.InvalidArgument("plan builder: no eligible gateway instance found, last error: gateway mntx eligibility check error: amount 1 USD below per-tx min limit 10")
|
||||
},
|
||||
}
|
||||
cmd := "ePaymentCommand{
|
||||
engine: engine,
|
||||
logger: mloggerfactory.NewLogger(false),
|
||||
}
|
||||
|
||||
resp, err := cmd.Execute(context.Background(), req)(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if resp == nil || resp.GetQuote() == nil {
|
||||
t.Fatalf("expected quote response, got %#v", resp)
|
||||
}
|
||||
if note := resp.GetExecutionNote(); !strings.Contains(note, "quote will not be executed") {
|
||||
t.Fatalf("expected non-executable note, got %q", note)
|
||||
}
|
||||
|
||||
stored := quotesStore.byID[req.GetIdempotencyKey()]
|
||||
if stored == nil {
|
||||
t.Fatalf("expected stored quote record")
|
||||
}
|
||||
if stored.Plan != nil {
|
||||
t.Fatalf("expected no stored payment plan for non-executable quote")
|
||||
}
|
||||
if stored.ExecutionNote != resp.GetExecutionNote() {
|
||||
t.Fatalf("expected stored execution note %q, got %q", resp.GetExecutionNote(), stored.ExecutionNote)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuotePaymentReuseReturnsStoredExecutionNote(t *testing.T) {
|
||||
org := bson.NewObjectID()
|
||||
req := "ationv1.QuotePaymentRequest{
|
||||
Meta: &sharedv1.RequestMeta{OrganizationRef: org.Hex()},
|
||||
IdempotencyKey: "idem-1",
|
||||
Intent: &sharedv1.PaymentIntent{
|
||||
Kind: sharedv1.PaymentKind_PAYMENT_KIND_PAYOUT,
|
||||
Amount: &moneyv1.Money{Currency: "USD", Amount: "1"},
|
||||
SettlementCurrency: "USD",
|
||||
},
|
||||
}
|
||||
|
||||
existing := &model.PaymentQuoteRecord{
|
||||
QuoteRef: "q1",
|
||||
IdempotencyKey: req.GetIdempotencyKey(),
|
||||
Hash: hashQuoteRequest(req),
|
||||
Quote: &model.PaymentQuoteSnapshot{QuoteRef: "q1"},
|
||||
ExecutionNote: "quote will not be executed: amount 1 USD below per-tx min limit 10",
|
||||
}
|
||||
quotesStore := "eCommandTestQuotesStore{
|
||||
byID: map[string]*model.PaymentQuoteRecord{
|
||||
req.GetIdempotencyKey(): existing,
|
||||
},
|
||||
}
|
||||
engine := "eCommandTestEngine{
|
||||
repo: quoteCommandTestRepo{quotes: quotesStore},
|
||||
buildQuoteFn: func(context.Context, string, *quotationv1.QuotePaymentRequest) (*sharedv1.PaymentQuote, time.Time, error) {
|
||||
t.Fatalf("build quote should not be called on idempotent reuse")
|
||||
return nil, time.Time{}, nil
|
||||
},
|
||||
buildPlanFn: func(context.Context, bson.ObjectID, *sharedv1.PaymentIntent, string, *sharedv1.PaymentQuote) (*model.PaymentPlan, error) {
|
||||
t.Fatalf("build plan should not be called on idempotent reuse")
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
cmd := "ePaymentCommand{
|
||||
engine: engine,
|
||||
logger: mloggerfactory.NewLogger(false),
|
||||
}
|
||||
|
||||
resp, err := cmd.Execute(context.Background(), req)(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if resp == nil {
|
||||
t.Fatalf("expected response")
|
||||
}
|
||||
if got, want := resp.GetExecutionNote(), existing.ExecutionNote; got != want {
|
||||
t.Fatalf("expected execution note %q, got %q", want, got)
|
||||
}
|
||||
if resp.GetQuote().GetQuoteRef() != "q1" {
|
||||
t.Fatalf("expected quote_ref q1, got %q", resp.GetQuote().GetQuoteRef())
|
||||
}
|
||||
}
|
||||
|
||||
type quoteCommandTestEngine struct {
|
||||
repo storage.Repository
|
||||
ensureErr error
|
||||
buildQuoteFn func(ctx context.Context, orgRef string, req *quotationv1.QuotePaymentRequest) (*sharedv1.PaymentQuote, time.Time, error)
|
||||
buildPlanFn func(ctx context.Context, orgID bson.ObjectID, intent *sharedv1.PaymentIntent, idempotencyKey string, quote *sharedv1.PaymentQuote) (*model.PaymentPlan, error)
|
||||
}
|
||||
|
||||
func (e *quoteCommandTestEngine) EnsureRepository(context.Context) error { return e.ensureErr }
|
||||
|
||||
func (e *quoteCommandTestEngine) BuildPaymentQuote(ctx context.Context, orgRef string, req *quotationv1.QuotePaymentRequest) (*sharedv1.PaymentQuote, time.Time, error) {
|
||||
if e.buildQuoteFn == nil {
|
||||
return nil, time.Time{}, nil
|
||||
}
|
||||
return e.buildQuoteFn(ctx, orgRef, req)
|
||||
}
|
||||
|
||||
func (e *quoteCommandTestEngine) BuildPaymentPlan(ctx context.Context, orgID bson.ObjectID, intent *sharedv1.PaymentIntent, idempotencyKey string, quote *sharedv1.PaymentQuote) (*model.PaymentPlan, error) {
|
||||
if e.buildPlanFn == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return e.buildPlanFn(ctx, orgID, intent, idempotencyKey, quote)
|
||||
}
|
||||
|
||||
func (e *quoteCommandTestEngine) ResolvePaymentQuote(context.Context, quoteResolutionInput) (*sharedv1.PaymentQuote, *sharedv1.PaymentIntent, *model.PaymentPlan, error) {
|
||||
return nil, nil, nil, nil
|
||||
}
|
||||
|
||||
func (e *quoteCommandTestEngine) Repository() storage.Repository { return e.repo }
|
||||
|
||||
type quoteCommandTestRepo struct {
|
||||
quotes storage.QuotesStore
|
||||
}
|
||||
|
||||
func (r quoteCommandTestRepo) Ping(context.Context) error { return nil }
|
||||
func (r quoteCommandTestRepo) Payments() storage.PaymentsStore { return nil }
|
||||
func (r quoteCommandTestRepo) Quotes() storage.QuotesStore { return r.quotes }
|
||||
func (r quoteCommandTestRepo) Routes() storage.RoutesStore { return nil }
|
||||
func (r quoteCommandTestRepo) PlanTemplates() storage.PlanTemplatesStore { return nil }
|
||||
|
||||
type quoteCommandTestQuotesStore struct {
|
||||
byID map[string]*model.PaymentQuoteRecord
|
||||
}
|
||||
|
||||
func (s *quoteCommandTestQuotesStore) Create(_ context.Context, rec *model.PaymentQuoteRecord) error {
|
||||
if s.byID == nil {
|
||||
s.byID = make(map[string]*model.PaymentQuoteRecord)
|
||||
}
|
||||
s.byID[rec.IdempotencyKey] = rec
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *quoteCommandTestQuotesStore) GetByRef(_ context.Context, _ bson.ObjectID, quoteRef string) (*model.PaymentQuoteRecord, error) {
|
||||
for _, rec := range s.byID {
|
||||
if rec != nil && rec.QuoteRef == quoteRef {
|
||||
return rec, nil
|
||||
}
|
||||
}
|
||||
return nil, storage.ErrQuoteNotFound
|
||||
}
|
||||
|
||||
func (s *quoteCommandTestQuotesStore) GetByIdempotencyKey(_ context.Context, _ bson.ObjectID, idempotencyKey string) (*model.PaymentQuoteRecord, error) {
|
||||
if rec, ok := s.byID[idempotencyKey]; ok {
|
||||
return rec, nil
|
||||
}
|
||||
return nil, storage.ErrQuoteNotFound
|
||||
}
|
||||
@@ -85,6 +85,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
|
||||
|
||||
Reference in New Issue
Block a user