quotation bff

This commit is contained in:
Stephan D
2025-12-09 16:29:29 +01:00
parent cecaebfc5e
commit ce59cb1b26
61 changed files with 2204 additions and 632 deletions

View File

@@ -54,8 +54,8 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect

View File

@@ -179,15 +179,15 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=

View File

@@ -97,6 +97,7 @@ func quoteSnapshotToModel(src *orchestratorv1.PaymentQuote) *model.PaymentQuoteS
FXQuote: cloneFXQuote(src.GetFxQuote()),
NetworkFee: cloneNetworkEstimate(src.GetNetworkFee()),
FeeQuoteToken: strings.TrimSpace(src.GetFeeQuoteToken()),
QuoteRef: strings.TrimSpace(src.GetQuoteRef()),
}
}
@@ -220,6 +221,7 @@ func modelQuoteToProto(src *model.PaymentQuoteSnapshot) *orchestratorv1.PaymentQ
FxQuote: cloneFXQuote(src.FXQuote),
NetworkFee: cloneNetworkEstimate(src.NetworkFee),
FeeQuoteToken: src.FeeQuoteToken,
QuoteRef: strings.TrimSpace(src.QuoteRef),
}
}

View File

@@ -19,13 +19,13 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
)
func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.PaymentQuote, error) {
func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.PaymentQuote, time.Time, error) {
intent := req.GetIntent()
amount := intent.GetAmount()
baseAmount := cloneMoney(amount)
feeQuote, err := s.quoteFees(ctx, orgRef, req)
if err != nil {
return nil, err
return nil, time.Time{}, err
}
feeTotal := extractFeeTotal(feeQuote.GetLines(), amount.GetCurrency())
@@ -33,7 +33,7 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orc
if shouldEstimateNetworkFee(intent) {
networkFee, err = s.estimateNetworkFee(ctx, intent)
if err != nil {
return nil, err
return nil, time.Time{}, err
}
}
@@ -41,13 +41,13 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orc
if shouldRequestFX(intent) {
fxQuote, err = s.requestFXQuote(ctx, orgRef, req)
if err != nil {
return nil, err
return nil, time.Time{}, err
}
}
debitAmount, settlementAmount := computeAggregates(baseAmount, feeTotal, networkFee)
return &orchestratorv1.PaymentQuote{
quote := &orchestratorv1.PaymentQuote{
DebitAmount: debitAmount,
ExpectedSettlementAmount: settlementAmount,
ExpectedFeeTotal: feeTotal,
@@ -56,7 +56,11 @@ func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orc
FxQuote: fxQuote,
NetworkFee: networkFee,
FeeQuoteToken: feeQuote.GetFeeQuoteToken(),
}, nil
}
expiresAt := quoteExpiry(s.clock.Now(), feeQuote, fxQuote)
return quote, expiresAt, nil
}
func (s *Service) quoteFees(ctx context.Context, orgRef string, req *orchestratorv1.QuotePaymentRequest) (*feesv1.PrecomputeFeesResponse, error) {

View File

@@ -2,6 +2,7 @@ package orchestrator
import (
"strings"
"time"
"github.com/shopspring/decimal"
oracleclient "github.com/tech/sendico/fx/oracle/client"
@@ -219,6 +220,23 @@ func ledgerLineTypeFromAccounting(lineType accountingv1.PostingLineType) ledgerv
}
}
func quoteExpiry(now time.Time, feeQuote *feesv1.PrecomputeFeesResponse, fxQuote *oraclev1.Quote) time.Time {
expiry := time.Time{}
if feeQuote != nil && feeQuote.GetExpiresAt() != nil {
expiry = feeQuote.GetExpiresAt().AsTime()
}
if expiry.IsZero() {
expiry = now.Add(time.Duration(defaultFeeQuoteTTLMillis) * time.Millisecond)
}
if fxQuote != nil && fxQuote.GetExpiresAtUnixMs() > 0 {
fxExpiry := time.UnixMilli(fxQuote.GetExpiresAtUnixMs()).UTC()
if fxExpiry.Before(expiry) {
expiry = fxExpiry
}
}
return expiry
}
func feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []*chainv1.ServiceFeeBreakdown {
if quote == nil {
return nil

View File

@@ -3,8 +3,8 @@ package orchestrator
import (
"time"
chainclient "github.com/tech/sendico/gateway/chain/client"
oracleclient "github.com/tech/sendico/fx/oracle/client"
chainclient "github.com/tech/sendico/gateway/chain/client"
ledgerclient "github.com/tech/sendico/ledger/client"
clockpkg "github.com/tech/sendico/pkg/clock"
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"

View File

@@ -16,6 +16,7 @@ import (
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
"go.mongodb.org/mongo-driver/bson/primitive"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
)
type serviceError string
@@ -132,6 +133,10 @@ func (s *Service) quotePaymentHandler(ctx context.Context, req *orchestratorv1.Q
if orgRef == "" {
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("organization_ref is required"))
}
orgObjectID, parseErr := primitive.ObjectIDFromHex(orgRef)
if parseErr != nil {
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("organization_ref must be a valid objectID"))
}
intent := req.GetIntent()
if intent == nil {
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("intent is required"))
@@ -140,11 +145,31 @@ func (s *Service) quotePaymentHandler(ctx context.Context, req *orchestratorv1.Q
return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("intent.amount is required"))
}
quote, err := s.buildPaymentQuote(ctx, orgRef, req)
quote, expiresAt, err := s.buildPaymentQuote(ctx, orgRef, req)
if err != nil {
return gsresponse.Auto[orchestratorv1.QuotePaymentResponse](s.logger, mservice.PaymentOrchestrator, err)
}
if !req.GetPreviewOnly() {
quotesStore := s.storage.Quotes()
if quotesStore == nil {
return gsresponse.Unavailable[orchestratorv1.QuotePaymentResponse](s.logger, mservice.PaymentOrchestrator, errStorageUnavailable)
}
quoteRef := primitive.NewObjectID().Hex()
quote.QuoteRef = quoteRef
record := &model.PaymentQuoteRecord{
QuoteRef: quoteRef,
Intent: intentFromProto(intent),
Quote: quoteSnapshotToModel(quote),
ExpiresAt: expiresAt,
}
record.SetID(primitive.NewObjectID())
record.SetOrganizationRef(orgObjectID)
if err := quotesStore.Create(ctx, record); err != nil {
return gsresponse.Auto[orchestratorv1.QuotePaymentResponse](s.logger, mservice.PaymentOrchestrator, err)
}
}
return gsresponse.Success(&orchestratorv1.QuotePaymentResponse{Quote: quote})
}
@@ -194,10 +219,34 @@ func (s *Service) initiatePaymentHandler(ctx context.Context, req *orchestratorv
return gsresponse.Auto[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, err)
}
quote := req.GetFeeQuoteToken()
quoteRef := strings.TrimSpace(req.GetQuoteRef())
quote := strings.TrimSpace(req.GetFeeQuoteToken())
var quoteSnapshot *orchestratorv1.PaymentQuote
if quote == "" {
quoteSnapshot, err = s.buildPaymentQuote(ctx, orgRef, &orchestratorv1.QuotePaymentRequest{
if quoteRef != "" {
quotesStore := s.storage.Quotes()
if quotesStore == nil {
return gsresponse.Unavailable[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, errStorageUnavailable)
}
record, err := quotesStore.GetByRef(ctx, orgObjectID, quoteRef)
if err != nil {
if err == storage.ErrQuoteNotFound {
return gsresponse.FailedPrecondition[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, "quote_not_found", merrors.InvalidArgument("quote_ref not found or expired"))
}
return gsresponse.Auto[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, err)
}
if !record.ExpiresAt.IsZero() && s.clock.Now().After(record.ExpiresAt) {
return gsresponse.FailedPrecondition[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, "quote_expired", merrors.InvalidArgument("quote_ref expired"))
}
if !proto.Equal(protoIntentFromModel(record.Intent), intent) {
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("quote_ref does not match intent"))
}
quoteSnapshot = modelQuoteToProto(record.Quote)
if quoteSnapshot == nil {
return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("stored quote is empty"))
}
quoteSnapshot.QuoteRef = quoteRef
} else if quote == "" {
quoteSnapshot, _, err = s.buildPaymentQuote(ctx, orgRef, &orchestratorv1.QuotePaymentRequest{
Meta: req.GetMeta(),
IdempotencyKey: req.GetIdempotencyKey(),
Intent: req.GetIntent(),
@@ -389,7 +438,7 @@ func (s *Service) initiateConversionHandler(ctx context.Context, req *orchestrat
FeePolicy: req.GetFeePolicy(),
}
quote, err := s.buildPaymentQuote(ctx, orgRef, &orchestratorv1.QuotePaymentRequest{
quote, _, err := s.buildPaymentQuote(ctx, orgRef, &orchestratorv1.QuotePaymentRequest{
Meta: req.GetMeta(),
IdempotencyKey: req.GetIdempotencyKey(),
Intent: intentProto,

View File

@@ -12,6 +12,7 @@ import (
"github.com/tech/sendico/payments/orchestrator/storage"
"github.com/tech/sendico/payments/orchestrator/storage/model"
"github.com/tech/sendico/pkg/api/routers/gsresponse"
"github.com/tech/sendico/pkg/merrors"
mo "github.com/tech/sendico/pkg/model"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
@@ -208,11 +209,43 @@ func TestProcessDepositObservedHandler_MatchesPayment(t *testing.T) {
// ----------------------------------------------------------------------
type stubRepository struct {
store *stubPaymentsStore
store *stubPaymentsStore
quotes storage.QuotesStore
}
func (r *stubRepository) Ping(context.Context) error { return nil }
func (r *stubRepository) Payments() storage.PaymentsStore { return r.store }
func (r *stubRepository) Quotes() storage.QuotesStore {
if r.quotes != nil {
return r.quotes
}
return &stubQuotesStore{}
}
type stubQuotesStore struct {
quotes map[string]*model.PaymentQuoteRecord
}
func (s *stubQuotesStore) Create(ctx context.Context, quote *model.PaymentQuoteRecord) error {
if quote == nil {
return merrors.InvalidArgument("nil quote")
}
if s.quotes == nil {
s.quotes = map[string]*model.PaymentQuoteRecord{}
}
s.quotes[strings.TrimSpace(quote.QuoteRef)] = quote
return nil
}
func (s *stubQuotesStore) GetByRef(ctx context.Context, orgRef primitive.ObjectID, quoteRef string) (*model.PaymentQuoteRecord, error) {
if s.quotes == nil {
return nil, storage.ErrQuoteNotFound
}
if q, ok := s.quotes[strings.TrimSpace(quoteRef)]; ok {
return q, nil
}
return nil, storage.ErrQuoteNotFound
}
type stubPaymentsStore struct {
payments map[string]*model.Payment

View File

@@ -119,6 +119,7 @@ type PaymentQuoteSnapshot struct {
FXQuote *oraclev1.Quote `bson:"fxQuote,omitempty" json:"fxQuote,omitempty"`
NetworkFee *chainv1.EstimateTransferFeeResponse `bson:"networkFee,omitempty" json:"networkFee,omitempty"`
FeeQuoteToken string `bson:"feeQuoteToken,omitempty" json:"feeQuoteToken,omitempty"`
QuoteRef string `bson:"quoteRef,omitempty" json:"quoteRef,omitempty"`
}
// ExecutionRefs links to downstream systems.

View File

@@ -0,0 +1,24 @@
package model
import (
"time"
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/model"
)
// PaymentQuoteRecord stores a quoted payment snapshot for later execution.
type PaymentQuoteRecord struct {
storable.Base `bson:",inline" json:",inline"`
model.OrganizationBoundBase `bson:",inline" json:",inline"`
QuoteRef string `bson:"quoteRef" json:"quoteRef"`
Intent PaymentIntent `bson:"intent" json:"intent"`
Quote *PaymentQuoteSnapshot `bson:"quote" json:"quote"`
ExpiresAt time.Time `bson:"expiresAt" json:"expiresAt"`
}
// Collection implements storable.Storable.
func (*PaymentQuoteRecord) Collection() string {
return "payment_quotes"
}

View File

@@ -18,6 +18,7 @@ type Store struct {
ping func(context.Context) error
payments storage.PaymentsStore
quotes storage.QuotesStore
}
// New constructs a Mongo-backed payments repository from a Mongo connection.
@@ -25,28 +26,37 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) {
if conn == nil {
return nil, merrors.InvalidArgument("payments.storage.mongo: connection is nil")
}
repo := repository.CreateMongoRepository(conn.Database(), (&model.Payment{}).Collection())
return NewWithRepository(logger, conn.Ping, repo)
paymentsRepo := repository.CreateMongoRepository(conn.Database(), (&model.Payment{}).Collection())
quotesRepo := repository.CreateMongoRepository(conn.Database(), (&model.PaymentQuoteRecord{}).Collection())
return NewWithRepository(logger, conn.Ping, paymentsRepo, quotesRepo)
}
// NewWithRepository constructs a payments repository using the provided primitives.
func NewWithRepository(logger mlogger.Logger, ping func(context.Context) error, paymentsRepo repository.Repository) (*Store, error) {
func NewWithRepository(logger mlogger.Logger, ping func(context.Context) error, paymentsRepo repository.Repository, quotesRepo repository.Repository) (*Store, error) {
if ping == nil {
return nil, merrors.InvalidArgument("payments.storage.mongo: ping func is nil")
}
if paymentsRepo == nil {
return nil, merrors.InvalidArgument("payments.storage.mongo: payments repository is nil")
}
if quotesRepo == nil {
return nil, merrors.InvalidArgument("payments.storage.mongo: quotes repository is nil")
}
childLogger := logger.Named("storage").Named("mongo")
paymentsStore, err := store.NewPayments(childLogger, paymentsRepo)
if err != nil {
return nil, err
}
quotesStore, err := store.NewQuotes(childLogger, quotesRepo)
if err != nil {
return nil, err
}
result := &Store{
logger: childLogger,
ping: ping,
payments: paymentsStore,
quotes: quotesStore,
}
return result, nil
@@ -65,4 +75,9 @@ func (s *Store) Payments() storage.PaymentsStore {
return s.payments
}
// Quotes returns the quotes store.
func (s *Store) Quotes() storage.QuotesStore {
return s.quotes
}
var _ storage.Repository = (*Store)(nil)

View File

@@ -0,0 +1,113 @@
package store
import (
"context"
"errors"
"strings"
"time"
"github.com/tech/sendico/payments/orchestrator/storage"
"github.com/tech/sendico/payments/orchestrator/storage/model"
"github.com/tech/sendico/pkg/db/repository"
ri "github.com/tech/sendico/pkg/db/repository/index"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
type Quotes struct {
logger mlogger.Logger
repo repository.Repository
}
// NewQuotes constructs a Mongo-backed quotes store.
func NewQuotes(logger mlogger.Logger, repo repository.Repository) (*Quotes, error) {
if repo == nil {
return nil, merrors.InvalidArgument("quotesStore: repository is nil")
}
indexes := []*ri.Definition{
{
Keys: []ri.Key{{Field: "quoteRef", Sort: ri.Asc}},
Unique: true,
},
{
Keys: []ri.Key{{Field: "organizationRef", Sort: ri.Asc}},
},
{
Keys: []ri.Key{{Field: "expiresAt", Sort: ri.Asc}},
ExpireAfterSeconds: 0,
},
}
for _, def := range indexes {
if err := repo.CreateIndex(def); err != nil {
logger.Error("failed to ensure quotes index", zap.Error(err), zap.String("collection", repo.Collection()))
return nil, err
}
}
return &Quotes{
logger: logger.Named("quotes"),
repo: repo,
}, nil
}
func (q *Quotes) Create(ctx context.Context, quote *model.PaymentQuoteRecord) error {
if quote == nil {
return merrors.InvalidArgument("quotesStore: nil quote")
}
quote.QuoteRef = strings.TrimSpace(quote.QuoteRef)
if quote.QuoteRef == "" {
return merrors.InvalidArgument("quotesStore: empty quoteRef")
}
if quote.OrganizationRef == primitive.NilObjectID {
return merrors.InvalidArgument("quotesStore: organization_ref is required")
}
if quote.ExpiresAt.IsZero() {
return merrors.InvalidArgument("quotesStore: expires_at is required")
}
if quote.Intent.Attributes != nil {
for k, v := range quote.Intent.Attributes {
quote.Intent.Attributes[k] = strings.TrimSpace(v)
}
}
quote.Update()
filter := repository.OrgFilter(quote.OrganizationRef).And(
repository.Filter("quoteRef", quote.QuoteRef),
)
if err := q.repo.Insert(ctx, quote, filter); err != nil {
if errors.Is(err, merrors.ErrDataConflict) {
return storage.ErrDuplicateQuote
}
return err
}
return nil
}
func (q *Quotes) GetByRef(ctx context.Context, orgRef primitive.ObjectID, quoteRef string) (*model.PaymentQuoteRecord, error) {
quoteRef = strings.TrimSpace(quoteRef)
if quoteRef == "" {
return nil, merrors.InvalidArgument("quotesStore: empty quoteRef")
}
if orgRef == primitive.NilObjectID {
return nil, merrors.InvalidArgument("quotesStore: organization_ref is required")
}
entity := &model.PaymentQuoteRecord{}
query := repository.OrgFilter(orgRef).And(repository.Filter("quoteRef", quoteRef))
if err := q.repo.FindOneByFilter(ctx, query, entity); err != nil {
if errors.Is(err, merrors.ErrNoData) {
return nil, storage.ErrQuoteNotFound
}
return nil, err
}
if !entity.ExpiresAt.IsZero() && time.Now().After(entity.ExpiresAt) {
return nil, storage.ErrQuoteNotFound
}
return entity, nil
}
var _ storage.QuotesStore = (*Quotes)(nil)

View File

@@ -18,12 +18,17 @@ var (
ErrPaymentNotFound = storageError("payments.orchestrator.storage: payment not found")
// ErrDuplicatePayment signals that idempotency constraints were violated.
ErrDuplicatePayment = storageError("payments.orchestrator.storage: duplicate payment")
// ErrQuoteNotFound signals that a stored quote does not exist or expired.
ErrQuoteNotFound = storageError("payments.orchestrator.storage: quote not found")
// ErrDuplicateQuote signals that a quote reference already exists.
ErrDuplicateQuote = storageError("payments.orchestrator.storage: duplicate quote")
)
// Repository exposes persistence primitives for the orchestrator domain.
type Repository interface {
Ping(ctx context.Context) error
Payments() PaymentsStore
Quotes() QuotesStore
}
// PaymentsStore manages payment lifecycle state.
@@ -35,3 +40,9 @@ type PaymentsStore interface {
GetByChainTransferRef(ctx context.Context, transferRef string) (*model.Payment, error)
List(ctx context.Context, filter *model.PaymentFilter) (*model.PaymentList, error)
}
// QuotesStore manages temporary stored payment quotes.
type QuotesStore interface {
Create(ctx context.Context, quote *model.PaymentQuoteRecord) error
GetByRef(ctx context.Context, orgRef primitive.ObjectID, quoteRef string) (*model.PaymentQuoteRecord, error)
}