service backend
This commit is contained in:
113
api/fx/storage/mongo/store/currency.go
Normal file
113
api/fx/storage/mongo/store/currency.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/tech/sendico/fx/storage"
|
||||
"github.com/tech/sendico/fx/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/mongo"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type currencyStore struct {
|
||||
logger mlogger.Logger
|
||||
repo repository.Repository
|
||||
}
|
||||
|
||||
func NewCurrency(logger mlogger.Logger, db *mongo.Database) (storage.CurrencyStore, error) {
|
||||
repo := repository.CreateMongoRepository(db, model.CurrenciesCollection)
|
||||
|
||||
index := &ri.Definition{
|
||||
Keys: []ri.Key{
|
||||
{Field: "code", Sort: ri.Asc},
|
||||
},
|
||||
Unique: true,
|
||||
}
|
||||
if err := repo.CreateIndex(index); err != nil {
|
||||
logger.Error("failed to ensure currencies index", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
childLogger := logger.Named(model.CurrenciesCollection)
|
||||
childLogger.Debug("currency store initialised", zap.String("collection", model.CurrenciesCollection))
|
||||
|
||||
return ¤cyStore{
|
||||
logger: childLogger,
|
||||
repo: repo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *currencyStore) Get(ctx context.Context, code string) (*model.Currency, error) {
|
||||
if code == "" {
|
||||
c.logger.Warn("attempt to fetch currency with empty code")
|
||||
return nil, merrors.InvalidArgument("currencyStore: empty code")
|
||||
}
|
||||
result := &model.Currency{}
|
||||
if err := c.repo.FindOneByFilter(ctx, repository.Filter("code", code), result); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
c.logger.Debug("currency not found", zap.String("code", code))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
c.logger.Debug("currency loaded", zap.String("code", code))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *currencyStore) List(ctx context.Context, codes ...string) ([]*model.Currency, error) {
|
||||
query := repository.Query()
|
||||
if len(codes) > 0 {
|
||||
values := make([]any, len(codes))
|
||||
for i, code := range codes {
|
||||
values[i] = code
|
||||
}
|
||||
query = query.In(repository.Field("code"), values...)
|
||||
}
|
||||
|
||||
currencies := make([]*model.Currency, 0)
|
||||
err := c.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error {
|
||||
doc := &model.Currency{}
|
||||
if err := cur.Decode(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
currencies = append(currencies, doc)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
c.logger.Error("failed to list currencies", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
c.logger.Debug("listed currencies", zap.Int("count", len(currencies)))
|
||||
return currencies, nil
|
||||
}
|
||||
|
||||
func (c *currencyStore) Upsert(ctx context.Context, currency *model.Currency) error {
|
||||
if currency == nil {
|
||||
c.logger.Warn("attempt to upsert nil currency")
|
||||
return merrors.InvalidArgument("currencyStore: nil currency")
|
||||
}
|
||||
if currency.Code == "" {
|
||||
c.logger.Warn("attempt to upsert currency with empty code")
|
||||
return merrors.InvalidArgument("currencyStore: empty code")
|
||||
}
|
||||
|
||||
existing := &model.Currency{}
|
||||
filter := repository.Filter("code", currency.Code)
|
||||
if err := c.repo.FindOneByFilter(ctx, filter, existing); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
c.logger.Debug("inserting new currency", zap.String("code", currency.Code))
|
||||
return c.repo.Insert(ctx, currency, filter)
|
||||
}
|
||||
c.logger.Error("failed to fetch currency", zap.Error(err), zap.String("code", currency.Code))
|
||||
return err
|
||||
}
|
||||
|
||||
if existing.GetID() != nil {
|
||||
currency.SetID(*existing.GetID())
|
||||
}
|
||||
c.logger.Debug("updating currency", zap.String("code", currency.Code))
|
||||
return c.repo.Update(ctx, currency)
|
||||
}
|
||||
104
api/fx/storage/mongo/store/currency_test.go
Normal file
104
api/fx/storage/mongo/store/currency_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/fx/storage/model"
|
||||
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||
rd "github.com/tech/sendico/pkg/db/repository/decoder"
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestCurrencyStoreGet(t *testing.T) {
|
||||
repo := &repoStub{
|
||||
findOneFn: func(_ context.Context, _ builder.Query, result storable.Storable) error {
|
||||
currency := result.(*model.Currency)
|
||||
currency.Code = "USD"
|
||||
return nil
|
||||
},
|
||||
}
|
||||
store := ¤cyStore{logger: zap.NewNop(), repo: repo}
|
||||
|
||||
res, err := store.Get(context.Background(), "USD")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if res.Code != "USD" {
|
||||
t.Fatalf("unexpected code: %s", res.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrencyStoreList(t *testing.T) {
|
||||
repo := &repoStub{
|
||||
findManyFn: func(_ context.Context, _ builder.Query, decode rd.DecodingFunc) error {
|
||||
return runDecoderWithDocs(t, decode, &model.Currency{Code: "USD"})
|
||||
},
|
||||
}
|
||||
store := ¤cyStore{logger: zap.NewNop(), repo: repo}
|
||||
|
||||
currencies, err := store.List(context.Background(), "USD")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(currencies) != 1 || currencies[0].Code != "USD" {
|
||||
t.Fatalf("unexpected list result: %+v", currencies)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrencyStoreUpsertInsert(t *testing.T) {
|
||||
inserted := false
|
||||
repo := &repoStub{
|
||||
findOneFn: func(context.Context, builder.Query, storable.Storable) error {
|
||||
return merrors.ErrNoData
|
||||
},
|
||||
insertFn: func(_ context.Context, obj storable.Storable, _ builder.Query) error {
|
||||
_ = cloneCurrency(t, obj)
|
||||
inserted = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
store := ¤cyStore{logger: zap.NewNop(), repo: repo}
|
||||
|
||||
if err := store.Upsert(context.Background(), &model.Currency{Code: "USD"}); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !inserted {
|
||||
t.Fatalf("expected insert to be called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrencyStoreGetInvalid(t *testing.T) {
|
||||
store := ¤cyStore{logger: zap.NewNop(), repo: &repoStub{}}
|
||||
if _, err := store.Get(context.Background(), ""); !errors.Is(err, merrors.ErrInvalidArg) {
|
||||
t.Fatalf("expected invalid argument error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrencyStoreUpsertUpdate(t *testing.T) {
|
||||
var updated *model.Currency
|
||||
repo := &repoStub{
|
||||
findOneFn: func(_ context.Context, _ builder.Query, result storable.Storable) error {
|
||||
currency := result.(*model.Currency)
|
||||
currency.SetID(primitive.NewObjectID())
|
||||
currency.Code = "USD"
|
||||
return nil
|
||||
},
|
||||
updateFn: func(_ context.Context, obj storable.Storable) error {
|
||||
updated = cloneCurrency(t, obj)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
store := ¤cyStore{logger: zap.NewNop(), repo: repo}
|
||||
|
||||
if err := store.Upsert(context.Background(), &model.Currency{Code: "USD"}); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if updated == nil || updated.GetID() == nil {
|
||||
t.Fatalf("expected update to preserve ID")
|
||||
}
|
||||
}
|
||||
111
api/fx/storage/mongo/store/pair.go
Normal file
111
api/fx/storage/mongo/store/pair.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/tech/sendico/fx/storage"
|
||||
"github.com/tech/sendico/fx/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/mongo"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type pairStore struct {
|
||||
logger mlogger.Logger
|
||||
repo repository.Repository
|
||||
}
|
||||
|
||||
func NewPair(logger mlogger.Logger, db *mongo.Database) (storage.PairStore, error) {
|
||||
repo := repository.CreateMongoRepository(db, model.PairsCollection)
|
||||
index := &ri.Definition{
|
||||
Keys: []ri.Key{
|
||||
{Field: "pair.base", Sort: ri.Asc},
|
||||
{Field: "pair.quote", Sort: ri.Asc},
|
||||
},
|
||||
Unique: true,
|
||||
}
|
||||
if err := repo.CreateIndex(index); err != nil {
|
||||
logger.Error("failed to ensure pairs index", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
logger.Debug("pair store initialised", zap.String("collection", model.PairsCollection))
|
||||
|
||||
return &pairStore{
|
||||
logger: logger.Named(model.PairsCollection),
|
||||
repo: repo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *pairStore) ListEnabled(ctx context.Context) ([]*model.Pair, error) {
|
||||
filter := repository.Query().Filter(repository.Field("isEnabled"), true)
|
||||
|
||||
pairs := make([]*model.Pair, 0)
|
||||
err := p.repo.FindManyByFilter(ctx, filter, func(cur *mongo.Cursor) error {
|
||||
doc := &model.Pair{}
|
||||
if err := cur.Decode(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
pairs = append(pairs, doc)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
p.logger.Error("failed to list enabled pairs", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
p.logger.Debug("listed enabled pairs", zap.Int("count", len(pairs)))
|
||||
return pairs, nil
|
||||
}
|
||||
|
||||
func (p *pairStore) Get(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) {
|
||||
if pair.Base == "" || pair.Quote == "" {
|
||||
p.logger.Warn("attempt to fetch pair with empty currency", zap.String("base", pair.Base), zap.String("quote", pair.Quote))
|
||||
return nil, merrors.InvalidArgument("pairStore: incomplete pair")
|
||||
}
|
||||
result := &model.Pair{}
|
||||
query := repository.Query().
|
||||
Filter(repository.Field("pair").Dot("base"), pair.Base).
|
||||
Filter(repository.Field("pair").Dot("quote"), pair.Quote)
|
||||
if err := p.repo.FindOneByFilter(ctx, query, result); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
p.logger.Debug("pair not found", zap.String("base", pair.Base), zap.String("quote", pair.Quote))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
p.logger.Debug("pair loaded", zap.String("base", pair.Base), zap.String("quote", pair.Quote))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p *pairStore) Upsert(ctx context.Context, pair *model.Pair) error {
|
||||
if pair == nil {
|
||||
p.logger.Warn("attempt to upsert nil pair")
|
||||
return merrors.InvalidArgument("pairStore: nil pair")
|
||||
}
|
||||
if pair.Pair.Base == "" || pair.Pair.Quote == "" {
|
||||
p.logger.Warn("attempt to upsert pair with empty currency", zap.String("base", pair.Pair.Base), zap.String("quote", pair.Pair.Quote))
|
||||
return merrors.InvalidArgument("pairStore: incomplete pair")
|
||||
}
|
||||
|
||||
existing := &model.Pair{}
|
||||
query := repository.Query().
|
||||
Filter(repository.Field("pair").Dot("base"), pair.Pair.Base).
|
||||
Filter(repository.Field("pair").Dot("quote"), pair.Pair.Quote)
|
||||
err := p.repo.FindOneByFilter(ctx, query, existing)
|
||||
if err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
p.logger.Debug("inserting new pair", zap.String("base", pair.Pair.Base), zap.String("quote", pair.Pair.Quote))
|
||||
return p.repo.Insert(ctx, pair, query)
|
||||
}
|
||||
p.logger.Error("failed to fetch pair", zap.Error(err), zap.String("base", pair.Pair.Base), zap.String("quote", pair.Pair.Quote))
|
||||
return err
|
||||
}
|
||||
|
||||
if existing.GetID() != nil {
|
||||
pair.SetID(*existing.GetID())
|
||||
}
|
||||
p.logger.Debug("updating pair", zap.String("base", pair.Pair.Base), zap.String("quote", pair.Pair.Quote))
|
||||
return p.repo.Update(ctx, pair)
|
||||
}
|
||||
101
api/fx/storage/mongo/store/pair_test.go
Normal file
101
api/fx/storage/mongo/store/pair_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/fx/storage/model"
|
||||
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||
rd "github.com/tech/sendico/pkg/db/repository/decoder"
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestPairStoreListEnabled(t *testing.T) {
|
||||
repo := &repoStub{
|
||||
findManyFn: func(_ context.Context, _ builder.Query, decode rd.DecodingFunc) error {
|
||||
docs := []interface{}{
|
||||
&model.Pair{Pair: model.CurrencyPair{Base: "USD", Quote: "EUR"}},
|
||||
}
|
||||
return runDecoderWithDocs(t, decode, docs...)
|
||||
},
|
||||
}
|
||||
store := &pairStore{logger: zap.NewNop(), repo: repo}
|
||||
|
||||
pairs, err := store.ListEnabled(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(pairs) != 1 || pairs[0].Pair.Base != "USD" {
|
||||
t.Fatalf("unexpected pairs result: %+v", pairs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPairStoreGetInvalid(t *testing.T) {
|
||||
store := &pairStore{logger: zap.NewNop(), repo: &repoStub{}}
|
||||
if _, err := store.Get(context.Background(), model.CurrencyPair{}); !errors.Is(err, merrors.ErrInvalidArg) {
|
||||
t.Fatalf("expected invalid argument error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPairStoreGetNotFound(t *testing.T) {
|
||||
repo := &repoStub{
|
||||
findOneFn: func(context.Context, builder.Query, storable.Storable) error {
|
||||
return merrors.ErrNoData
|
||||
},
|
||||
}
|
||||
store := &pairStore{logger: zap.NewNop(), repo: repo}
|
||||
|
||||
if _, err := store.Get(context.Background(), model.CurrencyPair{Base: "USD", Quote: "EUR"}); !errors.Is(err, merrors.ErrNoData) {
|
||||
t.Fatalf("expected ErrNoData, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPairStoreUpsertInsert(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var inserted *model.Pair
|
||||
repo := &repoStub{
|
||||
findOneFn: func(context.Context, builder.Query, storable.Storable) error {
|
||||
return merrors.ErrNoData
|
||||
},
|
||||
insertFn: func(_ context.Context, obj storable.Storable, _ builder.Query) error {
|
||||
inserted = clonePair(t, obj)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
store := &pairStore{logger: zap.NewNop(), repo: repo}
|
||||
pair := &model.Pair{Pair: model.CurrencyPair{Base: "USD", Quote: "EUR"}}
|
||||
if err := store.Upsert(ctx, pair); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if inserted == nil {
|
||||
t.Fatalf("expected insert to be called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPairStoreUpsertUpdate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var updated *model.Pair
|
||||
repo := &repoStub{
|
||||
findOneFn: func(_ context.Context, _ builder.Query, result storable.Storable) error {
|
||||
pair := result.(*model.Pair)
|
||||
pair.SetID(primitive.NewObjectID())
|
||||
return nil
|
||||
},
|
||||
updateFn: func(_ context.Context, obj storable.Storable) error {
|
||||
updated = clonePair(t, obj)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
store := &pairStore{logger: zap.NewNop(), repo: repo}
|
||||
|
||||
if err := store.Upsert(ctx, &model.Pair{Pair: model.CurrencyPair{Base: "USD", Quote: "EUR"}}); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if updated == nil || updated.GetID() == nil {
|
||||
t.Fatalf("expected update to preserve existing ID")
|
||||
}
|
||||
}
|
||||
198
api/fx/storage/mongo/store/quotes.go
Normal file
198
api/fx/storage/mongo/store/quotes.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/fx/storage"
|
||||
"github.com/tech/sendico/fx/storage/model"
|
||||
"github.com/tech/sendico/pkg/db/repository"
|
||||
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||
"github.com/tech/sendico/pkg/db/transaction"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type quotesStore struct {
|
||||
logger mlogger.Logger
|
||||
repo repository.Repository
|
||||
txFactory transaction.Factory
|
||||
}
|
||||
|
||||
func NewQuotes(logger mlogger.Logger, db *mongo.Database, txFactory transaction.Factory) (storage.QuotesStore, error) {
|
||||
repo := repository.CreateMongoRepository(db, model.QuotesCollection)
|
||||
indexes := []*ri.Definition{
|
||||
{
|
||||
Keys: []ri.Key{
|
||||
{Field: "quoteRef", Sort: ri.Asc},
|
||||
},
|
||||
Unique: true,
|
||||
},
|
||||
{
|
||||
Keys: []ri.Key{
|
||||
{Field: "status", Sort: ri.Asc},
|
||||
{Field: "expiresAtUnixMs", Sort: ri.Asc},
|
||||
},
|
||||
},
|
||||
{
|
||||
Keys: []ri.Key{
|
||||
{Field: "consumedByLedgerTxnRef", Sort: ri.Asc},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ttlSeconds := int32(0)
|
||||
indexes = append(indexes, &ri.Definition{
|
||||
Keys: []ri.Key{
|
||||
{Field: "expiresAt", Sort: ri.Asc},
|
||||
},
|
||||
TTL: &ttlSeconds,
|
||||
Name: "quotes_expires_at_ttl",
|
||||
})
|
||||
|
||||
for _, def := range indexes {
|
||||
if err := repo.CreateIndex(def); err != nil {
|
||||
logger.Error("failed to ensure quotes index", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
childLogger := logger.Named(model.QuotesCollection)
|
||||
childLogger.Debug("quotes store initialised", zap.String("collection", model.QuotesCollection))
|
||||
|
||||
return "esStore{
|
||||
logger: childLogger,
|
||||
repo: repo,
|
||||
txFactory: txFactory,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (q *quotesStore) Issue(ctx context.Context, quote *model.Quote) error {
|
||||
if quote == nil {
|
||||
q.logger.Warn("attempt to issue nil quote")
|
||||
return merrors.InvalidArgument("quotesStore: nil quote")
|
||||
}
|
||||
if quote.QuoteRef == "" {
|
||||
q.logger.Warn("attempt to issue quote with empty ref")
|
||||
return merrors.InvalidArgument("quotesStore: empty quoteRef")
|
||||
}
|
||||
|
||||
if quote.ExpiresAtUnixMs > 0 && quote.ExpiresAt == nil {
|
||||
expiry := time.UnixMilli(quote.ExpiresAtUnixMs)
|
||||
quote.ExpiresAt = &expiry
|
||||
}
|
||||
|
||||
quote.Status = model.QuoteStatusIssued
|
||||
quote.ConsumedByLedgerTxnRef = ""
|
||||
quote.ConsumedAtUnixMs = nil
|
||||
if err := q.repo.Insert(ctx, quote, repository.Filter("quoteRef", quote.QuoteRef)); err != nil {
|
||||
q.logger.Error("failed to insert quote", zap.Error(err), zap.String("quote_ref", quote.QuoteRef))
|
||||
return err
|
||||
}
|
||||
q.logger.Debug("quote issued", zap.String("quote_ref", quote.QuoteRef), zap.Bool("firm", quote.Firm))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *quotesStore) GetByRef(ctx context.Context, quoteRef string) (*model.Quote, error) {
|
||||
if quoteRef == "" {
|
||||
q.logger.Warn("attempt to fetch quote with empty ref")
|
||||
return nil, merrors.InvalidArgument("quotesStore: empty quoteRef")
|
||||
}
|
||||
quote := &model.Quote{}
|
||||
if err := q.repo.FindOneByFilter(ctx, repository.Filter("quoteRef", quoteRef), quote); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
q.logger.Debug("quote not found", zap.String("quote_ref", quoteRef))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
q.logger.Debug("quote loaded", zap.String("quote_ref", quoteRef), zap.String("status", string(quote.Status)))
|
||||
return quote, nil
|
||||
}
|
||||
|
||||
func (q *quotesStore) Consume(ctx context.Context, quoteRef, ledgerTxnRef string, when time.Time) (*model.Quote, error) {
|
||||
if quoteRef == "" || ledgerTxnRef == "" {
|
||||
q.logger.Warn("attempt to consume quote with missing identifiers", zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef))
|
||||
return nil, merrors.InvalidArgument("quotesStore: missing identifiers")
|
||||
}
|
||||
|
||||
if when.IsZero() {
|
||||
when = time.Now()
|
||||
}
|
||||
|
||||
q.logger.Debug("consuming quote", zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef))
|
||||
txn := q.txFactory.CreateTransaction()
|
||||
result, err := txn.Execute(ctx, func(txCtx context.Context) (any, error) {
|
||||
quote := &model.Quote{}
|
||||
if err := q.repo.FindOneByFilter(txCtx, repository.Filter("quoteRef", quoteRef), quote); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !quote.Firm {
|
||||
q.logger.Warn("quote not firm", zap.String("quote_ref", quoteRef))
|
||||
return nil, storage.ErrQuoteNotFirm
|
||||
}
|
||||
|
||||
if quote.Status == model.QuoteStatusExpired || quote.IsExpired(when) {
|
||||
quote.MarkExpired()
|
||||
if err := q.repo.Update(txCtx, quote); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q.logger.Info("quote expired during consume", zap.String("quote_ref", quoteRef))
|
||||
return nil, storage.ErrQuoteExpired
|
||||
}
|
||||
|
||||
if quote.Status == model.QuoteStatusConsumed {
|
||||
if quote.ConsumedByLedgerTxnRef == ledgerTxnRef {
|
||||
q.logger.Debug("quote already consumed by ledger", zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef))
|
||||
return quote, nil
|
||||
}
|
||||
q.logger.Warn("quote consumed by different ledger", zap.String("quote_ref", quoteRef), zap.String("existing_ledger_ref", quote.ConsumedByLedgerTxnRef))
|
||||
return nil, storage.ErrQuoteConsumed
|
||||
}
|
||||
|
||||
quote.MarkConsumed(ledgerTxnRef, when)
|
||||
if err := q.repo.Update(txCtx, quote); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q.logger.Info("quote consumed", zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef))
|
||||
return quote, nil
|
||||
})
|
||||
if err != nil {
|
||||
q.logger.Error("quote consumption failed", zap.Error(err), zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef))
|
||||
return nil, err
|
||||
}
|
||||
quote, _ := result.(*model.Quote)
|
||||
if quote == nil {
|
||||
return nil, merrors.Internal("quotesStore: transaction returned nil quote")
|
||||
}
|
||||
return quote, nil
|
||||
}
|
||||
|
||||
func (q *quotesStore) ExpireIssuedBefore(ctx context.Context, cutoff time.Time) (int, error) {
|
||||
if cutoff.IsZero() {
|
||||
q.logger.Warn("attempt to expire quotes with zero cutoff")
|
||||
return 0, merrors.InvalidArgument("quotesStore: cutoff time is zero")
|
||||
}
|
||||
|
||||
filter := repository.Query().
|
||||
Filter(repository.Field("status"), model.QuoteStatusIssued).
|
||||
Comparison(repository.Field("expiresAtUnixMs"), builder.Lt, cutoff.UnixMilli())
|
||||
|
||||
patch := repository.Patch().
|
||||
Set(repository.Field("status"), model.QuoteStatusExpired).
|
||||
Unset(repository.Field("consumedByLedgerTxnRef")).
|
||||
Unset(repository.Field("consumedAtUnixMs"))
|
||||
|
||||
updated, err := q.repo.PatchMany(ctx, filter, patch)
|
||||
if err != nil {
|
||||
q.logger.Error("failed to expire quotes", zap.Error(err))
|
||||
return 0, err
|
||||
}
|
||||
if updated > 0 {
|
||||
q.logger.Info("quotes expired", zap.Int("count", updated))
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
184
api/fx/storage/mongo/store/quotes_test.go
Normal file
184
api/fx/storage/mongo/store/quotes_test.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/fx/storage"
|
||||
"github.com/tech/sendico/fx/storage/model"
|
||||
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestQuotesStoreIssue(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var inserted *model.Quote
|
||||
repo := &repoStub{
|
||||
insertFn: func(_ context.Context, obj storable.Storable, _ builder.Query) error {
|
||||
inserted = cloneQuote(t, obj)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
store := "esStore{logger: zap.NewNop(), repo: repo, txFactory: &txFactoryStub{}}
|
||||
|
||||
quote := &model.Quote{QuoteRef: "q1"}
|
||||
if err := store.Issue(ctx, quote); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if inserted == nil || inserted.Status != model.QuoteStatusIssued {
|
||||
t.Fatalf("expected issued quote to be inserted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuotesStoreIssueSetsExpiryDate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var inserted *model.Quote
|
||||
repo := &repoStub{
|
||||
insertFn: func(_ context.Context, obj storable.Storable, _ builder.Query) error {
|
||||
inserted = cloneQuote(t, obj)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
store := "esStore{logger: zap.NewNop(), repo: repo, txFactory: &txFactoryStub{}}
|
||||
|
||||
expiry := time.Now().Add(2 * time.Minute).UnixMilli()
|
||||
quote := &model.Quote{
|
||||
QuoteRef: "q1",
|
||||
ExpiresAtUnixMs: expiry,
|
||||
}
|
||||
|
||||
if err := store.Issue(ctx, quote); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if inserted == nil || inserted.ExpiresAt == nil {
|
||||
t.Fatalf("expected expiry timestamp to be populated")
|
||||
}
|
||||
if inserted.ExpiresAt.UnixMilli() != expiry {
|
||||
t.Fatalf("expected expiry to equal %d, got %d", expiry, inserted.ExpiresAt.UnixMilli())
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuotesStoreIssueInvalidInput(t *testing.T) {
|
||||
store := "esStore{logger: zap.NewNop(), repo: &repoStub{}, txFactory: &txFactoryStub{}}
|
||||
if err := store.Issue(context.Background(), nil); !errors.Is(err, merrors.ErrInvalidArg) {
|
||||
t.Fatalf("expected invalid argument error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuotesStoreConsumeSuccess(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
ledgerRef := "ledger-1"
|
||||
|
||||
stored := &model.Quote{
|
||||
QuoteRef: "q1",
|
||||
Firm: true,
|
||||
Status: model.QuoteStatusIssued,
|
||||
ExpiresAtUnixMs: now.Add(5 * time.Minute).UnixMilli(),
|
||||
}
|
||||
var updated *model.Quote
|
||||
repo := &repoStub{
|
||||
findOneFn: func(_ context.Context, _ builder.Query, result storable.Storable) error {
|
||||
quote := result.(*model.Quote)
|
||||
*quote = *stored
|
||||
return nil
|
||||
},
|
||||
updateFn: func(_ context.Context, obj storable.Storable) error {
|
||||
updated = cloneQuote(t, obj)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
factory := &txFactoryStub{}
|
||||
store := "esStore{logger: zap.NewNop(), repo: repo, txFactory: factory}
|
||||
|
||||
res, err := store.Consume(ctx, "q1", ledgerRef, now)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if res == nil || res.Status != model.QuoteStatusConsumed {
|
||||
t.Fatalf("expected consumed quote")
|
||||
}
|
||||
if updated == nil || updated.ConsumedByLedgerTxnRef != ledgerRef {
|
||||
t.Fatalf("expected update with ledger ref")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuotesStoreConsumeExpired(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
stored := &model.Quote{
|
||||
QuoteRef: "q1",
|
||||
Firm: true,
|
||||
Status: model.QuoteStatusIssued,
|
||||
ExpiresAtUnixMs: time.Now().Add(-time.Minute).UnixMilli(),
|
||||
}
|
||||
var updated *model.Quote
|
||||
repo := &repoStub{
|
||||
findOneFn: func(_ context.Context, _ builder.Query, result storable.Storable) error {
|
||||
quote := result.(*model.Quote)
|
||||
*quote = *stored
|
||||
return nil
|
||||
},
|
||||
updateFn: func(_ context.Context, obj storable.Storable) error {
|
||||
updated = cloneQuote(t, obj)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
factory := &txFactoryStub{}
|
||||
store := "esStore{logger: zap.NewNop(), repo: repo, txFactory: factory}
|
||||
|
||||
_, err := store.Consume(ctx, "q1", "ledger", time.Now())
|
||||
if !errors.Is(err, storage.ErrQuoteExpired) {
|
||||
t.Fatalf("expected ErrQuoteExpired, got %v", err)
|
||||
}
|
||||
if updated == nil || updated.Status != model.QuoteStatusExpired {
|
||||
t.Fatalf("expected quote marked expired")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuotesStoreExpireIssuedBefore(t *testing.T) {
|
||||
repo := &repoStub{
|
||||
patchManyFn: func(context.Context, builder.Query, builder.Patch) (int, error) {
|
||||
return 3, nil
|
||||
},
|
||||
}
|
||||
store := "esStore{logger: zap.NewNop(), repo: repo, txFactory: &txFactoryStub{}}
|
||||
|
||||
count, err := store.ExpireIssuedBefore(context.Background(), time.Now())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if count != 3 {
|
||||
t.Fatalf("expected 3 expired quotes, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuotesStoreExpireZeroCutoff(t *testing.T) {
|
||||
store := "esStore{logger: zap.NewNop(), repo: &repoStub{}, txFactory: &txFactoryStub{}}
|
||||
if _, err := store.ExpireIssuedBefore(context.Background(), time.Time{}); !errors.Is(err, merrors.ErrInvalidArg) {
|
||||
t.Fatalf("expected invalid argument error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuotesStoreGetByRefNotFound(t *testing.T) {
|
||||
repo := &repoStub{
|
||||
findOneFn: func(context.Context, builder.Query, storable.Storable) error {
|
||||
return merrors.ErrNoData
|
||||
},
|
||||
}
|
||||
store := "esStore{logger: zap.NewNop(), repo: repo, txFactory: &txFactoryStub{}}
|
||||
|
||||
if _, err := store.GetByRef(context.Background(), "missing"); !errors.Is(err, merrors.ErrNoData) {
|
||||
t.Fatalf("expected ErrNoData, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuotesStoreGetByRefInvalid(t *testing.T) {
|
||||
store := "esStore{logger: zap.NewNop(), repo: &repoStub{}, txFactory: &txFactoryStub{}}
|
||||
if _, err := store.GetByRef(context.Background(), ""); !errors.Is(err, merrors.ErrInvalidArg) {
|
||||
t.Fatalf("expected invalid argument error")
|
||||
}
|
||||
}
|
||||
127
api/fx/storage/mongo/store/rates.go
Normal file
127
api/fx/storage/mongo/store/rates.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/fx/storage"
|
||||
"github.com/tech/sendico/fx/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/mongo"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ratesStore struct {
|
||||
logger mlogger.Logger
|
||||
repo repository.Repository
|
||||
}
|
||||
|
||||
func NewRates(logger mlogger.Logger, db *mongo.Database) (storage.RatesStore, error) {
|
||||
repo := repository.CreateMongoRepository(db, model.RatesCollection)
|
||||
|
||||
indexes := []*ri.Definition{
|
||||
{
|
||||
Keys: []ri.Key{
|
||||
{Field: "pair.base", Sort: ri.Asc},
|
||||
{Field: "pair.quote", Sort: ri.Asc},
|
||||
{Field: "provider", Sort: ri.Asc},
|
||||
{Field: "asOfUnixMs", Sort: ri.Desc},
|
||||
},
|
||||
},
|
||||
{
|
||||
Keys: []ri.Key{
|
||||
{Field: "rateRef", Sort: ri.Asc},
|
||||
},
|
||||
Unique: true,
|
||||
},
|
||||
}
|
||||
|
||||
ttlSeconds := int32(24 * 60 * 60)
|
||||
indexes = append(indexes, &ri.Definition{
|
||||
Keys: []ri.Key{
|
||||
{Field: "asOf", Sort: ri.Asc},
|
||||
},
|
||||
TTL: &ttlSeconds,
|
||||
Name: "rates_as_of_ttl",
|
||||
})
|
||||
|
||||
for _, def := range indexes {
|
||||
if err := repo.CreateIndex(def); err != nil {
|
||||
logger.Error("failed to ensure rates index", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
logger.Debug("rates store initialised", zap.String("collection", model.RatesCollection))
|
||||
return &ratesStore{
|
||||
logger: logger.Named(model.RatesCollection),
|
||||
repo: repo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *ratesStore) UpsertSnapshot(ctx context.Context, snapshot *model.RateSnapshot) error {
|
||||
if snapshot == nil {
|
||||
r.logger.Warn("attempt to upsert nil snapshot")
|
||||
return merrors.InvalidArgument("ratesStore: nil snapshot")
|
||||
}
|
||||
if snapshot.RateRef == "" {
|
||||
r.logger.Warn("attempt to upsert snapshot with empty rate_ref")
|
||||
return merrors.InvalidArgument("ratesStore: empty rateRef")
|
||||
}
|
||||
|
||||
if snapshot.AsOfUnixMs > 0 && snapshot.AsOf == nil {
|
||||
asOf := time.UnixMilli(snapshot.AsOfUnixMs).UTC()
|
||||
snapshot.AsOf = &asOf
|
||||
}
|
||||
|
||||
existing := &model.RateSnapshot{}
|
||||
filter := repository.Filter("rateRef", snapshot.RateRef)
|
||||
err := r.repo.FindOneByFilter(ctx, filter, existing)
|
||||
if err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
r.logger.Debug("inserting new rate snapshot", zap.String("rate_ref", snapshot.RateRef))
|
||||
return r.repo.Insert(ctx, snapshot, filter)
|
||||
}
|
||||
r.logger.Error("failed to query rate snapshot", zap.Error(err), zap.String("rate_ref", snapshot.RateRef))
|
||||
return err
|
||||
}
|
||||
|
||||
if existing.GetID() != nil {
|
||||
snapshot.SetID(*existing.GetID())
|
||||
}
|
||||
r.logger.Debug("updating rate snapshot", zap.String("rate_ref", snapshot.RateRef))
|
||||
return r.repo.Update(ctx, snapshot)
|
||||
}
|
||||
|
||||
func (r *ratesStore) LatestSnapshot(ctx context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) {
|
||||
query := repository.Query().
|
||||
Filter(repository.Field("pair").Dot("base"), pair.Base).
|
||||
Filter(repository.Field("pair").Dot("quote"), pair.Quote)
|
||||
|
||||
if provider != "" {
|
||||
query = query.Filter(repository.Field("provider"), provider)
|
||||
}
|
||||
|
||||
limit := int64(1)
|
||||
query = query.Sort(repository.Field("asOfUnixMs"), false).Limit(&limit)
|
||||
|
||||
var result *model.RateSnapshot
|
||||
err := r.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error {
|
||||
doc := &model.RateSnapshot{}
|
||||
if err := cur.Decode(doc); err != nil {
|
||||
return err
|
||||
}
|
||||
result = doc
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil {
|
||||
return nil, merrors.ErrNoData
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
87
api/fx/storage/mongo/store/rates_test.go
Normal file
87
api/fx/storage/mongo/store/rates_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/fx/storage/model"
|
||||
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||
rd "github.com/tech/sendico/pkg/db/repository/decoder"
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestRatesStoreUpsertInsert(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
var inserted *model.RateSnapshot
|
||||
|
||||
repo := &repoStub{
|
||||
findOneFn: func(context.Context, builder.Query, storable.Storable) error {
|
||||
return merrors.ErrNoData
|
||||
},
|
||||
insertFn: func(_ context.Context, obj storable.Storable, _ builder.Query) error {
|
||||
inserted = cloneRate(t, obj)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
store := &ratesStore{logger: zap.NewNop(), repo: repo}
|
||||
|
||||
snapshot := &model.RateSnapshot{RateRef: "r1"}
|
||||
if err := store.UpsertSnapshot(ctx, snapshot); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if inserted == nil || inserted.RateRef != "r1" {
|
||||
t.Fatalf("expected snapshot to be inserted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRatesStoreUpsertUpdate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
existingID := primitive.NewObjectID()
|
||||
var updated *model.RateSnapshot
|
||||
|
||||
repo := &repoStub{
|
||||
findOneFn: func(_ context.Context, _ builder.Query, result storable.Storable) error {
|
||||
snap := result.(*model.RateSnapshot)
|
||||
snap.SetID(existingID)
|
||||
snap.RateRef = "existing"
|
||||
return nil
|
||||
},
|
||||
updateFn: func(_ context.Context, obj storable.Storable) error {
|
||||
snap := obj.(*model.RateSnapshot)
|
||||
updated = snap
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
store := &ratesStore{logger: zap.NewNop(), repo: repo}
|
||||
toUpdate := &model.RateSnapshot{RateRef: "existing"}
|
||||
if err := store.UpsertSnapshot(ctx, toUpdate); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if updated == nil || updated.GetID() == nil || *updated.GetID() != existingID {
|
||||
t.Fatalf("expected update to preserve ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRatesStoreLatestSnapshot(t *testing.T) {
|
||||
now := time.Now().UnixMilli()
|
||||
repo := &repoStub{
|
||||
findManyFn: func(_ context.Context, _ builder.Query, decode rd.DecodingFunc) error {
|
||||
doc := &model.RateSnapshot{RateRef: "latest", AsOfUnixMs: now}
|
||||
return runDecoderWithDocs(t, decode, doc)
|
||||
},
|
||||
}
|
||||
|
||||
store := &ratesStore{logger: zap.NewNop(), repo: repo}
|
||||
res, err := store.LatestSnapshot(context.Background(), model.CurrencyPair{Base: "USD", Quote: "EUR"}, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if res.RateRef != "latest" || res.AsOfUnixMs != now {
|
||||
t.Fatalf("unexpected snapshot returned: %+v", res)
|
||||
}
|
||||
}
|
||||
189
api/fx/storage/mongo/store/testing_helpers_test.go
Normal file
189
api/fx/storage/mongo/store/testing_helpers_test.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/tech/sendico/fx/storage/model"
|
||||
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||
rd "github.com/tech/sendico/pkg/db/repository/decoder"
|
||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/db/transaction"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
pmodel "github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
)
|
||||
|
||||
type repoStub struct {
|
||||
insertFn func(ctx context.Context, obj storable.Storable, filter builder.Query) error
|
||||
insertManyFn func(ctx context.Context, objects []storable.Storable) error
|
||||
findOneFn func(ctx context.Context, query builder.Query, result storable.Storable) error
|
||||
findManyFn func(ctx context.Context, query builder.Query, decoder rd.DecodingFunc) error
|
||||
updateFn func(ctx context.Context, obj storable.Storable) error
|
||||
patchManyFn func(ctx context.Context, filter builder.Query, patch builder.Patch) (int, error)
|
||||
createIdxFn func(def *ri.Definition) error
|
||||
}
|
||||
|
||||
func (r *repoStub) Aggregate(ctx context.Context, b builder.Pipeline, decoder rd.DecodingFunc) error {
|
||||
return merrors.NotImplemented("Aggregate not used")
|
||||
}
|
||||
|
||||
func (r *repoStub) Insert(ctx context.Context, obj storable.Storable, filter builder.Query) error {
|
||||
if r.insertFn != nil {
|
||||
return r.insertFn(ctx, obj, filter)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repoStub) InsertMany(ctx context.Context, objects []storable.Storable) error {
|
||||
if r.insertManyFn != nil {
|
||||
return r.insertManyFn(ctx, objects)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repoStub) Get(ctx context.Context, id primitive.ObjectID, result storable.Storable) error {
|
||||
return merrors.NotImplemented("Get not used")
|
||||
}
|
||||
|
||||
func (r *repoStub) FindOneByFilter(ctx context.Context, query builder.Query, result storable.Storable) error {
|
||||
if r.findOneFn != nil {
|
||||
return r.findOneFn(ctx, query, result)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repoStub) FindManyByFilter(ctx context.Context, query builder.Query, decoder rd.DecodingFunc) error {
|
||||
if r.findManyFn != nil {
|
||||
return r.findManyFn(ctx, query, decoder)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repoStub) Update(ctx context.Context, obj storable.Storable) error {
|
||||
if r.updateFn != nil {
|
||||
return r.updateFn(ctx, obj)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repoStub) Patch(ctx context.Context, id primitive.ObjectID, patch builder.Patch) error {
|
||||
return merrors.NotImplemented("Patch not used")
|
||||
}
|
||||
|
||||
func (r *repoStub) PatchMany(ctx context.Context, filter builder.Query, patch builder.Patch) (int, error) {
|
||||
if r.patchManyFn != nil {
|
||||
return r.patchManyFn(ctx, filter, patch)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (r *repoStub) Delete(ctx context.Context, id primitive.ObjectID) error {
|
||||
return merrors.NotImplemented("Delete not used")
|
||||
}
|
||||
|
||||
func (r *repoStub) DeleteMany(ctx context.Context, query builder.Query) error {
|
||||
return merrors.NotImplemented("DeleteMany not used")
|
||||
}
|
||||
|
||||
func (r *repoStub) CreateIndex(def *ri.Definition) error {
|
||||
if r.createIdxFn != nil {
|
||||
return r.createIdxFn(def)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repoStub) ListIDs(ctx context.Context, query builder.Query) ([]primitive.ObjectID, error) {
|
||||
return nil, merrors.NotImplemented("ListIDs not used")
|
||||
}
|
||||
|
||||
func (r *repoStub) ListPermissionBound(ctx context.Context, query builder.Query) ([]pmodel.PermissionBoundStorable, error) {
|
||||
return nil, merrors.NotImplemented("ListPermissionBound not used")
|
||||
}
|
||||
|
||||
func (r *repoStub) ListAccountBound(ctx context.Context, query builder.Query) ([]pmodel.AccountBoundStorable, error) {
|
||||
return nil, merrors.NotImplemented("ListAccountBound not used")
|
||||
}
|
||||
|
||||
func (r *repoStub) Collection() string { return "test" }
|
||||
|
||||
type txFactoryStub struct {
|
||||
executeFn func(ctx context.Context, cb transaction.Callback) (any, error)
|
||||
}
|
||||
|
||||
func (f *txFactoryStub) CreateTransaction() transaction.Transaction {
|
||||
return &txStub{executeFn: f.executeFn}
|
||||
}
|
||||
|
||||
type txStub struct {
|
||||
executeFn func(ctx context.Context, cb transaction.Callback) (any, error)
|
||||
}
|
||||
|
||||
func (t *txStub) Execute(ctx context.Context, cb transaction.Callback) (any, error) {
|
||||
if t.executeFn != nil {
|
||||
return t.executeFn(ctx, cb)
|
||||
}
|
||||
return cb(ctx)
|
||||
}
|
||||
|
||||
func cloneRate(t *testing.T, obj storable.Storable) *model.RateSnapshot {
|
||||
t.Helper()
|
||||
rate, ok := obj.(*model.RateSnapshot)
|
||||
if !ok {
|
||||
t.Fatalf("expected *model.RateSnapshot, got %T", obj)
|
||||
}
|
||||
copy := *rate
|
||||
return ©
|
||||
}
|
||||
|
||||
func cloneQuote(t *testing.T, obj storable.Storable) *model.Quote {
|
||||
t.Helper()
|
||||
quote, ok := obj.(*model.Quote)
|
||||
if !ok {
|
||||
t.Fatalf("expected *model.Quote, got %T", obj)
|
||||
}
|
||||
copy := *quote
|
||||
return ©
|
||||
}
|
||||
|
||||
func clonePair(t *testing.T, obj storable.Storable) *model.Pair {
|
||||
t.Helper()
|
||||
pair, ok := obj.(*model.Pair)
|
||||
if !ok {
|
||||
t.Fatalf("expected *model.Pair, got %T", obj)
|
||||
}
|
||||
copy := *pair
|
||||
return ©
|
||||
}
|
||||
|
||||
func cloneCurrency(t *testing.T, obj storable.Storable) *model.Currency {
|
||||
t.Helper()
|
||||
currency, ok := obj.(*model.Currency)
|
||||
if !ok {
|
||||
t.Fatalf("expected *model.Currency, got %T", obj)
|
||||
}
|
||||
copy := *currency
|
||||
return ©
|
||||
}
|
||||
|
||||
func runDecoderWithDocs(t *testing.T, decode rd.DecodingFunc, docs ...interface{}) error {
|
||||
t.Helper()
|
||||
cur, err := mongo.NewCursorFromDocuments(docs, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create cursor: %v", err)
|
||||
}
|
||||
defer cur.Close(context.Background())
|
||||
|
||||
if len(docs) > 0 {
|
||||
if !cur.Next(context.Background()) {
|
||||
return cur.Err()
|
||||
}
|
||||
}
|
||||
|
||||
if err := decode(cur); err != nil {
|
||||
return err
|
||||
}
|
||||
return cur.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user