Назначение
wallet владеет внутренними балансами получателей вознаграждений и журналом их изменений: агент видит,
сколько ему причитается по агентским комиссиям, партнёр — по реферальной программе (раздельные балансы
одного пользователя), агентство — своё удержание, ПИН — свой доход и транзит средств. Это первая
реализация ledger-модели платформы
(ADR-0005): двойная
запись, append-only журнал, компенсирующие проводки
(ADR-0065).
wallet — учётный контур поверх фактов rewards: он не порождает денежных обязательств и не
командует rewards — источник сумм только события; кошелёк их проводит и показывает. Кредит холдеру
возможен строго после того, как ПИН подтвердил получение денег от застройщика (гейт MoneyReceived,
ADR-0067); дебет — по факту выплаты
платёжного поручения.
| Поле | Значение |
|---|
| Статус | draft |
| Owning team | PIN Platform (backend) |
| Domain | Деньги (учёт обязательств; границы — service-boundaries.md) |
| Data classification | Sensitive (балансы, суммы операций) |
| Runtime criticality | medium (учёт асинхронен: недоступность не блокирует сделки, начисления и выплаты в rewards; ЛК-балансы временно недоступны/устаревают) |
Ownership
| Artifact | Owner/Rule |
|---|
| PostgreSQL schema | wallet — единственное хранилище (PostgreSQL-only, без Redis/ClickHouse — ADR-0056) |
| Tenant isolation | вне tenant-модели: счета пользователей — user-centric (агент/партнёр; баланс переживает смену агентства — прецедент AgentPerformance, ADR-0061 п.5), record-level по холдеру; счета агентств — по HolderCompanyId; системные счета — платформенные |
| API areas | user (мой баланс и история), agency (балансы агентов агентства), platform (все счета, ручные корректировки) |
| Owned entities | WalletAccountEntity, LedgerTransactionEntity (+ LedgerEntryEntity); consumer-owned реплика UserDisplayReplicaEntity (подписи холдеров) |
| Produced events | pin.wallet.transaction-recorded.v1 |
| Consumed events | pin.rewards.commission-money-received.v1, pin.rewards.referral-accruals.v1, pin.rewards.payout-order-status-changed.v1; pin.identity.users.v1, pin.identity.companies.v1 (display-реплика холдеров) |
| Search indexes | none |
| Analytics facts | none (история и агрегации — SQL по PostgreSQL, ADR-0056) |
Ответственность И Границы
Делает:
- ведёт счета получателей по видам (
AgentCommission / ReferralCommission / AgencyCommission) и
системные счета платформы (PlatformSettlement / PlatformRevenue) — раздельные балансы, lazy-создание
при первом кредите (domain/wallet-account.md);
- проводит операции двойной записью в append-only журнал: кредиты по получению денег, дебеты по выплатам,
ручные корректировки и компенсации (domain/ledger-transaction.md);
- хранит
Balance на счёте для быстрых чтений ЛК и сверяет его пересчётом из журнала (журнал первичен,
расхождение — reconciliation-issue, ADR-0021);
- гарантирует идемпотентность повторной доставки любого события — unique
(OperationKind, SourceId);
- публикует каждую проводку холдерского счёта событием
pin.wallet.transaction-recorded.v1
(events/wallet-transaction-recorded.md) — уведомления
«начислено/выплачено»;
- отдаёт балансы и историю операций: холдеру — свои, руководителю агентства — счёт агентства и балансы
агентов текущего состава (ростер — по display-реплике холдеров, без синхронного
identity на
read-path), оператору ПИН — все счета, включая системные; подписи счетов (ФИО холдера, название
агентства) — из той же реплики (domain/user-display-replica.md).
Не делает:
| Не делает | Почему / где дом |
|---|
| Не исполняет платежи и не интегрируется с банком | Отметка выплаты (MarkPaid поручения) — ручная в rewards (решение №9 ТЗ); wallet лишь проводит её следствие |
| Не хранит реквизиты получателей | Дом — profile; журналу реквизиты не нужны |
| Не порождает денежных обязательств | Суммы считает и подтверждает rewards (начисления, распределения, акты, поручения); wallet — учёт следствий |
| Не даёт вывод средств, пополнение извне, p2p-переводы, мультивалютность | Баланс — внутренний управленческий учёт обязательств платформы, а не электронный кошелёк в смысле платёжного законодательства; валюта — RUB-only (ADR-0048, CurrencyId — типизированная ссылка на справочник) |
| Не правит журнал и балансы вручную | Append-only: исправления — компенсирующей транзакцией Correction либо командой владельца данных (rewards) через Reconciliation Center (ADR-0021) |
Source of truth: счета, журнал транзакций и проводок. Суммы-основания несут payload-ы событий rewards.
Единственная реплика чужих данных — display-реплика холдеров UserDisplayReplicaEntity (ФИО агента/
партнёра, название и id текущего агентства из identity): нужна, чтобы подписывать счета человекочитаемо
и резолвить ростер агентства без синхронного вызова identity на read-path
(domain/user-display-replica.md). Реплика косметическая: балансы и учёт
она не определяет (их ведёт журнал), при её лаге/пустоте подпись = null, а не отказ.
Ключевые решения
- Хранимый баланс + пересчёт из ledger.
Balance обновляется атомарно с проведением транзакции;
периодическая сверка «баланс = Σ кредитов − Σ дебетов» — детектор дрейфа (ADR-0021). Журнал первичен.
- Идемпотентность
(OperationKind, SourceId). Каждая транзакция несёт вид операции и id основания
(комиссионное начисление / рефералка / позиция поручения / исходная транзакция / ключ корректировки);
повтор — no-op.
- Кредит по гейту денег, дебет по факту выплаты. До
MoneyReceived суммы в кошельке не отражаются;
дебет без покрытия — детектор рассинхрона (DLQ + алерт + issue), не бизнес-отказ.
- PostgreSQL-only. Append-only таблицы дают аудируемость без event store и отдельной инфраструктуры
(ADR-0056); объём — единицы транзакций на сделку.
Зависимости
| Зависимость | Тип | Зачем Нужна | Failure mode |
|---|
PostgreSQL (schema wallet) | db | Счета, журнал, outbox | Недоступность = отказ API/consumer-ов; события накапливаются, обработка догоняет |
| Kafka | queue | Consume 3 топика rewards; produce pin.wallet.transaction-recorded.v1 | Lag = задержка отражения на балансах (eventual); идемпотентность защищает от дублей |
rewards | service (events) | Источник всех сумм: получение денег, апрув рефералок, выплаты | Нет событий → балансы не меняются; ЛК показывает последнее состояние |
identity | service (JWT/permissions) + events | Акторы, permissions (JWT); ФИО/название агентства/состав агентства — асинхронно через pin.identity.users.v1/companies.v1 в display-реплику (синхронного вызова на read-path нет) | Нет авторизации → API недоступно; лаг событий → устаревшие подписи/ростер (косметика), учёт балансов не страдает |
notifications | consumer наших событий | Уведомления «начислено/выплачено/скорректировано» | Потеря уведомлений не влияет на данные |
Проекты
src/services/wallet/
Pin.Wallet.Api/
Pin.Wallet.Application/
Pin.Wallet.Domain/
Pin.Wallet.Infrastructure/
Pin.Wallet.Background/
Pin.Wallet.Migrator/
Pin.Wallet.Contracts/
Основные Сущности
| Entity | Назначение | Source Of Truth | Документ |
|---|
WalletAccountEntity | Счёт «холдер + вид»: раздельные балансы агентских/реферальных комиссий пользователя, счёт агентства, системные счета платформы; хранимый Balance = Σ проводок | wallet | domain/wallet-account.md |
LedgerTransactionEntity (+ LedgerEntryEntity) | Бизнес-операция журнала (кредит/дебет/корректировка/компенсация) и её проводки двойной записью (Σ дебетов = Σ кредитов); append-only; идемпотентность (OperationKind, SourceId) | wallet | domain/ledger-transaction.md |
UserDisplayReplicaEntity | Display-реплика холдеров: ФИО агента/партнёра, id+название текущего агентства; подписи счетов и ростер агентства без синхронного identity (consumer-owned, read-only) | identity (проекция) | domain/user-display-replica.md |
ER-обзор
API
Карта областей; каждая операция документируется отдельным файлом (реестр и общие контрактные модели —
api/README.md).
- User (холдер: агент/партнёр; permissions
wallet.balance.view-own, wallet.transactions.view-own,
record-level по холдеру):
- Agency (руководитель агентства, permission
wallet.team.view, scope = организация):
- api/agency/list-agency-agent-balances.md —
GET /api/v1/agency/wallet/agent-balances: счёт агентства (AgencyCommission) + балансы
AgentCommission-счетов агентов текущего состава (ростер — по display-реплике холдеров).
- Platform (оператор/админ ПИН, платформенный scope):
Events И Consumers
Retry/DLQ: transient-ошибки — retry с backoff; дебет без покрытия (WALLET_BALANCE_NEGATIVE_FORBIDDEN)
— non-retriable → DLQ + алерт + issue Reconciliation Center; повторная доставка любого события — no-op по
(OperationKind, SourceId) (domain/ledger-transaction.md, INV-WLT-7).
Runtime И Эксплуатация
| Область | Требование |
|---|
| Health checks | /health/live, /health/ready (PostgreSQL, Kafka consumer lag), /health/startup (Migrator применён) |
| SLO/SLA | Чтение баланса p95 < 300 мс; отражение события rewards на балансе — минуты (eventual, лаг consumer-а) |
| Dashboard | wallet-overview: транзакции по видам, лаг consumer-ов, DLQ, дрейф балансов |
| Alerts | consumer lag > 15 мин; DLQ непуст (дебет без покрытия); wallet_balance_drift_total > 0 (сверка балансов) |
| Runbooks | разбор DLQ «дебет без покрытия» и дрейфа баланса — через Reconciliation Center (ADR-0021): исправление командой rewards либо Correction, журнал не правится |
| Feature flags | none |
Безопасность
- Actor types: холдер (агент/партнёр — user-centric счета), руководитель агентства (счёт компании +
балансы агентов), оператор/админ ПИН (платформенный scope), system (consumer-ы
rewards).
- Permission model (permission.md):
wallet.balance.view-own,
wallet.transactions.view-own (record-level по холдеру), wallet.team.view (scope = организация),
wallet.accounts.view-all, wallet.adjustments.manage (платформенный scope).
- Sensitive data classes: балансы и суммы операций — в логи не пишутся; причины корректировок — только
admin-журнал.
- Audit: журнал append-only и сам является audit trail; ручные операции — обязательная причина + актор.
- Tenant isolation: вне tenant-модели (см. Ownership); изоляция — record-level по холдеру + scope
permissions.
Известные Ограничения И Assumptions
- Не электронный кошелёк. Внутренний управленческий учёт обязательств платформы: вывода средств,
платёжных функций и банковской интеграции нет (ADR-0065 п.5).
- Кредиты предшествуют дебетам по бизнес-процессу (
MoneyReceived раньше Paid), но сквозной
порядок между топиками rewards не гарантирован — страховка: no-negative guard + DLQ + replay.
- События
rewards обязаны нести построчную детализацию: событие получения денег — три строки
распределения, событие выплаты при Paid — позиции Items[] с их id — иначе суммы не разнести по
счетам и идемпотентный SourceId дебетов не построить (ADR-0065).
- Счёт
AgencyCommission и PlatformRevenue в v1 только накапливают: механизма их выплаты/вывода нет.
- Баланс отражает события с лагом consumer-а (минуты) — ЛК показывает eventual-состояние.
Use Cases
Связанные Документы
- Домены: domain/wallet-account.md,
domain/ledger-transaction.md,
domain/user-display-replica.md (display-реплика холдеров).
- Событие: events/wallet-transaction-recorded.md.
- Источники фактов
rewards: commission-accrual.md,
referral-accrual.md,
payout-order.md,
platform-receipt.md.
- Snapshot-модели валюты: snapshots.md.
- ADR: 0065 — контекст wallet,
0005 — financial ledger,
0021 — Reconciliation Center,
0067 — гейт получения денег,
0048 — RUB-only,
0056 — simplicity-first,
0061 — user-centric прецедент.