DoubleEntryLedger.Entry (double_entry_ledger v0.1.0)
View SourceDefines and manages individual financial entries in the Double Entry Ledger system. Entries should always be created or updated through an Command to ensure proper handling of balance updates and history creation.
This module represents the fundamental building blocks of transactions in double-entry accounting, where each entry affects exactly one account and belongs to exactly one transaction. Entries come in two types - debits and credits - and must balance across a transaction.
Key Concepts
- Entry Types: Each entry must be either a
:debitor:credittype - Transaction Relationship: Entries are always linked to a transaction
- Account Relationship: Each entry affects exactly one account
- Balance History: Creating or updating entries automatically generates balance history records
- Currency Matching: An entry's currency must match its account's currency
Lifecycle
Entries go through several possible state transitions:
:posted- Direct posting to an account's finalized balance:pending- Creating a hold or authorization:pending_to_posted- Converting a pending entry to a posted entry:pending_to_pending- Modifying an existing pending entry:pending_to_archived- Canceling a pending entry The state transition determines how the entry affects the account's balance and what validations are applied. The state itself is stored with the transaction, not the entry.
Balance Updates
When entries are created or modified, the module automatically:
- Updates the associated account's balance
- Creates a balance history entry to track the change
- Validates that the currency matches the account
Summary
Functions
Creates a basic changeset for validating an entry without transition management.
Creates a changeset for validating and inserting an entry with transition state.
Creates a changeset for updating an existing entry with transition handling.
Types
@type t() :: %DoubleEntryLedger.Entry{ __meta__: term(), account: DoubleEntryLedger.Account.t() | Ecto.Association.NotLoaded.t(), account_id: Ecto.UUID.t() | nil, balance_history_entries: term(), id: Ecto.UUID.t() | nil, inserted_at: DateTime.t() | nil, transaction: DoubleEntryLedger.Transaction.t() | Ecto.Association.NotLoaded.t(), transaction_id: Ecto.UUID.t() | nil, type: DoubleEntryLedger.Types.credit_or_debit() | nil, updated_at: DateTime.t() | nil, value: Money.t() | nil }
Represents a single financial entry in a transaction.
An entry records a single financial event affecting one account in the ledger. It contains the monetary amount, entry type (debit or credit), and relationships to both the account it affects and the transaction it belongs to.
Fields
id: UUID primary keyvalue: Money struct containing amount and currencytype: Either:debitor:credittransaction: Association to the parent transactiontransaction_id: Foreign key to the transactionaccount: Association to the affected accountaccount_id: Foreign key to the accountbalance_history_entries: List of related balance history recordsinserted_at: Creation timestampupdated_at: Last update timestamp
Functions
@spec changeset(t(), map()) :: Ecto.Changeset.t()
Creates a basic changeset for validating an entry without transition management.
This simplified version validates the entry data but does not handle account balance updates or balance history creation. It's typically used for initial validation or when working with entries that don't immediately affect accounts.
Parameters
entry- The Entry struct to create a changeset forattrs- Map of attributes to apply to the entry
Returns
- An Ecto.Changeset with basic validations applied
Validations
- Required fields:
:type,:value,:account_id - Entry type must be
:debitor:credit
Examples
# Create a simple entry changeset
iex> attrs = %{
...> type: :credit,
...> value: %{amount: 5000, currency: :EUR},
...> account_id: "550e8400-e29b-41d4-a716-446655440000"
...> }
iex> changeset = Entry.changeset(%Entry{}, attrs)
iex> changeset.valid?
true
@spec changeset(t(), map(), DoubleEntryLedger.Transaction.state()) :: Ecto.Changeset.t()
Creates a changeset for validating and inserting an entry with transition state.
This function builds an Ecto changeset for an entry that manages a specific transaction state transition, handling account balance updates and history creation.
Parameters
entry- The Entry struct to create a changeset forattrs- Map of attributes to apply to the entrytransition- The transition state (e.g.,:posted,:pending,:pending_to_posted)
Returns
- An Ecto.Changeset with validations and associations
Account Updates
When this changeset is applied:
- The account balance is updated according to the entry details and transition type
- A balance history entry is created to record this change
Validations
- Required fields:
:type,:value,:account_id - Entry type must be
:debitor:credit - Entry currency must match account currency
Examples
# Create a posted debit entry
iex> alias DoubleEntryLedger.Stores.AccountStore
iex> alias DoubleEntryLedger.Stores.InstanceStore
iex> {:ok, instance} = InstanceStore.create(%{address: "Test:Instance"})
iex> {:ok, account} = AccountStore.create(instance.address, %{name: "Test Account", address: "account:main1", type: :asset, currency: :USD}, "unique_id_123")
iex> attrs = %{
...> type: :debit,
...> value: %{amount: 10000, currency: :USD},
...> account_id: account.id,
...> }
iex> changeset = Entry.changeset(%Entry{}, attrs, :posted)
iex> changeset.valid?
true
@spec update_changeset(t(), map(), DoubleEntryLedger.Types.trx_types()) :: Ecto.Changeset.t()
Creates a changeset for updating an existing entry with transition handling.
This function builds a changeset specifically for updating the value of an existing entry while properly handling account balance updates and history creation. It's used when modifying entries that are already associated with transactions.
Parameters
entry- The Entry struct to updateattrs- Map of attributes to update on the entry (only:valuecan be updated)transition- The transition state (e.g.,:pending_to_posted,:pending_to_pending)
Returns
- An Ecto.Changeset with validations, preloaded associations, and balance updates
Account Updates
When this changeset is applied:
- The account balance is updated according to the new entry value and transition type
- A balance history entry is created to record this change
Validations
- Required field:
:value - Entry currency must match account currency
Examples
# Update a pending entry to be posted
# An entry has to be created first using a command
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: "account:main1", type: :asset, currency: :EUR}, "unique_id_123")
iex> {:ok, account2} = AccountStore.create(instance.address, %{
...> name: "account2", address: "account:main2", 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> [entry | _]= Repo.all(Entry)
iex> attrs = %{value: %{amount: 120, currency: :EUR}}
iex> changeset = Entry.update_changeset(entry, attrs, :pending_to_posted)
iex> changeset.valid?
true