### Usage Example Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Calculations.md Example of loading the balance_as_of calculation on an Ash resource. ```elixir # Get account balance as of a specific time one_week_ago = DateTime.add(DateTime.utc_now(), -7, :day) account = YourApp.Ledger.Account |> Ash.Query.load(balance_as_of: %{timestamp: one_week_ago}) |> Ash.read_one!() IO.inspect(account.balance_as_of) # => #Money<:USD, 2300> ``` -------------------------------- ### Configure balance filtering Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Changes.md Example of building the filter for balance records during an adjustment. ```elixir changes: [ Ash.Resource.Builder.build_action_change( {Ash.Resource.Change.Filter, filter: expr( account_id in [^arg(:from_account_id), ^arg(:to_account_id)] and transfer_id > ^arg(:transfer_id) )} ), ... ] ``` -------------------------------- ### Create Transfer Example Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Preparations.md Example of creating a transfer between accounts using Ash changesets. ```elixir YourApp.Ledger.Transfer |> Ash.Changeset.for_create(:transfer, %{ from_account_id: checking_id, to_account_id: savings_id, amount: Money.new!(100, :USD) }) |> Ash.create!() ``` -------------------------------- ### Load Account Balance via ULID Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Calculations.md Example of loading an account balance as of a specific transfer ULID using Ash query syntax. ```elixir # Load account with balance as of a specific transfer account = YourApp.Ledger.Account |> Ash.Query.load(balance_as_of_ulid: %{ulid: transfer_ulid}) |> Ash.read_one!() # account.balance_as_of_ulid is the Money value at that transfer IO.inspect(account.balance_as_of_ulid) # => #Money<:USD, 1500> ``` -------------------------------- ### Deadlock scenario Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Preparations.md Example of a potential deadlock caused by inconsistent locking order. ```text Thread 1: Lock A, then lock B Thread 2: Lock B, then lock A Result: Deadlock ``` -------------------------------- ### Invoke lock_accounts in transfer verification Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Preparations.md Example of using the lock_accounts read action within a changeset verification process. ```elixir # In VerifyTransfer.change/3 accounts = changeset.resource |> AshDoubleEntry.Transfer.Info.transfer_account_resource!() |> Ash.Query.filter(id in ^[from_account_id, to_account_id]) |> Ash.Query.set_context(%{ash_double_entry?: true}) |> Ash.Query.for_read( :lock_accounts, # Uses LockForUpdate preparation %{}, Ash.Context.to_opts(context, ...) ) |> Ash.read!() ``` -------------------------------- ### Log Transfer Errors Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/errors.md Example of wrapping a transfer creation in a function to log errors or success states. ```elixir def create_transfer(from_id, to_id, amount) do case Ash.create(transfer_changeset) do {:error, errors} -> Logger.error("Transfer creation failed: #{inspect(errors)}") {:error, "Transfer failed"} {:ok, transfer} -> Logger.info("Transfer created: #{transfer.id}") {:ok, transfer} end end ``` -------------------------------- ### Define Transfer Data Structure Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/types.md Example of the map structure required when creating a transfer, including UUID strings, Money composites, and UTC DateTimes. ```elixir %{ from_account_id: "550e8400-e29b-41d4-a716-446655440000", # UUID string to_account_id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8", # UUID string amount: Money.new!(100, :USD), # Money composite timestamp: DateTime.utc_now() # UTC DateTime } ``` -------------------------------- ### Create a new account Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Account.md Uses the open action to initialize a new account with a unique identifier and currency. ```elixir YourApp.Ledger.Account |> Ash.Changeset.for_create(:open, %{identifier: "checking-001", currency: "USD"}) |> Ash.create!() ``` -------------------------------- ### Run Database Migrations Source: https://github.com/ash-project/ash_double_entry/blob/main/documentation/tutorials/getting-started-with-ash-double-entry.md Apply the generated migrations to your database. ```bash mix ash_postgres.migrate ``` -------------------------------- ### open (create) Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Account.md Creates a new account in the ledger. ```APIDOC ## open (create) ### Description Creates a new account in the ledger. ### Signature `def open(account, input, options)` ### Parameters - **identifier** (string) - Required - Unique identifier for the account - **currency** (string) - Required - Currency code ### Example ```elixir YourApp.Ledger.Account |> Ash.Changeset.for_create(:open, %{identifier: "checking-001", currency: "USD"}) |> Ash.create!() ``` ``` -------------------------------- ### Create an Account Source: https://github.com/ash-project/ash_double_entry/blob/main/documentation/tutorials/getting-started-with-ash-double-entry.md Create a new account using Ash.Changeset for the create action. ```elixir YourApp.Ledger.Account |> Ash.Changeset.for_create(:open, %{identifier: "account_one"}) |> Ash.create!() ``` -------------------------------- ### Configure Account.open action accept list Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Configures additional attributes accepted by the Account.open action. ```elixir account do open_action_accept [:account_number, :description, :tags] end # Changeset will accept: %{ identifier: "acc-001", # Always accepted currency: "USD", # Always accepted account_number: "123-456", # From open_action_accept description: "Main account", # From open_action_accept tags: ["primary", "operational"] # From open_action_accept } ``` -------------------------------- ### Configure PostgreSQL Resources with Default Settings Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Defines Account and Balance resources using AshPostgres.DataLayer with default composite type settings. ```elixir 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 transfer_resource YourApp.Ledger.Transfer balance_resource YourApp.Ledger.Balance end end 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 end balance do transfer_resource YourApp.Ledger.Transfer account_resource YourApp.Ledger.Account money_composite_type? true data_layer_can_add_money? true end end ``` -------------------------------- ### Define Custom Accept Lists Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Demonstrates best practices for limiting create_accept lists to only necessary attributes. ```elixir # Good transfer do create_accept [:memo] end # Overly broad transfer do create_accept [:memo, :tags, :notes, :description, :category, :subcategory] end ``` -------------------------------- ### Create balance records Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Changes.md Creates initial balance records for accounts after a transfer using bulk_create. ```elixir Ash.bulk_create( [ %{ account_id: from_account.id, transfer_id: result.id, balance: new_from_account_balance }, %{ account_id: to_account.id, transfer_id: result.id, balance: new_to_account_balance } ], balance_resource, :upsert_balance, Ash.Context.to_opts(context, upsert_fields: [:balance], ...) ) ``` -------------------------------- ### Create SQL Indexes for Balances Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/overview.md Optimize balance lookups by indexing account and transfer identifiers. ```sql CREATE INDEX balances_account_transfer ON balances(account_id, transfer_id DESC); CREATE INDEX balances_transfer ON balances(transfer_id DESC); ``` -------------------------------- ### Configure Create Accept Attributes Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Define additional attributes allowed during transfer creation and demonstrate their usage in a changeset. ```elixir transfer do create_accept [:memo, :reference_number, :category] end # Allows: YourApp.Ledger.Transfer |> Ash.Changeset.for_create(:transfer, %{ from_account_id: from_id, to_account_id: to_id, amount: Money.new!(100, :USD), memo: "Monthly rent payment", reference_number: "2024-01-001", category: "rent" }) ``` -------------------------------- ### Define Transfer Resource with AshDoubleEntry.Transfer Source: https://github.com/ash-project/ash_double_entry/blob/main/documentation/tutorials/getting-started-with-ash-double-entry.md Set up your transfer resource using the `AshDoubleEntry.Transfer` extension. This resource manages transactions between accounts and requires configuration for related account and balance resources. ```elixir 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 ``` -------------------------------- ### Define Account Resource with AshDoubleEntry.Account Source: https://github.com/ash-project/ash_double_entry/blob/main/documentation/tutorials/getting-started-with-ash-double-entry.md Configure your account resource using the `AshDoubleEntry.Account` extension. This sets up essential attributes, actions, relationships, and calculations for managing ledger accounts. ```elixir 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 ``` -------------------------------- ### Adding Destroy Action to Balance Resource Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/errors.md Shows how to resolve the configuration error by adding a destroy action to the balance resource. ```elixir # Add a destroy action to Balance resource defmodule YourApp.Ledger.Balance do use Ash.Resource, extensions: [AshDoubleEntry.Balance] actions do defaults [:destroy] # Or explicitly define destroy action end end ``` -------------------------------- ### Access AshDoubleEntry.Balance.Info Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/overview.md Functions for retrieving configuration details from a Balance resource. ```elixir balance_transfer_resource!(resource) -> module balance_account_resource!(resource) -> module balance_money_composite_type?(resource) -> boolean balance_data_layer_can_add_money?(resource) -> boolean balance_pre_check_identities_with(resource) -> {:ok, module} | :error ``` -------------------------------- ### Check an Account's Balance Source: https://github.com/ash-project/ash_double_entry/blob/main/documentation/tutorials/getting-started-with-ash-double-entry.md Retrieve an account and load its balance as of a specific point in time using the `balance_as_of` load option. ```elixir YourApp.Ledger.Account |> YourApp.Ledger.get!(account_id, load: :balance_as_of) |> Map.get(:balance_as_of) # => Money.new!(20, :USD) ``` -------------------------------- ### read Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Balance.md Reads balance records from the system. ```APIDOC ## read ### Description Reads balance records. Supports standard Ash read options and keyset pagination. ### Signature `def read(resource, options)` ### Example ```elixir YourApp.Ledger.Balance |> Ash.Query.filter(account_id == ^account_id) |> Ash.read!() ``` ``` -------------------------------- ### AshDoubleEntry.Balance.Info Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/overview.md Provides introspection functions for resources configured with the Balance extension. ```APIDOC ## AshDoubleEntry.Balance.Info ### Methods - **balance_transfer_resource!(resource)** -> module - **balance_account_resource!(resource)** -> module - **balance_money_composite_type?(resource)** -> boolean - **balance_data_layer_can_add_money?(resource)** -> boolean - **balance_pre_check_identities_with(resource)** -> {:ok, module} | :error ``` -------------------------------- ### Implement LockForUpdate preparation Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Preparations.md The implementation checks for data layer support before applying the lock to the query. ```elixir def prepare(query, _, _) do if Ash.DataLayer.data_layer_can?(query.resource, {:lock, :for_update}) do Ash.Query.lock(query, :for_update) else query end end ``` -------------------------------- ### Configure Balance Extension Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/README.md Defines the balance resource configuration for the AshDoubleEntry system. ```elixir balance do transfer_resource YourApp.Ledger.Transfer account_resource YourApp.Ledger.Account money_composite_type? true data_layer_can_add_money? true end ``` -------------------------------- ### Configure Account Extension Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/README.md Defines the account resource configuration for the AshDoubleEntry system. ```elixir account do transfer_resource YourApp.Ledger.Transfer balance_resource YourApp.Ledger.Balance open_action_accept [:account_number] pre_check_identities_with YourApp.Ledger end ``` -------------------------------- ### balance Source: https://github.com/ash-project/ash_double_entry/blob/main/documentation/dsls/DSL-AshDoubleEntry.Balance.md Configures the double-entry ledger balance extension. ```APIDOC ## balance ### Description Configures the double-entry ledger balance extension. ### Options #### transfer_resource - **Type**: `module` - **Required**: Yes - **Description**: The resource used for transfers. #### account_resource - **Type**: `module` - **Required**: Yes - **Description**: The resource used for accounts. #### pre_check_identities_with - **Type**: `module` - **Required**: No - **Description**: A domain to use to precheck generated identities. Required by certain data layers. #### money_composite_type? - **Type**: `boolean` - **Default**: `true` - **Required**: No - **Description**: Whether the balance is stored as a composite type. #### data_layer_can_add_money? - **Type**: `boolean` - **Default**: `true` - **Required**: No - **Description**: Whether or not the data layer supports adding money. ``` -------------------------------- ### Configure Balance Resource Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Specify the balance resource to enable balance tracking for the transfer. ```elixir transfer do balance_resource YourApp.Ledger.Balance end ``` -------------------------------- ### Eagerly Loading Calculations Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Calculations.md Demonstrates loading calculations on existing records or within an Ash query. ```elixir # Load single calculation account |> Ash.load!(balance_as_of_ulid: %{ulid: transfer_ulid}) # Load both calculations account |> Ash.load!( balance_as_of_ulid: %{ulid: transfer_ulid}, balance_as_of: %{timestamp: DateTime.utc_now()} ) # Load in query YourApp.Ledger.Account |> Ash.Query.load(balance_as_of: %{timestamp: one_month_ago}) |> Ash.read!() ``` -------------------------------- ### Access AshDoubleEntry.Account.Info Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/overview.md Functions for retrieving configuration details from an Account resource. ```elixir account_transfer_resource!(resource) -> module account_balance_resource!(resource) -> module account_open_action_accept!(resource) -> [atom] account_pre_check_identities_with(resource) -> {:ok, module} | :error ``` -------------------------------- ### Configure Open Action Acceptance Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Allows additional attributes to be accepted by the auto-generated open create action. ```elixir account do open_action_accept [:account_number, :account_type, :description] end # This allows creating accounts with: YourApp.Ledger.Account |> Ash.Changeset.for_create(:open, %{ identifier: "checking-001", currency: "USD", account_number: "123-456-789", account_type: "checking", description: "Main operating account" }) ``` -------------------------------- ### Enable Identity Pre-Checking Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Configures an account resource to perform identity pre-checks, useful for performance-critical uniqueness constraints in PostgreSQL. ```elixir account do transfer_resource YourApp.Ledger.Transfer balance_resource YourApp.Ledger.Balance pre_check_identities_with YourApp.Ledger end ``` -------------------------------- ### Creating a Transfer Data Flow Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/overview.md Outlines the sequence of operations from user input to account locking and balance record creation. ```text 1. User creates transfer from_account_id: UUID to_account_id: UUID amount: Money timestamp: DateTime (optional) 2. VerifyTransfer change: - Validates from_account_id ≠ to_account_id - Generates ULID from timestamp - Locks both accounts (FOR UPDATE) 3. After successful insert: - Load current balances for both accounts - Calculate new balances after transfer - Create balance records (upsert) - Adjust all future balance records 4. Result: - Transfer record created - Two balance records created - All future balance records updated - Accounts unlocked ``` -------------------------------- ### Introspect Balance Configuration Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Balance.md Retrieves configuration details for a balance resource using the Info module. ```elixir # Get transfer resource AshDoubleEntry.Balance.Info.balance_transfer_resource!(resource) # Get account resource AshDoubleEntry.Balance.Info.balance_account_resource!(resource) # Check storage configuration AshDoubleEntry.Balance.Info.balance_money_composite_type?(resource) # Check if data layer supports atomic money operations AshDoubleEntry.Balance.Info.balance_data_layer_can_add_money?(resource) ``` -------------------------------- ### Project Directory Structure Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/overview.md Visual representation of the library's file organization. ```text lib/ ├── ash_double_entry.ex # Empty root module ├── ulid.ex # ULID type ├── account/ │ ├── account.ex # Account extension DSL │ ├── info.ex # Account introspection │ ├── transformers/ │ │ └── add_structure.ex # Account structure transformer │ ├── calculations/ │ │ ├── balance_as_of.ex # Balance at datetime calculation │ │ └── balance_as_of_ulid.ex # Balance before transfer calculation │ └── preparations/ │ └── lock_for_update.ex # Account row locking ├── transfer/ │ ├── transfer.ex # Transfer extension DSL │ ├── info.ex # Transfer introspection │ ├── transformers/ │ │ └── add_structure.ex # Transfer structure transformer │ └── changes/ │ └── verify_transfer.ex # Transfer verification and balance updates ├── balance/ │ ├── balance.ex # Balance extension DSL │ ├── info.ex # Balance introspection │ ├── transformers/ │ │ └── add_structure.ex # Balance structure transformer │ └── changes/ │ └── adjust_balance.ex # Balance adjustment handler └── mix/ └── tasks/ └── ash_double_entry.install.ex # Installation generator task ``` -------------------------------- ### Handling Balance Creation Failures Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/errors.md Provides a pattern for handling errors when balance records fail to create during a transfer. ```elixir case Ash.create(transfer_changeset) do {:error, errors} when is_list(errors) -> # Multiple errors, including balance creation failures Enum.each(errors, fn error -> IO.inspect(error) end) {:ok, transfer} -> # Success end ``` -------------------------------- ### bingenerate(timestamp) Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/ULID.md Generates a 128-bit (16-byte) binary representation of a ULID. ```APIDOC ## bingenerate(timestamp) ### Description Generates a binary ULID consisting of 16 raw bytes. ### Parameters - **timestamp** (integer) - Optional - Unix timestamp in milliseconds. ### Returns - **binary** - 16-byte binary representation. ``` -------------------------------- ### Create Transfers Between Accounts Source: https://github.com/ash-project/ash_double_entry/blob/main/documentation/tutorials/getting-started-with-ash-double-entry.md Initiate a transfer between two accounts using the `transfer` action and specifying amount, currency, and account IDs. ```elixir 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 }) |> Ash.create!() ``` -------------------------------- ### Use balance_as_of calculation Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Account.md Loads the account balance as of a specific datetime using Ash.load!. ```elixir account = Ash.read!(YourApp.Ledger.Account) historical = Ash.load!(account, balance_as_of: %{timestamp: one_month_ago}) ``` -------------------------------- ### Implement Double-Entry Accounting Pattern Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/overview.md Defines account resources, performs transfers, and queries balances using AshDoubleEntry extensions. ```elixir # 1. Define resources defmodule MyApp.Ledger.Account do use Ash.Resource, extensions: [AshDoubleEntry.Account] account do transfer_resource MyApp.Ledger.Transfer balance_resource MyApp.Ledger.Balance end end # 2. Create accounts account1 = MyApp.Ledger.Account |> Ash.Changeset.for_create(:open, ...) |> Ash.create!() account2 = MyApp.Ledger.Account |> Ash.Changeset.for_create(:open, ...) |> Ash.create!() # 3. Create transfers transfer = MyApp.Ledger.Transfer |> Ash.Changeset.for_create(:transfer, %{ from_account_id: account1.id, to_account_id: account2.id, amount: Money.new!(100, :USD) }) |> Ash.create!() # 4. Query balances account1 = account1 |> Ash.load!(balance_as_of_ulid: %{ulid: transfer.id}) IO.inspect(account1.balance_as_of_ulid) # #Money<:USD, -100> account2 = account2 |> Ash.load!(balance_as_of_ulid: %{ulid: transfer.id}) IO.inspect(account2.balance_as_of_ulid) # #Money<:USD, 100> ``` -------------------------------- ### Introspect Account Configuration Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Account.md Use these functions to retrieve resource and action configuration details from an account resource. ```elixir # Get transfer resource AshDoubleEntry.Account.Info.account_transfer_resource!(resource) # Get balance resource AshDoubleEntry.Account.Info.account_balance_resource!(resource) # Get additional open action attributes AshDoubleEntry.Account.Info.account_open_action_accept!(resource) ``` -------------------------------- ### balance_as_of Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Account.md Calculates the account balance as of a specific datetime. ```APIDOC ## balance_as_of ### Description Calculates the account balance as of a specific datetime. ### Arguments - **timestamp** (:utc_datetime_usec) - Optional - Defaults to current time. ### Returns - Money value representing the balance at that datetime. ### Example ```elixir account = Ash.read!(YourApp.Ledger.Account) historical = Ash.load!(account, balance_as_of: %{timestamp: one_month_ago}) ``` ``` -------------------------------- ### Handling Balance Adjustment Failures Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/errors.md Provides a pattern for logging and handling errors when future balance adjustments fail. ```elixir case Ash.create(transfer_changeset) do {:error, errors} when is_list(errors) -> # Log errors for investigation IO.puts("Balance adjustment failed: #{inspect(errors)}") # May need manual reconciliation {:ok, transfer} -> # Success end ``` -------------------------------- ### Convert ULID to binary with dump_to_native Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/ULID.md Converts a 26-character Crockford Base32 ULID string into a 16-byte binary for database storage. ```elixir AshDoubleEntry.ULID.dump_to_native("01ARZ3NDEKTSV4RRFFQ69G5FAV", []) # => {:ok, <<0, 161, 163, ...>>} ``` -------------------------------- ### Access AshDoubleEntry.Transfer.Info Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/overview.md Functions for retrieving configuration details from a Transfer resource. ```elixir transfer_account_resource!(resource) -> module transfer_balance_resource!(resource) -> module transfer_create_accept!(resource) -> [atom] transfer_destroy_balances?(resource) -> boolean transfer_pre_check_identities_with(resource) -> {:ok, module} | :error ``` -------------------------------- ### Configure Identity Pre-checking Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Sets the domain used for pre-checking unique identity references. ```elixir balance do pre_check_identities_with YourApp.Ledger end ``` -------------------------------- ### Implementing Fallback Balance Logic Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Calculations.md Provides a zero-balance fallback for accounts without historical records using composite_type for type safety. ```elixir first(...) || composite_type(%{currency: currency, amount: 0}, AshMoney.Types.Money) ``` -------------------------------- ### Extension Execution Order Logic Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/overview.md Defines the execution sequence for transformers and the transformation process within the Spark DSL extension system. ```text Transformer.before?/1 — Determines execution order relative to other transformers Account.Transformers.AddStructure.before?/1: - true for SetRelationshipSource - true for BelongsToAttribute - true for CachePrimaryKey - false for others Transformer.transform/1 — Modifies DSL after earlier transformers complete - Adds attributes - Adds relationships - Adds actions - Adds calculations - Adds identities ``` -------------------------------- ### AshDoubleEntry.Account.Info Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Account.md Functions for introspecting account-related resources and actions. ```APIDOC ## AshDoubleEntry.Account.Info ### Description Provides introspection utilities for account configurations in AshDoubleEntry. ### Methods - `account_transfer_resource!(resource)`: Retrieves the transfer resource for the given account resource. - `account_balance_resource!(resource)`: Retrieves the balance resource for the given account resource. - `account_open_action_accept!(resource)`: Retrieves additional open action attributes for the given account resource. ``` -------------------------------- ### AshDoubleEntry.Transfer.Info Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Transfer.md Provides methods to introspect the configuration of transfer resources. ```APIDOC ## Introspection Methods ### Description Use the `AshDoubleEntry.Transfer.Info` module to retrieve configuration details for a given transfer resource. ### Methods - `transfer_account_resource!(resource)`: Get the account resource. - `transfer_balance_resource!(resource)`: Get the balance resource. - `transfer_destroy_balances?(resource)`: Check if balances must be manually destroyed. - `transfer_create_accept!(resource)`: Get additional create attributes. ``` -------------------------------- ### Handle Account Identifier Uniqueness Errors Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/errors.md Demonstrates triggering an identity constraint violation when creating an account with a duplicate identifier and how to catch it. ```elixir YourApp.Ledger.Account |> Ash.Changeset.for_create(:open, %{ identifier: "checking-001", # Already exists currency: "USD" }) |> Ash.create!() # Raises: Invalid error: identifier "is already taken" ``` ```elixir case Ash.create(account_changeset) do {:error, changeset} -> errors = Ash.Changeset.errors(changeset) if Enum.any?(errors, &String.contains?(to_string(&1), "is already taken")) do # Handle duplicate identifier IO.puts("Account identifier already exists") end {:ok, account} -> # Success end ``` -------------------------------- ### Configure PostgreSQL Balance with JSON Storage Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Configures the Balance resource to use JSONB storage instead of composite types by setting money_composite_type? to false. ```elixir 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 end balance do transfer_resource YourApp.Ledger.Transfer account_resource YourApp.Ledger.Account money_composite_type? false # Use jsonb instead of composite type data_layer_can_add_money? true end end ``` -------------------------------- ### Query Balance at a Point in Time Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/README.md Loads the balance for an account as of a specific timestamp. ```elixir account |> Ash.load!(balance_as_of: %{timestamp: one_week_ago}) |> Map.get(:balance_as_of) ``` -------------------------------- ### SQL FOR UPDATE lock syntax Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Preparations.md The underlying SQL generated when the preparation is applied. ```sql SELECT * FROM accounts WHERE id IN (...) FOR UPDATE ``` -------------------------------- ### Configure Balance Extension in Ash Resource Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Defines the transfer and account resources within the balance DSL block. ```elixir defmodule YourApp.Ledger.Balance do use Ash.Resource, extensions: [AshDoubleEntry.Balance] balance do transfer_resource YourApp.Ledger.Transfer account_resource YourApp.Ledger.Account end end ``` -------------------------------- ### Configure Transfer.transfer action accept list Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Configures additional attributes accepted by the Transfer.transfer action. ```elixir transfer do create_accept [:memo, :tags, :external_id] end # Changeset will accept: %{ amount: Money.new!(100, :USD), # Always accepted from_account_id: uuid1, # Always accepted to_account_id: uuid2, # Always accepted timestamp: DateTime.utc_now(), # Always accepted memo: "Rent payment", # From create_accept tags: ["monthly", "recurring"], # From create_accept external_id: "EXT-12345" # From create_accept } ``` -------------------------------- ### AshDoubleEntry.ULID.dump_to_native/2 Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/ULID.md Converts a Crockford Base32 ULID string into a 16-byte binary format suitable for database storage. ```APIDOC ## AshDoubleEntry.ULID.dump_to_native(value, constraints) ### Description Converts a 26-character Crockford Base32 ULID string to a 16-byte binary for database storage. ### Signature `def dump_to_native(value :: String.t() | nil, constraints :: Keyword.t()) :: {:ok, binary()} | :error` ### Parameters - **value** (String.t() | nil) - Required - 26-char Crockford Base32 string or nil. - **constraints** (Keyword.t()) - Required - Ash type constraints (ignored). ### Returns - **{:ok, binary}** - 16-byte binary representation. - **:error** - If value is not a valid ULID. ``` -------------------------------- ### Configure Transfer Extension Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/README.md Defines the transfer resource configuration for the AshDoubleEntry system. ```elixir transfer do account_resource YourApp.Ledger.Account balance_resource YourApp.Ledger.Balance create_accept [:memo] destroy_balances? false end ``` -------------------------------- ### Compare and Order ULIDs Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/ULID.md Demonstrates that ULID strings maintain chronological order when compared lexicographically. ```elixir # Generated at earlier timestamp ulid1 = "01ARZ3NDEKTSV4RRFFQ69G5FAV" # Generated at later timestamp ulid2 = "01ARZ3NDEKTSV4RRFFQ69G5FB0" # Works correctly with string comparison ulid1 < ulid2 # => true ``` -------------------------------- ### Concurrency control comparison Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Preparations.md Comparison of race conditions without locking versus serialized execution with locking. ```text Thread 1: Read account balance (100) Thread 2: Read account balance (100) Thread 1: Deduct 50, write back (50) Thread 2: Add 50, write back (150) Result: Lost update; balance should be 100 (100 - 50 + 50) ``` ```text Thread 1: Lock account, read balance (100) Thread 2: Waits to lock... Thread 1: Deduct 50, write back (50), release lock Thread 2: Acquires lock, reads balance (50) Thread 2: Add 50, write back (100), release lock Result: Correct; balance is 100 ``` -------------------------------- ### Generate Migrations for Double Entry Ledger Source: https://github.com/ash-project/ash_double_entry/blob/main/documentation/tutorials/getting-started-with-ash-double-entry.md Use the Ash Postgres mix task to generate the necessary database migrations for your double entry ledger. ```bash mix ash_postgres.generate_migrations --name add_double_entry_ledger ``` -------------------------------- ### Query Balances by ULID Range Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/types.md Demonstrates filtering ledger balances using ULID bounds generated from timestamps. ```elixir # ULID query bounds lower = AshDoubleEntry.ULID.generate_last(one_week_ago) upper = AshDoubleEntry.ULID.generate(DateTime.utc_now()) YourApp.Ledger.Balance |> Ash.Query.filter(transfer_id >= ^lower and transfer_id <= ^upper) ``` -------------------------------- ### Function Signature Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Calculations.md The signature for the BalanceAsOf calculation expression. ```elixir def expression(_opts, context) :: Ash.Expr.t() ``` -------------------------------- ### Configure Balance Storage Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Balance.md Defines how balance data is stored in the database. Use composite types for native PostgreSQL support or map storage for JSONB. ```elixir balance do transfer_resource YourApp.Ledger.Transfer account_resource YourApp.Ledger.Account money_composite_type? true data_layer_can_add_money? true end ``` ```elixir balance do transfer_resource YourApp.Ledger.Transfer account_resource YourApp.Ledger.Account money_composite_type? false data_layer_can_add_money? false end ``` -------------------------------- ### Use balance_as_of_ulid calculation Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Account.md Loads the account balance as of a specific transfer ID using Ash.load!. ```elixir account = Ash.read!(YourApp.Ledger.Account) Ash.load!(account, balance_as_of_ulid: %{ulid: transfer_ulid}) ``` -------------------------------- ### Define LockForUpdate preparation signature Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Preparations.md The function signature for the Ash resource preparation. ```elixir def prepare(query, _, _) :: Ash.Query.t() ``` -------------------------------- ### Configure Money Storage Type Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Sets the storage format for money values, choosing between PostgreSQL composite types or JSONB maps. ```elixir balance do money_composite_type? true end # Database storage: balance money_with_currency # Example value: (currency => 'USD', amount => 1000.00) ``` ```elixir balance do money_composite_type? false end # Database storage: balance jsonb # Example value: {"currency": "USD", "amount": 1000.00} ``` -------------------------------- ### Querying Historical Balance Data Flow Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/overview.md Details the logic for calculating an account balance at a specific point in time using ULID filtering. ```text 1. Load account with balance_as_of_ulid calculation ulid: transfer_id (specific point in time) 2. BalanceAsOfUlid calculation: - Queries balances for this account - Filters: transfer_id <= requested_ulid - Sorts descending by transfer_id - Returns first (most recent before ulid) - Falls back to zero balance if none exist 3. Result: - Account balance at that specific transfer - O(1) database query with index ``` -------------------------------- ### Introspect Transfer Configuration Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Transfer.md Access metadata and configuration details for transfer resources using the Info module. ```elixir # Get account resource AshDoubleEntry.Transfer.Info.transfer_account_resource!(resource) # Get balance resource AshDoubleEntry.Transfer.Info.transfer_balance_resource!(resource) # Check if balances must be manually destroyed AshDoubleEntry.Transfer.Info.transfer_destroy_balances?(resource) # Get additional create attributes AshDoubleEntry.Transfer.Info.transfer_create_accept!(resource) ``` -------------------------------- ### read (primary) Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Account.md Reads accounts from the ledger. ```APIDOC ## read (primary) ### Description Reads accounts from the ledger. Auto-generated if no primary read action exists. ### Signature `def read(resource, options)` ### Options - Standard Ash read options (filters, sorts, pagination with keyset pagination support) ### Example ```elixir YourApp.Ledger.Account |> Ash.Query.filter(identifier == "checking-001") |> Ash.read!() ``` ``` -------------------------------- ### Triggering Transfer Destruction Error Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/errors.md Demonstrates the configuration that leads to an error when destroy_balances? is true but no destroy action exists. ```elixir # Transfer configuration: transfer do destroy_balances? true end # Balance resource has no destroy action -> ERROR when destroying transfer transfer |> Ash.destroy!() # Raises: "Must configure a primary destroy action for ... to destroy transactions" ``` -------------------------------- ### Configure Transfer Resource Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/overview.md Define the transfer resource settings, including resource associations and behavior flags. ```elixir transfer do account_resource YourApp.Ledger.Account balance_resource YourApp.Ledger.Balance create_accept [:memo] destroy_balances? false pre_check_identities_with YourApp.Ledger end ``` -------------------------------- ### bingenerate_last(timestamp) Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/ULID.md Generates a 16-byte binary ULID with the random portion set to maximum values for range upper bounds. ```APIDOC ## bingenerate_last(timestamp) ### Description Generates a binary ULID with the random portion set to 0xFF, intended for use as an upper-bound query parameter. ### Parameters - **timestamp** (integer) - Optional - Unix timestamp in milliseconds. ### Returns - **binary** - 16-byte binary with random portion set to 0xFF. ``` -------------------------------- ### Configure Data Layer Compatibility Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Sets the data_layer_can_add_money? flag based on the underlying data layer's atomicity support. ```elixir balance do transfer_resource YourApp.Ledger.Transfer account_resource YourApp.Ledger.Account data_layer_can_add_money? true # Verify your data layer supports this end ``` -------------------------------- ### Define Balance Resource Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Specifies the balance resource for tracking account history. ```elixir account do balance_resource YourApp.Ledger.Balance end ``` -------------------------------- ### Define balance_as_of calculation Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Account.md Defines the calculation signature for retrieving an account balance as of a specific timestamp. ```elixir calculation :balance_as_of, type: AshMoney.Types.Money, arguments: [timestamp: :utc_datetime_usec] ``` -------------------------------- ### Lock Preparation Trigger Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Preparations.md Explicitly triggering a lock on a query. ```elixir Ash.Query.lock(query, :for_update) ``` -------------------------------- ### Destroy Action Balance Handling Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Changes.md Conditionally triggers bulk destruction of related balance records based on resource configuration. ```elixir defp maybe_destroy_balances(changeset, context) { if changeset.action.type == :destroy do if AshDoubleEntry.Transfer.Info.transfer_destroy_balances?(changeset.resource) do # Bulk destroy all balance records for this transfer balance_resource |> Ash.Query.filter(transfer_id == ^changeset.data.id) |> Ash.bulk_destroy(:destroy_action, ...) end end } ``` -------------------------------- ### Define Balance Resource with Ash Double Entry Source: https://github.com/ash-project/ash_double_entry/blob/main/documentation/tutorials/getting-started-with-ash-double-entry.md Configure the Balance resource to interact with Transfer and Account resources. Ensure cascade deletion is handled if your data layer does not support it automatically. ```elixir 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 ``` -------------------------------- ### Generate binary ULID Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/ULID.md Generates a 16-byte raw binary representation of a ULID. ```elixir def bingenerate(timestamp \ System.system_time(:millisecond)) :: binary() ``` ```elixir ulid_binary = AshDoubleEntry.ULID.bingenerate() # => <<1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16>> ``` -------------------------------- ### transfer (create) Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Transfer.md Creates a new transfer between two accounts with automatic balance updates. ```APIDOC ## transfer (create) ### Description Creates a new transfer between two accounts with automatic balance updates. This action validates account IDs, generates a ULID, locks accounts for concurrency, and updates balance records. ### Signature `def transfer(transfer, input, options)` ### Parameters - **amount** (required) - Money value to transfer - **from_account_id** (required) - UUID of source account - **to_account_id** (required) - UUID of destination account - **timestamp** (optional) - datetime, defaults to current time; can be in the past ### Returns The created transfer with generated ULID and updated balances. ### Example ```elixir YourApp.Ledger.Transfer |> Ash.Changeset.for_create(:transfer, %{ from_account_id: checking_id, to_account_id: savings_id, amount: Money.new!(100, :USD), timestamp: DateTime.utc_now() }) |> Ash.create!() ``` ``` -------------------------------- ### Implement atomic balance adjustment Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Changes.md Atomic operation definition for database-level balance updates. ```elixir {:atomic, %{ balance: expr( if account_id == ^changeset.arguments.from_account_id do ^atomic_ref(:balance) + ^negative_amount_delta else ^atomic_ref(:balance) + ^amount_delta end ) }} ``` -------------------------------- ### Calculation Implementation Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Calculations.md The core expression logic that wraps the balance_as_of_ulid function. ```elixir Ash.Expr.expr( balance_as_of_ulid(ulid: lazy({__MODULE__, :ulid, [context.arguments.timestamp]})) ) ``` -------------------------------- ### Skipping Balance Updates Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Transfer.md Demonstrates how to bypass automatic balance updates during a transfer creation, typically used for data migration or error recovery. ```APIDOC ## Skipping Balance Updates ### Description Allows skipping automatic balance updates when creating a transfer by providing the appropriate context to the changeset. ### Usage ```elixir transfer |> Ash.Changeset.put_context(ash_double_entry: %{skip_balance_updates: true}) |> Ash.create!() ``` ``` -------------------------------- ### Configure Destroy Balances Behavior Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Set whether balance records are automatically destroyed when the transfer is destroyed. ```elixir transfer do destroy_balances? false # Balances auto-deleted when transfer is destroyed end ``` -------------------------------- ### Generate binary upper-bound ULID Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/ULID.md Generates a 16-byte binary ULID with the random portion set to 0xFF for query bounds. ```elixir def bingenerate_last(timestamp \ System.system_time(:millisecond)) :: binary() ``` -------------------------------- ### Configure non-atomic fallback strategy Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Changes.md Defines the fallback strategy sequence when atomic execution is unavailable. ```elixir strategy: [:atomic, :stream, :atomic_batches] ``` -------------------------------- ### Define balance_as_of_ulid calculation Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Account.md Defines the calculation signature for retrieving an account balance as of a specific transfer ULID. ```elixir calculation :balance_as_of_ulid, type: AshMoney.Types.Money, arguments: [ulid: AshDoubleEntry.ULID] ``` -------------------------------- ### Set Pre-check Identities Domain Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Specifies a domain for validating unique identifier constraints before execution. ```elixir account do pre_check_identities_with YourApp.Ledger end ``` -------------------------------- ### read_transfers (read) Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Transfer.md Reads transfers with keyset-based pagination support. ```APIDOC ## read_transfers (read) ### Description Reads transfers with keyset-based pagination support. ### Signature `def read_transfers(resource, options)` ### Returns List of transfers with pagination. ### Example ```elixir YourApp.Ledger.Transfer |> Ash.Query.filter(from_account_id == ^account_id) |> Ash.read!(action: :read_transfers) ``` ``` -------------------------------- ### Default Argument Configuration Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Calculations.md Configuration for the timestamp argument default value. ```elixir Ash.Resource.Builder.build_calculation_argument( :timestamp, :utc_datetime_usec, allow_nil?: false, default: &DateTime.utc_now/0 ) ``` -------------------------------- ### Configure Pre-check Identities Domain Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Specify the Ash domain used for pre-checking identities on the transfer resource. ```elixir transfer do pre_check_identities_with YourApp.Ledger end ``` -------------------------------- ### Handle Transaction Timeouts Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/Preparations.md Pattern for catching and handling lock timeout errors. ```elixir case Ash.create(transfer_changeset) do {:error, error} -> if String.contains?(inspect(error), "timeout") do # Another transaction held the lock too long IO.puts("Transfer timed out; please try again") end {:ok, transfer} -> # Success end ``` -------------------------------- ### Handle Transfer Validation Errors Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/errors.md Demonstrates triggering a transfer validation error when source and destination accounts are identical, and how to handle the resulting changeset error. ```elixir YourApp.Ledger.Transfer |> Ash.Changeset.for_create(:transfer, %{ from_account_id: same_account_id, to_account_id: same_account_id, # ERROR: same as from_account_id amount: Money.new!(100, :USD) }) |> Ash.create!() # Raises: Invalid error: to_account_id "must be different from the from account" ``` ```elixir case Ash.create(transfer_changeset) do {:error, changeset} -> errors = Ash.Changeset.errors(changeset) if Enum.any?(errors, &(elem(&1, 0) == :to_account_id)) do # Handle same-account transfer error IO.puts("Cannot transfer to the same account") end {:ok, transfer} -> # Success end ``` -------------------------------- ### Convert binary to ULID with cast_stored Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/ULID.md Converts a 16-byte binary representation back into a 26-character Crockford Base32 string. ```elixir AshDoubleEntry.ULID.cast_stored(<<0, 161, 163, ...>>, []) # => {:ok, "01ARZ3NDEKTSV4RRFFQ69G5FAV"} ``` -------------------------------- ### Configure Atomic Money Operations Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/configuration.md Determines whether to use atomic database operations or streaming updates for balance adjustments. ```elixir balance do data_layer_can_add_money? true end # Balance adjustments execute atomically: # UPDATE balances SET balance = balance + $1 WHERE ... ``` ```elixir balance do data_layer_can_add_money? false end # Balance adjustments stream: # SELECT balance FROM balances WHERE ... (for each, UPDATE balance = new_value) ``` -------------------------------- ### Encode binary to ULID with encode Source: https://github.com/ash-project/ash_double_entry/blob/main/_autodocs/api-reference/ULID.md Encodes a 128-bit binary value into its corresponding 26-character Crockford Base32 string representation. ```elixir AshDoubleEntry.ULID.encode(<<0, 161, 163, 201, ...>>) # => {:ok, "01ARZ3NDEKTSV4RRFFQ69G5FAV"} ```