Logistiki — an embedded OTP accounting engine for Elixir applications.
Logistiki is the accounting execution layer that applications embed. It is not a bank, an ERP, a payment system, or a compliance platform.
The primary API
Applications publish business events:
Logistiki.process(%Logistiki.Event.DepositReceived{...})Logistiki decides whether and how those events become accounting entries. Applications should not directly create journals or postings as their normal workflow — those are internal accounting artifacts.
Querying the results
Logistiki.balance(account)
Logistiki.statement(account)
Logistiki.trial_balance()
Logistiki.general_ledger()
Logistiki.balance_sheet()
Logistiki.income_statement()Balances are projections over immutable postings — never the source of truth.
Administrative API
Lower-level functions live under explicit namespaces (Logistiki.Accounting,
Logistiki.BusinessEntities, Logistiki.VirtualAccounts,
Logistiki.Relationships, Logistiki.Knowledge, Logistiki.Audit).
Summary
Functions
Computes the balance(s) for an account.
Computes balances for accounts linked to entity.
Computes balances for accounts linked to entity or any of its descendants.
Builds a balance sheet (assets, liabilities, equity).
Returns the Logistiki.Error struct module (for documentation/introspection).
Builds a general ledger view of all posted and reversed postings.
Builds an income statement (income, expenses, net profit).
Returns the configured ledger backend module.
Processes a business event through the full accounting pipeline.
Sets the ledger backend at runtime (mainly for tests and demos).
Returns the Logistiki.Accounting.Result struct module (for documentation/introspection).
Builds a running-balance statement for an account.
Builds a trial balance across all posted and reversed journals.
Functions
@spec balance( VirtualAccount.t() | String.t() | integer(), keyword() ) :: {:ok, [Logistiki.Projections.Balance.t()]} | {:error, term()}
Computes the balance(s) for an account.
Aggregates descendant postings when account_or_code is a parent account,
using the closure table for fast subtree queries.
Arguments
account_or_code— one of:%Logistiki.VirtualAccounts.VirtualAccount{}— the account structString.t— an account code (e.g."ASSETS:CASH:USD:NOSTRO") or idinteger()— an account id
opts—keyword()of options::currency—String.t— filter to a single currency (e.g."USD")
Returns
{:ok, [Logistiki.Projections.Balance.t()]}— one balance per currency.{:error, :account_not_found}— the account does not exist.
Examples
iex> {:ok, [balance]} = Logistiki.balance("ASSETS:CASH:USD:NOSTRO")
iex> balance.net
Decimal.new("1000.00")
iex> {:ok, balances} = Logistiki.balance("LIABILITIES:CLIENT_DEPOSITS:USD:ACME")
iex> Enum.reduce(balances, Decimal.new(0), &Decimal.add(&1.net, &2.net))
Decimal.new("-1000.00")
iex> {:error, :account_not_found} = Logistiki.balance("UNKNOWN:CODE")
@spec balance_for_entity( BusinessEntity.t() | integer(), keyword() ) :: {:ok, [Logistiki.Projections.Balance.t()]}
Computes balances for accounts linked to entity.
Uses the entity_accounts relationship table to find all accounts owned by
(or otherwise linked to) the entity, then sums postings across them.
Arguments
entity—%Logistiki.BusinessEntities.BusinessEntity{}or entity id.opts—keyword()of options::currency—String.t— filter to a currency (e.g."USD"):relationship_type—atom()— filter by type (e.g.:owner):at—Date.t— effective date for temporal filtering
Returns
{:ok, [Logistiki.Projections.Balance.t()]}— balances per currency.
Examples
iex> {:ok, balances} = Logistiki.balance_for_entity(acme_entity)
iex> hd(balances).net
Decimal.new("-1000.00")
iex> {:ok, balances} = Logistiki.balance_for_entity(acme_entity, relationship_type: :owner)
@spec balance_for_entity_tree( BusinessEntity.t() | integer(), keyword() ) :: {:ok, [Logistiki.Projections.Balance.t()]}
Computes balances for accounts linked to entity or any of its descendants.
Uses the business-entity closure table to find all descendant entities, then finds all accounts linked to any of them, and sums postings across the set.
Arguments
entity—%Logistiki.BusinessEntities.BusinessEntity{}or entity id.opts—keyword()of options::currency—String.t— filter to a currency:relationship_type—atom()— filter by type:at—Date.t— effective date
Returns
{:ok, [Logistiki.Projections.Balance.t()]}— balances per currency.
Examples
iex> {:ok, balances} = Logistiki.balance_for_entity_tree(acme_holdings)
iex> Enum.reduce(balances, Decimal.new(0), &Decimal.add(&1.net, &2.net))
Decimal.new("-1500.00")
@spec balance_sheet(keyword()) :: Logistiki.Projections.BalanceSheet.t()
Builds a balance sheet (assets, liabilities, equity).
Arguments
opts—keyword()of options::currency—String.t— filter to a currency
Returns
%Logistiki.Projections.BalanceSheet{}— withassets,liabilities,equitysections (each withbalancesandtotals_by_currency), andtotals_by_currency.
Examples
iex> bs = Logistiki.balance_sheet()
iex> bs.totals_by_currency["USD"][:assets]
Decimal.new("1000.00")
iex> bs.totals_by_currency["USD"][:liabilities]
Decimal.new("-1000.00")
@spec error() :: module()
Returns the Logistiki.Error struct module (for documentation/introspection).
Examples
iex> Logistiki.error()
Logistiki.Error
@spec general_ledger(keyword()) :: Logistiki.Projections.GeneralLedger.t()
Builds a general ledger view of all posted and reversed postings.
Returns all postings ordered by effective date, journal insertion, then posting sequence.
Arguments
opts—keyword()of options::currency—String.t— filter to a currency
Returns
%Logistiki.Projections.GeneralLedger{}— withlines.
Examples
iex> gl = Logistiki.general_ledger()
iex> length(gl.lines)
4
iex> hd(gl.lines).account_code
"ASSETS:CASH:USD:NOSTRO"
@spec income_statement(keyword()) :: Logistiki.Projections.IncomeStatement.t()
Builds an income statement (income, expenses, net profit).
Arguments
opts—keyword()of options::currency—String.t— filter to a currency
Returns
%Logistiki.Projections.IncomeStatement{}— withincome,expensessections andnet_profit_by_currency.
Examples
iex> is = Logistiki.income_statement()
iex> is.net_profit_by_currency["USD"]
Decimal.new("-25.00")
@spec ledger_backend() :: module()
Returns the configured ledger backend module.
Returns
module()— e.g.Logistiki.Ledger.SimulationorLogistiki.Ledger.Beancount.
Examples
iex> Logistiki.ledger_backend()
Logistiki.Ledger.Simulation
@spec process(struct(), keyword()) :: {:ok, Logistiki.Accounting.Result.t()} | {:error, Logistiki.Error.t()}
Processes a business event through the full accounting pipeline.
The event is normalized, Datalog facts are generated, business rules are evaluated, an accounting policy is selected, a posting template is resolved, a journal is built and validated, the ledger backend executes it, projections are updated, and audit evidence is recorded. Every stage is emitted as telemetry and recorded in the audit trail.
Arguments
event— a business event struct implementingLogistiki.Event(e.g.%Logistiki.Event.DepositReceived{},%Logistiki.Event.FeeAssessed{}).opts—keyword()of options::currency—String.t— filter projections to a currency (e.g."USD")
Returns
{:ok, %Logistiki.Accounting.Result{}}— the pipeline succeeded. A result withjournal: nilis valid (an event with no accounting impact).{:error, %Logistiki.Error{}}— the pipeline rejected the event.
Examples
iex> event = %Logistiki.Event.DepositReceived{
...> id: "evt_001",
...> entity_type: "corporate",
...> account_code: "LIABILITIES:CLIENT_DEPOSITS:USD:ACME:OPERATING",
...> cash_account_code: "ASSETS:CASH:USD:NOSTRO",
...> amount: Decimal.new("1000.00"),
...> currency: "USD",
...> effective_date: ~D[2026-07-07]
...> }
iex> {:ok, result} = Logistiki.process(event)
iex> result.policy
:cash_deposit
iex> result.journal.status
"posted"
iex> event = %Logistiki.Event.AccountOpened{id: "evt_002", account_code: "LIAB:ACME"}
iex> {:ok, result} = Logistiki.process(event)
iex> result.journal
nil
iex> {:error, %Logistiki.Error{code: :blocked_event}} = Logistiki.process(blocked_event)
@spec put_ledger_backend(module()) :: :ok
Sets the ledger backend at runtime (mainly for tests and demos).
Arguments
module—module()— a module implementingLogistiki.Ledger.Behaviour(e.g.Logistiki.Ledger.Beancount).
Examples
iex> Logistiki.put_ledger_backend(Logistiki.Ledger.Beancount)
:ok
iex> Logistiki.ledger_backend()
Logistiki.Ledger.Beancount
@spec result() :: module()
Returns the Logistiki.Accounting.Result struct module (for documentation/introspection).
Examples
iex> Logistiki.result()
Logistiki.Accounting.Result
@spec statement( VirtualAccount.t() | String.t() | integer(), keyword() ) :: {:ok, [Logistiki.Projections.StatementLine.t()]} | {:error, term()}
Builds a running-balance statement for an account.
Returns ordered statement lines (by effective date, journal insertion, then posting sequence) each carrying a running balance per currency.
Arguments
account_or_code—%VirtualAccount{}, account code (String.t), or id (integer()).opts—keyword()of options::currency—String.t— filter to a currency
Returns
{:ok, [Logistiki.Projections.StatementLine.t()]}— ordered lines.{:error, :account_not_found}— the account does not exist.
Examples
iex> {:ok, lines} = Logistiki.statement("LIABILITIES:CLIENT_DEPOSITS:USD:ACME:OPERATING")
iex> hd(lines).running_balance
Decimal.new("-1000.00")
iex> hd(lines).selected_policy
"cash_deposit"
@spec trial_balance(keyword()) :: {:ok, Logistiki.Projections.TrialBalance.t()}
Builds a trial balance across all posted and reversed journals.
Returns debit/credit totals per account per currency. Debits and credits must
balance per currency (balanced flag).
Arguments
opts—keyword()of options::currency—String.t— filter to a currency
Returns
{:ok, %Logistiki.Projections.TrialBalance{}}— withlines,currencies,balanced.
Examples
iex> {:ok, tb} = Logistiki.trial_balance()
iex> tb.balanced
true
iex> tb.currencies
["USD"]
iex> hd(tb.lines).account_code
"ASSETS:CASH:USD:NOSTRO"