Ash Double Entry is implemented as a set of Ash resource extensions. You build the resources yourself, and the extensions add the attributes, relationships, actions and validations required for them to constitute a double entry system.
- Account balances are updated automatically as transfers are introduced.
- Arbitrary custom validations and behavior by virtue of modifying your own resources.
- Transactions can be entered in the past, and all future balances are updated (and therefore validated).
Follow the setup guide for AshMoney
. If you are using with AshPostgres
, be sure to include the :ex_money_sql
dependency in your mix.exs
.
{:ash_double_entry, "~> 1.0.3"}
defmodule YourApp.Ledger.Account do
use Ash.Resource,
domain: YourApp.Ledger,
data_layer: AshPostgres.DataLayer,
extensions: [AshDoubleEntry.Account]
postgres do
table "accounts"
repo YourApp.Repo
end
account do
# configure the other resources it will interact with
transfer_resource YourApp.Ledger.Transfer
balance_resource YourApp.Ledger.Balance
# accept custom attributes in the autogenerated `open` create action
open_action_accept [:account_number]
end
attributes do
# Add custom attributes
attribute :account_number, :string do
allow_nil? false
end
end
end
- Adds the following attributes:
:id
, a:uuid
primary key:currency
, a:string
representing the currency of the account.:inserted_at
, a:utc_datetime_usec
timestamp:identifier
, a:string
and a unique identifier for the account
- Adds the following actions:
- A primary read called
:read
, unless a primary read action already exists. - A create action called
open
, that acceptsidentifier
,currency
, and the attributes inopen_action_accept
- A read action called
:lock_accounts
that can be used to lock a list of accounts while in a transaction(for data layers that support it)
- A primary read called
- Adds a
has_many
relationship calledbalances
, referring to all related balances of an account - Adds an aggregate called
balance
, referring to the latest balance as adecimal
for that account - Adds the following calculations:
- A
balance_as_of_ulid
calculation that takes an argument calledulid
, which corresponds to a transfer id and returns the balance. - A
balance_as_of
calculation that takes autc_datetime_usec
and returns the balance as of that datetime. - Adds an identity called
unique_identifier
that ensuresidentifier
is unique.
defmodule YourApp.Ledger.Transfer do
use Ash.Resource,
domain: YourApp.Ledger,
data_layer: AshPostgres.DataLayer,
extensions: [AshDoubleEntry.Transfer]
postgres do
table "transfers"
repo YourApp.Repo
end
transfer do
# configure the other resources it will interact with
account_resource YourApp.Ledger.Account
balance_resource YourApp.Ledger.Balance
# you only need this if you are using `postgres`
# and so cannot add the `references` block shown below
# destroy_balances? true
end
end
- Adds the following attributes
:id
, aAshDoubleEntry.ULID
primary key which is sortable based on thetimestamp
of the transfer.:amount
, aAshMoney.Types.Money
representing the amount and currency of the transfer:timestamp
, a:utc_datetime_usec
representing when the transfer occurred:inserted_at
, a:utc_datetime_usec
timestamp
- Adds the following relationships
:from_account
, abelongs_to
relationship of the account the transfer is from:to_account
, abelongs_to
relationship of the account the transfer is to
- Adds a
:read
action called:read_transfers
with keyset pagination enabled. Required for streaming transfers, used for validating balances. - Adds a change that runs on all create and update actions that reifies the balances table. It inserts a balance for the transfer, and updates any affected future balances.
defmodule YourApp.Ledger.Balance do
use Ash.Resource,
domain: YourApp.Ledger,
data_layer: AshPostgres.DataLayer,
extensions: [AshDoubleEntry.Balance]
postgres do
table "balances"
repo YourApp.Repo
references do
reference :transfer, on_delete: :delete
end
end
balance do
# configure the other resources it will interact with
transfer_resource YourApp.Ledger.Transfer
account_resource YourApp.Ledger.Account
end
actions do
read :read do
primary? true
# configure keyset pagination for streaming
pagination keyset?: true, required?: false
end
end
end
If you are not using a data layer capable of automatic cascade deletion, you must add
destroy_balances? true
to thetransfer
resource! We do this with thereferences
block inash_postgres
as shown above.
- Adds the following attributes:
:id
, a:uuid
primary key:balance
, the balance as a decimal of the account at the time of the related transfer
- Adds the following relationships:
:transfer
a:belongs_to
relationship, pointing to the transfer that this balance is as of.:account
a:belongs_to
relationship, pointing to the account the balance is for
- Adds the following actions:
- a primary read action called
:read
, if a priamry read action doesn't exist - configure primary read action to have keyset pagination enabled
- a create action caleld
:upsert_balance
, which will create or update the relevant balance, bytransfer_id
andaccount_id
- a primary read action called
- Adds an identity that ensures that
account_id
andtransfer_id
are unique
defmodule YourApp.Ledger do
use Ash.Domain
resources do
resource YourApp.Ledger.Account
resource YourApp.Ledger.Balance
resource YourApp.Ledger.Transfer
end
end
And add the domain to your config
config :your_app, ash_domains: [..., YourApp.Ledger]
mix ash_postgres.generate_migrations --name add_double_entry_ledger
mix ash_postgres.migrate
YourApp.Ledger.Account
|> Ash.Changeset.for_create(:open, %{identifier: "account_one"})
|> YourApp.Ledger.create!()
YourApp.Ledger.Transfer
|> Ash.Changeset.for_create(:transfer, %{
amount: Money.new!(20, :USD),
from_account_id: account_one.id,
to_account_id: account_two.id
})
|> YourApp.Ledger.create!()
YourApp.Ledger.Account
|> YourApp.Ledger.get!(account_id, load: :balance_as_of)
|> Map.get(:balance_as_of)
# => Money.new!(20, :USD)
There are tons of things you can do with your resources. You can add code interfaces to give yourself a nice functional api. You can add custom attributes, aggregates, calculations, relationships, validations, changes, all the great things built into Ash.Resource
! See the docs for more: AshHq.