DoubleEntryLedger.Stores.AccountStore (double_entry_ledger v0.1.0)
View SourceProvides functions for managing and querying accounts in the double-entry ledger system.
This module serves as the primary interface for all account-related operations, including creating, retrieving, updating, and deleting accounts. It also provides specialized query functions to retrieve accounts by various criteria and access account balance history.
Key Functionality
- Account Management: Create, retrieve, update, and delete accounts with full validation
- Account Queries: Find accounts by instance, type, address, and ID combinations
- Balance History: Access the historical record of account balance changes with pagination
- Command Sourcing: Create and update account operations are tracked through the command pipeline
Data Integrity
All account operations maintain strict data integrity through:
- Command sourcing for complete audit trails
- Validation of account types and currencies
- Unique address constraints within instances
- Referential integrity with instances and transactions
Usage Examples
Creating a new account:
{:ok, instance} = DoubleEntryLedger.Stores.InstanceStore.create(%{address: "Business:Ledger"})
{:ok, account} = DoubleEntryLedger.Stores.AccountStore.create(%{
name: "Cash Account",
address: "cash:main",
instance_address: instance.address,
currency: :USD,
type: :asset
})Retrieving accounts for an instance:
{:ok, accounts} = DoubleEntryLedger.Stores.AccountStore.get_all_accounts_by_instance_address(instance.address)Accessing an account's balance history:
{:ok, history} = DoubleEntryLedger.Stores.AccountStore.get_balance_history(account.id)Implementation Notes
All functions perform appropriate validation and return standardized results:
- Success:
{:ok, result} - Error:
{:error, reason}where reason can be an atom, string, or Ecto.Changeset
The module integrates with the ledger's command pipeline to ensure account integrity and enforce business rules for the double-entry accounting system. All create and update operations generate corresponding commands for complete auditability.
Error Handling
Common error conditions include:
:no_accounts_found- When querying returns no results:some_accounts_not_found- When some requested accounts don't existEcto.Changeset.t()- For validation errors during create/update operations- String messages - For specific error conditions like "Account not found"
Summary
Functions
Creates a new account with the given attributes.
Deletes an account by its ID. This only works if the account has no associated transactions which means it can be safely removed from the ledger. Should only be used if the account was created in error and has no transactions. Deletion will currently not show up in the event log.
Get a list of accounts by instance address and a list of account addresses.
Retrieves accounts by instance ID and a list of account addresses.
Retrieves accounts by instance ID and account type.
Retrieves all accounts by instance ID.
Retrieves all accounts by instance ID.
Retrieves an account's balance history by its address within a specific instance, with pagination support.
Retrieves an account's balance history by its ID with pagination support.
Retrieves an account by its address within a specific instance.
Retrieves an account by its ID.
Updates an account with the given attributes.
Types
@type create_map() :: %{ address: String.t(), currency: DoubleEntryLedger.Utils.Currency.currency_atom(), type: DoubleEntryLedger.Types.account_type(), name: String.t() | nil, description: String.t() | nil, context: map() | nil, normal_balance: DoubleEntryLedger.Types.credit_or_debit() | nil, allow_negative: boolean() | nil }
Functions
@spec create(String.t(), create_map(), String.t()) :: {:ok, DoubleEntryLedger.Account.t()} | {:error, Ecto.Changeset.t(DoubleEntryLedger.Command.AccountCommandMap.t()) | String.t()}
Creates a new account with the given attributes.
Creates an account through the command pipeline, ensuring proper audit trail and validation. The account is associated with the specified instance and must have a unique address within that instance.
Parameters
instance_address(String.t()): Address of the instanceattrs(map): A map of attributes for the account containing::name(String.t(), optional) - Human-readable account name:address(String.t(), required) - Unique address within the instance:instance_address(String.t(), required) - Address of the owning instance:currency(atom, required) - Currency code (e.g., :USD, :EUR):type(atom, required) - Account type (:asset, :liability, :equity, :income, :expense):description(String.t(), optional) - Account description:context(map, optional) - Additional context information:normal_balance(atom, optional) - Normal balance (:debit or :credit) if different from type default:allow_negative(boolean, optional) - Whether negative balances are allowed (default: false)
source(String.t(), optional): Source identifier for the operation (defaults to "AccountStore.create/1")
Returns
{:ok, Account.t()}: On successful creation with the created account.{:error, Ecto.Changeset.t() | String.t()}: If validation fails or other errors occur.
Validation Rules
- Account address must be unique within the instance
- Account type must be one of the valid types
- Currency must be a valid currency code
- Instance must exist
Examples
iex> {:ok, %{address: address}} = InstanceStore.create(%{address: "Sample:Instance"})
iex> attrs = %{address: "account:main1", currency: :EUR, type: :asset}
iex> {:ok, account} = AccountStore.create(address, attrs)
iex> account.address
"account:main1"
@spec delete(Ecto.UUID.t()) :: {:ok, DoubleEntryLedger.Account.t()} | {:error, Ecto.Changeset.t()}
Deletes an account by its ID. This only works if the account has no associated transactions which means it can be safely removed from the ledger. Should only be used if the account was created in error and has no transactions. Deletion will currently not show up in the event log.
Parameters
id(Ecto.UUID.t()): The unique ID of the account to delete.
Returns
{:ok, Account.t()}: On successful deletion with the deleted account struct.{:error, Ecto.Changeset.t()}: If the account cannot be deleted (e.g., has active transactions).
Constraints
- Accounts with existing transactions cannot be deleted
Examples
iex> {:ok, %{address: instance_address}} = InstanceStore.create(%{address: "Sample:Instance"})
iex> attrs = %{name: "Test Account", address: "account:main1", currency: :EUR, type: :asset}
iex> {:ok, account} = AccountStore.create(instance_address, attrs, "unique_id_123")
iex> {:ok, _} = AccountStore.delete(account.id)
iex> AccountStore.get_by_id(account.id) == nil
true
@spec get_accounts_by_instance_address(String.t(), [String.t()]) :: {:ok, [DoubleEntryLedger.Account.t()]} | {:error, :no_accounts_found | :some_accounts_not_found}
Get a list of accounts by instance address and a list of account addresses.
Parameters
instance_address(String.t()): The address of the instance.account_addresses(list(String.t())): The list of account addresses.
Returns
{:ok, accounts}: On success.{:error, message}: If some accounts were not found.
Examples
iex> {:ok, %{address: instance_address}} = InstanceStore.create(%{address: "Sample:Instance"})
iex> attrs = %{address: "account:main1", currency: :EUR, type: :asset}
iex> {:ok, account1} = AccountStore.create(instance_address, attrs, "unique_id_123")
iex> {:ok, account2} = AccountStore.create(instance_address, %{attrs | address: "account:main2"}, "unique_id_456")
iex> {:ok, _} = AccountStore.create(instance_address, %{attrs | address: "account:main3"}, "unique_id_789")
iex> {:ok, accounts} = AccountStore.get_accounts_by_instance_address(instance_address, [account1.address, account2.address])
iex> length(accounts)
2
@spec get_accounts_by_instance_id(Ecto.UUID.t(), [String.t()]) :: {:ok, [DoubleEntryLedger.Account.t()]} | {:error, :no_accounts_found | :some_accounts_not_found | :no_accounts_provided}
Retrieves accounts by instance ID and a list of account addresses.
Parameters
instance_id(Ecto.UUID.t()): The ID of the instance.account_addresses(list(String.t())): The list of account addresses.
Returns
{:ok, accounts}: On success.{:error, message}: If some accounts were not found.
Examples
iex> {:ok, %{address: instance_address, id: instance_id}} = InstanceStore.create(%{address: "Sample:Instance"})
iex> attrs = %{address: "account:main1", currency: :EUR, type: :asset}
iex> {:ok, account1} = AccountStore.create(instance_address, attrs, "unique_id_123")
iex> {:ok, account2} = AccountStore.create(instance_address, %{attrs | address: "account:main2"}, "unique_id_456")
iex> {:ok, _} = AccountStore.create(instance_address, %{attrs | address: "account:main3"}, "unique_id_789")
iex> {:ok, accounts} = AccountStore.get_accounts_by_instance_id(instance_id, [account1.address, account2.address])
iex> length(accounts)
2
@spec get_accounts_by_instance_id_and_type( Ecto.UUID.t(), DoubleEntryLedger.Types.account_type() ) :: {:ok, [DoubleEntryLedger.Account.t()]} | {:error, :no_accounts_found_for_provided_type}
Retrieves accounts by instance ID and account type.
Parameters
instance_id(Ecto.UUID.t()): The ID of the instance.type(Types.account_type()): The type of the accounts.
Returns
{:ok, accounts}: On success.{:error, message}: If no accounts of the specified type were found.
Examples
iex> {:ok, %{address: instance_address, id: instance_id}} = InstanceStore.create(%{address: "Sample:Instance"})
iex> attrs = %{name: "Test Account", address: "account:main1", currency: :EUR, type: :asset}
iex> {:ok, _} = AccountStore.create(instance_address, attrs, "unique_id_123")
iex> {:ok, _} = AccountStore.create(instance_address, %{attrs | address: "account:main2", name: "Account 2"}, "unique_id_456")
iex> {:ok, _} = AccountStore.create(instance_address, %{attrs | address: "account:main3", name: "Account 3", type: :liability}, "unique_id_789")
iex> {:ok, accounts} = AccountStore.get_accounts_by_instance_id_and_type(instance_id, :asset)
iex> length(accounts)
2
@spec get_all_accounts_by_instance_address(String.t()) :: {:ok, [DoubleEntryLedger.Account.t()]} | {:error, :no_accounts_found}
Retrieves all accounts by instance ID.
Parameters
instance_address(String.t()): The address of the instance.
Returns
{:ok, accounts}: On success.{:error, message}: If no accounts were found.
Examples
iex> {:ok, %{address: instance_address}} = InstanceStore.create(%{address: "Sample:Instance"})
iex> {:ok, %{address: instance_address2}} = InstanceStore.create(%{address: "Sample:Instance2"})
iex> attrs = %{name: "Test Account", address: "account:main1", currency: :EUR, type: :asset}
iex> {:ok, _} = AccountStore.create(instance_address, attrs, "unique_id_123")
iex> {:ok, _} = AccountStore.create(instance_address, %{attrs | address: "account:main2", name: "Account 2"}, "unique_id_456")
iex> {:ok, _} = AccountStore.create(instance_address, %{attrs | address: "account:main3", name: "Account 3"}, "unique_id_789")
iex> {:ok, _} = AccountStore.create(instance_address2, %{attrs | address: "account:main3", name: "Account 3"}, "unique_id_101")
iex> {:ok, accounts} = AccountStore.get_all_accounts_by_instance_address(instance_address)
iex> length(accounts)
3
@spec get_all_accounts_by_instance_id(Ecto.UUID.t()) :: {:ok, [DoubleEntryLedger.Account.t()]} | {:error, :no_accounts_found}
Retrieves all accounts by instance ID.
Parameters
instance_id(Ecto.UUID.t()): The ID of the instance.
Returns
{:ok, accounts}: On success.{:error, message}: If no accounts were found.
Examples
iex> {:ok, %{id: instance_id, address: instance_address}} = InstanceStore.create(%{address: "Sample:Instance"})
iex> {:ok, %{address: instance_address2}} = InstanceStore.create(%{address: "Sample:Instance2"})
iex> attrs = %{name: "Test Account", address: "account:main1", currency: :EUR, type: :asset}
iex> {:ok, _} = AccountStore.create(instance_address, attrs, "unique_id_123")
iex> {:ok, _} = AccountStore.create(instance_address, %{attrs | address: "account:main2", name: "Account 2"}, "unique_id_456")
iex> {:ok, _} = AccountStore.create(instance_address, %{attrs | address: "account:main3", name: "Account 3"}, "unique_id_789")
iex> {:ok, _} = AccountStore.create(instance_address2, %{attrs | address: "account:main3", name: "Account 3"}, "unique_id_101")
iex> {:ok, accounts} = AccountStore.get_all_accounts_by_instance_id(instance_id)
iex> length(accounts)
3
@spec get_balance_history_by_account( DoubleEntryLedger.Account.t(), non_neg_integer(), non_neg_integer() ) :: {:ok, [DoubleEntryLedger.BalanceHistoryEntry.t()]}
@spec get_balance_history_by_address( String.t(), String.t(), non_neg_integer(), non_neg_integer() ) :: {:ok, [DoubleEntryLedger.BalanceHistoryEntry.t()]} | {:error, :account_not_found}
Retrieves an account's balance history by its address within a specific instance, with pagination support.
Returns a paginated list of balance history entries showing how the account's balance has changed over time. Each entry includes the associated transaction ID for complete traceability.
Parameters
instance_address(String.t()): The address of the instance.account_address(String.t()): The address of the account within the instance.page(non_neg_integer(), optional): The page number for pagination (default: 1).per_page(non_neg_integer(), optional): The number of entries per page (default: 40).
Returns
{:ok, list(BalanceHistoryEntry)}: A list of balance history entries on success.{:error, message}: If the account is not found.
Examples
iex> {:ok, %{address: instance_address}} = InstanceStore.create(%{address: "Sample:Instance"})
iex> attrs = %{name: "Test Account", address: "account:main1", currency: :EUR, type: :asset}
iex> {:ok, account} = AccountStore.create(instance_address, attrs, "unique_id_123")
iex> {:ok, balance_history} = AccountStore.get_balance_history_by_address(instance_address, account.address)
iex> is_list(balance_history)
true
iex> {:ok, %{address: instance_address}} = InstanceStore.create(%{address: "Sample:Instance"})
iex> {:error, :account_not_found} = AccountStore.get_balance_history_by_address(instance_address, "nonexistent_account")
@spec get_balance_history_by_id(Ecto.UUID.t(), non_neg_integer(), non_neg_integer()) :: {:ok, [DoubleEntryLedger.BalanceHistoryEntry.t()]} | {:error, :account_not_found}
Retrieves an account's balance history by its ID with pagination support.
Returns a paginated list of balance history entries showing how the account's balance has changed over time. Each entry includes the associated transaction ID for complete traceability.
Parameters
id(Ecto.UUID.t()): The unique ID of the account.page(non_neg_integer(), optional): The page number for pagination (default: 1).per_page(non_neg_integer(), optional): The number of entries per page (default: 40).
Returns
{:ok, list(BalanceHistoryEntry)}: A list of balance history entries on success.{:error, message}: If the account is not found.
Examples
iex> {:ok, %{address: instance_address}} = InstanceStore.create(%{address: "Sample:Instance"})
iex> attrs = %{name: "Test Account", address: "account:main1", currency: :EUR, type: :asset}
iex> {:ok, account} = AccountStore.create(instance_address, attrs, "unique_id_123")
iex> {:ok, balance_history} = AccountStore.get_balance_history_by_id(account.id)
iex> is_list(balance_history)
true
iex> {:ok, %{id: instance_id}} = InstanceStore.create(%{address: "Sample:Instance"})
iex> {:error, :account_not_found} = AccountStore.get_balance_history_by_id(instance_id)
@spec get_by_address(String.t(), String.t()) :: DoubleEntryLedger.Account.t() | nil
Retrieves an account by its address within a specific instance.
Instance address is required to ensure uniqueness of account addresses across different instances in a multi-tenant system. Returns the account with preloaded journal events for complete context.
Parameters
instance_address(String.t()): The unique address of the instance.account_address(String.t()): The unique address of the account within the instance.
Returns
Account.t() | nil: The account struct with preloaded journal events, ornilif not found.
Preloaded Associations
:journal_events- All journal events associated with this account
Examples
iex> {:ok, %{address: instance_address}} = InstanceStore.create(%{address: "Sample:Instance"})
iex> attrs = %{address: "account:main1", currency: :EUR, type: :asset}
iex> {:ok, account} = AccountStore.create(instance_address, attrs, "unique_id_123")
iex> retrieved = AccountStore.get_by_address(instance_address, account.address)
iex> retrieved.id
account.id
@spec get_by_id(Ecto.UUID.t()) :: DoubleEntryLedger.Account.t() | nil
Retrieves an account by its ID.
Loads the account with its associated journal events for complete context. Returns nil if the account doesn't exist.
Parameters
id(Ecto.UUID.t()): The unique ID of the account to retrieve.
Returns
Account.t() | nil: The account struct with preloaded journal events, ornilif not found.
Preloaded Associations
:journal_events- All journal events associated with this account for audit trail access
Examples
iex> {:ok, %{address: instance_address}} = InstanceStore.create(%{address: "Sample:Instance"})
iex> attrs = %{name: "Test Account", address: "account:main1", currency: :EUR, type: :asset}
iex> {:ok, account} = AccountStore.create(instance_address, attrs, "unique_id_123")
iex> retrieved = AccountStore.get_by_id(account.id)
iex> retrieved.id == account.id
true
@spec update(String.t(), String.t(), update_map(), String.t()) :: {:ok, DoubleEntryLedger.Account.t()} | {:error, Ecto.Changeset.t(DoubleEntryLedger.Command.AccountCommandMap.t()) | String.t()}
Updates an account with the given attributes.
Updates an existing account through the event sourcing system. Only allows changes to specific fields (description and context) to maintain data integrity. The update creates a new event linking to the original creation event.
Parameters
instance_address(String.t()): Address of the instanceaddress(String.t()): The address of the account to update within the instance.attrs(map): The attributes to update containing::instance_address(String.t., required) - Address of the owning instance:name(String.t., optional) - Updated account name:description(String.t., optional) - Updated description:context(map, optional) - Updated context information
source(String.t., optional): Update source identifier for the operation (defaults to "AccountStore.update/2")
Returns
{:ok, Account.t()}: On successful update with the updated account.{:error, Ecto.Changeset.t()}: If validation fails or the account doesn't exist.
Updateable Fields
name- Account display namedescription- Account description textcontext- Additional contextual information
Immutable Fields
The following fields cannot be changed after creation:
name,address,type,currency,instance_id
Examples
iex> {:ok, %{address: address}} = InstanceStore.create(%{address: "Sample:Instance"})
iex> attrs = %{name: "Test Account", address: "account:main1", description: "Test Description", currency: :EUR, type: :asset}
iex> {:ok, account} = AccountStore.create(address, attrs)
iex> {:ok, updated_account} = AccountStore.update(address, account.address, %{instance_address: address, description: "Updated Description"})
iex> updated_account.description
"Updated Description"