ex_double_entry
ex_double_entry copied to clipboard
An Elixir double-entry library inspired by Ruby's DoubleEntry. Brought to you by CoinJar.
ExDoubleEntry
An Elixir double-entry library inspired by Ruby's DoubleEntry. Brought to you by CoinJar.

Supported Databases
- Postgres 9.4+ (for
JSONBsupport) - MySQL 8.0+ (for row locking support)
Installation
def deps do
[
{:ex_double_entry, github: "coinjar/ex_double_entry"},
# pick one DB package
{:postgrex, ">= 0.0.0"},
{:myxql, ">= 0.0.0"},
]
end
DB Migration
You will need to copy and run the migration file to create the DB tables.
Configuration
config :ex_double_entry,
db: :postgres,
db_table_prefix: "ex_double_entry_",
repo: YourProject.Repo,
default_currency: :USD,
# all accounts need to be defined here
accounts: %{
# account identifier: account options
#
# valid options are:
# "positive_only": whether the account can go into negative balance
bank: [],
savings: [positive_only: true],
checking: [],
},
# all transfers need to be defined here
transfers: %{
# transfer code: transfer pairs
#
# for each transfer pair:
# - the first element is the source account
# - the second element is the destination account
deposit: [
{:bank, :savings},
{:bank, :checking},
{:checking, :savings},
],
withdraw: [
{:savings, :checking},
],
}
Usage
Accounts & Balances
# creates a new account with 0 balance
ExDoubleEntry.make_account!(
# identifier of the account, in atom
:savings,
# currency can be any arbitrary atom
currency: :USD,
# optional, scope can be any arbitrary string
#
# due to DB index on `NULL` values, scope value can only be `nil` (stored as
# an empty string in the DB) or non-empty strings
scope: "user/1"
)
# looks up an account with its balance
ExDoubleEntry.lookup_account!(
:savings,
currency: :USD,
scope: "user/1"
)
Both functions return an ExDoubleEntry.Account struct that looks like this:
%ExDoubleEntry.Account{
id: 1,
identifier: :savings,
currency: :USD,
scope: "user/1",
positive_only?: true,
balance: Money.new(0, :USD),
}
Transfers
There are two transfer modes, transfer and transfer!.
Note: ExDoubleEntry relies on the money library for balances and amounts.
# accounts need to exist in the DB otherwise
# `ExDoubleEntry.Account.NotFoundError` is raised
ExDoubleEntry.transfer(
money: Money.new(100_00, :USD),
# accounts need to be defined in the config
from: account_a,
to: account_b,
# transfer code is required, and must be defined in the config
code: :deposit,
# optional, metadata can be any arbitrary map, it gets stored in the DB
# as either a JSON string (MySQL) or a JSONB object (Postgres)
metadata: %{diamond: "hands"}
)
# accounts will be created in the DB if they don't exist
# once accounts are created they will be locked during the transfer
ExDoubleEntry.transfer!(
money: Money.new(100_00, :USD),
from: account_a,
to: account_b,
code: :deposit
)
Locking
Transfer itself will already lock the accounts involved. However, if there are
other tasks that need to be performed atomically with the transfer, you can
perform them using lock_accounts.
Transactions can be nested arbitrarily, since in Ecto, transactions are flattened and are committed or rolled back based on the outer most transaction.
Read more on Ecto's transaction handling here.
ExDoubleEntry.lock_accounts([account_a, account_b], fn ->
ExDoubleEntry.transfer!(
money: Money.new(100, :USD),
from: account_a,
to: account_b,
code: :deposit
)
# perform other tasks that should be committed atomically with the transfer
end)
License
Licensed under MIT.