# Logistiki v0.1.0 — End-to-End Demo
```elixir
System.put_env("LOGISTIKI_DB_ADAPTER", "sqlite")
Mix.install(
[
# {:logistiki, path: Path.expand("../..", __DIR__)},
{:logistiki, "~> 0.1.0"},
{:ecto_sqlite3, "~> 0.24.1"}
],
config: [
{:logistiki, [{Logistiki.Repo, [database: ":memory:", pool_size: 1]}]},
{:beancount_ex, [{:engine, Beancount.Engine.Elixir}, {Beancount.Repo, [database: ":memory:", pool_size: 1]}]}
]
)
```
## Section
```elixir
# Run migrations against the in-memory SQLite database.
Ecto.Migrator.run(
Logistiki.Repo,
Application.app_dir(:logistiki, "priv/repo/migrations"),
:up,
all: true
)
"Migrations complete — in-memory SQLite ready."
```
> **No external dependencies.** This livebook runs entirely in-memory using
> SQLite. No PostgreSQL installation is required.
## 1. Load the knowledge program
The Datalog knowledge base (`Logistiki.Knowledge.Program`) holds business rules,
accounting policies, posting templates, and account mappings as facts and rules.
```elixir
program = Logistiki.Knowledge.load_program()
```
The program module declares:
* **Relations** for per-event runtime facts (`event_type`, `event_amount_cents`, `event_account`, …)
* **Rules** that derive `blocked`, `requires_approval`, `policy`, and `account_role`
* **Static facts** for templates, template postings, required dimensions, and static account mappings
```elixir
Logistiki.Knowledge.Rules.categories()
```
## 2. Create the business entity tree
```
Acme Holdings
Acme Trading Ltd
Acme Treasury Ltd
Bluewater Trust
Bluewater Operating Company
```
```elixir
alias Logistiki.BusinessEntities
{:ok, acme_holdings} = BusinessEntities.create_entity(%{name: "Acme Holdings", entity_type: "company", status: "active"})
{:ok, acme_trading} = BusinessEntities.create_entity(%{name: "Acme Trading Ltd", entity_type: "company", status: "active", parent_id: acme_holdings.id})
{:ok, acme_treasury} = BusinessEntities.create_entity(%{name: "Acme Treasury Ltd", entity_type: "company", status: "active", parent_id: acme_holdings.id})
{:ok, bluewater} = BusinessEntities.create_entity(%{name: "Bluewater Trust", entity_type: "trust", status: "active"})
{:ok, bluewater_op} = BusinessEntities.create_entity(%{name: "Bluewater Operating Company", entity_type: "company", status: "active", parent_id: bluewater.id})
entities = %{acme_holdings: acme_holdings, acme_trading: acme_trading, acme_treasury: acme_treasury, bluewater: bluewater, bluewater_op: bluewater_op}
```
Verify the closure table:
```elixir
BusinessEntities.list_descendants(acme_holdings) |> Enum.map(& &1.name)
```
## 3. Create the virtual account tree
```
Assets
Cash
USD
Nostro USD
Liabilities
Client Deposits
USD
Acme Holdings
Operating
Payroll
Escrow
Bluewater Trust
Operating
Income
Fees
Wire Fees
Expenses
Interest Expense
Suspense
USD Suspense
```
```elixir
alias Logistiki.VirtualAccounts
# Helper to create the tree recursively
create = fn parent_id, code, type, normal, currency, children, f ->
{:ok, account} = VirtualAccounts.create_account(%{
code: code,
name: code |> String.split(":") |> List.last() |> String.capitalize(),
account_type: type,
currency: currency,
normal_balance: normal,
posting_allowed: children == [],
parent_id: parent_id
})
child_accounts = Enum.reduce(children, %{}, fn {c, t, n, cur, ch}, acc ->
Map.merge(acc, f.(account.id, c, t, n, cur, ch, f))
end)
Map.put(child_accounts, code, account)
end
accounts = create.(nil, "ASSETS", "asset", "debit", nil, [
{"ASSETS:CASH", "asset", "debit", nil, [
{"ASSETS:CASH:USD", "asset", "debit", nil, [
{"ASSETS:CASH:USD:NOSTRO", "asset", "debit", "USD", []}
]}
]}
], create)
accounts = Map.merge(accounts, create.(nil, "LIABILITIES", "liability", "credit", nil, [
{"LIABILITIES:CLIENT_DEPOSITS", "liability", "credit", nil, [
{"LIABILITIES:CLIENT_DEPOSITS:USD", "liability", "credit", nil, [
{"LIABILITIES:CLIENT_DEPOSITS:USD:ACME", "client", "credit", nil, [
{"LIABILITIES:CLIENT_DEPOSITS:USD:ACME:OPERATING", "client", "credit", "USD", []},
{"LIABILITIES:CLIENT_DEPOSITS:USD:ACME:PAYROLL", "client", "credit", "USD", []},
{"LIABILITIES:CLIENT_DEPOSITS:USD:ACME:ESCROW", "client", "credit", "USD", []}
]},
{"LIABILITIES:CLIENT_DEPOSITS:USD:BLUEWATER", "client", "credit", nil, [
{"LIABILITIES:CLIENT_DEPOSITS:USD:BLUEWATER:OPERATING", "client", "credit", "USD", []}
]}
]}
]}
], create))
accounts = Map.merge(accounts, create.(nil, "INCOME", "income", "credit", nil, [
{"INCOME:FEES", "income", "credit", nil, [
{"INCOME:FEES:WIRE", "fee", "credit", "USD", []}
]}
], create))
accounts = Map.merge(accounts, create.(nil, "EXPENSES", "expense", "debit", nil, [
{"EXPENSES:INTEREST", "expense", "debit", "USD", []}
], create))
accounts = Map.merge(accounts, create.(nil, "SUSPENSE", "suspense", "debit", nil, [
{"SUSPENSE:USD", "suspense", "debit", "USD", []}
], create))
"Created #{map_size(accounts)} accounts."
```
## 4. Link entities to accounts
```elixir
alias Logistiki.Relationships
Relationships.link_entity_account(entities.acme_holdings, accounts["LIABILITIES:CLIENT_DEPOSITS:USD:ACME:OPERATING"], :owner)
Relationships.link_entity_account(entities.acme_holdings, accounts["LIABILITIES:CLIENT_DEPOSITS:USD:ACME:PAYROLL"], :owner)
Relationships.link_entity_account(entities.acme_holdings, accounts["LIABILITIES:CLIENT_DEPOSITS:USD:ACME:ESCROW"], :owner)
Relationships.link_entity_account(entities.bluewater, accounts["LIABILITIES:CLIENT_DEPOSITS:USD:BLUEWATER:OPERATING"], :owner)
"Entities linked to accounts."
```
## 5. Process a deposit event
A `$1,000` deposit received into Acme Operating. The pipeline will:
normalize → generate Datalog facts → evaluate business rules → select a policy →
resolve a template → build a journal → validate invariants → execute via the
ledger backend → record audit evidence.
```elixir
deposit = Logistiki.Demo.Seeds.deposit_event(
"LIABILITIES:CLIENT_DEPOSITS:USD:ACME:OPERATING",
"1000.00"
)
{:ok, deposit_result} = Logistiki.process(deposit)
```
## 6. Show the policy selected by Datalog
The knowledge layer evaluated the event's facts and derived
`policy(:evt, :cash_deposit)` — the only matching policy.
```elixir
deposit_result.policy
```
## 7. Show the generated journal
```elixir
deposit_result.journal.status
```
```elixir
deposit_result.journal.selected_policy
```
```elixir
deposit_result.journal.idempotency_key
```
## 8. Show the generated postings
Two postings: debit cash (nostro), credit client liability.
```elixir
alias Logistiki.Accounting
Accounting.list_postings(deposit_result.journal)
```
## 9. Show the ledger backend result
```elixir
deposit_result.ledger_result.backend
```
```elixir
deposit_result.ledger_result.status
```
## 10. Show balance
Cash account (debit balance = positive):
```elixir
{:ok, [cash_balance]} = Logistiki.balance("ASSETS:CASH:USD:NOSTRO")
cash_balance
```
Client liability account (credit balance = negative):
```elixir
{:ok, [client_balance]} = Logistiki.balance("LIABILITIES:CLIENT_DEPOSITS:USD:ACME:OPERATING")
client_balance
```
Parent account aggregates descendants:
```elixir
{:ok, acme_balances} = Logistiki.balance("LIABILITIES:CLIENT_DEPOSITS:USD:ACME")
Enum.reduce(acme_balances, Decimal.new(0), fn b, acc -> Decimal.add(b.net, acc) end)
```
Entity-owned accounts:
```elixir
{:ok, entity_balances} = Logistiki.balance_for_entity(entities.acme_holdings)
Enum.reduce(entity_balances, Decimal.new(0), fn b, acc -> Decimal.add(b.net, acc) end)
```
## 11. Show statement (with running balance)
```elixir
{:ok, lines} = Logistiki.statement("LIABILITIES:CLIENT_DEPOSITS:USD:ACME:OPERATING")
Enum.map(lines, fn line ->
%{
date: line.date,
account: line.account_code,
debit: line.debit,
credit: line.credit,
running_balance: line.running_balance,
policy: line.selected_policy
}
end)
```
## 12. Process a wire fee event
A `$25` wire fee assessed to Acme Operating.
```elixir
fee = Logistiki.Demo.Seeds.fee_event(
"LIABILITIES:CLIENT_DEPOSITS:USD:ACME:OPERATING",
"25.00"
)
{:ok, fee_result} = Logistiki.process(fee)
```
The knowledge layer selected `:corporate_wire_fee` because:
* `event_type(:evt, :fee_assessed)`
* `event_fee_type(:evt, :wire_fee)`
* `event_entity_type(:evt, :corporate)`
```elixir
fee_result.policy
```
Balances after the fee (client liability reduced by $25):
```elixir
{:ok, [client_after_fee]} = Logistiki.balance("LIABILITIES:CLIENT_DEPOSITS:USD:ACME:OPERATING")
client_after_fee.net
```
```elixir
{:ok, [fee_income]} = Logistiki.balance("INCOME:FEES:WIRE")
fee_income.net
```
## 13. Reverse the mistaken fee
The fee was applied in error. Reversing creates a **new** journal that exactly
negates the original postings, and marks the original `reversed`.
```elixir
{:ok, reversal_result} = Logistiki.Ledger.reverse_journal(fee_result.journal, %{reason: "mistaken fee"})
```
```elixir
reversal_result.status
```
```elixir
reversal_result.details[:reversal].reversal_of_id
```
The original journal is now `reversed`:
```elixir
original = Accounting.get_journal!(fee_result.journal.id)
original.status
```
## 14. Show balance restored
The reversal cancels the fee. The client balance is back to -$1,000:
```elixir
{:ok, [client_restored]} = Logistiki.balance("LIABILITIES:CLIENT_DEPOSITS:USD:ACME:OPERATING")
client_restored.net
```
Fee income is back to zero:
```elixir
{:ok, [fee_restored]} = Logistiki.balance("INCOME:FEES:WIRE")
fee_restored.net
```
## 15. Show audit evidence explaining everything
Every pipeline stage was recorded as an audit event. The full trail explains
*why* each posting exists — the business event, the rules, the policy, the
template, the journal, and the ledger result.
```elixir
trail = Logistiki.Audit.trail_for_event(deposit.id)
Enum.map(trail, fn e ->
%{action: e.action, resource_type: e.resource_type, journal_id: e.journal_id}
end)
```
The fee event's audit trail:
```elixir
fee_trail = Logistiki.Audit.trail_for_event(fee.id)
Enum.map(fee_trail, & &1.action)
```
## 16. Trial balance
A trial balance across all posted and reversed journals. Debits and credits
must balance per currency.
```elixir
{:ok, tb} = Logistiki.trial_balance()
```
```elixir
tb.balanced
```
```elixir
tb.currencies
```
```elixir
Enum.map(tb.lines, fn line ->
%{
account: line.account_code,
debits: line.debit_total,
credits: line.credit_total,
net: line.net,
currency: line.currency
}
end)
```
## 17. Balance sheet
```elixir
bs = Logistiki.balance_sheet()
```
```elixir
%{
assets: bs.totals_by_currency["USD"][:assets],
liabilities: bs.totals_by_currency["USD"][:liabilities],
equity: bs.totals_by_currency["USD"][:equity]
}
```
## 18. Income statement
```elixir
is = Logistiki.income_statement()
```
```elixir
is.net_profit_by_currency["USD"]
```
## 19. Process a no-accounting-impact event
A customer account was opened. This event has `has_accounting_impact: false` —
it produces audit evidence but no journal.
```elixir
account_opened = Logistiki.Demo.Seeds.account_opened_event(
"LIABILITIES:CLIENT_DEPOSITS:USD:ACME:ESCROW"
)
{:ok, open_result} = Logistiki.process(account_opened)
```
```elixir
open_result.journal
```
```elixir
open_result.explanation[:reason]
```
## 20. Switch to the Beancount oracle backend
The Beancount backend verifies every journal with `Beancount.check/1` before
persisting. It is the **executable specification** for Logistiki accounting
behavior.
```elixir
Logistiki.put_ledger_backend(Logistiki.Ledger.Beancount)
another_deposit = Logistiki.Demo.Seeds.deposit_event(
"LIABILITIES:CLIENT_DEPOSITS:USD:ACME:PAYROLL",
"500.00"
)
{:ok, oracle_result} = Logistiki.process(another_deposit)
```
```elixir
oracle_result.ledger_result.backend
```
```elixir
oracle_result.ledger_result.details[:oracle]
```
Verify the full ledger with `Beancount.check/1`:
```elixir
Logistiki.Ledger.Beancount.verify_ledger()
```
Get oracle balances (BQL `SELECT account, sum(position) GROUP BY account`):
```elixir
{:ok, oracle_balances} = Logistiki.Ledger.Beancount.oracle_balances()
```
```elixir
# Show the oracle's view of the cash account
{:ok, cash_account} = Logistiki.VirtualAccounts.get_account_by_code("ASSETS:CASH:USD:NOSTRO")
bc_name = Logistiki.Ledger.BeancountMapper.to_beancount_account(cash_account)
oracle_balances[bc_name]
```
## 21. General ledger view
All posted and reversed postings, ordered by effective date and journal:
```elixir
gl = Logistiki.general_ledger()
Enum.map(gl.lines, fn line ->
%{
date: line.date,
account: line.account_code,
direction: if(line.debit, do: "debit", else: "credit"),
amount: line.debit || line.credit,
currency: line.currency,
policy: line.selected_policy
}
end)
```
## 22. Backend agreement
The simulation and Beancount backends should agree. Both compute balances from
the same persisted postings; the Beancount backend additionally verifies each
journal with the oracle.
```elixir
# Switch back to simulation for the final comparison
Logistiki.put_ledger_backend(Logistiki.Ledger.Simulation)
{:ok, [sim_cash]} = Logistiki.balance("ASSETS:CASH:USD:NOSTRO")
```
```elixir
# Oracle view
oracle_balances[bc_name]
```
```elixir
# Simulation view
sim_cash.net
```
Both should show the same positive (debit) balance for the cash account.
---
## Summary
| Step | What happened |
| ------- | ----------------------------------------------------------------------------- |
| Setup | SQLite in-memory database created and migrated — no PostgreSQL needed |
| 1 | Loaded the Datalog knowledge program |
| 2 | Created the Acme Holdings + Bluewater Trust entity trees |
| 3 | Created the Assets / Liabilities / Income / Expenses / Suspense account trees |
| 4 | Linked entities to accounts with `:owner` relationships |
| 5 | Processed a $1,000 deposit — `Logistiki.process/1` |
| 6 | Datalog selected `:cash_deposit` |
| 7 | Journal built and posted (status `"posted"`) |
| 8 | Two postings: debit cash, credit client liability |
| 9 | Simulation backend executed |
| 10 | Balances: cash +$1,000, client -$1,000 |
| 11 | Statement with running balance |
| 12 | Processed a $25 wire fee — `:corporate_wire_fee` |
| 13 | Reversed the mistaken fee — new journal, original marked `reversed` |
| 14 | Balances restored: client back to -$1,000, fee income back to $0 |
| 15 | Audit trail explains every step |
| 16 | Trial balance balances per currency |
| 17–18 | Balance sheet and income statement |
| 19 | No-accounting-impact event (account opened) — no journal, audit only |
| 20 | Beancount oracle backend verifies journals |
| 21 | General ledger view |
| 22 | Simulation and Beancount backends agree |