DoubleEntryLedger.Command.TransactionCommandMap (double_entry_ledger v0.1.0)

View Source

Defines the TransactionCommandMap schema for representing transaction command data in the Double Entry Ledger system.

This module provides an embedded schema and related functions for creating and validating transaction command maps, which serve as the primary data structure for transaction creation and updates. TransactionCommandMap represents the pre-persistence state of a TransactionCommand, containing all necessary data to either create a new transaction or update an existing one.

Purpose

TransactionCommandMap acts as a validation and structuring layer for transaction-related commands before they are processed into persistent database records. It ensures data integrity and provides a consistent interface for transaction operations across the system.

Architecture

This module extends the base DoubleEntryLedger.Command.CommandMap behavior by:

  • Using the CommandMap macro to inject common fields and functionality
  • Implementing the payload_to_map/1 callback for TransactionData serialization
  • Providing action-specific validation through custom changeset logic
  • Supporting both create and update transaction operations

Structure

TransactionCommandMap extends the base CommandMap functionality with transaction-specific payload handling. It contains the following fields:

  • action: The type of action to perform (:create_transaction or :update_transaction)
  • instance_address: unique address of the instance this command belongs to
  • source: Identifier of the external system generating the command
  • source_data: Optional map containing additional metadata from the source system
  • source_idempk: Primary identifier from the source system (used for idempotency)
  • update_idempk: Unique identifier for update operations, enabling multiple distinct updates to the same original transaction while maintaining idempotency
  • payload: Embedded TransactionData containing entries and transaction details

Key Functions

  • create/1: Creates and validates a TransactionCommandMap from a map of attributes
  • changeset/2: Builds a changeset for validating TransactionCommandMap data with action-specific logic
  • payload_to_map/1: Converts TransactionData payload to a plain map (CommandMap callback)
  • to_map/1: Converts a TransactionCommandMap struct to a plain map representation (inherited)
  • log_trace/1,2: Builds a map of trace metadata for logging from a TransactionCommandMap (inherited)

Implementation Details

Action-Specific Validation

The changeset function applies different validation strategies based on the action type:

  • Create operations (:create_transaction): Uses standard TransactionData validation requiring complete transaction information including balanced entries
  • Update operations (:update_transaction): Uses specialized update validation that allows partial data and requires update_idempk for idempotency

Idempotency Enforcement

The system enforces idempotency differently depending on the action type:

  • Create actions: Idempotency is enforced using a combination of :create_transaction action value, source and the source_idempk. This ensures the same external transaction is never created twice.

  • Update actions: Idempotency uses a combination of :update_transaction action value, the original source and source_idempk (identifying which create action to update), and the update_idempk (identifying this specific update). This allows multiple distinct updates to the same original transaction.

Both combinations are protected by unique indexes in the database to prevent duplicate processing. The TransactionCommandMap schema itself does not enforce these constraints, as it is not persisted directly. Instead, the TransactionCommand schema handles this at the database level. Only transactions with status :pending can be updated.

Workflow Integration

TransactionCommandMaps are typically created from external input data, validated, and then processed by the CommandWorker system to create or update transactions in the ledger.

Examples

Creating a TransactionCommandMap for a new transaction:

{:ok, command_map} = TransactionCommandMap.create(%{
  action: "create_transaction",
  instance_address: "some:address",
  source: "accounting_system",
  source_idempk: "invoice_123",
  payload: %{
    status: "pending",
    entries: [
      %{account_id: "c24a758c-7300-4e94-a2fe-d2dc9b1c2db8", amount: 100, currency: "USD"},
      %{account_id: "c24a758c-7300-4e94-a2fe-d2dc9b1c2db7", amount: -100, currency: "USD"}
    ]
  }
})

Creating a TransactionCommandMap for updating an existing transaction:

{:ok, update_map} = TransactionCommandMap.create(%{
  action: "update_transaction",
  instance_address: "some:address",
  source: "accounting_system",
  source_idempk: "invoice_123",
  update_idempk: "invoice_123_update_1",
  payload: %{
    status: "posted"
  }
})

Summary

Types

t()

Represents a TransactionCommandMap structure for transaction creation or updates.

Functions

Creates a changeset for validating TransactionCommandMap attributes with action-specific logic.

Builds a validated TransactionCommandMap or returns a changeset with errors.

Types

t()

@type t() :: %DoubleEntryLedger.Command.TransactionCommandMap{
  action: :create_transaction | :update_transaction,
  instance_address: String.t(),
  payload: DoubleEntryLedger.Command.TransactionData.t(),
  source: String.t(),
  source_idempk: String.t(),
  update_idempk: String.t() | nil,
  update_source: String.t() | nil
}

Represents a TransactionCommandMap structure for transaction creation or updates.

This type extends the parameterized CommandMap type with TransactionData as the payload type, providing type safety and clear documentation for transaction-specific command operations.

Type Specification

This is equivalent to DoubleEntryLedger.Command.CommandMap.t(TransactionData.t()) and includes:

