Files
sendico/api/ledger/internal/service/ledger/topology.go
2026-02-03 00:40:46 +01:00

126 lines
4.2 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"
"github.com/tech/sendico/pkg/model/account_role"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
const LedgerTopologyVersion = 1
var RequiredRolesV1 = []account_role.AccountRole{
account_role.AccountRoleOperating,
account_role.AccountRoleHold,
account_role.AccountRolePending,
account_role.AccountRoleTransit,
account_role.AccountRoleSettlement,
}
func isRequiredTopologyRole(role account_role.AccountRole) bool {
for _, required := range RequiredRolesV1 {
if role == required {
return true
}
}
return false
}
func (s *Service) ensureLedgerTopology(ctx context.Context, orgRef bson.ObjectID, currency string) error {
if s.storage == nil || s.storage.Accounts() == nil {
return errStorageNotInitialized
}
if orgRef.IsZero() {
return merrors.InvalidArgument("organization_ref is required")
}
normalizedCurrency := strings.ToUpper(strings.TrimSpace(currency))
if normalizedCurrency == "" {
return merrors.InvalidArgument("currency is required")
}
for _, role := range RequiredRolesV1 {
if _, err := s.ensureRoleAccount(ctx, orgRef, normalizedCurrency, role); err != nil {
return err
}
}
return nil
}
func (s *Service) ensureRoleAccount(ctx context.Context, orgRef bson.ObjectID, currency string, role account_role.AccountRole) (*pmodel.LedgerAccount, error) {
if s.storage == nil || s.storage.Accounts() == nil {
return nil, errStorageNotInitialized
}
if orgRef.IsZero() {
return nil, merrors.InvalidArgument("organization_ref is required")
}
normalizedCurrency := strings.ToUpper(strings.TrimSpace(currency))
if normalizedCurrency == "" {
return nil, merrors.InvalidArgument("currency is required")
}
if strings.TrimSpace(string(role)) == "" {
return nil, merrors.InvalidArgument("role is required")
}
account, err := s.storage.Accounts().GetByRole(ctx, orgRef, normalizedCurrency, role)
if err == nil {
return account, nil
}
if !errors.Is(err, storage.ErrAccountNotFound) {
s.logger.Warn("Failed to resolve ledger account by role", zap.Error(err),
mzap.ObjRef("organization_ref", orgRef), zap.String("currency", normalizedCurrency),
zap.String("role", string(role)))
return nil, merrors.Internal("failed to resolve ledger account")
}
account = newSystemAccount(orgRef, normalizedCurrency, role)
if err := s.storage.Accounts().Create(ctx, account); err != nil {
if errors.Is(err, merrors.ErrDataConflict) {
existing, lookupErr := s.storage.Accounts().GetByRole(ctx, orgRef, normalizedCurrency, role)
if lookupErr == nil && existing != nil {
return existing, nil
}
s.logger.Warn("Duplicate ledger account create but failed to load existing",
zap.Error(lookupErr),
mzap.ObjRef("organization_ref", orgRef),
zap.String("currency", normalizedCurrency),
zap.String("role", string(role)))
return nil, merrors.Internal("failed to resolve ledger account after conflict")
}
s.logger.Warn("Failed to create system ledger account", zap.Error(err),
mzap.ObjRef("organization_ref", orgRef), zap.String("currency", normalizedCurrency),
zap.String("role", string(role)), zap.String("account_code", account.AccountCode))
return nil, merrors.Internal("failed to create ledger account")
}
s.logger.Info("System ledger account created", mzap.ObjRef("organization_ref", orgRef),
zap.String("currency", normalizedCurrency), zap.String("role", string(role)),
zap.String("account_code", account.AccountCode))
return account, nil
}
func newSystemAccount(orgRef bson.ObjectID, currency string, role account_role.AccountRole) *pmodel.LedgerAccount {
ref := bson.NewObjectID()
account := &pmodel.LedgerAccount{
AccountCode: generateAccountCode(pmodel.LedgerAccountTypeAsset, currency, ref),
AccountType: pmodel.LedgerAccountTypeAsset,
Currency: currency,
Status: pmodel.LedgerAccountStatusActive,
AllowNegative: false,
Role: role,
Scope: pmodel.LedgerAccountScopeOrganization,
Metadata: map[string]string{
"system": "true",
},
}
account.OrganizationRef = &orgRef
account.SetID(ref)
return account
}