The accounting pipeline

Copy Markdown View Source

The accounting pipeline is the defining mental model of Logistiki. Each stage is represented in code (Logistiki.Accounting.Pipeline), recorded in the audit trail, and emitted as telemetry.

Business Event
    
    
Event Normalization
    
    
Datalog Fact Generation
    
    
Business Rule Evaluation
    
    
Accounting Policy Selection
    
    
Accounting Template Resolution
    
    
Journal Builder
    
    
Posting Builder
    
    
Ledger Invariant Validation
    
    
Ledger Backend Execution
    
    
Projection Generation
    
    
Audit Evidence

Stages

1. Event normalization

Logistiki.Event.normalize/1 dispatches to the event struct's Logistiki.Event implementation and returns a Logistiki.Event.Normalized struct — the canonical, flattened representation the knowledge layer consumes.

2. Datalog fact generation

Logistiki.Knowledge.Facts.generate/1 turns the normalized event into a list of {relation, values} facts (e.g. {:event_type, [:evt, :deposit_received]}, {:event_amount_cents, [:evt, 100000]}). The event is represented inside the Datalog program by the fixed atom :evt.

3. Business rule evaluation

The knowledge program (Logistiki.Knowledge.Program) derives blocked(:evt) and requires_approval(:evt). A blocked event short-circuits with %Logistiki.Error{code: :blocked_event}; an event requiring approval short-circuits with %Logistiki.Error{code: :approval_required}. Events with no accounting impact produce a valid result with journal: nil.

4. Accounting policy selection

The program derives policy(:evt, policy_name) from the event's facts. Exactly one policy must match; zero matches yields :no_policy_found, and ambiguity is resolved deterministically (sorted, first wins) with a warning.

5. Template resolution

For the selected policy, the program's template_posting/6 facts describe the posting specs (sequence, direction, role, amount/currency variables). The account_role/3 rules resolve each symbolic role (:cash_account, :client_liability_account, :fee_income_account, ...) to a concrete account code — either from event-provided facts or a static account_role_static/2 fallback.

6–7. Journal & posting builder

Logistiki.Accounting.JournalBuilder and Logistiki.Accounting.PostingBuilder materialize the knowledge result into a draft %Journal{} with draft %Posting{} structs. Datalog never constructs structs; Elixir does.

8. Ledger invariant validation

Logistiki.Accounting.InvariantValidator enforces the hard accounting invariants in pure Elixir (debits = credits per currency, positive amounts, leaf posting accounts, idempotency uniqueness, exact reversal). These checks never depend on Datalog.

9. Ledger backend execution

The configured backend (Logistiki.Ledger.Simulation or Logistiki.Ledger.Beancount) executes the journal — persisting it as posted and (for the Beancount backend) verifying it with Beancount.check/1.

10. Projection generation

Balances, statements, trial balances, and other projections are computed from the now-immutable postings by Logistiki.Projections.ProjectionEngine.

11. Audit evidence

Every stage is recorded as an audit_events row by Logistiki.Audit, so the full chain — event → rules → policy → template → journal → postings → ledger result — can be explained later.

Telemetry

Each stage emits :telemetry events (see Logistiki.Telemetry.events/0), e.g. [:logistiki, :knowledge, :evaluate, :stop], [:logistiki, :journal, :build, :stop], [:logistiki, :ledger, :execute, :stop], [:logistiki, :audit, :write, :stop].