DoubleEntryLedger.Apis.CommandApi (double_entry_ledger v0.1.0)
View SourcePublic boundary for submitting ledger commands.
All requests are regular Elixir maps (typically with string keys coming from JSON) that describe which action to run, which instance to target, and the payload for either account or transaction work. The common wire format looks like:
%{
"instance_address" => String.t(),
"action" => "create_transaction" | "update_transaction" | "create_account" | "update_account",
"source" => String.t(),
"source_idempk" => String.t(),
"update_idempk" => String.t() | nil,
"update_source" => String.t() | nil,
"payload" => map()
}Use create_from_params/1 to enqueue commands for asynchronous processing or
process_from_params/2 to run the full worker pipeline synchronously.
Summary
Functions
Creates an immutable Command from external params and queues it for background processing.
Validates the params and runs the command worker immediately.
Types
@type logable() :: DoubleEntryLedger.Command.t() | DoubleEntryLedger.Command.AccountCommandMap.t() | DoubleEntryLedger.Command.TransactionCommandMap.t() | map()
@type on_error() :: :retry | :fail
Functions
@spec create_from_params(command_params()) :: {:ok, DoubleEntryLedger.Command.t()} | {:error, Ecto.Changeset.t( DoubleEntryLedger.Command.AccountCommandMap.t() | DoubleEntryLedger.Command.TransactionCommandMap.t() ) | :instance_not_found | :action_not_supported}
Creates an immutable Command from external params and queues it for background processing.
This function validates the payload using the appropriate AccountCommandMap or
TransactionCommandMap, resolves the instance, and persists a Command with an attached
CommandQueueItem in the :pending state. InstanceMonitor will later claim and process
the command; callers only need to inspect the returned struct to track progress.
Parameters
command_params: Map containing string keys for"instance_address","action", idempotency keys, and the"payload".
Returns
{:ok, command}: On success with the queued command (status:pending){:error, changeset}: When the payload could not be cast into a command map{:error, :instance_not_found | :action_not_supported}: When the instance or action is invalid
Examples
iex> {:ok, instance} = InstanceStore.create(%{address: "Sample:Instance"})
iex> account_data = %{address: "Cash:Account", type: :asset, currency: :USD, instance_address: instance.address}
iex> {:ok, event} = CommandApi.create_from_params(%{
...> "instance_address" => instance.address,
...> "action" => "create_account",
...> "source" => "frontend",
...> "source_idempk" => "unique_id_123",
...> "payload" => account_data
...> })
iex> event.command_queue_item.status
:pending
iex> {:error, %Ecto.Changeset{data: %AccountCommandMap{}}= changeset} = CommandApi.create_from_params(%{
...> "instance_address" => instance.address,
...> "action" => "create_account",
...> "source" => "frontend",
...> "source_idempk" => "unique_id_124",
...> "payload" => %{}
...> })
iex> changeset.valid?
false
iex> CommandApi.create_from_params(%{"action" => "unsupported"})
iex> {:error, :action_not_supported}
@spec process_from_params(command_params(), [{:on_error, on_error()}]) :: DoubleEntryLedger.Workers.CommandWorker.success_tuple() | DoubleEntryLedger.Workers.CommandWorker.error_tuple()
Validates the params and runs the command worker immediately.
Transaction actions ("create_transaction" / "update_transaction") support retries;
pass [on_error: :fail] to surface validation errors without storing the command when you
want the caller to handle failures directly. Account actions run through the same worker
stack but currently skip retries and only support the no-save-on-error path.
Parameters
command_params: Map describing the action, instance, idempotency keys, and payload.opts: Keyword list (currentlyon_error: :retry | :failfor transaction commands).
Returns
{:ok, transaction | account, command}on success with the created/updated projection.{:error, command}when the worker persisted an error state (queued for retry).{:error, changeset}when payload validation fails.{:error, reason}for other failures (e.g.,:action_not_supported).
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> {:ok, transaction, event} = CommandApi.process_from_params(%{
...> "instance_address" => instance.address,
...> "action" => "create_transaction",
...> "source" => "frontend",
...> "source_idempk" => "unique_id_123",
...> "payload" => %{
...> status: :posted,
...> entries: [
...> %{account_address: asset_account.address, amount: 100, currency: :USD},
...> %{account_address: liability_account.address, amount: 100, currency: :USD}
...> ]
...> }
...> })
iex> trx = (event |> Repo.preload(:transaction)).transaction
iex> trx.id == transaction.id
true
iex> {:ok, instance} = InstanceStore.create(%{address: "Sample:Instance"})
iex> {:ok, _account, _event} = CommandApi.process_from_params(%{
...> "instance_address" => instance.address,
...> "action" => "create_account",
...> "source" => "frontend",
...> "source_idempk" => "unique_id_123",
...> "payload" => %{
...> type: :asset,
...> address: "asset:owner:1",
...> currency: :EUR
...> }
...> }, [on_error: :fail])
iex> CommandApi.process_from_params(%{"action" => "unsupported"})
iex> {:error, :action_not_supported}