Entity / Aggregate: WalletAccountEntity
Дом полей агрегата «Счёт кошелька» контекста wallet. Проекции (балансы ЛК, payload событий,
admin-реестры счетов) ссылаются сюда и описывают только дельту.
Назначение
WalletAccountEntity — внутренний счёт получателя вознаграждений: сколько платформа должна агенту,
партнёру или агентству (холдерские счета) и как движутся средства платформы (системные счета). Счёт
определяется парой «холдер + вид счёта» (AccountKind): у одного пользователя одновременно живут
раздельные балансы «агентские комиссии по сделкам» и «реферальные комиссии» — они не смешиваются ни в
отображении, ни в выплатах (ADR-0065).
Счёт не является электронным кошельком в смысле платёжного законодательства: это внутренний
управленческий учёт обязательств платформы. Пополнений извне, вывода средств по инициативе холдера и
p2p-переводов нет; баланс меняется только проводками журнала
(ledger-transaction.md) по фактам контекста rewards и ручными корректировками
ПИН-админа. Balance хранится на счёте для быстрых чтений ЛК и всегда равен сумме проводок журнала —
журнал первичен, расхождение — инцидент рассинхрона (ADR-0021), а не повод править данные.
Счета создаются lazy — при первой кредитующей проводке в адрес холдера; предварительной регистрации
нет. Первый кредит возможен только после гейта получения денег (MoneyReceived,
commission-accrual.md) — до него обязательства существуют
лишь как статусы начислений в rewards и в кошельке не отражаются.
Классификация
| Поле | Значение |
|---|---|
| Контекст | wallet |
| Kind | AggregateRoot |
| Source of truth | wallet (счета и балансы); суммы-основания — факты rewards (события) |
| Таблица | wallet.wallet_accounts |
| Tenant isolation | вне tenant-модели: холдерские счета пользователей — user-centric (переживают смену агентства, прецедент AgentPerformance — ADR-0061 п.5), record-level по холдеру; счёт агентства — по HolderCompanyId; системные счета — платформенные |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Уникальный идентификатор счёта. |
AccountKind | WalletAccountKind | Да | хранится как text, varchar(32) | app | Вид счёта (enum ниже); задаётся при создании, неизменяем. |
HolderUserId | Guid? | Да для AgentCommission/ReferralCommission, иначе null | unique ux_wallet_accounts_holder_user_kind (holder_user_id, account_kind) where holder_user_id is not null; check формы холдера (INV-WLT-1) | событие-источник (получатель) | Пользователь-холдер: агент (AgentCommission) / партнёр (ReferralCommission). Логическая ссылка на identity, без FK. |
HolderCompanyId | Guid? | Да для AgencyCommission, иначе null | unique ux_wallet_accounts_holder_company_kind (holder_company_id, account_kind) where holder_company_id is not null; check формы холдера (INV-WLT-1) | событие-источник (агентство) | Компания-холдер: агентство (AgencyCommission). Логическая ссылка на identity, без FK. |
Balance | Money (owned) | Да | balance_value numeric(18,2) — без check >= 0 (системные счета отрицательны, INV-WLT-3) | вычисление | Хранимый баланс = Σ кредитов − Σ дебетов проводок счёта (ledger-transaction.md); обновляется атомарно с проведением транзакции. |
Version | long | Да | >= 1 | app | Доменная версия (инкремент при каждой затронувшей счёт транзакции; попадает в события). |
CreatedAt | DateTimeOffset | Да | index | app | Момент создания счёта (= момент первой кредитующей проводки для холдерских). |
UpdatedAt | DateTimeOffset | Да | — | app | Момент последней проводки по счёту. |
Для системных счетов (PlatformSettlement, PlatformRevenue) оба холдер-поля null; уникальность —
partial unique ux_wallet_accounts_system_kind (account_kind) where holder_user_id is null and
holder_company_id is null: по одному системному счёту каждого вида на всю платформу (INV-WLT-2).
Enum WalletAccountKind
| Значение | Тип счёта | Холдер | Назначение |
|---|---|---|---|
AgentCommission | холдерский | пользователь (агент) | Агентские комиссии по сделкам — кредитуется Agent-строками распределения начислений (commission-accrual.md). |
ReferralCommission | холдерский | пользователь (партнёр) | Реферальные вознаграждения — кредитуется Approved-начислениями (referral-accrual.md). Отдельный от AgentCommission баланс того же пользователя. |
AgencyCommission | холдерский | компания (агентство) | Удержание агентства — кредитуется Agency-строками распределения. В платёжные поручения не включается (доход агентства, вывода в v1 нет). |
PlatformSettlement | системный | — | Корреспондирующий (транзитный) счёт платформы: дебетуется при каждом кредите получателям, кредитуется при выплатах. Знак баланса — см. INV-WLT-3. |
PlatformRevenue | системный | — | Доход ПИН — кредитуется Platform-строками распределения (удержание платформы, ADR-0066). |
Value objects
Money (единый VO контекста wallet)
| Поле | Тип | Обязательность | Ограничения | Колонка (пример для Balance) |
|---|---|---|---|---|
Value | decimal | Да | numeric(18,2); у Balance допускает < 0 только на системных счетах (INV-WLT-3), у сумм проводок — строго > 0 (ledger-transaction.md) | balance_value |
CurrencyId | short | Да | ссылка на валюту справочника (../../handbook/domain/currency.md, логическая, без FK); дефолт — RUB; допустимость валют — RUB-only (ADR-0048) | balance_currency_id |
Инвариант VO: операции сложения/вычитания допустимы только при равном CurrencyId
(WALLET_CURRENCY_MISMATCH). В payload событий денежные суммы передаются как Value + CurrencyId +
CurrencySnapshot — см. ../../handbook/domain/snapshots.md
(справочник валют ≠ мультивалютность).
Связи
| Связь | Тип | Ключ | Delete behavior |
|---|---|---|---|
LedgerEntryEntity | reference (обратная: проводки журнала по счёту) | ledger_entries.account_id → wallet_accounts.id | restrict (счёт с проводками не удаляется; удаления счетов не предусмотрены вовсе) |
Пользователь (identity) | external reference (без FK) | HolderUserId | — |
Компания (identity) | external reference (без FK) | HolderCompanyId | — |
Инварианты
| Инвариант | Условие | Когда проверяется | Ошибка |
|---|---|---|---|
| INV-WLT-1 HolderMatchesKind | Форма холдера соответствует виду счёта: AgentCommission/ReferralCommission — ровно HolderUserId (компания null); AgencyCommission — ровно HolderCompanyId (пользователь null); PlatformSettlement/PlatformRevenue — оба null. У холдерского счёта ровно один холдер (XOR), у системного — ни одного | создание счёта + db check | WALLET_ACCOUNT_HOLDER_INVALID |
| INV-WLT-2 OneAccountPerHolderKind | На пару (холдер, AccountKind) — не более одного счёта; на каждый системный вид — один счёт на платформу. Гонка lazy-создания разрешается уникальным индексом: проигравший использует уже созданный счёт (идемпотентно, без ошибки) | lazy-создание при первом кредите (unique index) | конфликт → повтор с существующим счётом |
| INV-WLT-3 NoNegativeHolderBalance | После проведения любой транзакции баланс каждого затронутого холдерского счёта >= 0. Системные счета отрицательны штатно: у PlatformSettlement при единой формуле Balance = Σ кредитов − Σ дебетов баланс <= 0, а ` | Balance | = объём средств, разнесённых по счетам получателей и ещё не погашенных выплатами. Дебет холдерского счёта сверх баланса — не бизнес-отказ, а детектор рассинхронаrewards↔wallet` (DLQ + алерт, ledger-transaction.md); для ручной корректировки — обычный отказ |
| INV-WLT-4 SingleCurrency | Валюта каждой проводки равна валюте счёта (Balance.CurrencyId); все счета платформы — RUB (ADR-0048). Проводка в иной валюте отклоняется до применения | проведение транзакции (VO-guard) | WALLET_CURRENCY_MISMATCH |
Сверка баланса. Хранимый Balance — производная величина: периодический пересчёт «Balance =
Σ кредитов − Σ дебетов проводок счёта» — штатный детектор Reconciliation Center
(ADR-0021). Расхождение — инцидент
(wallet_balance_drift_total + issue); баланс восстанавливается пересчётом из журнала (журнал первичен),
журнал не правится никогда.
Доменные и интеграционные события
Собственных событий счёт не публикует: каждое изменение баланса — следствие проведённой транзакции
журнала, которая публикуется как pin.wallet.transaction-recorded.v1 по каждой проводке холдерского
счёта (payload несёт AccountId, AccountKind, BalanceAfter — контракт
../events/wallet-transaction-recorded.md).
Конкурентность и восстановление
- Гонка lazy-создания. Два параллельных кредита одному холдеру (например, две строки распределения разных сделок) могут одновременно не найти счёт; уникальный индекс (холдер, вид) пропускает одно создание, проигравший повторяет проведение с уже созданным счётом (INV-WLT-2).
- Конкурентные проводки по одному счёту. Обновление
Balanceвыполняется атомарно с записью проводок под блокировкой строки счёта: два параллельных проведения сериализуются, каждое видит актуальный баланс (guard INV-WLT-3 не обходится гонкой);Versionрастёт монотонно — потребители событий упорядочивают изменения per-account. - Восстановление. При любом сомнении в балансе — пересчёт из журнала; счёт не удаляется и не «обнуляется» вручную, исправления — только компенсирующими транзакциями (ledger-transaction.md, INV-WLT-6).
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK; ux_wallet_accounts_holder_user_kind unique btree (holder_user_id, account_kind) where holder_user_id is not null; ux_wallet_accounts_holder_company_kind unique btree (holder_company_id, account_kind) where holder_company_id is not null; ux_wallet_accounts_system_kind unique btree (account_kind) where holder_user_id is null and holder_company_id is null |
| Партиционирование / retention | none — счетов не больше, чем «активные получатели × виды» (десятки тысяч, ADR-0056); retention бессрочный (финансовая история) |
| Concurrency | блокировка строки счёта при проведении транзакции (сериализация изменений баланса); Version — монотонный guard потребителей |
| Permissions / PII | Холдер-пользователь — только свои счета (record-level HolderUserId, permission wallet.balance.view-own); руководитель агентства — счёт AgencyCommission своей компании и балансы AgentCommission-счетов агентов текущего состава (ростер — по display-реплике холдеров user-display-replica.md, без синхронного identity; wallet.team.view, scope = организация — ../api/agency/list-agency-agent-balances.md); оператор/админ ПИН — все счета (wallet.accounts.view-all, платформенный scope). Балансы — Sensitive-класс: в логи не пишутся |
| Audit | Version + штампы; происхождение каждого изменения баланса — строка журнала (append-only, ledger-transaction.md) |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity (Replica): UserDisplayReplicaEntity, Entity / Aggregate: LedgerTransactionEntity (+ LedgerEntryEntity)
- API (10): GET /api/v1/agency/wallet/agent-balances — Балансы агентов агентства, GET /api/v1/my/wallet — Мой кошелёк (балансы счетов), GET /api/v1/my/wallet/transactions — История транзакций моего кошелька, GET /api/v1/platform/wallet/accounts — Реестр счетов кошелька, GET /api/v1/platform/wallet/accounts/{accountId}/ledger — Журнал проводок счёта, POST /api/v1/agency/wallet/statement-exports — Постановка выписки по счёту агентства/агента (XLSX), POST /api/v1/my/wallet/statement-exports — Постановка выписки по моему счёту (XLSX), POST /api/v1/platform/wallet/adjustments — Ручная корректировка счёта, POST /api/v1/platform/wallet/statement-exports — Постановка выписки по любому счёту (XLSX), Wallet API — реестр операций и общие контрактные модели
- Use Cases (8): UC-RWD-017. Уволенный агент: судьба заработанных комиссий, UC-WLT-001. Получение денег кредитует балансы получателей, UC-WLT-002. Выплата поручения дебетует баланс получателя, UC-WLT-003. ПИН-админ вручную корректирует баланс, UC-WLT-004. Холдер смотрит баланс и историю операций, UC-WLT-005. Оператор ПИН смотрит журнал проводок счёта (сверка), UC-WLT-006. Компенсирующее начисление отражается в балансе после MoneyReceived, US-реестр: Реферальный партнёр (US-PRT)
- События (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): Отчет/Export: Выписка по счёту кошелька (wallet-statement)
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
- index.md (1): Wallet Service
Связанные документы
- Журнал (единственный источник изменений баланса): ledger-transaction.md.
- Событие: ../events/wallet-transaction-recorded.md.
- Источники фактов: ../../rewards/domain/commission-accrual.md
(строки распределения, гейт
MoneyReceived), ../../rewards/domain/referral-accrual.md (Approved-рефералки), ../../rewards/domain/payout-order.md (позиции выплат). - Snapshot-модели валюты: ../../handbook/domain/snapshots.md.
- ADR-0065 (контекст, счета, гейты), ADR-0005 (ledger-модель), ADR-0021 (детекторы), ADR-0048 (RUB-only), ADR-0061 п.5 (user-centric прецедент), ADR-0056 (simplicity-first).