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

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
KindAggregateRoot
Source of truthwallet (счета и балансы); суммы-основания — факты rewards (события)
Таблицаwallet.wallet_accounts
Tenant isolationвне tenant-модели: холдерские счета пользователей — user-centric (переживают смену агентства, прецедент AgentPerformance — ADR-0061 п.5), record-level по холдеру; счёт агентства — по HolderCompanyId; системные счета — платформенные

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
IdGuidДаPKappУникальный идентификатор счёта.
AccountKindWalletAccountKindДахранится как text, varchar(32)appВид счёта (enum ниже); задаётся при создании, неизменяем.
HolderUserIdGuid?Да для AgentCommission/ReferralCommission, иначе nullunique 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.
HolderCompanyIdGuid?Да для AgencyCommission, иначе nullunique 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.
BalanceMoney (owned)Даbalance_value numeric(18,2)без check >= 0 (системные счета отрицательны, INV-WLT-3)вычислениеХранимый баланс = Σ кредитов − Σ дебетов проводок счёта (ledger-transaction.md); обновляется атомарно с проведением транзакции.
VersionlongДа>= 1appДоменная версия (инкремент при каждой затронувшей счёт транзакции; попадает в события).
CreatedAtDateTimeOffsetДаindexappМомент создания счёта (= момент первой кредитующей проводки для холдерских).
UpdatedAtDateTimeOffsetДа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)
ValuedecimalДаnumeric(18,2); у Balance допускает < 0 только на системных счетах (INV-WLT-3), у сумм проводок — строго > 0 (ledger-transaction.md)balance_value
CurrencyIdshortДассылка на валюту справочника (../../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
LedgerEntryEntityreference (обратная: проводки журнала по счёту)ledger_entries.account_id → wallet_accounts.idrestrict (счёт с проводками не удаляется; удаления счетов не предусмотрены вовсе)
Пользователь (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 checkWALLET_ACCOUNT_HOLDER_INVALID
INV-WLT-2 OneAccountPerHolderKindНа пару (холдер, AccountKind) — не более одного счёта; на каждый системный вид — один счёт на платформу. Гонка lazy-создания разрешается уникальным индексом: проигравший использует уже созданный счёт (идемпотентно, без ошибки)lazy-создание при первом кредите (unique index)конфликт → повтор с существующим счётом
INV-WLT-3 NoNegativeHolderBalanceПосле проведения любой транзакции баланс каждого затронутого холдерского счёта >= 0. Системные счета отрицательны штатно: у PlatformSettlement при единой формуле Balance = Σ кредитов − Σ дебетов баланс <= 0, а `Balance= объём средств, разнесённых по счетам получателей и ещё не погашенных выплатами. Дебет холдерского счёта сверх баланса — не бизнес-отказ, а детектор рассинхронаrewardswallet` (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
Партиционирование / retentionnone — счетов не больше, чем «активные получатели × виды» (десятки тысяч, 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-класс: в логи не пишутся
AuditVersion + штампы; происхождение каждого изменения баланса — строка журнала (append-only, ledger-transaction.md)

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

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

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