Files
sendico/api/ledger/internal/model/account.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

134 lines
4.0 KiB
Go

package ledger
import (
"regexp"
"strconv"
"strings"
"time"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// AccountType controls normal balance side.
type AccountType string
const (
AccountTypeAsset AccountType = "asset"
AccountTypeLiability AccountType = "liability"
AccountTypeRevenue AccountType = "revenue"
AccountTypeExpense AccountType = "expense"
)
type AccountStatus string
const (
AccountStatusActive AccountStatus = "active"
AccountStatusFrozen AccountStatus = "frozen"
)
// lowercase a-z0-9 segments separated by ':'
var accountKeyRe = regexp.MustCompile(`^[a-z0-9]+(?:[:][a-z0-9]+)*$`)
type Account struct {
model.PermissionBound `bson:",inline" json:",inline"`
// Immutable identifier used by postings, balances, etc.
AccountKey string `bson:"accountKey" json:"accountKey"` // e.g., "asset:cash:operating"
PathParts []string `bson:"pathParts,omitempty" json:"pathParts,omitempty"` // optional: ["asset","cash","operating"]
// Classification
AccountType AccountType `bson:"accountType" json:"accountType"`
Currency string `bson:"currency,omitempty" json:"currency,omitempty"`
// Managing entity in your platform (not legal owner).
OrganizationRef *primitive.ObjectID `bson:"organizationRef,omitempty" json:"organizationRef,omitempty"`
// Posting policy & lifecycle
AllowNegative bool `bson:"allowNegative" json:"allowNegative"`
Status AccountStatus `bson:"status" json:"status"`
// Legal ownership history
Ownerships []Ownership `bson:"ownerships,omitempty" json:"ownerships,omitempty"`
CurrentOwners []Ownership `bson:"currentOwners,omitempty" json:"currentOwners,omitempty"` // denormalized cache
// Operational flags
IsSettlement bool `bson:"isSettlement,omitempty" json:"isSettlement,omitempty"`
}
func (a *Account) NormalizeKey() {
a.AccountKey = strings.TrimSpace(strings.ToLower(a.AccountKey))
if len(a.PathParts) == 0 && a.AccountKey != "" {
a.PathParts = strings.Split(a.AccountKey, ":")
}
}
func (a *Account) Validate() error {
var verr *ValidationError
if strings.TrimSpace(a.AccountKey) == "" {
veAdd(&verr, "accountKey", "required", "accountKey is required")
} else if !accountKeyRe.MatchString(a.AccountKey) {
veAdd(&verr, "accountKey", "invalid_format", "use lowercase a-z0-9 segments separated by ':'")
}
switch a.AccountType {
case AccountTypeAsset, AccountTypeLiability, AccountTypeRevenue, AccountTypeExpense:
default:
veAdd(&verr, "accountType", "invalid", "expected asset|liability|revenue|expense")
}
switch a.Status {
case AccountStatusActive, AccountStatusFrozen:
default:
veAdd(&verr, "status", "invalid", "expected active|frozen")
}
// Validate ownership arrays with index context
for i := range a.Ownerships {
if err := a.Ownerships[i].Validate(); err != nil {
veAdd(&verr, "ownerships["+strconv.Itoa(i)+"]", "invalid", err.Error())
}
}
for i := range a.CurrentOwners {
if err := a.CurrentOwners[i].Validate(); err != nil {
veAdd(&verr, "currentOwners["+strconv.Itoa(i)+"]", "invalid", err.Error())
}
}
return verr
}
// ResolveCurrentOwners recomputes CurrentOwners for a given moment.
func (a *Account) ResolveCurrentOwners(asOf time.Time) {
dst := dstSlice(a.CurrentOwners, 0, len(a.Ownerships))
for _, o := range a.Ownerships {
if o.ActiveAt(asOf) {
dst = append(dst, o)
}
}
a.CurrentOwners = dst
}
// BalanceSide returns +1 for debit-normal (asset, expense), -1 for credit-normal (liability, revenue).
func (a *Account) BalanceSide() int {
switch a.AccountType {
case AccountTypeAsset, AccountTypeExpense:
return +1
default:
return -1
}
}
// CloseOwnershipPeriod sets the To date for the first matching active ownership.
func (a *Account) CloseOwnershipPeriod(partyID primitive.ObjectID, role OwnershipRole, to time.Time) bool {
for i := range a.Ownerships {
o := &a.Ownerships[i]
if o.OwnerPartyRef == partyID && o.Role == role && o.ActiveAt(to) {
o.To = &to
return true
}
}
return false
}