Перейти к основному содержимому

ADR-0065 — контекст wallet (внутренние балансы и ledger)

Статус

Accepted

Дата

2026-07-15

Контекст

Финансовый контур rewards доводит комиссию до статусов «деньги получены» и «выплачено» (commission-accrual.md, payout-order.md, гейт получения денег — ADR-0067, три строки распределения Platform/Agency/Agent — ADR-0066), но нигде на платформе нет внутренних балансов получателей: агент и агентство не видят «сколько мне причитается и ещё не выплачено», партнёр — сколько накоплено по реферальной программе, ПИН — сколько удержано в свою пользу. Продуктовое требование — балансы раздельные: «агентские комиссии по сделкам» и «реферальные комиссии» — разные суммы у одного и того же пользователя, они не смешиваются ни в отображении, ни в выплатах.

При этом:

  • ADR-0005 (Financial Ledger и Double-Entry Accounting) давно зафиксировал модель immutable ledger (LedgerTransaction + LedgerEntry, компенсирующие проводки вместо UPDATE), но ни один контекст её ещё не реализует — деньги существуют только как статусы начислений и поручений в rewards.
  • Контекст rewards уже вмещает четыре разные ответственности: расчёт начислений по правилам застройщиков, политику распределения агентства, акты поступления средств и платёжные поручения. Добавление туда ещё и журнала балансов разрастило бы контекст и смешало модель «документы со статусами» (mutable агрегаты) с моделью «append-only журнал» (иная семантика консистентности и аудита).
  • Расхождения между обещанными и проведёнными суммами должны разбираться штатно — через детекторы и Reconciliation Center (ADR-0021), а не ручной правкой таблиц.

Решение

1. Отдельный bounded context wallet — первая реализация ADR-0005

  • Вводится 21-й bounded context wallet (схема БД wallet, PostgreSQL): внутренние балансы получателей вознаграждений и журнал их изменений. Это первая реализация ledger-модели ADR-0005:
    • LedgerTransaction — бизнес-операция (кредит по начислению, дебет по выплате, корректировка);
    • LedgerEntry — проводка по счёту внутри транзакции, направление Debit/Credit;
    • инвариант каждой транзакции: Σ дебетов = Σ кредитов;
    • журнал append-only: UPDATE/DELETE проводок и транзакций запрещены; любое исправление — новая компенсирующая транзакция Correction со ссылкой на исходную и обязательной причиной.
  • wallet — учётный read-model поверх фактов rewards: он не порождает денежных обязательств и не командует rewards; источник сумм — события rewards, wallet их проводит и показывает.

2. Счета — WalletAccount

  • Счёт WalletAccount определяется парой «холдер + вид счёта», AccountKind:

    KindТипХолдерНазначение
    AgentCommissionхолдерскийпользователь (агент)Агентские комиссии по сделкам (Agent-строки распределения).
    ReferralCommissionхолдерскийпользователь (партнёр)Реферальные вознаграждения.
    AgencyCommissionхолдерскийкомпания (агентство)Удержание агентства (Agency-строки распределения).
    PlatformSettlementсистемныйКорреспондирующий (транзитный) счёт платформы: получено от застройщиков минус распределено/выплачено.
    PlatformRevenueсистемныйДоход ПИН (Platform-строки распределения, ADR-0066).
  • У холдерского счёта ровно один холдер: пользователь или компания (XOR). Уникальность — unique (холдер, kind): у одного пользователя может быть одновременно счёт AgentCommission и счёт ReferralCommission — раздельность этих балансов и есть продуктовое требование.

  • Счета создаются lazy — при первой кредитующей проводке в адрес холдера; предварительной регистрации счетов нет.

  • Balance хранится на счёте (быстрые чтения ЛК) и пересчитывается из ledger: периодическая сверка «хранимый баланс = Σ кредитов − Σ дебетов проводок счёта»; расхождение — инцидент рассинхрона (ADR-0021), а не повод править журнал.

  • Валюта — RUB-only (ADR-0048); проводка в иной валюте отклоняется (WALLET_CURRENCY_MISMATCH).

3. Проводки: кредиты строго по гейту денег, дебеты — по факту выплаты

  • Кредит холдеру возможен только после того, как ПИН подтвердил получение денег (гейт MoneyReceived, ADR-0067). До этого суммы существуют лишь как статусы начислений в rewards и в wallet не отражаются.

  • Виды операций — enum WalletOperationKind; каждая транзакция несёт (OperationKind, SourceId) с unique-ограничением — идемпотентность повторной доставки событий:

    OperationKindТриггер (SourceId)ДебетКредит
    CommissionCreditполучение денег по начислению из pin.rewards.commission-money-received.v1 (SourceId = id начисления; одна транзакция на событие с кредитами по строкам Distributions[])PlatformSettlementAgentCommission / AgencyCommission / PlatformRevenue — по RecipientType строки
    ReferralCreditреферальное начисление перешло в Approved (pin.rewards.referral-accruals.v1; SourceId = id начисления)PlatformSettlementReferralCommission партнёра
    PayoutDebitпозиция платёжного поручения при переходе поручения в Paid (pin.rewards.payout-order-status-changed.v1; SourceId = id позиции)холдерский счёт получателяPlatformSettlement
    Adjustmentручная корректировка ПИН-админа (обязательная причина, аудит актора)по направлению корректировкипо направлению корректировки
    Correctionкомпенсация ранее проведённой транзакции (SourceId = id исходной транзакции; в т.ч. отмена реферального начисления после Approved)зеркально исходнойзеркально исходной
  • Проведённая транзакция публикуется как pin.wallet.transaction-recorded.v1 (потребитель — notifications: «вам начислено» / «вам выплачено»).

  • Детальная карта проводок, поля сущностей и инварианты — в domain-доках контекста (docs/06-services/wallet/); ADR фиксирует границу, счета, виды операций и гейты.

