Event sourcing

Принцип: каждое изменение — событие, текущее состояние — результат их суммы. Объясняет, как event sourcing применён к транзакциям сейчас и куда расширяется в целевой архитектуре.

В системе event sourcing нет колонки «текущий баланс». Есть лог событий (append-only), а баланс вычисляется как SUM() всех транзакций до «сейчас». Снапшоты — кэш для производительности, не источник правды. Удалить событие нельзя — только пометить отменённым (soft delete, deleted_at).


Транзакции в проде: event sourcing в узком смысле

Текущая реализация — таблица transactions (миграция 034). Это append-only лог денежных операций и движения предметов. Никакой хранимой колонки balance — баланс PC и сташа считается через SUM() при каждом запросе.

Виды транзакций (kind)

kindСмысл
moneyденежное движение: cp/sp/gp/pp (знаковые int)
itemпредмет вошёл/вышел из инвентаря; item_qty ≥ 1
transferперевод между PC или в общак; обе ноги связаны transfer_group_id

Парные переводы через transfer_group_id

Перевод — всегда две строки с одним transfer_group_id: одна нога с отрицательным amount_* (уходит) и вторая с положительным (приходит). Никаких обновлений existing rows. Если нужно откатить — добавляется обратная пара.

Статусы (status)

pending → заявка игрока ожидает одобрения DM. approved → применено (дефолт для DM-записей). rejected → отклонено; в баланс не включается.

Статус изменяется server action'ом; сама строка не обновляется по event-sourcing-принципу — но status — единственное поле, которое меняется после создания (это компромисс между строгим event sourcing и практичностью).

autogen-поля

Некоторые транзакции создаются автоматически (стартовый набор, авто-лут). Поля autogen_label и autogen_reconcile_key маркируют их как machine-generated и позволяют реконсайлу обнаружить дубликаты при повторной генерации.

Почему SUM() вместо хранимого баланса

Хранимый баланс — источник рассинхронизации: если запись пропала или была откорректирована, баланс врёт без видимых следов. SUM() по append-only логу всегда даёт консистентный результат. Цена — чуть медленнее на больших лентах (индекс по actor_pc_id + фильтр по status='approved' это компенсирует).

Целевая архитектура: универсальный events лог

Таблица transactions — частный случай паттерна. Цель engine pivot — расширить до универсального events:

events (
  id, event_type, at_tick, actor_id, location_id,
  payload jsonb, visibility jsonb,
  persistence_scope text,
  deleted_at timestamptz
)

Все значимые изменения мира — событие: DM-операция, движение NPC, изменение погоды, добыча лута. transactions становятся одним из event_type.

roadmap/generic-events-table.md

Ключевые гарантии

  • Append-only: insert-only, no delete (кроме deleted_at).
  • Полный аудит: любое состояние восстановимо по логу.
  • Безопасный откат: отмена = добавление обратного события, не удаление.
  • Visibility: каждое событие несёт visibility — кто его видит. → visibility.md

persistence-scope.md — теги loop / character / meta на событиях для управления сбросом петли. → features/accounting/README.md — текущая реализация бухгалтерии.