Bookk.Ledger (bookk v1.0.0)

Copy Markdown View Source

A ledger is a book that holds accounts. Traditionally, ledgers would also hold the journal entries that changed the accounts but, in this library, persisting those journal entries is considered off scope. You may persist state the way the best fits your needs.

Summary

Types

t()

The struct that represents a ledger.

Functions

Checks whether the ledger is balanced.

Calculates a Bookk.JournalEntry represending the diff between two ledgers where, if such journal entry were to be applied to ledger "a", its state would become equal to ledger "b".

Checks whether the given ledger is empty (no accounts with balance).

Get an account from the ledger by its Bookk.AccountHead. If the account doesn't exist yet, then an account will be returned with empty state.

Merges a non-empty set of Ledgers into one (ledger id MUST be the same).

Merges two ledgers into one (ledger id MUST be the same).

Creates a new Bookk.Ledger from its id and, optionally, a list of Bookk.Account.

Posts a Bookk.JournalEntry to a ledger. This means that the balance change described in each operation of the journal entry will be applied to their respective accounts of the ledger. If there's a change to an account that doesn't exist yet, then the account is first created.

Types

t()

@type t() :: %Bookk.Ledger{
  accounts_by_name: %{required(name :: String.t()) => Bookk.Account.t()},
  id: String.t()
}

The struct that represents a ledger.

Fields

  • id: the id of the ledger;
  • accounts_by_name: a map of the accounts known by the ledger, grouped by their name.

Functions

balanced?(ledger)

@spec balanced?(t()) :: boolean()

Checks whether the ledger is balanced.

A ledger is considered balance when the sum of balance from its debit accounts is equal the sum of balance from its credit accounts. You know if an account is a "debit account" or a "credit account" by the natural balance of its class.

See Bookk.AccountClass for more information on natural balance.

Examples

Is balanced when the ledger is empty:

iex> Bookk.Ledger.new("acme")
iex> |> Bookk.Ledger.balanced?()
true

Is balanced when the sum of debit accounts balances is equal the sum of credit accounts balances:

iex> ledger = Bookk.Ledger.new("acme")
iex>
iex> journal_entry = %Bookk.JournalEntry{
iex>   operations: [
iex>     debit(fixture_account_head(:cash), Decimal.new(50)),
iex>     credit(fixture_account_head(:deposits), Decimal.new(50))
iex>   ]
iex> }
iex>
iex> Bookk.Ledger.post(ledger, journal_entry)
iex> |> Bookk.Ledger.balanced?()
true

Is unbalanced when the sum of debit accounts balances isn't equal the sum of credit accounts balances:

iex> ledger = Bookk.Ledger.new("acme")
iex>
iex> journal_entry = %Bookk.JournalEntry{
iex>   operations: [
iex>     debit(fixture_account_head(:cash), Decimal.new(50))
iex>   ]
iex> }
iex>
iex> Bookk.Ledger.post(ledger, journal_entry)
iex> |> Bookk.Ledger.balanced?()
false

diff(a, b)

@spec diff(a :: t(), b :: t()) :: Bookk.JournalEntry.t()

Calculates a Bookk.JournalEntry represending the diff between two ledgers where, if such journal entry were to be applied to ledger "a", its state would become equal to ledger "b".

Examples

iex> a = Bookk.Ledger.new("acme", [
iex>   Bookk.Account.new(fixture_account_head(:cash), Decimal.new(10)),
iex>   Bookk.Account.new(fixture_account_head(:deposits), Decimal.new(10)),
iex> ])
iex>
iex> b = Bookk.Ledger.new("acme", [
iex>   Bookk.Account.new(fixture_account_head(:cash), Decimal.new(50)),
iex>   Bookk.Account.new(fixture_account_head({:unspent_cash, {:user, "1234"}}), Decimal.new(50))
iex> ])
iex>
iex> Bookk.Ledger.diff(a, b)
Bookk.JournalEntry.new([
  Bookk.Operation.debit(fixture_account_head(:cash), Decimal.new(40)),
  Bookk.Operation.debit(fixture_account_head(:deposits), Decimal.new(10)),
  Bookk.Operation.credit(fixture_account_head({:unspent_cash, {:user, "1234"}}), Decimal.new(50)),
])

empty?(ledger)

@spec empty?(t()) :: boolean()

Checks whether the given ledger is empty (no accounts with balance).

Examples

iex> Bookk.Ledger.new("acme")
iex> |> Bookk.Ledger.empty?()
true

iex> ledger = Bookk.Ledger.new("acme", [
iex>   Bookk.Account.new(fixture_account_head(:cash), Decimal.new(0)),
iex>   Bookk.Account.new(fixture_account_head(:deposits), Decimal.new(0)),
iex> ])
iex>
iex> Bookk.Ledger.empty?(ledger)
true

iex> ledger = Bookk.Ledger.new("acme", [
iex>   Bookk.Account.new(fixture_account_head(:cash), Decimal.new(10))
iex> ])
iex>
iex> Bookk.Ledger.empty?(ledger)
false

get_account(ledger, head)

@spec get_account(t(), Bookk.AccountHead.t()) :: Bookk.Account.t()

Get an account from the ledger by its Bookk.AccountHead. If the account doesn't exist yet, then an account will be returned with empty state.

Examples

Returns the account when it exists in the ledger:

