core

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 5, 2026 License: MIT Imports: 8 Imported by: 0

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:

  1. Valid configuration: the ledger currency is active and ratable, and the rate tolerance is non-negative.
  2. Valid transaction fields: the ID and metadata keys are bounded and free of control characters, and the effective date is stored in UTC.
  3. Non-empty entries: a transaction contains at least one valid entry.
  4. Currency consistency: every entry uses the ledger's functional currency.
  5. Zero-sum: the exact sum of all functional amounts is zero.
  6. Period closure: no non-adjusting transaction falls in a closed period.
  7. 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

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

func (a Account) CurrencyCode() string

CurrencyCode returns the canonical uppercase ISO 4217 currency code.

func (Account) ID

func (a Account) ID() string

ID returns the account identifier.

func (Account) Metadata

func (a Account) Metadata() map[string]any

Metadata returns a defensive shallow copy of the account's metadata.

func (Account) Type

func (a Account) Type() AccountType

Type returns the account's reporting classification.

func (Account) Validate

func (a Account) Validate() error

Validate re-validates the account without modifying it.

Validation checks the ID, type, measurement category, and currency in that order and returns the first failure as a *ValidationError.

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

type Clock interface {
	Now() time.Time
}

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 ClockFunc

type ClockFunc func() time.Time

ClockFunc adapts a function to the Clock interface.

func (ClockFunc) Now

func (f ClockFunc) Now() time.Time

Now calls f(). A nil ClockFunc falls back to time.Now.

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) AccountID

func (e Entry) AccountID() string

AccountID returns the unique identifier of the associated account.

func (Entry) ExchangeRate

func (e Entry) ExchangeRate() decimal.Decimal

ExchangeRate returns the rate used to convert TxAmount to FunctionalAmount.

func (Entry) FunctionalAmount

func (e Entry) FunctionalAmount() decimal.Decimal

FunctionalAmount returns the converted amount in the functional currency. This value is rounded to the precision defined by the functional currency.

func (Entry) FunctionalCurrency

func (e Entry) FunctionalCurrency() string

FunctionalCurrency returns the canonical uppercase ISO 4217 reporting currency code.

func (Entry) FunctionalMoney

func (e Entry) FunctionalMoney() currency.Money

FunctionalMoney returns the functional amount as a currency-aware Money value.

func (Entry) TxAmount

func (e Entry) TxAmount() decimal.Decimal

TxAmount returns the raw amount in the transaction currency. Positive values indicate a debit; negative values indicate a credit.

func (Entry) TxCurrency

func (e Entry) TxCurrency() string

TxCurrency returns the canonical uppercase ISO 4217 transaction currency code.

func (Entry) TxMoney

func (e Entry) TxMoney() currency.Money

TxMoney returns the transaction amount as a currency-aware Money value.

func (Entry) Validate

func (e Entry) Validate() error

Validate re-validates the entry without modifying it.

A zero-value Entry returns ErrInvalidEntry. Otherwise the stored fields and currency-aware Money values are checked against a recomputed functional amount. Validation is idempotent and read-only.

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

func (b *TxBuilder) AddEntry(e Entry) *TxBuilder

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

func (b *TxBuilder) SetAdjustingEvent(isAdjusting bool) *TxBuilder

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

func (b *TxBuilder) WithMetadata(meta map[string]any) *TxBuilder

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL