Gamend.Ledger (gamend_core v1.0.1296)

Copy Markdown View Source

The mechanics both ledgered balances share: Gamend.Economy (currency in a wallet) and Gamend.Inventory (quantity of an item).

The two contexts are near-isomorphic — the same run_change, apply_delta, record_ledger, idem_applied? set, differing only in whether the noun is a currency or an item. Most of that difference is real: a wallet and an item stack are different tables with different constraints, and folding them into one generic ledger would trade a little duplication for a lot of indirection.

What is not different is the transaction shape around them, which is what lives here:

  • apply the delta and write the ledger row in one transaction;
  • treat a duplicate idempotency_key as a replay rather than an error, because losing that race means the other request already applied it — so the caller should be told the resulting balance, not a failure;
  • roll back anything else.

Getting that wrong is a double-spend or a phantom grant, so it is worth having exactly one copy of it.

Summary

Functions

Runs apply_fun and record_fun in one transaction and normalises the result.

A page of ledger entries from query, newest first, with the user loaded — the admin listing both ledgers expose.

Classifies a failed ledger insert: a duplicate idempotency key is a replay, anything else is a genuine error. Rolls back either way.

Functions

change(apply_fun, record_fun, replay_fun)

@spec change(
  (-> {:ok, integer()} | {:error, term()}),
  (integer() -> term()),
  (-> integer())
) ::
  {:ok, integer()} | {:error, term()}

Runs apply_fun and record_fun in one transaction and normalises the result.

apply_fun returns {:ok, new_total} or {:error, reason}. record_fun receives the new total and writes the ledger row; it is expected to Repo.rollback(:idempotent_replay) when the idempotency key already exists.

replay_fun is called only on that replay, to read back the total the winning request produced.

list_entries(query, opts)

@spec list_entries(Ecto.Queryable.t(), keyword()) :: [struct()]

A page of ledger entries from query, newest first, with the user loaded — the admin listing both ledgers expose.

rollback_insert_error(changeset, error_tag)

@spec rollback_insert_error(Ecto.Changeset.t(), atom()) :: no_return()

Classifies a failed ledger insert: a duplicate idempotency key is a replay, anything else is a genuine error. Rolls back either way.