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 позиции)холдерский счёт получателя PlatformSettlementAdjustmentручная корректировка ПИН-админа (обязательная причина, аудит актора) по направлению корректировки по направлению корректировки 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). |
Риски
| Риск | Компенсация |
|---|---|
Рассинхрон rewards ↔ wallet (потерянное/задержанное событие → дебет без покрытия) | Идемпотентность (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 бессрочный как финансовая история. |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: LedgerTransactionEntity (+ LedgerEntryEntity), Entity / Aggregate: WalletAccountEntity
- API (3): GET /api/v1/my/wallet — Мой кошелёк (балансы счетов), POST /api/v1/platform/wallet/adjustments — Ручная корректировка счёта, Wallet API — реестр операций и общие контрактные модели
- Use Cases (6): UC-WLT-001. Получение денег кредитует балансы получателей, UC-WLT-002. Выплата поручения дебетует баланс получателя, UC-WLT-003. ПИН-админ вручную корректирует баланс, UC-WLT-004. Холдер смотрит баланс и историю операций, UC-WLT-005. Оператор ПИН смотрит журнал проводок счёта (сверка), UC-WLT-006. Компенсирующее начисление отражается в балансе после MoneyReceived
- События (1): Event Contract: pin.wallet.transaction-recorded (WalletTransactionRecordedKafkaEvent)
- Консюмеры (5): Consumer: Rewards Commission Distribution Adjusted, Consumer: Rewards Commission Money Received, Consumer: Rewards Payout Order Status Changed, Consumer: Rewards Referral Accrual Status Changed, Consumer: Wallet Events → Notifications (notifications-wallet-events)
- Саги (1): Saga «Commission Settlement» (финансовый контур вознаграждений)
- Отчёты (1): Отчет/Export: Выписка по счёту кошелька (wallet-statement)
- adr (2): ADR-0066 — удержание ПИН и реферальные ставки, ADR-0067 — гейт получения денег (акт поступления средств)
- scenarios (1): UC-OVR-010. От завершённой сделки до выплаты комиссии и реферального вознаграждения
- index.md (2): Rewards Service, Wallet Service
Связанные документы
- 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.