# `Logistiki.VirtualAccounts`
[🔗](https://github.com/thanos/logistiki/blob/v0.1.0/lib/logistiki/virtual_accounts/context.ex#L1)

The context for hierarchical virtual accounts.

Virtual accounts represent accounting accounts and account-like projections.
They form a tree maintained through a closure table
(`Logistiki.VirtualAccounts.VirtualAccountClosure`).

## Rules enforced

  * account code is unique
  * only leaf accounts (no children) may allow postings
  * posting accounts require a currency
  * aggregation accounts may have a nil currency
  * a child cannot be created under a posting account
  * cycles are forbidden (the closure table is rewritten on move)

# `ancestor_ids`
*since 0.1.0* 

```elixir
@spec ancestor_ids(Logistiki.VirtualAccounts.VirtualAccount.t()) :: [integer()]
```

Returns the ids of all ancestors of `account` (including itself).

## Arguments

  * `account` — `%VirtualAccount{}`.

## Returns

  * `[integer()]` — ancestor ids including `account.id`.

## Examples

    iex> Logistiki.VirtualAccounts.ancestor_ids(nostro_account)
    [4, 3, 2, 1]

# `create_account`
*since 0.1.0* 

```elixir
@spec create_account(map()) ::
  {:ok, Logistiki.VirtualAccounts.VirtualAccount.t()} | {:error, term()}
```

Creates a new virtual account. When `:parent_id` is given the account is
linked as a child and the closure table is updated.

## Arguments

  * `attrs` — `map()` or `keyword()` of account attributes:
      * `:code` — `String.t` — **required** (e.g. `"ASSETS:CASH:USD:NOSTRO"`)
      * `:name` — `String.t` — **required** (e.g. `"Nostro USD"`)
      * `:account_type` — `String.t` — **required** (e.g. `"asset"`, `"liability"`)
      * `:status` — `String.t` — defaults to `"active"`
      * `:parent_id` — `integer() | nil` — the parent account id; `nil` for roots
      * `:currency` — `String.t` — **required when `posting_allowed: true`** (e.g. `"USD"`)
      * `:posting_allowed` — `boolean()` — defaults to `false`; only `true` for leaf accounts
      * `:normal_balance` — `String.t` — `"debit"` or `"credit"`
      * `:external_id` — `String.t`
      * `:metadata` — `map()`

## Returns

  * `{:ok, %VirtualAccount{}}` — the created account.
  * `{:error, %Ecto.Changeset{}}` — validation failed.
  * `{:error, :parent_not_found}` — the `parent_id` does not reference an existing account.
  * `{:error, :parent_is_posting_account}` — the parent is a posting account (cannot have children).

## Examples

    iex> {:ok, root} = Logistiki.VirtualAccounts.create_account(%{
    ...>   code: "ASSETS", name: "Assets", account_type: "asset"
    ...> })
    iex> root.parent_id
    nil

    iex> {:ok, leaf} = Logistiki.VirtualAccounts.create_account(%{
    ...>   code: "ASSETS:CASH:USD:NOSTRO", name: "Nostro USD", account_type: "asset",
    ...>   currency: "USD", posting_allowed: true, normal_balance: "debit",
    ...>   parent_id: root.id
    ...> })
    iex> leaf.posting_allowed
    true

    iex> {:error, changeset} = Logistiki.VirtualAccounts.create_account(%{
    ...>   code: "BAD", name: "Bad", account_type: "asset", posting_allowed: true
    ...> })
    iex> errors_on(changeset)[:currency]
    ["can't be blank when posting is allowed"]

# `descendant_ids`
*since 0.1.0* 

```elixir
@spec descendant_ids(Logistiki.VirtualAccounts.VirtualAccount.t()) :: [integer()]
```

Returns the ids of all descendants of `account` (including itself).

Uses the closure table directly — faster than `list_descendants/1` when only
ids are needed (e.g. for balance aggregation queries).

## Arguments

  * `account` — `%VirtualAccount{}`.

## Returns

  * `[integer()]` — descendant ids including `account.id`.

## Examples

    iex> Logistiki.VirtualAccounts.descendant_ids(assets_root)
    [1, 2, 3, 4]

# `get_account`
*since 0.1.0* 

```elixir
@spec get_account(integer()) ::
  {:ok, Logistiki.VirtualAccounts.VirtualAccount.t()} | {:error, :not_found}
```

Fetches a single account by id.

## Arguments

  * `id` — `integer()` — the account primary key.

## Returns

  * `{:ok, %VirtualAccount{}}` — the account was found.
  * `{:error, :not_found}` — no account with that id.

## Examples

    iex> {:ok, account} = Logistiki.VirtualAccounts.get_account(10)
    iex> account.code
    "ASSETS:CASH:USD:NOSTRO"

    iex> {:error, :not_found} = Logistiki.VirtualAccounts.get_account(999)

# `get_account!`
*since 0.1.0* 

```elixir
@spec get_account!(integer()) :: Logistiki.VirtualAccounts.VirtualAccount.t()
```

Fetches a single account by id, raising if not found.

## Arguments

  * `id` — `integer()` — the account primary key.

## Returns

  * `%VirtualAccount{}` — the account. Raises `Ecto.NoResultsError` if not found.

## Examples

    iex> account = Logistiki.VirtualAccounts.get_account!(10)
    iex> account.code
    "ASSETS:CASH:USD:NOSTRO"

# `get_account_by_code`
*since 0.1.0* 

```elixir
@spec get_account_by_code(String.t()) ::
  {:ok, Logistiki.VirtualAccounts.VirtualAccount.t()} | {:error, :not_found}
```

Fetches a single account by code.

## Arguments

  * `code` — `String.t()` — the account code (e.g. `"ASSETS:CASH:USD:NOSTRO"`).

## Returns

  * `{:ok, %VirtualAccount{}}` — the account was found.
  * `{:error, :not_found}` — no account with that code.

## Examples

    iex> {:ok, account} = Logistiki.VirtualAccounts.get_account_by_code("ASSETS:CASH:USD:NOSTRO")
    iex> account.code
    "ASSETS:CASH:USD:NOSTRO"

    iex> {:error, :not_found} = Logistiki.VirtualAccounts.get_account_by_code("UNKNOWN")

# `get_account_by_code!`
*since 0.1.0* 

```elixir
@spec get_account_by_code!(String.t()) :: Logistiki.VirtualAccounts.VirtualAccount.t()
```

Fetches a single account by code, raising if not found.

## Arguments

  * `code` — `String.t()` — the account code (e.g. `"ASSETS:CASH:USD:NOSTRO"`).

## Returns

  * `%VirtualAccount{}` — the account. Raises `Ecto.NoResultsError` if not found.

## Examples

    iex> account = Logistiki.VirtualAccounts.get_account_by_code!("ASSETS:CASH:USD:NOSTRO")
    iex> account.currency
    "USD"

# `leaf?`
*since 0.1.0* 

```elixir
@spec leaf?(Logistiki.VirtualAccounts.VirtualAccount.t()) :: boolean()
```

True when the account is a leaf (has no children). Roots are aggregation
nodes and are not leaves.

## Arguments

  * `account` — `%VirtualAccount{}`.

## Returns

  * `boolean()` — `true` if the account has no children, `false` otherwise.

## Examples

    iex> Logistiki.VirtualAccounts.leaf?(nostro_account)
    true

    iex> Logistiki.VirtualAccounts.leaf?(assets_root)
    false

# `list_accounts`
*since 0.1.0* 

```elixir
@spec list_accounts(keyword()) :: [Logistiki.VirtualAccounts.VirtualAccount.t()]
```

Lists accounts, optionally filtered.

## Arguments

  * `opts` — `keyword()` of options:
      * `:status` — `String.t` — e.g. `"active"`, `"frozen"`
      * `:account_type` — `String.t` — e.g. `"asset"`, `"liability"`
      * `:currency` — `String.t` — e.g. `"USD"`
      * `:parent_id` — `integer() | nil` — filter by parent; `nil` lists roots

## Returns

  * `[VirtualAccount.t()]` — ordered by code ascending. Empty list if none match.

## Examples

    iex> Logistiki.VirtualAccounts.list_accounts(account_type: "asset")
    [%VirtualAccount{code: "ASSETS", ...}, %VirtualAccount{code: "ASSETS:CASH", ...}]

    iex> Logistiki.VirtualAccounts.list_accounts(status: "active", currency: "USD")
    [%VirtualAccount{code: "ASSETS:CASH:USD:NOSTRO", ...}]

# `list_ancestors`
*since 0.1.0* 

```elixir
@spec list_ancestors(Logistiki.VirtualAccounts.VirtualAccount.t()) :: [
  Logistiki.VirtualAccounts.VirtualAccount.t()
]
```

Lists all ancestors of `account` ordered from nearest to farthest, using the
closure table.

## Arguments

  * `account` — `%VirtualAccount{}` — the descendant account.

## Returns

  * `[VirtualAccount.t()]` — ordered by `depth` ascending. Empty list if the
    account is a root.

## Examples

    iex> Logistiki.VirtualAccounts.list_ancestors(nostro_account)
    [%VirtualAccount{code: "ASSETS:CASH:USD", ...}, %VirtualAccount{code: "ASSETS:CASH", ...}, %VirtualAccount{code: "ASSETS", ...}]

# `list_children`
*since 0.1.0* 

```elixir
@spec list_children(Logistiki.VirtualAccounts.VirtualAccount.t()) :: [
  Logistiki.VirtualAccounts.VirtualAccount.t()
]
```

Lists direct children of `account`.

## Arguments

  * `account` — `%VirtualAccount{}` — the parent account.

## Returns

  * `[VirtualAccount.t()]` — ordered by code. Empty list if no children.

## Examples

    iex> Logistiki.VirtualAccounts.list_children(assets_root)
    [%VirtualAccount{code: "ASSETS:CASH", ...}]

# `list_descendants`
*since 0.1.0* 

```elixir
@spec list_descendants(Logistiki.VirtualAccounts.VirtualAccount.t()) :: [
  Logistiki.VirtualAccounts.VirtualAccount.t()
]
```

Lists all descendants of `account` (excluding the account itself), using the
closure table for O(rows) performance.

## Arguments

  * `account` — `%VirtualAccount{}` — the root of the subtree.

## Returns

  * `[VirtualAccount.t()]` — ordered by code. Empty list if no descendants.

## Examples

    iex> Logistiki.VirtualAccounts.list_descendants(cash_root)
    [%VirtualAccount{code: "ASSETS:CASH:USD", ...}, %VirtualAccount{code: "ASSETS:CASH:USD:NOSTRO", ...}]

# `move_account`
*since 0.1.0* 

```elixir
@spec move_account(
  Logistiki.VirtualAccounts.VirtualAccount.t(),
  Logistiki.VirtualAccounts.VirtualAccount.t() | integer() | nil
) ::
  {:ok, Logistiki.VirtualAccounts.VirtualAccount.t()}
  | {:error, :cycle_detected | term()}
```

Moves `account` under `new_parent` (which may be `nil` for a root), rewriting
the closure table for the entire subtree. Cycles are prevented.

## Arguments

  * `account` — `%VirtualAccount{}` — the account to move.
  * `new_parent` — one of:
      * `%VirtualAccount{}` — the new parent
      * `nil` — move to root
      * `integer()` — the new parent's id

## Returns

  * `{:ok, %VirtualAccount{}}` — the moved account with updated `parent_id`.
  * `{:error, :cycle_detected}` — moving would create a cycle.
  * `{:error, term()}` — the move failed (rolled back).

## Examples

    iex> {:ok, moved} = Logistiki.VirtualAccounts.move_account(cash_account, new_root)
    iex> moved.parent_id
    20

    iex> {:error, :cycle_detected} = Logistiki.VirtualAccounts.move_account(parent, child)

# `posting_account?`
*since 0.1.0* 

```elixir
@spec posting_account?(Logistiki.VirtualAccounts.VirtualAccount.t()) :: boolean()
```

True when the account accepts postings — i.e. `posting_allowed: true` and
`status: "active"`.

## Arguments

  * `account` — `%VirtualAccount{}` or any term.

## Returns

  * `boolean()` — `true` only when `posting_allowed == true` and
    `status == "active"`.

## Examples

    iex> Logistiki.VirtualAccounts.posting_account?(nostro_account)
    true

    iex> Logistiki.VirtualAccounts.posting_account?(assets_root)
    false

    iex> Logistiki.VirtualAccounts.posting_account?(%{posting_allowed: true, status: "frozen"})
    false

# `resolve`
*since 0.1.0* 

```elixir
@spec resolve(Logistiki.VirtualAccounts.VirtualAccount.t() | String.t() | integer()) ::
  {:ok, Logistiki.VirtualAccounts.VirtualAccount.t()} | {:error, :not_found}
```

Resolves an account from either a `%VirtualAccount{}`, an id, or an account
code. Tries code first for binary inputs, then falls back to numeric id.

## Arguments

  * `account` — one of:
      * `%VirtualAccount{}` — returned directly
      * `String.t` — interpreted as an account code first, then as a numeric id
      * `integer()` — interpreted as an account id

## Returns

  * `{:ok, %VirtualAccount{}}` — the account was found.
  * `{:error, :not_found}` — the account could not be resolved.

## Examples

    iex> {:ok, account} = Logistiki.VirtualAccounts.resolve("ASSETS:CASH:USD:NOSTRO")
    iex> account.id
    10

    iex> {:ok, account} = Logistiki.VirtualAccounts.resolve(10)
    iex> account.code
    "ASSETS:CASH:USD:NOSTRO"

    iex> {:ok, account} = Logistiki.VirtualAccounts.resolve(existing_account_struct)

# `update_account`
*since 0.1.0* 

```elixir
@spec update_account(Logistiki.VirtualAccounts.VirtualAccount.t(), map()) ::
  {:ok, Logistiki.VirtualAccounts.VirtualAccount.t()}
  | {:error, Ecto.Changeset.t()}
```

Updates a virtual account's mutable attributes (not its parent — use
`move_account/2` to change the parent).

## Arguments

  * `account` — `%VirtualAccount{}` — the account to update.
  * `attrs` — `map()` of attributes to change (e.g.
    `%{status: "frozen", normal_balance: "credit"}`).

## Returns

  * `{:ok, %VirtualAccount{}}` — the updated account.
  * `{:error, %Ecto.Changeset{}}` — validation failed.

## Examples

    iex> {:ok, updated} = Logistiki.VirtualAccounts.update_account(account, %{status: "frozen"})
    iex> updated.status
    "frozen"

---

*Consult [api-reference.md](api-reference.md) for complete listing*