4. Инварианты и обработка рассинхрона

  • Холдерский баланс не уходит в минус — после любой транзакции баланс каждого затронутого холдерского счёта >= 0. Системные счета могут быть отрицательными (контрсчёт PlatformSettlement); интерпретация знака фиксируется в domain-доке.
  • Недостаток баланса при дебете (выплата, не покрытая кредитами) — это не бизнес-отказ, а детектор рассинхрона между rewards и wallet: consumer не проводит операцию, сообщение уходит в DLQ + алерт, создаётся issue Reconciliation Center (ADR-0021); исправление — командой владельца данных (rewards) либо компенсирующей проводкой, никогда — правкой журнала.
  • Повторная доставка любого события — no-op по unique (OperationKind, SourceId).

5. Вне scope

  • Исполнение платежей и банковская интеграция — не входят: отметка MarkPaid платёжного поручения в rewards остаётся ручной (решение №9 ТЗ), wallet лишь проводит её следствие.
  • Реквизиты получателей — не хранятся (дом — profile, как и раньше).
  • Вывод средств по инициативе холдера, пополнение извне, p2p-переводы между счетами холдеров, мультивалютность — отсутствуют. Баланс wallet — внутренний управленческий учёт обязательств платформы, а не электронный кошелёк в смысле платёжного законодательства.

Последствия

КонтекстПоследствие
wallet (новый)Домены WalletAccount, LedgerTransaction/LedgerEntry; 3 consumers событий rewards; событие pin.wallet.transaction-recorded.v1; API областей user/agency/platform (мой баланс и история; балансы агентов агентства; все счета и ручная корректировка).
rewardsСобытия обязаны нести построчную детализацию: commission-money-received — строки распределения, payout-order-status-changed при Paid — позиции; иначе wallet не сможет провести суммы по счетам.
identityНовые permissions wallet.{balance.view-own, transactions.view-own, team.view, accounts.view-all, adjustments.manage}.
notificationsНовые уведомления по pin.wallet.transaction-recorded.v1 (начисление/выплата на баланс).
Reconciliation CenterДва штатных детектора: «дебет без покрытия» (DLQ + алерт) и «хранимый Balance ≠ пересчёт из ledger».
ПлатформаADR-0005 получает первую реализацию; счёт контекстов 20 → 21.

Альтернативы (рассмотрено и отклонено)

ВариантПричина отклонения
Балансы и ledger внутри rewardsРазрастание контекста: rewards уже совмещает расчёт начислений, политику распределения агентств, акты поступления и платёжные поручения. Ledger — другая модель данных (append-only журнал против mutable-документов со статусами) и другой lifecycle; связка через события чище и повторяет каноническую границу ADR-0005 «владелец фактов ≠ владелец учёта».
Event-sourcing полного стека (event store как source of truth счетов)Отклонено по ADR-0056 (simplicity-first): append-only ledger-таблицы в PostgreSQL дают ту же аудируемость и воспроизводимость без отдельной ES-инфраструктуры, снапшотов и replay-механики; хранимый Balance — обычная сверяемая проекция.
Единый баланс холдера без разделения по видамПротиворечит продуктовому требованию раздельности «агентские комиссии по сделкам» и «реферальные комиссии»; смешение ломает и раздельные источники выплат (поручения AgencyCommission против ReferralReward).

Риски

РискКомпенсация
Рассинхрон rewardswallet (потерянное/задержанное событие → дебет без покрытия)Идемпотентность (OperationKind, SourceId); DLQ + алерт; детекторы и runbook ADR-0021; исправление командой rewards или Correction.
Отмена/корректировка начисления в rewards после MoneyReceivedВ wallet — только компенсирующая Correction-транзакция со ссылкой и причиной; прямых сторно-правок журнала нет.
Гонка lazy-создания счёта при первом кредитеUnique (холдер, kind) + повтор операции; проигравший использует уже созданный счёт.
Дрейф хранимого Balance относительно журналаПериодический пересчёт из ledger; расхождение — reconciliation-issue, баланс восстанавливается из журнала (журнал первичен).
Трактовка баланса как «электронных денег»Явная фиксация в README контекста: внутренний учёт обязательств, вывода средств и платёжных функций нет; исполнение платежей — вне платформы.
Рост append-only таблицНа целевой нагрузке (ADR-0056) объём мал: единицы транзакций на сделку; индексы по (счёт, дата), retention бессрочный как финансовая история.

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы

  • ADR-0005 — базис: immutable ledger, double-entry, компенсирующие проводки (этот ADR — его первая реализация).
  • ADR-0021 — детекторы рассинхрона, issues, correction commands.
  • ADR-0048 — RUB-only, money safety; ADR-0056 — simplicity-first (PostgreSQL, без event store).
  • ADR-0066 — три строки распределения (Platform/Agency/Agent), реферальные ставки; ADR-0067 — гейт MoneyReceived и акт поступления средств.
  • Источники фактов: commission-accrual.md, payout-order.md и события pin.rewards.commission-money-received.v1, pin.rewards.referral-accruals.v1, pin.rewards.payout-order-status-changed.v1.
  • Детальное ТЗ контекста: docs/06-services/wallet/ (README, domain/wallet-account.md, domain/ledger-transaction.md, consumers, events, api, use-cases).
  • financial-code-rules.md.