Files
sendico/api/ledger/internal/service/ledger/system_accounts.go
2026-01-31 00:26:42 +01:00

148 lines
4.4 KiB
Go

package ledger
import (
"context"
"errors"
"strings"
"github.com/tech/sendico/ledger/storage"
"github.com/tech/sendico/pkg/merrors"
pmodel "github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/v2/bson"
)
// EnsureSystemAccounts initializes required system accounts once at startup.
func (s *Service) EnsureSystemAccounts(ctx context.Context) error {
return s.ensureSystemAccounts(ctx)
}
func (s *Service) ensureSystemAccounts(ctx context.Context) error {
if s == nil || s.storage == nil || s.storage.Accounts() == nil {
return errStorageNotInitialized
}
if ctx == nil {
ctx = context.Background()
}
for _, currency := range pmodel.SupportedCurrencies {
normalized := strings.ToUpper(strings.TrimSpace(string(currency)))
if normalized == "" {
continue
}
if err := s.ensureSystemAccountForCurrency(ctx, pmodel.SystemAccountPurposeExternalSource, normalized); err != nil {
return err
}
if err := s.ensureSystemAccountForCurrency(ctx, pmodel.SystemAccountPurposeExternalSink, normalized); err != nil {
return err
}
}
return nil
}
func (s *Service) ensureSystemAccountForCurrency(ctx context.Context, purpose pmodel.SystemAccountPurpose, currency string) error {
account, err := s.storage.Accounts().GetSystemAccount(ctx, purpose, currency)
if err == nil && account != nil {
s.cacheSystemAccount(purpose, currency, account)
return nil
}
if err != nil && !errors.Is(err, storage.ErrAccountNotFound) {
return err
}
account = newExternalSystemAccount(purpose, currency)
if err := s.storage.Accounts().Create(ctx, account); err != nil {
if errors.Is(err, merrors.ErrDataConflict) {
existing, lookupErr := s.storage.Accounts().GetSystemAccount(ctx, purpose, currency)
if lookupErr != nil {
return lookupErr
}
s.cacheSystemAccount(purpose, currency, existing)
return nil
}
return err
}
s.cacheSystemAccount(purpose, currency, account)
return nil
}
func (s *Service) systemAccount(ctx context.Context, purpose pmodel.SystemAccountPurpose, currency string) (*pmodel.LedgerAccount, error) {
if s == nil || s.storage == nil || s.storage.Accounts() == nil {
return nil, errStorageNotInitialized
}
normalized := strings.ToUpper(strings.TrimSpace(currency))
if normalized == "" {
return nil, merrors.InvalidArgument("currency is required")
}
if acc := s.cachedSystemAccount(purpose, normalized); acc != nil {
return acc, nil
}
account, err := s.storage.Accounts().GetSystemAccount(ctx, purpose, normalized)
if err != nil {
return nil, err
}
s.cacheSystemAccount(purpose, normalized, account)
return account, nil
}
func (s *Service) cachedSystemAccount(purpose pmodel.SystemAccountPurpose, currency string) *pmodel.LedgerAccount {
s.systemAccounts.mu.RLock()
defer s.systemAccounts.mu.RUnlock()
switch purpose {
case pmodel.SystemAccountPurposeExternalSource:
if s.systemAccounts.externalSource == nil {
return nil
}
return s.systemAccounts.externalSource[currency]
case pmodel.SystemAccountPurposeExternalSink:
if s.systemAccounts.externalSink == nil {
return nil
}
return s.systemAccounts.externalSink[currency]
default:
return nil
}
}
func (s *Service) cacheSystemAccount(purpose pmodel.SystemAccountPurpose, currency string, account *pmodel.LedgerAccount) {
if account == nil {
return
}
s.systemAccounts.mu.Lock()
defer s.systemAccounts.mu.Unlock()
switch purpose {
case pmodel.SystemAccountPurposeExternalSource:
if s.systemAccounts.externalSource == nil {
s.systemAccounts.externalSource = make(map[string]*pmodel.LedgerAccount)
}
s.systemAccounts.externalSource[currency] = account
case pmodel.SystemAccountPurposeExternalSink:
if s.systemAccounts.externalSink == nil {
s.systemAccounts.externalSink = make(map[string]*pmodel.LedgerAccount)
}
s.systemAccounts.externalSink[currency] = account
}
}
func newExternalSystemAccount(purpose pmodel.SystemAccountPurpose, currency string) *pmodel.LedgerAccount {
ref := bson.NewObjectID()
purposeCopy := purpose
account := &pmodel.LedgerAccount{
AccountCode: generateAccountCode(pmodel.LedgerAccountTypeAsset, currency, ref),
AccountType: pmodel.LedgerAccountTypeAsset,
Currency: currency,
Status: pmodel.LedgerAccountStatusActive,
AllowNegative: true,
Scope: pmodel.LedgerAccountScopeSystem,
SystemPurpose: &purposeCopy,
Metadata: map[string]string{
"system": "true",
},
}
account.SetID(ref)
return account
}