DoubleEntryLedger.Account (double_entry_ledger v0.1.0)
View SourceManages financial accounts in the Double Entry Ledger system.
This module defines the Account schema and provides functions for account creation, updates, and balance management following double-entry bookkeeping principles.
Key Concepts
- Normal Balance: Each account has a default balance direction (debit/credit) based on its type. Normal balances are automatically assigned but can be overridden for special cases like contra accounts.
- Balance Types: Accounts track both posted (finalized) and pending balances separately.
- Available Balance: The calculated balance that accounts for both posted transactions and pending holds/authorizations.
Schema Fields
id: UUID primary keyaddress: Human-readable account address in the format "abc1:def2:(:[a-zA-Z_0-9]+){0,}" (required)name: Human-readable account namedescription: Optional text descriptioncurrency: The currency code as an atom (e.g.,:USD,:EUR)type: Account classification (:asset,:liability,:equity,:revenue,:expense)normal_balance: Whether the account normally increases with:debitor:creditentriesavailable: Calculated available balance (posted minus relevant pending)allowed_negative: Whether the account can have a negative available balancecontext: JSON map for additional metadataposted: Embedded Balance struct for settled transactionspending: Embedded Balance struct for pending transactionslock_version: Integer for optimistic concurrency controlinstance_id: Foreign key to the ledger instanceinserted_at/updated_at: Timestamps
Transaction Processing
The module handles various transaction types:
:posted- Direct postings to finalized balance:pending- Holds/authorizations for future settlement:pending_to_posted- Converting pending entries to posted:pending_to_pending- Modifying pending entries:pending_to_archived- Removing pending entries
Relationships
- Belongs to:
instance - Has many:
entries - Has many:
balance_history_entries
Summary
Functions
Creates a changeset for validating and creating a new Account.
Creates a changeset for safely deleting an account.
Updates account balances based on an entry and transaction type.
Creates a changeset for updating an existing Account.
Types
@type t() :: %DoubleEntryLedger.Account{ __meta__: term(), address: String.t() | nil, allowed_negative: boolean(), available: integer(), balance_history_entries: term(), context: map() | nil, currency: DoubleEntryLedger.Utils.Currency.currency_atom() | nil, description: String.t() | nil, entries: term(), id: binary() | nil, inserted_at: DateTime.t() | nil, instance: term(), instance_id: binary() | nil, journal_event_account_links: term(), journal_events: term(), lock_version: integer(), name: String.t() | nil, normal_balance: DoubleEntryLedger.Types.credit_or_debit() | nil, pending: DoubleEntryLedger.Balance.t() | nil, posted: DoubleEntryLedger.Balance.t() | nil, type: DoubleEntryLedger.Types.account_type() | nil, updated_at: DateTime.t() | nil }
Represents a financial account in the double-entry ledger system.
An account is the fundamental unit that holds balances and participates in transactions. Each account has a type, normal balance direction, and tracks both pending and posted amounts.
Fields
id: UUID primary keyname: Account name must unique. Can't be changed after creationaddress: Human-readable account address in the format "abc1:def2:(:[a-zA-Z_0-9]+){0,}" (required)description: Optional text descriptioncurrency: Currency code atom (e.g.,:USD,:EUR). Can't be changed after creationtype: Account classification. Can't be changed after creationnormal_balance: Default balance direction. Can't be changed after creationavailable: Calculated available balanceallowed_negative: Whether negative balances are allowedcontext: Additional metadata as a mapposted: Balance struct for posted transactionspending: Balance struct for pending transactionslock_version: Version for optimistic concurrency controlinstance_id: Reference to ledger instanceinserted_at: Creation timestampupdated_at: Last update timestamp
Functions
@spec account_types() :: [DoubleEntryLedger.Types.account_type()]
@spec address_regex() :: Regex.t()
@spec changeset(t(), map()) :: Ecto.Changeset.t()
Creates a changeset for validating and creating a new Account.
Enforces required fields, validates types, and automatically sets the normal_balance based on the account type if not explicitly provided.
Parameters
account- The account struct to create a changeset forattrs- Map of attributes to apply to the account
Returns
- An Ecto.Changeset with validations applied
Examples
# Create a valid asset account
iex> changeset = Account.changeset(%Account{}, %{
...> address: "cash:main:1",
...> currency: :USD,
...> instance_id: "550e8400-e29b-41d4-a716-446655440000",
...> type: :asset
...> })
iex> changeset.valid?
true
iex> Ecto.Changeset.get_field(changeset, :normal_balance)
:debit
# Invalid without required fields
iex> changeset = Account.changeset(%Account{}, %{})
iex> changeset.valid?
false
iex> MapSet.new(Keyword.keys(changeset.errors))
MapSet.new([:currency, :address, :instance_id, :type])
@spec delete_changeset(t()) :: Ecto.Changeset.t()
Creates a changeset for safely deleting an account.
Validates that there are no associated entries (transactions) before deletion, ensuring accounting integrity is maintained.
Parameters
account- The account to delete
Returns
- An Ecto.Changeset that will fail if the account has entries
Examples
# Attempt to delete an account with no entries
iex> alias DoubleEntryLedger.Stores.AccountStore
iex> alias DoubleEntryLedger.Stores.InstanceStore
iex> {:ok, instance} = InstanceStore.create(%{address: "instance1"})
iex> {:ok, account} = AccountStore.create(instance.address, %{
...> name: "account1", address: "cash:main:1", type: :asset, currency: :EUR}, "unique_id_123")
iex> {:ok, %{id: account_id}} = Repo.delete(Account.delete_changeset(account))
iex> account.id == account_id
true
# Attempt to delete an account with entries
# This is a database constraint error, not a changeset error
iex> alias DoubleEntryLedger.Stores.AccountStore
iex> alias DoubleEntryLedger.Stores.InstanceStore
iex> alias DoubleEntryLedger.Apis.CommandApi
iex> {:ok, instance} = InstanceStore.create(%{address: "instance1"})
iex> {:ok, account1} = AccountStore.create(instance.address, %{
...> name: "account1", address: "cash:main:1", type: :asset, currency: :EUR}, "unique_id_123")
iex> {:ok, account2} = AccountStore.create(instance.address, %{
...> name: "account2", address: "cash:main:2", type: :liability, currency: :EUR}, "unique_id_456")
iex> {:ok, _, _} = CommandApi.process_from_params(%{"instance_address" => instance.address,
...> "source" => "s1", "source_idempk" => "1", "action" => "create_transaction",
...> "payload" => %{"status" => "pending", "entries" => [
...> %{"account_address" => account1.address, "amount" => 100, "currency" => "EUR"},
...> %{"account_address" => account2.address, "amount" => 100, "currency" => "EUR"},
...> ]}})
iex> {:error, changeset} = Account.delete_changeset(account1)
...> |> Repo.delete()
iex> [entries: {"are still associated with this entry", _}] = changeset.errors
@spec update_balances(t(), %{ entry: DoubleEntryLedger.Entry.t() | Ecto.Changeset.t(), trx: DoubleEntryLedger.Types.trx_types() }) :: Ecto.Changeset.t()
Updates account balances based on an entry and transaction type.
This function handles all the complex account balance updates for different transaction scenarios like posting, pending holds, and settlement.
Parameters
account- The account to updateparams- Map containing:entry- Entry struct or changeset with the transaction detailstrx- Transaction type (:posted,:pending,:pending_to_posted, etc.)
Returns
- A changeset with updated balance fields and optimistic lock version increment
Implementation Notes
- Validates that entry and account currencies match
- Handles various transaction types with different balance update logic
- Enforces non-negative balance if
allowed_negativeis false - Uses optimistic locking to prevent concurrent balance modifications
Transaction Types
:posted- Direct posting to finalized balance:pending- Holds/authorizations for future settlement:pending_to_posted- Converting pending entries to posted:pending_to_pending- Modifying existing pending entries:pending_to_archived- Removing pending entries without posting
@spec update_changeset(t(), map()) :: Ecto.Changeset.t()
Creates a changeset for updating an existing Account.
Limited to updating only the description, and context fields,
protecting critical fields like type and currency from modification.
Parameters
account- The account struct to updateattrs- Map of attributes to update
Returns
- An Ecto.Changeset with validations applied
Examples
# Update account description
iex> account = %Account{description: "Old Description", instance_id: "inst-123"}
iex> changeset = Account.update_changeset(account, %{
...> description: "Updated Description"
...> })
iex> changeset.valid?
true
iex> Ecto.Changeset.get_change(changeset, :description)
"Updated Description"