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

Wallet Service

Назначение

wallet владеет внутренними балансами получателей вознаграждений и журналом их изменений: агент видит, сколько ему причитается по агентским комиссиям, партнёр — по реферальной программе (раздельные балансы одного пользователя), агентство — своё удержание, ПИН — свой доход и транзит средств. Это первая реализация ledger-модели платформы (ADR-0005): двойная запись, append-only журнал, компенсирующие проводки (ADR-0065).

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

Metadata

ПолеЗначение
Статусdraft
Owning teamPIN Platform (backend)
DomainДеньги (учёт обязательств; границы — service-boundaries.md)
Data classificationSensitive (балансы, суммы операций)
Runtime criticalitymedium (учёт асинхронен: недоступность не блокирует сделки, начисления и выплаты в rewards; ЛК-балансы временно недоступны/устаревают)

Ownership

ArtifactOwner/Rule
PostgreSQL schemawallet — единственное хранилище (PostgreSQL-only, без Redis/ClickHouse — ADR-0056)
Tenant isolationвне tenant-модели: счета пользователей — user-centric (агент/партнёр; баланс переживает смену агентства — прецедент AgentPerformance, ADR-0061 п.5), record-level по холдеру; счета агентств — по HolderCompanyId; системные счета — платформенные
API areasuser (мой баланс и история), agency (балансы агентов агентства), platform (все счета, ручные корректировки)
Owned entitiesWalletAccountEntity, LedgerTransactionEntity (+ LedgerEntryEntity); consumer-owned реплика UserDisplayReplicaEntity (подписи холдеров)
Produced eventspin.wallet.transaction-recorded.v1
Consumed eventspin.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 indexesnone
Analytics factsnone (история и агрегации — 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-ов; события накапливаются, обработка догоняет
KafkaqueueConsume 3 топика rewards; produce pin.wallet.transaction-recorded.v1Lag = задержка отражения на балансах (eventual); идемпотентность защищает от дублей
rewardsservice (events)Источник всех сумм: получение денег, апрув рефералок, выплатыНет событий → балансы не меняются; ЛК показывает последнее состояние
identityservice (JWT/permissions) + eventsАкторы, permissions (JWT); ФИО/название агентства/состав агентства — асинхронно через pin.identity.users.v1/companies.v1 в display-реплику (синхронного вызова на read-path нет)Нет авторизации → API недоступно; лаг событий → устаревшие подписи/ростер (косметика), учёт балансов не страдает
notificationsconsumer наших событийУведомления «начислено/выплачено/скорректировано»Потеря уведомлений не влияет на данные

Проекты

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 = Σ проводокwalletdomain/wallet-account.md
LedgerTransactionEntity (+ LedgerEntryEntity)Бизнес-операция журнала (кредит/дебет/корректировка/компенсация) и её проводки двойной записью (Σ дебетов = Σ кредитов); append-only; идемпотентность (OperationKind, SourceId)walletdomain/ledger-transaction.md
UserDisplayReplicaEntityDisplay-реплика холдеров: ФИО агента/партнёра, 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 по холдеру):
    • api/user/get-my-wallet.mdGET /api/v1/my/wallet: балансы всех своих счетов раздельно по видам;
    • api/user/list-my-wallet-transactions.mdGET /api/v1/my/wallet/transactions: история операций счёта (фильтры: вид счёта, вид операции, период; пагинация).
  • Agency (руководитель агентства, permission wallet.team.view, scope = организация):
    • api/agency/list-agency-agent-balances.mdGET /api/v1/agency/wallet/agent-balances: счёт агентства (AgencyCommission) + балансы AgentCommission-счетов агентов текущего состава (ростер — по display-реплике холдеров).
  • Platform (оператор/админ ПИН, платформенный scope):
    • api/platform/list-wallet-accounts.mdGET /api/v1/platform/wallet/accounts (wallet.accounts.view-all): все счета с фильтрами (вид, холдер), включая системные балансы;
    • api/platform/create-wallet-adjustment.mdPOST /api/v1/platform/wallet/adjustments (wallet.adjustments.manage): ручная корректировка Adjustment с обязательной причиной.

Events И Consumers

TypeNameTopicНазначениеLink
Producedwallet-transaction-recordedpin.wallet.transaction-recorded.v1Проводка холдерского счёта: уведомления «начислено/выплачено» (потребитель notifications)events/wallet-transaction-recorded.md
Consumedcommission-money-receivedpin.rewards.commission-money-received.v1Кредиты по строкам распределения (CommissionCredit: Agent/Agency/PlatformRevenue)consumers/rewards-commission-money-received.md; источник — rewards/domain/commission-accrual.md
Consumedcommission-distribution-adjustedpin.rewards.commission-distribution-adjusted.v1Авто-Adjustment: перенос дельты долей между счетами AgentCommissionAgencyCommission при корректировке распределения в MoneyReceived (идемпотентность (Adjustment, AccrualId + Version))consumers/rewards-commission-distribution-adjusted.md; контракт — rewards/events/commission-distribution-adjusted.md
Consumedreferral-accrual-status-changedpin.rewards.referral-accruals.v1ApprovedReferralCredit партнёру; Cancelled после Approved → компенсация Correctionconsumers/rewards-referral-accrual-status-changed.md; источник — rewards/domain/referral-accrual.md
Consumedpayout-order-status-changedpin.rewards.payout-order-status-changed.v1PaidPayoutDebit по позициям Items[]; недостаток баланса → DLQ + алерт (ADR-0021)consumers/rewards-payout-order-status-changed.md; контракт — rewards/events/payout-order-status-changed.md
Consumeduser-upserted / company-upsertedpin.identity.users.v1, pin.identity.companies.v1Наполнение display-реплики холдеров (ФИО + id/название текущего агентства); version-guard, upsertconsumers/identity-users.md; контракты — identity/events/user-upserted.md, identity/events/company-upserted.md

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-а)
Dashboardwallet-overview: транзакции по видам, лаг consumer-ов, DLQ, дрейф балансов
Alertsconsumer lag > 15 мин; DLQ непуст (дебет без покрытия); wallet_balance_drift_total > 0 (сверка балансов)
Runbooksразбор DLQ «дебет без покрытия» и дрейфа баланса — через Reconciliation Center (ADR-0021): исправление командой rewards либо Correction, журнал не правится
Feature flagsnone

Безопасность

  • 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

UCНазваниеДокумент
UC-WLT-001Получение денег кредитует балансы получателейuse-cases/UC-WLT-001-commission-money-received-credits-balances.md
UC-WLT-002Выплата поручения дебетует баланс получателяuse-cases/UC-WLT-002-payout-paid-debits-balance.md
UC-WLT-003ПИН-админ вручную корректирует балансuse-cases/UC-WLT-003-admin-manual-adjustment.md
UC-WLT-004Холдер смотрит баланс и историю операцийuse-cases/UC-WLT-004-holder-views-balance-and-history.md

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