service backend
This commit is contained in:
133
api/ledger/internal/model/account.go
Normal file
133
api/ledger/internal/model/account.go
Normal file
@@ -0,0 +1,133 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user