DoubleEntryLedger.Stores.TransactionStore (double_entry_ledger v0.1.0)
View SourceProvides functions for managing transactions in the double-entry ledger system.
Key Functionality
- Complex Queries: Find transactions by instance ID and account relationships
- Multi Integration: Build operations that integrate with Ecto.Multi for atomic operations
- Optimistic Concurrency: Handle Ecto.StaleEntryError with appropriate error handling
- Status Transitions: Manage transaction state transitions with validation
Usage Examples
Retrieving a transaction by ID:
transaction = DoubleEntryLedger.Stores.TransactionStore.get_by_id(transaction_id)Getting transactions for an instance:
transactions = DoubleEntryLedger.Stores.TransactionStore.list_all_for_instance(instance.id)Getting transactions for an account in an instance:
transactions = DoubleEntryLedger.Stores.TransactionStore.list_all_for_instance_and_account(instance.id, account.id)
Summary
Functions
Creates a new transaction with the given attributes. If the creation fails, the command is saved to the command queue and retried later.
Retrieves a transaction by its ID.
Lists all transactions for a given instance address. The output is paginated.
It's like list_all_for_instance_id_and_account_id/4 but takes instance and account addresses instead of IDs.
Lists all transactions for a given instance. The output is paginated.
Lists all transactions for a given instance and account. This function joins the transactions with their associated entries, accounts, and the latest balance history entry for each entry. The output is paginated.
Updates a transaction with the given attributes. If the update fails, the command is saved to the command queue and retried later.
Types
@type create_map() :: %{ status: DoubleEntryLedger.Transaction.state(), entries: [entry_map()] }
@type entry_map() :: %{ account_address: String.t(), amount: integer(), currency: DoubleEntryLedger.Utils.Currency.currency_atom() }
@type update_map() :: %{ status: DoubleEntryLedger.Transaction.state(), entries: [entry_map()] | nil }
Functions
@spec create(String.t(), create_map(), String.t(), on_error: DoubleEntryLedger.Apis.CommandApi.on_error(), source: String.t() ) :: {:ok, DoubleEntryLedger.Transaction.t()} | {:error, Ecto.Changeset.t(DoubleEntryLedger.Command.TransactionCommandMap.t()) | String.t()}
Creates a new transaction with the given attributes. If the creation fails, the command is saved to the command queue and retried later.
Parameters
attrs(map): A map containing the transaction attributes.:instance_address(String.t()): The address of the instance.:status(Transaction.state()): The initial status of the transaction.:entries(list(entry_map())): A list of entry maps, each containing::account_address(String.t()): The address of the account.:amount(integer()): The amount for the entry.:currency(Currency.currency_atom()): The currency for the entry.
idempotent_id(String.t()): A unique identifier to ensure idempotency of the creation request.opts(Keyword.t(), optional): A string indicating the source of the creation request.:sourceDefaults to "transaction_store-create".:on_error- :retry (default) The command will be saved in the CommandQueue for retry after a processing error.
- :fail if you want to handle errors manually without saving the command to the CommandQueue.
Returns
{:ok, transaction}: On successful creation, returns the created transaction.{:error, reason}: On failure, returns an error tuple with the reason.
Examples
iex> {:ok, instance} = InstanceStore.create(%{address: "Sample:Instance"})
iex> account_data = %{address: "Cash:Account", type: :asset, currency: :USD}
iex> {:ok, asset_account} = AccountStore.create(instance.address, account_data, "unique_id_123")
iex> {:ok, liability_account} = AccountStore.create(instance.address, %{account_data | address: "Liability:Account", type: :liability}, "unique_id_456")
iex> create_attrs = %{
...> status: :posted,
...> entries: [
...> %{account_address: asset_account.address, amount: 100, currency: :USD},
...> %{account_address: liability_account.address, amount: 100, currency: :USD}
...> ]}
iex> {:ok, transaction} = TransactionStore.create(instance.address, create_attrs, "unique_id_123")
iex> transaction.status
:posted
iex> {:error, %Ecto.Changeset{data: %DoubleEntryLedger.Command.TransactionCommandMap{}} = changeset} = TransactionStore.create(instance.address, create_attrs , "unique_id_123")
iex> {idempotent_error, _} = Keyword.get(changeset.errors, :key_hash)
iex> idempotent_error
"idempotency violation"
@spec get_by_id(Ecto.UUID.t(), list()) :: DoubleEntryLedger.Transaction.t() | nil
Retrieves a transaction by its ID.
Parameters
id(Ecto.UUID.t()): The ID of the transaction.
Returns
transaction: The transaction struct, ornilif not found.
@spec list_all_for_instance_address(String.t(), non_neg_integer(), non_neg_integer()) :: [ DoubleEntryLedger.Transaction.t() ]
Lists all transactions for a given instance address. The output is paginated.
Parameters
instance_address- The address of the instance.page- The page number (defaults to 1).per_page- The number of transactions per page (defaults to 40).
Returns
- A list of transactions.
Examples
iex> {:ok, instance} = InstanceStore.create(%{address: "Sample:Instance"})
iex> account_data = %{address: "Cash:Account", type: :asset, currency: :USD}
iex> {:ok, asset_account} = AccountStore.create(instance.address, account_data, "unique_id_123")
iex> {:ok, liability_account} = AccountStore.create(instance.address, %{account_data | address: "Liability:Account", type: :liability}, "unique_id_456")
iex> create_attrs = %{
...> status: :posted,
...> entries: [
...> %{account_address: asset_account.address, amount: 100, currency: :USD},
...> %{account_address: liability_account.address, amount: 100, currency: :USD}
...> ]}
iex> {:ok, transaction} = TransactionStore.create(instance.address, create_attrs, "unique_id_123")
iex> [trx|_] = TransactionStore.list_all_for_instance_address(instance.address)
iex> trx.id == transaction.id && trx.status == :posted
true
iex> TransactionStore.list_all_for_instance_address("NonExistentInstance", 2, 10)
[]
It's like list_all_for_instance_id_and_account_id/4 but takes instance and account addresses instead of IDs.
Parameters
instance_address- Address of the instance.account_address- Address of the accountpage- The page number (defaults to 1).per_page- The number of transactions per page (defaults to 40).
Returns
- A list of tuples containing the transaction, account, entry, and the latest balance history entry.
Examples
iex> {:ok, instance} = InstanceStore.create(%{address: "Sample:Instance"})
iex> account_data = %{address: "Cash:Account", type: :asset, currency: :USD}
iex> {:ok, asset_account} = AccountStore.create(instance.address, account_data, "unique_id_123")
iex> {:ok, liability_account} = AccountStore.create(instance.address, %{account_data | address: "Liability:Account", type: :liability}, "unique_id_456")
iex> create_attrs = %{
...> status: :posted,
...> entries: [
...> %{account_address: asset_account.address, amount: 100, currency: :USD},
...> %{account_address: liability_account.address, amount: 100, currency: :USD}
...> ]}
iex> {:ok, transaction1} = TransactionStore.create(instance.address, create_attrs, "unique_id_123")
iex> {:ok, transaction2} = TransactionStore.create(instance.address, create_attrs, "unique_id_456")
iex> [{trx1, acc1, _ , _}, {trx2, acc2, _ , _}| _] = TransactionStore.list_all_for_instance_address_and_account_address(instance.address, asset_account.address)
iex> trx1.id == transaction2.id && trx1.status == :posted && acc1.id == asset_account.id
true
iex> trx2.id == transaction1.id && trx1.status == :posted && acc2.id == asset_account.id
true
iex> TransactionStore.list_all_for_instance_address_and_account_address("NonExistentInstance", "NonExistentAccount", 2, 1)
[]
@spec list_all_for_instance_id(Ecto.UUID.t(), non_neg_integer(), non_neg_integer()) :: [ DoubleEntryLedger.Transaction.t() ]
Lists all transactions for a given instance. The output is paginated.
Parameters
instance_id- The UUID of the instance.page- The page number (defaults to 1).per_page- The number of transactions per page (defaults to 40).
Returns
- A list of transactions.
Examples
iex> {:ok, instance} = InstanceStore.create(%{address: "Sample:Instance"})
iex> account_data = %{address: "Cash:Account", type: :asset, currency: :USD}
iex> {:ok, asset_account} = AccountStore.create(instance.address, account_data, "unique_id_123")
iex> {:ok, liability_account} = AccountStore.create(instance.address, %{account_data | address: "Liability:Account", type: :liability}, "unique_id_456")
iex> create_attrs = %{
...> status: :posted,
...> entries: [
...> %{account_address: asset_account.address, amount: 100, currency: :USD},
...> %{account_address: liability_account.address, amount: 100, currency: :USD}
...> ]}
iex> {:ok, transaction} = TransactionStore.create(instance.address, create_attrs, "unique_id_123")
iex> [trx|_] = TransactionStore.list_all_for_instance_id(instance.id)
iex> trx.id == transaction.id && trx.status == :posted
true
iex> TransactionStore.list_all_for_instance_id(Ecto.UUID.generate(), 2, 10)
[]
iex> TransactionStore.list_all_for_instance_id(Ecto.UUID.generate(), 0, 1)
[]
iex> TransactionStore.list_all_for_instance_id(Ecto.UUID.generate(), 1, 0)
[]
@spec list_all_for_instance_id_and_account_id( Ecto.UUID.t(), Ecto.UUID.t(), non_neg_integer(), non_neg_integer() ) :: [ {DoubleEntryLedger.Transaction.t(), DoubleEntryLedger.Account.t(), DoubleEntryLedger.Entry.t(), DoubleEntryLedger.BalanceHistoryEntry.t()} ]
Lists all transactions for a given instance and account. This function joins the transactions with their associated entries, accounts, and the latest balance history entry for each entry. The output is paginated.
Parameters
instance_id- The UUID of the instance.account_id- The UUID of the accountpage- The page number (defaults to 1).per_page- The number of transactions per page (defaults to 40).
Returns
- A list of tuples containing the transaction, account, entry, and the latest balance history entry.
Examples
iex> {:ok, instance} = InstanceStore.create(%{address: "Sample:Instance"})
iex> account_data = %{address: "Cash:Account", type: :asset, currency: :USD}
iex> {:ok, asset_account} = AccountStore.create(instance.address, account_data, "unique_id_123")
iex> {:ok, liability_account} = AccountStore.create(instance.address, %{account_data | address: "Liability:Account", type: :liability}, "unique_id_456")
iex> create_attrs = %{
...> status: :posted,
...> entries: [
...> %{account_address: asset_account.address, amount: 100, currency: :USD},
...> %{account_address: liability_account.address, amount: 100, currency: :USD}
...> ]}
iex> {:ok, transaction1} = TransactionStore.create(instance.address, create_attrs, "unique_id_123")
iex> {:ok, transaction2} = TransactionStore.create(instance.address, create_attrs, "unique_id_456")
iex> [{trx1, acc1, _ , bh1}, {trx2, acc2, _ , _}| _] = TransactionStore.list_all_for_instance_id_and_account_id(instance.id, asset_account.id)
iex> trx1.id == transaction2.id && trx1.status == :posted && acc1.id == asset_account.id
true
iex> trx2.id == transaction1.id && trx1.status == :posted && acc2.id == asset_account.id
true
iex> bh1.available == 200
true
iex> # Test pagination
iex> [{trx3, acc3, _ , _}| _] = tuple_list = TransactionStore.list_all_for_instance_id_and_account_id(instance.id, asset_account.id, 2, 1)
iex> trx3.id == transaction1.id && trx1.status == :posted && acc3.id == asset_account.id
true
iex> length(tuple_list)
1
iex> TransactionStore.list_all_for_instance_id_and_account_id(Ecto.UUID.generate(), Ecto.UUID.generate(), 2, 1)
[]
@spec update(String.t(), Ecto.UUID.t(), update_map(), String.t(), on_error: DoubleEntryLedger.Apis.CommandApi.on_error(), update_source: String.t() ) :: {:ok, DoubleEntryLedger.Transaction.t()} | {:error, Ecto.Changeset.t(DoubleEntryLedger.Command.TransactionCommandMap.t()) | String.t()}
Updates a transaction with the given attributes. If the update fails, the command is saved to the command queue and retried later.
Parameters
id(Ecto.UUID.t()): The ID of the transaction to update.attrs(map): A map containing the transaction attributes.:instance_address(String.t()): The address of the instance.:status(Transaction.state()): The new status of the transaction.:entries(list(entry_map())): A list of entry maps, each containing::account_address(String.t()): The address of the account.:amount(integer()): The amount for the entry.:currency(Currency.currency_atom()): The currency for the entry.
update_idempk(String.t()): A unique identifier to ensure idempotency of the update request.opts(Keyword.t(), optional): A string indicating the source of the creation request.:update_sourceDefaults to "transaction_store-update". Use if the source of the change is different from the initial source when creating the command.:on_error- :retry (default) The command will be saved in the CommandQueue for retry after a processing error.
- :fail if you want to handle errors manually without saving the command to the CommandQueue.
Returns
{:ok, transaction}: On successful creation, returns the created transaction.{:error, reason}: On failure, returns an error tuple with the reason.
Examples
iex> {:ok, instance} = InstanceStore.create(%{address: "Sample:Instance"})
iex> account_data = %{address: "Cash:Account", type: :asset, currency: :USD}
iex> {:ok, asset_account} = AccountStore.create(instance.address, account_data, "unique_id_123")
iex> {:ok, liability_account} = AccountStore.create(instance.address, %{account_data | address: "Liability:Account", type: :liability}, "unique_id_456")
iex> create_attrs = %{
...> status: :pending,
...> entries: [
...> %{account_address: asset_account.address, amount: 100, currency: :USD},
...> %{account_address: liability_account.address, amount: 100, currency: :USD}
...> ]}
iex> {:ok, pending} = TransactionStore.create(instance.address, create_attrs, "unique_id_123")
iex> pending.status
:pending
iex> update_attrs = %{status: :posted}
iex> {:ok, posted} = TransactionStore.update(instance.address, pending.id, update_attrs, "unique_id_456")
iex> posted.status == :posted && posted.id == pending.id
iex> {:error, %Ecto.Changeset{data: %DoubleEntryLedger.Command.TransactionCommandMap{}} = changeset} = TransactionStore.update(instance.address, pending.id, update_attrs , "unique_id_456")
iex> {idempotent_error, _} = Keyword.get(changeset.errors, :key_hash)
iex> idempotent_error
"idempotency violation"