DoubleEntryLedger.Stores.TransactionStore (double_entry_ledger v0.4.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:
{:ok, {transactions, meta}} = DoubleEntryLedger.Stores.TransactionStore.list_for_instance(instance.id)Getting transactions for an account in an instance (each element is a
{Transaction, Account, Entry, BalanceHistoryEntry} tuple):
{:ok, {tuples, meta}} =
DoubleEntryLedger.Stores.TransactionStore.list_for_instance_and_account(instance.id, account.id)
# tuples is e.g. [{%Transaction{}, %Account{}, %Entry{}, %BalanceHistoryEntry{}}, ...]
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 transactions for an instance with cursor pagination.
Lists transactions for an instance address with cursor pagination.
Lists transactions scoped to an instance+account, returning tuples of
{Transaction.t(), Account.t(), Entry.t(), BalanceHistoryEntry.t()} where
the BalanceHistoryEntry is the latest history row for each entry (via a
lateral join).
Address-keyed variant of list_for_instance_and_account/3. Returns the
same {Transaction, Account, Entry, BalanceHistoryEntry} 4-tuples.
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_for_instance(DoubleEntryLedger.Instance.t() | Ecto.UUID.t(), map()) :: {:ok, {[DoubleEntryLedger.Transaction.t()], Flop.Meta.t()}} | {:error, Flop.Meta.t()}
Lists transactions for an instance with cursor pagination.
Accepts an %Instance{} struct or its UUID string.
Parameters
instance_or_id(Instance.t() | Ecto.UUID.t()): Parent instance or its id.flop_params(map, optional): Flop params. Filterable::status.
Returns
{:ok, {[Transaction.t()], Flop.Meta.t()}}on success.{:error, Flop.Meta.t()}on invalid params.
Examples
iex> {:ok, instance} = InstanceStore.create(%{address: "Sample:Instance"})
iex> account_data = %{address: "Cash:Account", type: :asset, currency: :USD}
iex> {:ok, a1} = AccountStore.create(instance.address, account_data, "u1")
iex> {:ok, a2} = AccountStore.create(instance.address, %{account_data | address: "Liab:Account", type: :liability}, "u2")
iex> attrs = %{status: :posted, entries: [
...> %{account_address: a1.address, amount: 100, currency: :USD},
...> %{account_address: a2.address, amount: 100, currency: :USD}]}
iex> {:ok, _} = TransactionStore.create(instance.address, attrs, "idem-1")
iex> {:ok, {[trx], %Flop.Meta{}}} = TransactionStore.list_for_instance(instance)
iex> trx.status
:posted
@spec list_for_instance_address(String.t(), map()) :: {:ok, {[DoubleEntryLedger.Transaction.t()], Flop.Meta.t()}} | {:error, Flop.Meta.t()}
Lists transactions for an instance address with cursor pagination.
Parameters
instance_address(String.t()): Address of the parent instance.flop_params(map, optional): Flop params. Filterable::status.
Returns
{:ok, {[Transaction.t()], Flop.Meta.t()}}on success.{:error, Flop.Meta.t()}on invalid params.
@spec list_for_instance_and_account( DoubleEntryLedger.Instance.t() | Ecto.UUID.t(), DoubleEntryLedger.Account.t() | Ecto.UUID.t(), map() ) :: {:ok, {[ {DoubleEntryLedger.Transaction.t(), DoubleEntryLedger.Account.t(), DoubleEntryLedger.Entry.t(), DoubleEntryLedger.BalanceHistoryEntry.t()} ], Flop.Meta.t()}} | {:error, Flop.Meta.t()}
Lists transactions scoped to an instance+account, returning tuples of
{Transaction.t(), Account.t(), Entry.t(), BalanceHistoryEntry.t()} where
the BalanceHistoryEntry is the latest history row for each entry (via a
lateral join).
Accepts %Instance{} or its UUID string for the first arg and %Account{}
or its UUID string for the second.
Parameters
instance_or_id(Instance.t() | Ecto.UUID.t()): Parent instance.account_or_id(Account.t() | Ecto.UUID.t()): Scoping account.flop_params(map, optional): Flop params. Filterable::status.
Returns
{:ok, {[{Transaction.t(), Account.t(), Entry.t(), BalanceHistoryEntry.t()}], Flop.Meta.t()}}on success.{:error, Flop.Meta.t()}on invalid params.
@spec list_for_instance_and_account_address(String.t(), String.t(), map()) :: {:ok, {[ {DoubleEntryLedger.Transaction.t(), DoubleEntryLedger.Account.t(), DoubleEntryLedger.Entry.t(), DoubleEntryLedger.BalanceHistoryEntry.t()} ], Flop.Meta.t()}} | {:error, Flop.Meta.t()}
Address-keyed variant of list_for_instance_and_account/3. Returns the
same {Transaction, Account, Entry, BalanceHistoryEntry} 4-tuples.
Parameters
instance_address(String.t()): Address of the parent instance.account_address(String.t()): Address of the scoping account.flop_params(map, optional): Flop params. Filterable::status.
Returns
{:ok, {[{Transaction.t(), Account.t(), Entry.t(), BalanceHistoryEntry.t()}], Flop.Meta.t()}}on success.{:error, Flop.Meta.t()}on invalid params.
@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"