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 balancesStore struct { logger mlogger.Logger repo repository.Repository } func NewBalances(logger mlogger.Logger, db *mongo.Database) (storage.BalancesStore, error) { repo := repository.CreateMongoRepository(db, model.AccountBalancesCollection) // Create unique index on accountRef (one balance per account) uniqueIndex := &ri.Definition{ Keys: []ri.Key{ {Field: "accountRef", Sort: ri.Asc}, }, Unique: true, } if err := repo.CreateIndex(uniqueIndex); err != nil { logger.Error("failed to ensure balances unique index", zap.Error(err)) return nil, err } childLogger := logger.Named(model.AccountBalancesCollection) childLogger.Debug("balances store initialised", zap.String("collection", model.AccountBalancesCollection)) return &balancesStore{ logger: childLogger, repo: repo, }, nil } func (b *balancesStore) Get(ctx context.Context, accountRef primitive.ObjectID) (*model.AccountBalance, error) { if accountRef.IsZero() { b.logger.Warn("attempt to get balance with zero account ID") return nil, merrors.InvalidArgument("balancesStore: zero account ID") } query := repository.Filter("accountRef", accountRef) result := &model.AccountBalance{} if err := b.repo.FindOneByFilter(ctx, query, result); err != nil { if errors.Is(err, merrors.ErrNoData) { b.logger.Debug("balance not found", zap.String("accountRef", accountRef.Hex())) return nil, storage.ErrBalanceNotFound } b.logger.Warn("failed to get balance", zap.Error(err), zap.String("accountRef", accountRef.Hex())) return nil, err } b.logger.Debug("balance loaded", zap.String("accountRef", accountRef.Hex()), zap.String("balance", result.Balance)) return result, nil } func (b *balancesStore) Upsert(ctx context.Context, balance *model.AccountBalance) error { if balance == nil { b.logger.Warn("attempt to upsert nil balance") return merrors.InvalidArgument("balancesStore: nil balance") } if balance.AccountRef.IsZero() { b.logger.Warn("attempt to upsert balance with zero account ID") return merrors.InvalidArgument("balancesStore: zero account ID") } existing := &model.AccountBalance{} filter := repository.Filter("accountRef", balance.AccountRef) if err := b.repo.FindOneByFilter(ctx, filter, existing); err != nil { if errors.Is(err, merrors.ErrNoData) { b.logger.Debug("inserting new balance", zap.String("accountRef", balance.AccountRef.Hex())) return b.repo.Insert(ctx, balance, filter) } b.logger.Warn("failed to fetch balance", zap.Error(err), zap.String("accountRef", balance.AccountRef.Hex())) return err } if existing.GetID() != nil { balance.SetID(*existing.GetID()) } b.logger.Debug("updating balance", zap.String("accountRef", balance.AccountRef.Hex()), zap.String("balance", balance.Balance)) return b.repo.Update(ctx, balance) } func (b *balancesStore) IncrementBalance(ctx context.Context, accountRef primitive.ObjectID, amount string) error { if accountRef.IsZero() { b.logger.Warn("attempt to increment balance with zero account ID") return merrors.InvalidArgument("balancesStore: zero account ID") } // Note: This implementation uses $inc on a string field, which won't work. // In a real implementation, you'd need to: // 1. Fetch the balance // 2. Parse amount strings to decimal // 3. Add them // 4. Update with optimistic locking via version field // For now, return not implemented to indicate this needs proper decimal handling b.logger.Warn("IncrementBalance not fully implemented - requires decimal arithmetic") return merrors.NotImplemented("IncrementBalance requires proper decimal handling") }