Files
sendico/api/ledger/storage/mongo/store/accounts.go
Stephan D 62a6631b9a
All checks were successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
service backend
2025-11-07 18:35:26 +01:00

221 lines
7.4 KiB
Go

package store
import (
"context"
"errors"
"github.com/tech/sendico/ledger/storage"
"github.com/tech/sendico/ledger/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.mongodb.org/mongo-driver/mongo"
"go.uber.org/zap"
)
type accountsStore struct {
logger mlogger.Logger
repo repository.Repository
}
func NewAccounts(logger mlogger.Logger, db *mongo.Database) (storage.AccountsStore, error) {
repo := repository.CreateMongoRepository(db, model.AccountsCollection)
// Create compound index on organizationRef + accountCode + currency (unique)
uniqueIndex := &ri.Definition{
Keys: []ri.Key{
{Field: "organizationRef", Sort: ri.Asc},
{Field: "accountCode", Sort: ri.Asc},
{Field: "currency", Sort: ri.Asc},
},
Unique: true,
}
if err := repo.CreateIndex(uniqueIndex); err != nil {
logger.Error("failed to ensure accounts unique index", zap.Error(err))
return nil, err
}
// Create index on organizationRef for listing
orgIndex := &ri.Definition{
Keys: []ri.Key{
{Field: "organizationRef", Sort: ri.Asc},
},
}
if err := repo.CreateIndex(orgIndex); err != nil {
logger.Error("failed to ensure accounts organization index", zap.Error(err))
return nil, err
}
childLogger := logger.Named(model.AccountsCollection)
childLogger.Debug("accounts store initialised", zap.String("collection", model.AccountsCollection))
return &accountsStore{
logger: childLogger,
repo: repo,
}, nil
}
func (a *accountsStore) Create(ctx context.Context, account *model.Account) error {
if account == nil {
a.logger.Warn("attempt to create nil account")
return merrors.InvalidArgument("accountsStore: nil account")
}
if err := a.repo.Insert(ctx, account, nil); err != nil {
if mongo.IsDuplicateKeyError(err) {
a.logger.Warn("duplicate account code", zap.String("accountCode", account.AccountCode),
zap.String("currency", account.Currency))
return merrors.DataConflict("account with this code and currency already exists")
}
a.logger.Warn("failed to create account", zap.Error(err))
return err
}
a.logger.Debug("account created", zap.String("accountCode", account.AccountCode),
zap.String("currency", account.Currency))
return nil
}
func (a *accountsStore) Get(ctx context.Context, accountRef primitive.ObjectID) (*model.Account, error) {
if accountRef.IsZero() {
a.logger.Warn("attempt to get account with zero ID")
return nil, merrors.InvalidArgument("accountsStore: zero account ID")
}
result := &model.Account{}
if err := a.repo.Get(ctx, accountRef, result); err != nil {
if errors.Is(err, merrors.ErrNoData) {
a.logger.Debug("account not found", zap.String("accountRef", accountRef.Hex()))
return nil, storage.ErrAccountNotFound
}
a.logger.Warn("failed to get account", zap.Error(err), zap.String("accountRef", accountRef.Hex()))
return nil, err
}
a.logger.Debug("account loaded", zap.String("accountRef", accountRef.Hex()),
zap.String("accountCode", result.AccountCode))
return result, nil
}
func (a *accountsStore) GetByAccountCode(ctx context.Context, orgRef primitive.ObjectID, accountCode, currency string) (*model.Account, error) {
if orgRef.IsZero() {
a.logger.Warn("attempt to get account with zero organization ID")
return nil, merrors.InvalidArgument("accountsStore: zero organization ID")
}
if accountCode == "" {
a.logger.Warn("attempt to get account with empty code")
return nil, merrors.InvalidArgument("accountsStore: empty account code")
}
if currency == "" {
a.logger.Warn("attempt to get account with empty currency")
return nil, merrors.InvalidArgument("accountsStore: empty currency")
}
query := repository.Query().
Filter(repository.Field("organizationRef"), orgRef).
Filter(repository.Field("accountCode"), accountCode).
Filter(repository.Field("currency"), currency)
result := &model.Account{}
if err := a.repo.FindOneByFilter(ctx, query, result); err != nil {
if errors.Is(err, merrors.ErrNoData) {
a.logger.Debug("account not found by code", zap.String("accountCode", accountCode),
zap.String("currency", currency))
return nil, storage.ErrAccountNotFound
}
a.logger.Warn("failed to get account by code", zap.Error(err), zap.String("accountCode", accountCode))
return nil, err
}
a.logger.Debug("account loaded by code", zap.String("accountCode", accountCode),
zap.String("currency", currency))
return result, nil
}
func (a *accountsStore) GetDefaultSettlement(ctx context.Context, orgRef primitive.ObjectID, currency string) (*model.Account, error) {
if orgRef.IsZero() {
a.logger.Warn("attempt to get default settlement with zero organization ID")
return nil, merrors.InvalidArgument("accountsStore: zero organization ID")
}
if currency == "" {
a.logger.Warn("attempt to get default settlement with empty currency")
return nil, merrors.InvalidArgument("accountsStore: empty currency")
}
limit := int64(1)
query := repository.Query().
Filter(repository.Field("organizationRef"), orgRef).
Filter(repository.Field("currency"), currency).
Filter(repository.Field("isSettlement"), true).
Limit(&limit)
result := &model.Account{}
if err := a.repo.FindOneByFilter(ctx, query, result); err != nil {
if errors.Is(err, merrors.ErrNoData) {
a.logger.Debug("default settlement account not found",
zap.String("currency", currency),
zap.String("organizationRef", orgRef.Hex()))
return nil, storage.ErrAccountNotFound
}
a.logger.Warn("failed to get default settlement account", zap.Error(err),
zap.String("organizationRef", orgRef.Hex()),
zap.String("currency", currency))
return nil, err
}
a.logger.Debug("default settlement account loaded",
zap.String("accountRef", result.GetID().Hex()),
zap.String("currency", currency))
return result, nil
}
func (a *accountsStore) ListByOrganization(ctx context.Context, orgRef primitive.ObjectID, limit int, offset int) ([]*model.Account, error) {
if orgRef.IsZero() {
a.logger.Warn("attempt to list accounts with zero organization ID")
return nil, merrors.InvalidArgument("accountsStore: zero organization ID")
}
limit64 := int64(limit)
offset64 := int64(offset)
query := repository.Query().
Filter(repository.Field("organizationRef"), orgRef).
Limit(&limit64).
Offset(&offset64)
accounts := make([]*model.Account, 0)
err := a.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error {
doc := &model.Account{}
if err := cur.Decode(doc); err != nil {
return err
}
accounts = append(accounts, doc)
return nil
})
if err != nil {
a.logger.Warn("failed to list accounts", zap.Error(err))
return nil, err
}
a.logger.Debug("listed accounts", zap.Int("count", len(accounts)))
return accounts, nil
}
func (a *accountsStore) UpdateStatus(ctx context.Context, accountRef primitive.ObjectID, status model.AccountStatus) error {
if accountRef.IsZero() {
a.logger.Warn("attempt to update account status with zero ID")
return merrors.InvalidArgument("accountsStore: zero account ID")
}
patch := repository.Patch().Set(repository.Field("status"), status)
if err := a.repo.Patch(ctx, accountRef, patch); err != nil {
a.logger.Warn("failed to update account status", zap.Error(err), zap.String("accountRef", accountRef.Hex()))
return err
}
a.logger.Debug("account status updated", zap.String("accountRef", accountRef.Hex()),
zap.String("status", string(status)))
return nil
}