Documentation
¶
Overview ¶
Package core provides the normative validation engine for NomLedger.
It acts as a storage-agnostic validation engine that enforces strict financial invariants on in-memory data structures. Any Transaction produced by the builders or facades is structurally immutable and satisfies these guarantees:
- Valid configuration: the ledger currency is active and ratable, and the rate tolerance is non-negative.
- Valid transaction fields: the ID and metadata keys are bounded and free of control characters, and the effective date is stored in UTC.
- Non-empty entries: a transaction contains at least one valid entry.
- Currency consistency: every entry uses the ledger's functional currency.
- Zero-sum: the exact sum of all functional amounts is zero.
- Period closure: no non-adjusting transaction falls in a closed period.
- Rate integrity: foreign-entry rates match the provider within the configured tolerance.
After builder-option and context checks, validation follows a fixed order: configuration, transaction ID, effective date, non-empty entries, metadata keys, entry validity, currency consistency, zero-sum, period closure, and rate integrity. It fails fast, returns the zero Transaction on every error, and performs no later provider calls after an earlier failure.
Ledger.ValidateTransaction(ctx, TransactionInput) is the canonical validation path. It treats a nil context as context.Background() and checks cancellation before any validation work. TxBuilder.Build runs the same shared pipeline.
A Ledger or TxBuilder may be given an injected Clock; the default is time.Now. PostedAt is read once per successful validation and stored in UTC, so identical inputs, provider responses, and clock yield identical transactions.
Failures are *ValidationError values wrapping typed sentinels from the pkg/errors package, so callers can use errors.Is and errors.As. Provider errors are wrapped with ErrRateProvider while preserving their original cause.
The primary entry point is the Ledger struct, which is safe for concurrent use. Lower-level primitives like TxBuilder are provided for granular control but require manual lifecycle management: a successful Build finalizes the builder, while a failed Build leaves it retryable. Metadata values are shallowly copied and remain caller-owned.
Example (ValidationError) ¶
config := core.LedgerConfig{
FunctionalCurrency: codeUSD,
ClosedDate: time.Date(2023, 12, 31, 23, 59, 59, 0, time.UTC),
}
provider := interfaces.RateProviderFunc(func(context.Context, interfaces.RateRequest) (decimal.Decimal, error) {
return decimal.NewFromInt(1), nil
})
ledger, err := core.NewLedger(config, provider, core.WithClock(exampleClock()))
if err != nil {
panic(err)
}
debit, err := core.NewEntry("acc-cash", decimal.NewFromInt(100), codeUSD, decimal.NewFromInt(1), codeUSD)
if err != nil {
panic(err)
}
credit, err := core.NewEntry("acc-revenue", decimal.NewFromInt(-99), codeUSD, decimal.NewFromInt(1), codeUSD)
if err != nil {
panic(err)
}
_, err = ledger.ValidateTransaction(context.Background(), core.TransactionInput{
ID: "tx-unbalanced",
EffectiveDate: time.Date(2024, 6, 1, 12, 0, 0, 0, time.UTC),
Entries: []core.Entry{debit, credit},
})
var validationErr *pkgerr.ValidationError
structured := errors.As(err, &validationErr)
zeroSum := errors.Is(err, pkgerr.ErrZeroSumViolation)
operation := ""
entryIndex := 0
if structured {
operation = validationErr.Operation
entryIndex = validationErr.EntryIndex
}
fmt.Printf("zeroSum=%t structured=%t operation=%s entry=%d\n",
zeroSum, structured, operation, entryIndex)
Output: zeroSum=true structured=true operation=ValidateTransaction entry=-1
Index ¶
- type Account
- type AccountType
- type BuilderOption
- type Clock
- type ClockFunc
- type Entry
- func (e Entry) AccountID() string
- func (e Entry) ExchangeRate() decimal.Decimal
- func (e Entry) FunctionalAmount() decimal.Decimal
- func (e Entry) FunctionalCurrency() string
- func (e Entry) FunctionalMoney() currency.Money
- func (e Entry) TxAmount() decimal.Decimal
- func (e Entry) TxCurrency() string
- func (e Entry) TxMoney() currency.Money
- func (e Entry) Validate() error
- type Ledger
- func (l *Ledger) Config() LedgerConfig
- func (l *Ledger) UpdateConfig(config LedgerConfig) (*Ledger, error)
- func (l *Ledger) ValidateTransaction(ctx context.Context, in TransactionInput) (Transaction, error)
- func (l *Ledger) ValidateTransactionLegacy(id string, effectiveDate time.Time, isAdjustingEvent bool, entries ...Entry) (Transaction, error)deprecated
- type LedgerConfig
- type LedgerOption
- type MeasurementCategory
- type Transaction
- type TransactionInput
- type TxBuilder
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Account ¶
type Account struct {
// contains filtered or unexported fields
}
Account is the fundamental unit of storage for financial classification.
Accounts are structurally immutable after construction through NewAccount. Normative data (ID, type, category, and currency) is validated at construction and by Account.Validate. Measurement categories are declarative tags only.
Metadata is non-normative and is never consulted by validation. The map is shallowly copied on construction and on every Metadata call; values that are reference types remain shared with the caller.
func NewAccount ¶
func NewAccount( id string, typ AccountType, cat MeasurementCategory, currencyCode string, metadata map[string]any, ) (Account, error)
NewAccount constructs and validates a structurally immutable Account.
It copies the supplied metadata map, treating nil as an empty map. Validation checks the ID, type, measurement category, and currency in that order. The returned error is a *ValidationError wrapping the relevant sentinel.
func (Account) Category ¶
func (a Account) Category() MeasurementCategory
Category returns the account's declarative IFRS 9 measurement tag.
func (Account) CurrencyCode ¶
CurrencyCode returns the canonical uppercase ISO 4217 currency code.
func (Account) Type ¶
func (a Account) Type() AccountType
Type returns the account's reporting classification.
type AccountType ¶
type AccountType int8
AccountType represents the high-level reporting classification of an account.
These types are used for report generation (e.g., distinguishing Assets from Liabilities). Their membership is validated by NewAccount and Account.Validate, but the engine does not enforce natural-balance or reporting policy, allowing for flexible transaction structures.
const ( // Asset identifies resources controlled by the entity. Asset AccountType = iota + 1 // Liability identifies present obligations of the entity. Liability // Equity identifies the residual interest in the assets of the entity. Equity // Income identifies increases in economic benefits during the accounting period. Income // Expense identifies decreases in economic benefits during the accounting period. Expense )
type BuilderOption ¶
type BuilderOption func(*builderOptions) error
BuilderOption configures a TxBuilder during construction.
func WithBuilderClock ¶
func WithBuilderClock(c Clock) BuilderOption
WithBuilderClock selects the clock used by a TxBuilder.
A nil clock means the default (time.Now). The option itself never returns an error.
type Clock ¶
Clock supplies the current time for transaction construction.
The default clock is time.Now. Implementations must be safe for concurrent use when the ledger is shared; time.Now is. Values read from a clock are normalized to UTC before they are stored in Transaction.PostedAt.
type Entry ¶
type Entry struct {
// contains filtered or unexported fields
}
Entry represents a single line item in a financial transaction.
It enforces a strict signed-amount convention where positive values represent debits and negative values represent credits. Fields are unexported to keep the entry structurally immutable after construction.
NewEntry validates the account ID, both currency codes, the transaction amount's precision and non-zero value, and the exchange rate. It rejects same-currency entries unless the rate is exactly one and uses HalfEven rounding for the functional amount. A non-zero transaction amount that rounds to zero in the functional currency is rejected.
func NewEntry ¶
func NewEntry( accountID string, txAmount decimal.Decimal, txCurrency string, exchangeRate decimal.Decimal, functionalCurrency string, ) (Entry, error)
NewEntry constructs and validates a structurally immutable Entry.
Functional amounts use canonical HalfEven rounding at the functional currency's minor units. The returned error is a *ValidationError wrapping the relevant sentinel.
func (Entry) ExchangeRate ¶
ExchangeRate returns the rate used to convert TxAmount to FunctionalAmount.
func (Entry) FunctionalAmount ¶
FunctionalAmount returns the converted amount in the functional currency. This value is rounded to the precision defined by the functional currency.
func (Entry) FunctionalCurrency ¶
FunctionalCurrency returns the canonical uppercase ISO 4217 reporting currency code.
func (Entry) FunctionalMoney ¶
FunctionalMoney returns the functional amount as a currency-aware Money value.
func (Entry) TxAmount ¶
TxAmount returns the raw amount in the transaction currency. Positive values indicate a debit; negative values indicate a credit.
func (Entry) TxCurrency ¶
TxCurrency returns the canonical uppercase ISO 4217 transaction currency code.
type Ledger ¶
type Ledger struct {
// contains filtered or unexported fields
}
Ledger is the concurrency-safe entry point for the validation engine.
It encapsulates the validated configuration and rate provider required to enforce consistent validation rules across all transactions. Fields are unexported and immutable after construction; each validation uses a fresh internal builder.
func NewLedger ¶
func NewLedger(config LedgerConfig, provider interfaces.RateProvider, opts ...LedgerOption) (*Ledger, error)
NewLedger constructs a new Ledger instance.
It validates and normalizes the configuration before accepting it, applies options, defaults a nil clock to time.Now, and rejects a nil rate provider with ErrNilRateProvider. Option failures and other errors are *ValidationError values with Operation "NewLedger" and Field "clock", "config", or "provider".
func (*Ledger) Config ¶
func (l *Ledger) Config() LedgerConfig
Config returns a copy of the ledger's normalized configuration.
func (*Ledger) UpdateConfig ¶
func (l *Ledger) UpdateConfig(config LedgerConfig) (*Ledger, error)
UpdateConfig returns a new validated Ledger with updated configuration.
It preserves the existing rate provider and clock, and never mutates the receiver. Errors are *ValidationError values with Operation "UpdateConfig" and Field "config".
func (*Ledger) ValidateTransaction ¶
func (l *Ledger) ValidateTransaction(ctx context.Context, in TransactionInput) (Transaction, error)
ValidateTransaction validates a TransactionInput and returns an immutable Transaction. It is the canonical validation path.
A nil context is treated as context.Background(). A canceled context returns a *ValidationError matching both ErrRateProvider and the context error before any validation work, including for same-currency-only transactions. The input entries and metadata map are copied, so later mutation of the input cannot change the returned transaction. Validation uses the same pipeline as TxBuilder.Build and does not mutate the ledger. The ledger's injected clock determines PostedAt, which is always stored in UTC.
Example ¶
config := core.LedgerConfig{
FunctionalCurrency: codeUSD,
ClosedDate: time.Date(2023, 12, 31, 23, 59, 59, 0, time.UTC),
}
provider := interfaces.RateProviderFunc(func(context.Context, interfaces.RateRequest) (decimal.Decimal, error) {
return decimal.NewFromInt(1), nil
})
ledger, err := core.NewLedger(config, provider, core.WithClock(exampleClock()))
if err != nil {
panic(err)
}
debit, err := core.NewEntry("acc-cash", decimal.NewFromInt(100), codeUSD, decimal.NewFromInt(1), codeUSD)
if err != nil {
panic(err)
}
credit, err := core.NewEntry("acc-revenue", decimal.NewFromInt(-100), codeUSD, decimal.NewFromInt(1), codeUSD)
if err != nil {
panic(err)
}
transaction, err := ledger.ValidateTransaction(context.Background(), core.TransactionInput{
ID: "tx-example",
EffectiveDate: time.Date(2024, 6, 1, 12, 0, 0, 0, time.UTC),
Entries: []core.Entry{debit, credit},
})
if err != nil {
panic(err)
}
fmt.Printf("id=%s entries=%d posted=%s\n",
transaction.ID(), len(transaction.Entries()), transaction.PostedAt().Format(time.RFC3339))
Output: id=tx-example entries=2 posted=2024-06-01T12:00:00Z
Example (Foreign) ¶
config := core.LedgerConfig{
FunctionalCurrency: codeUSD,
ClosedDate: time.Date(2023, 12, 31, 23, 59, 59, 0, time.UTC),
RateTolerance: decimal.RequireFromString("0.01"),
}
provider := interfaces.RateProviderFunc(func(context.Context, interfaces.RateRequest) (decimal.Decimal, error) {
return decimal.RequireFromString("1.1"), nil
})
ledger, err := core.NewLedger(config, provider, core.WithClock(exampleClock()))
if err != nil {
panic(err)
}
debit, err := core.NewEntry(
"acc-eur",
decimal.RequireFromString("100.00"),
"EUR",
decimal.RequireFromString("1.1"),
codeUSD,
)
if err != nil {
panic(err)
}
credit, err := core.NewEntry(
"acc-usd",
decimal.RequireFromString("-110.00"),
codeUSD,
decimal.NewFromInt(1),
codeUSD,
)
if err != nil {
panic(err)
}
transaction, err := ledger.ValidateTransaction(context.Background(), core.TransactionInput{
ID: "tx-foreign",
EffectiveDate: time.Date(2024, 6, 1, 12, 0, 0, 0, time.UTC),
Entries: []core.Entry{debit, credit},
})
if err != nil {
panic(err)
}
entry := transaction.Entries()[0]
fmt.Printf("%s %s\n", entry.FunctionalAmount().StringFixed(2), entry.FunctionalCurrency())
Output: 110.00 USD
func (*Ledger) ValidateTransactionLegacy
deprecated
func (l *Ledger) ValidateTransactionLegacy( id string, effectiveDate time.Time, isAdjustingEvent bool, entries ...Entry, ) (Transaction, error)
ValidateTransactionLegacy validates a transaction using the pre-1.0 call shape. It delegates to ValidateTransaction with context.Background().
Deprecated: use ValidateTransaction.
type LedgerConfig ¶
type LedgerConfig struct {
// ClosedDate is the timestamp up to which the books are finalized. A zero
// value disables the period-closure check.
ClosedDate time.Time
// FunctionalCurrency is the base currency for this ledger instance.
// Used to ensure all entries in a transaction conform to the entity's reporting currency.
FunctionalCurrency string
// RateTolerance defines the maximum allowable absolute difference between
// the provided entry exchange rate and the RateProvider's rate. A zero value
// requires exact equality.
RateTolerance decimal.Decimal
}
LedgerConfig holds the immutable configuration for a validation batch. It acts as the "Closure state definition" for the ledger.
Invariant 3 (Period Closure): The ClosedDate acts as a watermark. Transactions with an EffectiveDate <= ClosedDate are rejected unless they are explicitly marked as Adjusting Events. A zero ClosedDate disables the watermark.
func (LedgerConfig) Validate ¶
func (c LedgerConfig) Validate() error
Validate ensures the configuration is well-formed.
FunctionalCurrency must be a valid, ratable, active ISO 4217 currency and RateTolerance must be zero or positive. ClosedDate may be zero, which disables the period-closure check. The returned error is a *ValidationError wrapping ErrInvalidConfiguration.
type LedgerOption ¶
type LedgerOption func(*ledgerOptions) error
LedgerOption configures a Ledger during construction.
func WithClock ¶
func WithClock(c Clock) LedgerOption
WithClock selects the ledger clock.
A nil clock means the default (time.Now). The option itself never returns an error.
type MeasurementCategory ¶
type MeasurementCategory int8
MeasurementCategory represents IFRS 9 classification tags.
These are declarative markers used for downstream reporting and valuation logic. The library stores these tags but does not enforce business-model tests, amortization logic, or any measurement treatment.
const ( // NotApplicable indicates that no IFRS 9 measurement category applies. NotApplicable MeasurementCategory = iota // AmortizedCost indicates the asset is held to collect contractual cash flows. AmortizedCost // FVTPL indicates the asset is measured at Fair Value Through Profit or Loss. FVTPL // FVOCI indicates the asset is measured at Fair Value Through Other // Comprehensive Income. FVOCI )
type Transaction ¶
type Transaction struct {
// contains filtered or unexported fields
}
Transaction represents a persisted, valid financial state.
It is the structurally immutable result of a successful validation process. Callers can safely persist this struct or share it across goroutines. Fields are unexported to enforce structural immutability.
Entries and the top-level metadata map are copied on build and on every accessor call. Metadata values remain caller-owned: if they are reference types, the underlying data remains shared.
Transaction IDs and metadata keys are bounded and control-character-free; Build enforces those rules. Effective dates are stored in UTC.
func (Transaction) EffectiveDate ¶
func (t Transaction) EffectiveDate() time.Time
EffectiveDate returns the date on which the transaction impacts the ledger, normalized to UTC.
func (Transaction) Entries ¶
func (t Transaction) Entries() []Entry
Entries returns a defensive copy of the immutable entries associated with this transaction. This prevents external mutation of the underlying slice.
func (Transaction) ID ¶
func (t Transaction) ID() string
ID returns the validated transaction identifier.
func (Transaction) IsAdjustingEvent ¶
func (t Transaction) IsAdjustingEvent() bool
IsAdjustingEvent reports whether the transaction is an IAS 10 adjusting event.
func (Transaction) Metadata ¶
func (t Transaction) Metadata() map[string]any
Metadata returns a defensive copy of the transaction's metadata map.
A nil internal map returns a non-nil empty map. The copy is shallow: metadata values that are reference types remain caller-owned.
func (Transaction) PostedAt ¶
func (t Transaction) PostedAt() time.Time
PostedAt returns the UTC instant at which the transaction was built.
type TransactionInput ¶
type TransactionInput struct {
// ID is the transaction identifier.
ID string
// EffectiveDate is the transaction effective date; it is normalized to UTC.
EffectiveDate time.Time
// IsAdjustingEvent marks the transaction as an IAS 10 adjusting event.
IsAdjustingEvent bool
// Entries are the transaction line items; the slice is copied on validation.
Entries []Entry
// Metadata is non-normative data; the map is shallowly copied on validation.
Metadata map[string]any
}
TransactionInput is the canonical value-type input for validation.
Entries and the top-level Metadata map are copied during validation, so the caller may safely reuse or mutate the input after the call returns. The copy is shallow: metadata values that are reference types remain caller-owned and shared.
type TxBuilder ¶
type TxBuilder struct {
// contains filtered or unexported fields
}
TxBuilder accumulates state for constructing a Transaction.
A builder starts accumulating, a failed Build leaves it retryable, and a successful Build finalizes it. After finalization, AddEntry, SetAdjustingEvent, and WithMetadata are no-ops, and Build returns ErrBuilderFinalized. It is stateful and not safe for concurrent use. TxBuilder.Build runs the same validation pipeline as Ledger.ValidateTransaction.
func NewTxBuilder ¶
func NewTxBuilder( id string, date time.Time, config LedgerConfig, rp interfaces.RateProvider, opts ...BuilderOption, ) *TxBuilder
NewTxBuilder initializes a builder with the required validation context.
Options configure the builder; a nil clock option means time.Now. Option errors are stored and returned by Build as a structured error with Field "clock".
func (*TxBuilder) AddEntry ¶
AddEntry appends a line item to the transaction.
It is a no-op after a successful Build.
func (*TxBuilder) Build ¶
func (b *TxBuilder) Build() (Transaction, error)
Build validates the accumulated state and returns an immutable Transaction.
It delegates to the same validation pipeline used by Ledger.ValidateTransaction. After builder-option and context checks, validation follows a fixed order: configuration, transaction ID, effective date, non-empty entries, metadata keys, entry validity, currency consistency, zero-sum balance, period closure, and rate integrity. It fails fast, calls no later checks or providers after a failure, and returns the zero Transaction on every error. Field-level failures are *ValidationError values; provider errors are wrapped with ErrRateProvider while preserving their cause.
A failed Build leaves the builder retryable. A successful Build finalizes the builder; a subsequent Build returns ErrBuilderFinalized. A builder-option error is returned before other validation. PostedAt is read once from the builder clock (default time.Now) and stored in UTC.
func (*TxBuilder) SetAdjustingEvent ¶
SetAdjustingEvent marks the transaction as an IAS 10 Adjusting Event.
The flag is a caller-supplied classification and only bypasses the period closure check; it does not affect any other validation rule. It is a no-op after a successful Build.
func (*TxBuilder) WithMetadata ¶
WithMetadata adds non-normative data to the transaction.
It performs a shallow copy of the provided map into the builder, so mutating the caller's top-level map after this call does not affect the builder. Metadata values that are reference types remain caller-owned. Keys are validated when Build runs: they must be non-empty, at most 64 bytes, and free of NUL and ASCII control characters. It is a no-op after a successful Build.