Documentation
¶
Overview ¶
Package ledger provides double-entry accounting primitives for Maniflex applications.
Quick start ¶
server.MustRegister(ledger.Models()...)
l := ledger.New(server)
// Inside a Service middleware:
entry, err := l.Post(ctx,
ledger.EntryInput{Description: "Sale invoice #1001", PeriodID: "2026-05", Date: time.Now()},
[]ledger.LineInput{
{AccountID: "ar-001", Debit: money.New(100_00, "SAR")},
{AccountID: "rev-001", Credit: money.New(100_00, "SAR")},
},
)
Models ¶
The package provides three Maniflex model types that become standard REST endpoints when registered:
- LedgerAccount — chart of accounts
- LedgerEntry — journal entry header (read-only once posted)
- LedgerLine — individual debit or credit leg
Balance queries ¶
bal, err := ledger.Balance(ctx, "ar-001", "SAR", time.Now()) tb, err := ledger.TrialBalance(ctx, "SAR", time.Now())
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrImbalanced is returned when the sum of debit cents does not equal the // sum of credit cents for any currency represented in the entry. ErrImbalanced = errors.New("ledger: entry is not balanced") // ErrNoLines is returned when Post is called with fewer than two lines. ErrNoLines = errors.New("ledger: entry must have at least two lines") )
Sentinel errors returned by Post.
Functions ¶
func Balance ¶
func Balance(ctx *maniflex.ServerContext, accountID, currency string, asOf time.Time) (money.Amount, error)
Balance returns the net balance of accountID in the given currency as of asOf. Positive = net debit balance; negative = net credit balance.
ctx.Tx is honoured: when an active transaction is set, the query runs within it.
Types ¶
type AccountBalance ¶
type AccountBalance struct {
AccountID string
Currency string
// Balance is positive for a net debit balance, negative for a net credit
// balance. Whether a positive or negative balance is "normal" depends on
// the account type (asset/expense accounts have normal debit balances;
// liability/equity/revenue have normal credit balances).
Balance money.Amount
}
AccountBalance is one row in a TrialBalance result.
func TrialBalance ¶
func TrialBalance(ctx *maniflex.ServerContext, currency string, asOf time.Time) ([]AccountBalance, error)
TrialBalance returns the net balance of every account that has any activity in the given currency as of asOf, ordered by account_id.
type EntryInput ¶
type EntryInput struct {
Description string
Reference string // optional memo / source document reference
PeriodID string // e.g. "2026-05" or "2026-Q2" — opaque to pkg/ledger
Date time.Time // accounting date (may differ from PostedAt)
}
EntryInput carries the journal entry header for a Post call.
type Ledger ¶
type Ledger struct{}
Ledger provides the Post operation.
func New ¶
func New() *Ledger
New returns a Ledger. The models from Models() must be registered on the server before calling Post.
func (*Ledger) Post ¶
func (l *Ledger) Post(ctx *maniflex.ServerContext, entry EntryInput, lines []LineInput) (*LedgerEntry, error)
Post records a balanced journal entry atomically.
Validation (performed before any DB work):
- At least two lines are required.
- Each line must have exactly one non-zero amount (Debit or Credit).
- Amounts must be non-negative.
- Each line must specify an AccountID and a currency (via the non-zero Amount).
- Sum of debits must equal sum of credits per currency.
If ctx.Tx is already set, Post joins the active transaction and does not commit — the caller is responsible. Otherwise Post opens and commits its own transaction; if any step fails the transaction is rolled back.
PostedAt on the returned entry is set to time.Now().UTC().
type LedgerAccount ¶
type LedgerAccount struct {
maniflex.BaseModel
Code string `json:"code" db:"code" mfx:"required,unique,filterable,sortable"`
Name string `json:"name" db:"name" mfx:"required,filterable,sortable,searchable"`
Type string `json:"type" db:"type" mfx:"required,filterable,enum:asset|liability|equity|revenue|expense"`
ParentID string `json:"parent_id" db:"parent_id" mfx:"filterable"`
}
LedgerAccount is a chart-of-accounts entry. Register it via Models().
type LedgerEntry ¶
type LedgerEntry struct {
maniflex.BaseModel
Description string `json:"description" db:"description" mfx:"required,filterable,searchable"`
Reference string `json:"reference" db:"reference" mfx:"filterable"`
PeriodID string `json:"period_id" db:"period_id" mfx:"required,filterable"`
Date time.Time `json:"date" db:"date" mfx:"required,filterable,sortable"`
PostedAt *time.Time `json:"posted_at" db:"posted_at" mfx:"readonly,filterable,sortable"`
}
LedgerEntry is a journal entry header. PostedAt is set automatically by Post() and is read-only via the REST API.
type LedgerLine ¶
type LedgerLine struct {
maniflex.BaseModel
EntryID string `json:"entry_id" db:"entry_id" mfx:"required,filterable"`
AccountID string `json:"account_id" db:"account_id" mfx:"required,filterable"`
DebitCents int64 `json:"debit_cents" db:"debit_cents" mfx:"filterable,sortable"`
CreditCents int64 `json:"credit_cents" db:"credit_cents" mfx:"filterable,sortable"`
Currency string `json:"currency" db:"currency" mfx:"required,filterable"`
Notes string `json:"notes" db:"notes" mfx:"filterable"`
}
LedgerLine is one leg of a journal entry. Exactly one of DebitCents or CreditCents is non-zero.
type LineInput ¶
type LineInput struct {
AccountID string
Debit money.Amount // set for debit legs; leave zero for credit legs
Credit money.Amount // set for credit legs; leave zero for debit legs
Notes string // optional free-text annotation
}
LineInput is one debit or credit leg. Set Debit or Credit — not both. Currency is taken from whichever Amount is non-zero.