Logistiki (logistiki v0.1.0)

Copy Markdown View Source

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

balance(account_or_code, opts \\ [])

(since 0.1.0)
@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 struct
    • String.t — an account code (e.g. "ASSETS:CASH:USD:NOSTRO") or id
    • integer() — an account id
  • optskeyword() of options:

    • :currencyString.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")

balance_for_entity(entity, opts \\ [])

(since 0.1.0)
@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.
  • optskeyword() of options:
    • :currencyString.t — filter to a currency (e.g. "USD")
    • :relationship_typeatom() — filter by type (e.g. :owner)
    • :atDate.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)

balance_for_entity_tree(entity, opts \\ [])

(since 0.1.0)
@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.
  • optskeyword() of options:
    • :currencyString.t — filter to a currency
    • :relationship_typeatom() — filter by type
    • :atDate.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")

balance_sheet(opts \\ [])

(since 0.1.0)
@spec balance_sheet(keyword()) :: Logistiki.Projections.BalanceSheet.t()

Builds a balance sheet (assets, liabilities, equity).

Arguments

  • optskeyword() of options:
    • :currencyString.t — filter to a currency

Returns

  • %Logistiki.Projections.BalanceSheet{} — with assets, liabilities, equity sections (each with balances and totals_by_currency), and totals_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")

error()

(since 0.1.0)
@spec error() :: module()

Returns the Logistiki.Error struct module (for documentation/introspection).

Examples

iex> Logistiki.error()
Logistiki.Error

general_ledger(opts \\ [])

(since 0.1.0)
@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

  • optskeyword() of options:
    • :currencyString.t — filter to a currency

Returns

  • %Logistiki.Projections.GeneralLedger{} — with lines.

Examples

iex> gl = Logistiki.general_ledger()
iex> length(gl.lines)
4
iex> hd(gl.lines).account_code
"ASSETS:CASH:USD:NOSTRO"

income_statement(opts \\ [])

(since 0.1.0)
@spec income_statement(keyword()) :: Logistiki.Projections.IncomeStatement.t()

Builds an income statement (income, expenses, net profit).

Arguments

  • optskeyword() of options:
    • :currencyString.t — filter to a currency

Returns

  • %Logistiki.Projections.IncomeStatement{} — with income, expenses sections and net_profit_by_currency.

Examples

iex> is = Logistiki.income_statement()
iex> is.net_profit_by_currency["USD"]
Decimal.new("-25.00")

ledger_backend()

(since 0.1.0)
@spec ledger_backend() :: module()

Returns the configured ledger backend module.

Returns

Examples

iex> Logistiki.ledger_backend()
Logistiki.Ledger.Simulation

process(event, opts \\ [])

(since 0.1.0)
@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 implementing Logistiki.Event (e.g. %Logistiki.Event.DepositReceived{}, %Logistiki.Event.FeeAssessed{}).

  • optskeyword() of options:

    • :currencyString.t — filter projections to a currency (e.g. "USD")

Returns

  • {:ok, %Logistiki.Accounting.Result{}} — the pipeline succeeded. A result with journal: nil is 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)

put_ledger_backend(module)

(since 0.1.0)
@spec put_ledger_backend(module()) :: :ok

Sets the ledger backend at runtime (mainly for tests and demos).

Arguments

Examples

iex> Logistiki.put_ledger_backend(Logistiki.Ledger.Beancount)
:ok
iex> Logistiki.ledger_backend()
Logistiki.Ledger.Beancount

result()

(since 0.1.0)
@spec result() :: module()

Returns the Logistiki.Accounting.Result struct module (for documentation/introspection).

Examples

iex> Logistiki.result()
Logistiki.Accounting.Result

statement(account_or_code, opts \\ [])

(since 0.1.0)
@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()).
  • optskeyword() of options:
    • :currencyString.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"

trial_balance(opts \\ [])

(since 0.1.0)
@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

  • optskeyword() of options:
    • :currencyString.t — filter to a currency

Returns

  • {:ok, %Logistiki.Projections.TrialBalance{}} — with lines, 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"