# Getting Started with Ash Double Entry 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. ## What makes it special? 1. Account balances are updated automatically as transfers are introduced. 2. Arbitrary custom validations and behavior by virtue of modifying your own resources. 3. Transactions can be entered in the past, and all future balances are updated (and therefore validated). ## Setup ### Define your account resource #### Example ```elixir defmodule YourApp.Leger.Account do use Ash.Resource, 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 ``` #### What does this extension do? - Adds the following attributes: - `:id`, a `:uuid` primary key - `:currency`, a `:string` representing the currency of the transfer - `: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 accepts `identifier`, `currency`, and the attributes in `open_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) - Adds a `has_many` relationship called `balances`, referring to all related balances of an account - Adds an aggregate called `balance`, referring to the latest balance as a `decimal` for that account - Adds the following calculations: - A `balance_as_of_ulid` calculation that takes an argument called `ulid`, which corresponds to a transfer id and returns the balance as a decimal. - A `balance_as_of` calculation that takes a `utc_datetime_usec` and returns the balance as of that datetime. - Adds an identity called `unique_identifier` that ensures `identifier` is unique. ### Define your transfer resource #### Example ```elixir defmodule YourApp.Transfer do use Ash.Resource, 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 end end ``` #### What does this extension do? - Adds the following attributes - `:id`, a `AshDoubleEntry.ULID` primary key which is sortable based on the `timestamp` of the transfer. - `:amount`, a `:decimal` representing the amount 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`, a `belongs_to` relationship of the account the transfer is from - `:to_account`, a `belongs_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. ### Define your balance resource #### Example ```elixir defmodule YourApp.Ledger.Balance do use Ash.Resource, data_layer: AshPostgres.DataLayer, extensions: [AshDoubleEntry.Balance] postgres do table "balances" repo YourApp.Repo end balance do # configure the other resources it will interact with transfer_resource YourApp.Ledger.Transfer account_resource YourApp.Ledger.Account end changes do # add custom behavior. In this case, we're preventing certain balances from being less than zero change after_action(&validate_balance/2) end defp validate_balance(changeset, result) do account = result |> changeset.api.load!(:account) |> Map.get(:account) if account.allow_zero_balance == false && Decimal.negative?(result.balance) do {:error, Ash.Error.Changes.InvalidAttribute.exception( value: result.balance, field: :balance, message: "balance cannot be negative" )} else {:ok, result} end end end ``` #### What does this extension do? - 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 - a create action caleld `:upsert_balance`, which will create or update the relevant balance, by `transfer_id` and `account_id` - Adds an identity that ensures that `account_id` and `transfer_id` are unique ### Define an Ash api to use them through ```elixir defmodule YourApp.Ledger do use Ash.Api resources do resource YourApp.Ledger.Account resource YourApp.Ledger.Balance resource YourApp.Ledger.Transfer end end ``` And add the API to your config `config :your_app, ash_apis: [..., YourApp.Ledger]` ### Generate Migrations `mix ash_postgres.generate_migrations --name add_double_entry_ledger` ### Run them `mix ash_postgres.migrate` ### Use them #### Create an account ```elixir YourApp.Ledger.Account |> Ash.Changeset.for_create(:open, %{identifier: "account_one", currency: "USD"}) |> YourApp.Ledger.create!() ``` #### Create transfers between accounts ```elixir YourApp.Ledger.Transfer |> Ash.Changeset.for_create(:transfer, %{ amount: Decimal.new(20), from_account_id: account_one.id, to_account_id: account_two.id }) |> YourApp.Ledger.create!() ``` #### Check an account's balance ```elixir YourApp.Leger.Account |> YourApp.Ledger.get!(account_id, load: :balance) |> Map.get(:balance) # => Decimal.new("20") ``` ## What else can you do? 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](https://ash-hq.org).