iex> ledger = Bookk.Ledger.new("acme", [
iex>   %Bookk.Account{
iex>     head: fixture_account_head(:cash),
iex>     balance: Decimal.new(25)
iex>   }
iex> ])
iex>
iex> Bookk.Ledger.get_account(ledger, fixture_account_head(:cash))
%Bookk.Account{
  head: fixture_account_head(:cash),
  balance: Decimal.new(25)
}

Returns an empty account when the it doesn't exist in the ledger:

iex> Bookk.Ledger.new("acme")
iex> |> Bookk.Ledger.get_account(fixture_account_head(:cash))
%Bookk.Account{
  head: fixture_account_head(:cash),
  balance: Decimal.new(0)
}

merge(list)

@spec merge([t(), ...]) :: t()

Merges a non-empty set of Ledgers into one (ledger id MUST be the same).

Examples

iex> a = Bookk.Ledger.new("acme", [
iex>   Bookk.Account.new(fixture_account_head(:cash), Decimal.new(5)),
iex>   Bookk.Account.new(fixture_account_head(:deposits), Decimal.new(5)),
iex> ])
iex>
iex> b = Bookk.Ledger.new("acme", [
iex>   Bookk.Account.new(fixture_account_head(:cash), Decimal.new(15)),
iex>   Bookk.Account.new(fixture_account_head(:deposits), Decimal.new(15)),
iex> ])
iex>
iex> c = Bookk.Ledger.new("acme", [
iex>   Bookk.Account.new(fixture_account_head(:cash), Decimal.new(30)),
iex>   Bookk.Account.new(fixture_account_head(:deposits), Decimal.new(30)),
iex> ])
iex>
iex> Bookk.Ledger.merge([a, b, c])
Bookk.Ledger.new("acme", [
  Bookk.Account.new(fixture_account_head(:cash), Decimal.new(50)),
  Bookk.Account.new(fixture_account_head(:deposits), Decimal.new(50))
])

If you try to merge an empty list of ledgers, an error will be raised.

merge(a, b)

@spec merge(t(), t()) :: t()

Merges two ledgers into one (ledger id MUST be the same).

Examples

iex> a = Bookk.Ledger.new("acme", [
iex>   Bookk.Account.new(fixture_account_head(:cash), Decimal.new(5)),
iex>   Bookk.Account.new(fixture_account_head(:deposits), Decimal.new(5)),
iex> ])
iex>
iex> b = Bookk.Ledger.new("acme", [
iex>   Bookk.Account.new(fixture_account_head(:cash), Decimal.new(15)),
iex>   Bookk.Account.new(fixture_account_head(:deposits), Decimal.new(15)),
iex> ])
iex>
iex> Bookk.Ledger.merge(a, b)
Bookk.Ledger.new("acme", [
  Bookk.Account.new(fixture_account_head(:cash), Decimal.new(20)),
  Bookk.Account.new(fixture_account_head(:deposits), Decimal.new(20))
])

It will raise if ledgers have different ids:

iex> a = Bookk.Ledger.new("acme")
iex> b = Bookk.Ledger.new("foo")
iex> Bookk.Ledger.merge(a, b)
** (FunctionClauseError) no function clause matching in Bookk.Ledger.merge/2

new(id, accounts \\ [])

@spec new(id :: String.t(), [Bookk.Account.t()]) :: t()

Creates a new Bookk.Ledger from its id and, optionally, a list of Bookk.Account.

post(ledger, journal_entry)

@spec post(t(), Bookk.JournalEntry.t()) :: t()

Posts a Bookk.JournalEntry to a ledger. This means that the balance change described in each operation of the journal entry will be applied to their respective accounts of the ledger. If there's a change to an account that doesn't exist yet, then the account is first created.

Examples

When account doesn't exist then it gets created:

iex> ledger = Bookk.Ledger.new("acme")
iex>
iex> journal_entry = %Bookk.JournalEntry{
iex>   operations: [
iex>     debit(fixture_account_head(:cash), Decimal.new(50)),
iex>     credit(fixture_account_head(:deposits), Decimal.new(50))
iex>   ]
iex> }
iex>
iex> updated_ledger = Bookk.Ledger.post(ledger, journal_entry)
iex>
iex> [
iex>   Bookk.Ledger.get_account(updated_ledger, fixture_account_head(:cash)),
iex>   Bookk.Ledger.get_account(updated_ledger, fixture_account_head(:deposits))
iex> ]
[
  %Bookk.Account{head: fixture_account_head(:cash), balance: Decimal.new(50)},
  %Bookk.Account{head: fixture_account_head(:deposits), balance: Decimal.new(50)}
]

When account exists then it gets updated:

iex> ledger = Bookk.Ledger.new("acme")
iex>
iex> journal_entry = %Bookk.JournalEntry{
iex>   operations: [
iex>     debit(fixture_account_head(:cash), Decimal.new(50)),
iex>     credit(fixture_account_head(:deposits), Decimal.new(50))
iex>   ]
iex> }
iex>
iex> updated_ledger =
iex>   ledger
iex>   |> Bookk.Ledger.post(journal_entry)
iex>   |> Bookk.Ledger.post(journal_entry) # post twice
iex>
iex> [
iex>   Bookk.Ledger.get_account(updated_ledger, fixture_account_head(:cash)),
iex>   Bookk.Ledger.get_account(updated_ledger, fixture_account_head(:deposits))
iex> ]
[
  %Bookk.Account{head: fixture_account_head(:cash), balance: Decimal.new(100)},
  %Bookk.Account{head: fixture_account_head(:deposits), balance: Decimal.new(100)}
]