Inherited Fields (from CommandMap)

  • action: The operation type (:create_transaction or :update_transaction)
  • instance_address: unique address of the ledger instance this command belongs to
  • source: Identifier of the external system generating the command
  • source_data: Optional metadata from the source system (default: %{})
  • source_idempk: Primary identifier used for idempotency
  • update_idempk: Unique identifier for update operations to maintain idempotency

Transaction-Specific Field

  • payload: The embedded TransactionData.t() structure containing transaction details

Usage in Function Signatures

@spec process_transaction_command(TransactionCommandMap.t()) ::
  {:ok, Transaction.t()} | {:error, Changeset.t()}

Examples

# Type annotation in function
@spec validate_command(TransactionCommandMap.t()) :: boolean()
def validate_command(%TransactionCommandMap{} = command_map) do
  # Implementation
end

Functions

actions()

base_changeset(struct, attrs)

changeset(command_map, attrs)

@spec changeset(t() | map(), map()) :: Ecto.Changeset.t(t())

Creates a changeset for validating TransactionCommandMap attributes with action-specific logic.

This function builds an Ecto changeset that validates the required fields and structure of a TransactionCommandMap. It applies different validation rules depending on the action type, ensuring that create and update operations have appropriate requirements.

Parameters

  • command_map: The TransactionCommandMap struct to create a changeset for
  • attrs: Map of attributes to apply to the struct

Returns

Validation Strategy

The function uses action-aware validation:

Create Transaction Validation

  • Applies base CommandMap validation (action, instance_address, source, source_idempk required)
  • Validates payload using TransactionData.changeset/2 (requires complete transaction data)
  • Does not require update_idempk

Update Transaction Validation

  • Applies update CommandMap validation (includes all base validation plus requires update_idempk)
  • Validates payload using TransactionData.update_command_changeset/2 (allows partial data)
  • Enforces update-specific business rules

Implementation Details

The function normalizes string action values to atoms and routes to the appropriate validation strategy. This allows flexible input handling while maintaining type safety.

Examples

iex> alias DoubleEntryLedger.Command.TransactionCommandMap
iex> attrs = %{
...>   action: "create_transaction",
...>   instance_address: "some:address",
...>   source: "accounting_system",
...>   source_idempk: "invoice_123",
...>   payload: %{
...>     status: "pending",
...>     entries: [
...>       %{account_address: "cash:account", amount: 100, currency: "USD"},
...>       %{account_address: "asset:account", amount: -100, currency: "USD"}
...>     ]
...>   }
...> }
iex> changeset = TransactionCommandMap.changeset(%TransactionCommandMap{}, attrs)
iex> changeset.valid?
true

iex> alias DoubleEntryLedger.Command.TransactionCommandMap
iex> update_attrs = %{
...>   action: "update_transaction",
...>   instance_address: "some:address",
...>   source: "accounting_system",
...>   source_idempk: "invoice_123",
...>   update_idempk: "update_1",
...>   payload: %{status: "posted"}
...> }
iex> changeset = TransactionCommandMap.changeset(%TransactionCommandMap{}, update_attrs)
iex> changeset.valid?
true

iex> alias DoubleEntryLedger.Command.TransactionCommandMap
iex> invalid_attrs = %{action: "update_transaction", source: "test"}
iex> changeset = TransactionCommandMap.changeset(%TransactionCommandMap{}, invalid_attrs)
iex> changeset.valid?
false

create(attrs)

@spec create(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t(t())}

Builds a validated TransactionCommandMap or returns a changeset with errors.

This function creates a complete TransactionCommandMap from raw input data by applying all necessary validations. It serves as the primary entry point for creating validated transaction commands from external input.

Parameters

  • attrs: A map containing the command data with both common CommandMap fields and transaction payload

Returns

  • {:ok, command_map} - Successfully validated TransactionCommandMap struct
  • {:error, changeset} - Ecto.Changeset containing validation errors

Validation Process

The function applies comprehensive validation including:

  1. Common CommandMap field validation (action, instance_address, source, etc.)
  2. Action-specific requirements (update_idempk for updates)
  3. TransactionData payload validation appropriate to the action type
  4. Cross-field validation and business rule enforcement

Examples

iex> alias DoubleEntryLedger.Command.TransactionCommandMap
iex> attrs = %{
...>   action: "create_transaction",
...>   instance_address: "some:address",
...>   source: "accounting_system",
...>   source_idempk: "invoice_123",
...>   payload: %{
...>     status: "pending",
...>     entries: [
...>       %{account_address: "asset:account", amount: 100, currency: "USD"},
...>       %{account_address: "cash:account", amount: -100, currency: "USD"}
...>     ]
...>   }
...> }
iex> {:ok, command_map} = TransactionCommandMap.create(attrs)
iex> command_map.action
:create_transaction
iex> command_map.source
"accounting_system"

iex> alias DoubleEntryLedger.Command.TransactionCommandMap
iex> invalid_attrs = %{action: "create_transaction", source: "test"}
iex> {:error, changeset} = TransactionCommandMap.create(invalid_attrs)
iex> changeset.valid?
false

to_map(command_map)

@spec to_map(struct()) :: map()

update_changeset(struct, attrs)