335 lines
12 KiB
Go
335 lines
12 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/tech/sendico/ledger/storage"
|
|
"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"
|
|
pkm "github.com/tech/sendico/pkg/model"
|
|
"github.com/tech/sendico/pkg/model/account_role"
|
|
"github.com/tech/sendico/pkg/mservice"
|
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
"go.mongodb.org/mongo-driver/v2/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, mservice.LedgerAccounts)
|
|
|
|
// 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 compound index on organizationRef + currency + role (unique)
|
|
roleIndex := &ri.Definition{
|
|
Keys: []ri.Key{
|
|
{Field: "organizationRef", Sort: ri.Asc},
|
|
{Field: "currency", Sort: ri.Asc},
|
|
{Field: "role", Sort: ri.Asc},
|
|
},
|
|
Unique: true,
|
|
PartialFilter: repository.Filter(
|
|
"scope",
|
|
pkm.LedgerAccountScopeOrganization,
|
|
),
|
|
}
|
|
if err := repo.CreateIndex(roleIndex); err != nil {
|
|
logger.Error("Failed to ensure accounts role index", zap.Error(err))
|
|
return nil, err
|
|
}
|
|
|
|
// Create compound index on scope + systemPurpose + currency (unique) for system accounts
|
|
systemIndex := &ri.Definition{
|
|
Keys: []ri.Key{
|
|
{Field: "scope", Sort: ri.Asc},
|
|
{Field: "systemPurpose", Sort: ri.Asc},
|
|
{Field: "currency", Sort: ri.Asc},
|
|
},
|
|
Unique: true,
|
|
PartialFilter: repository.Filter("scope", pkm.LedgerAccountScopeSystem),
|
|
}
|
|
if err := repo.CreateIndex(systemIndex); err != nil {
|
|
logger.Error("Failed to ensure system accounts 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(mservice.LedgerAccounts)
|
|
childLogger.Info("Accounts store initialised", zap.String("collection", mservice.LedgerAccounts))
|
|
|
|
return &accountsStore{
|
|
logger: childLogger,
|
|
repo: repo,
|
|
}, nil
|
|
}
|
|
|
|
func (a *accountsStore) Create(ctx context.Context, account *pkm.LedgerAccount) 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("account_code", 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("account_code", account.AccountCode),
|
|
zap.String("currency", account.Currency))
|
|
return nil
|
|
}
|
|
|
|
func (a *accountsStore) Get(ctx context.Context, accountRef bson.ObjectID) (*pkm.LedgerAccount, error) {
|
|
if accountRef.IsZero() {
|
|
a.logger.Warn("Attempt to get account with zero ID")
|
|
return nil, merrors.InvalidArgument("accountsStore: zero account ID")
|
|
}
|
|
|
|
result := &pkm.LedgerAccount{}
|
|
if err := a.repo.Get(ctx, accountRef, result); err != nil {
|
|
if errors.Is(err, merrors.ErrNoData) {
|
|
a.logger.Debug("Account not found", mzap.ObjRef("account_ref", accountRef))
|
|
return nil, storage.ErrAccountNotFound
|
|
}
|
|
a.logger.Warn("Failed to get account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
|
|
return nil, err
|
|
}
|
|
|
|
a.logger.Debug("Account loaded", mzap.ObjRef("account_ref", accountRef), zap.String("account_code", result.AccountCode))
|
|
return result, nil
|
|
}
|
|
|
|
func (a *accountsStore) GetByAccountCode(ctx context.Context, orgRef bson.ObjectID, accountCode, currency string) (*pkm.LedgerAccount, 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 := &pkm.LedgerAccount{}
|
|
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("account_code", accountCode), zap.String("currency", currency))
|
|
return nil, storage.ErrAccountNotFound
|
|
}
|
|
a.logger.Warn("Failed to get account by code", zap.Error(err), zap.String("account_code", accountCode))
|
|
return nil, err
|
|
}
|
|
|
|
a.logger.Debug("Account loaded by code", zap.String("account_code", accountCode), zap.String("currency", currency))
|
|
return result, nil
|
|
}
|
|
|
|
func (a *accountsStore) GetByRole(ctx context.Context, orgRef bson.ObjectID, currency string, role account_role.AccountRole) (*pkm.LedgerAccount, error) {
|
|
if orgRef.IsZero() {
|
|
a.logger.Warn("Attempt to get account with zero organization ID")
|
|
return nil, merrors.InvalidArgument("accountsStore: zero organization ID")
|
|
}
|
|
if currency == "" {
|
|
a.logger.Warn("Attempt to get account with empty currency")
|
|
return nil, merrors.InvalidArgument("accountsStore: empty currency")
|
|
}
|
|
if strings.TrimSpace(string(role)) == "" {
|
|
a.logger.Warn("Attempt to get account with empty role")
|
|
return nil, merrors.InvalidArgument("accountsStore: empty role")
|
|
}
|
|
|
|
limit := int64(1)
|
|
query := repository.Query().
|
|
Filter(repository.Field("organizationRef"), orgRef).
|
|
Filter(repository.Field("currency"), currency).
|
|
Filter(repository.Field("role"), role).
|
|
Limit(&limit)
|
|
|
|
result := &pkm.LedgerAccount{}
|
|
if err := a.repo.FindOneByFilter(ctx, query, result); err != nil {
|
|
if errors.Is(err, merrors.ErrNoData) {
|
|
a.logger.Debug("Account not found by role", zap.String("currency", currency),
|
|
zap.String("role", string(role)), mzap.ObjRef("organization_ref", orgRef))
|
|
return nil, storage.ErrAccountNotFound
|
|
}
|
|
a.logger.Warn("Failed to get account by role", zap.Error(err), mzap.ObjRef("organization_ref", orgRef),
|
|
zap.String("currency", currency), zap.String("role", string(role)))
|
|
return nil, err
|
|
}
|
|
|
|
a.logger.Debug("Account loaded by role", mzap.ObjRef("accountRef", *result.GetID()),
|
|
zap.String("currency", currency), zap.String("role", string(role)))
|
|
return result, nil
|
|
}
|
|
|
|
func (a *accountsStore) GetSystemAccount(ctx context.Context, purpose pkm.SystemAccountPurpose, currency string) (*pkm.LedgerAccount, error) {
|
|
if strings.TrimSpace(string(purpose)) == "" {
|
|
a.logger.Warn("Attempt to get system account with empty purpose")
|
|
return nil, merrors.InvalidArgument("accountsStore: empty system purpose")
|
|
}
|
|
normalizedCurrency := strings.ToUpper(strings.TrimSpace(currency))
|
|
if normalizedCurrency == "" {
|
|
a.logger.Warn("Attempt to get system account with empty currency")
|
|
return nil, merrors.InvalidArgument("accountsStore: empty currency")
|
|
}
|
|
|
|
limit := int64(1)
|
|
query := repository.Query().
|
|
Filter(repository.Field("scope"), pkm.LedgerAccountScopeSystem).
|
|
Filter(repository.Field("systemPurpose"), purpose).
|
|
Filter(repository.Field("currency"), normalizedCurrency).
|
|
Limit(&limit)
|
|
|
|
result := &pkm.LedgerAccount{}
|
|
if err := a.repo.FindOneByFilter(ctx, query, result); err != nil {
|
|
if errors.Is(err, merrors.ErrNoData) {
|
|
a.logger.Debug("System account not found", zap.String("currency", normalizedCurrency),
|
|
zap.String("purpose", string(purpose)))
|
|
return nil, storage.ErrAccountNotFound
|
|
}
|
|
a.logger.Warn("Failed to get system account", zap.Error(err),
|
|
zap.String("currency", normalizedCurrency), zap.String("purpose", string(purpose)))
|
|
return nil, err
|
|
}
|
|
|
|
a.logger.Debug("System account loaded", mzap.ObjRef("accountRef", *result.GetID()),
|
|
zap.String("currency", normalizedCurrency), zap.String("purpose", string(purpose)))
|
|
return result, nil
|
|
}
|
|
|
|
func (a *accountsStore) GetDefaultSettlement(ctx context.Context, orgRef bson.ObjectID, currency string) (*pkm.LedgerAccount, 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("role"), account_role.AccountRoleSettlement).
|
|
Limit(&limit)
|
|
|
|
result := &pkm.LedgerAccount{}
|
|
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),
|
|
mzap.ObjRef("organization_ref", orgRef))
|
|
return nil, storage.ErrAccountNotFound
|
|
}
|
|
a.logger.Warn("Failed to get default settlement account", zap.Error(err),
|
|
mzap.ObjRef("organization_ref", orgRef),
|
|
zap.String("currency", currency))
|
|
return nil, err
|
|
}
|
|
|
|
a.logger.Debug("Default settlement account loaded", mzap.ObjRef("accountRef", *result.GetID()), zap.String("currency", currency))
|
|
return result, nil
|
|
}
|
|
|
|
func (a *accountsStore) ListByOrganization(ctx context.Context, orgRef bson.ObjectID, filter *storage.AccountsFilter, limit int, offset int) ([]*pkm.LedgerAccount, error) {
|
|
if orgRef.IsZero() {
|
|
a.logger.Warn("Attempt to list accounts with zero organization reference")
|
|
return nil, merrors.InvalidArgument("accountsStore: zero organization reference")
|
|
}
|
|
|
|
limit64 := int64(limit)
|
|
offset64 := int64(offset)
|
|
query := repository.Query().
|
|
Filter(repository.Field("organizationRef"), orgRef).
|
|
Limit(&limit64).
|
|
Offset(&offset64)
|
|
|
|
if filter != nil && filter.OwnerRefFilter != nil {
|
|
if filter.OwnerRefFilter.IsZero() {
|
|
// Filter for accounts with nil owner_ref
|
|
query = query.Filter(repository.Field("ownerRef"), nil)
|
|
} else {
|
|
// Filter for accounts matching owner_ref
|
|
query = query.Filter(repository.Field("ownerRef"), *filter.OwnerRefFilter)
|
|
}
|
|
}
|
|
|
|
accounts := make([]*pkm.LedgerAccount, 0)
|
|
err := a.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error {
|
|
doc := &pkm.LedgerAccount{}
|
|
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 bson.ObjectID, status pkm.LedgerAccountStatus) error {
|
|
if accountRef.IsZero() {
|
|
a.logger.Warn("Attempt to update account status with zero reference")
|
|
return merrors.InvalidArgument("accountsStore: zero account reference")
|
|
}
|
|
|
|
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), mzap.ObjRef("account_ref", accountRef))
|
|
return err
|
|
}
|
|
|
|
a.logger.Debug("Account status updated", mzap.ObjRef("account_ref", accountRef), zap.String("status", string(status)))
|
|
return nil
|
|
}
|