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

Entity (Replica): UserDisplayReplicaEntity

Дом полей display-реплики холдеров контекста wallet — справочник отображаемых имён получателей вознаграждений (ФИО агента/партнёра, название агентства), чтобы read-path балансов рисовал человекочитаемые подписи без синхронного запроса в identity. Проекции ответов API (HolderDisplayName в реестре счетов, балансах агентов агентства, «моём кошельке») ссылаются сюда и перечисляют только дельту.

Назначение

UserDisplayReplicaEntity — read-only проекция учётной записи платформы (source of truth — identity/domain/user.md; название компании — identity/domain/company.md) в контекст wallet. Держит отображаемое имя холдера и, для агента, его текущее агентство (id + название), чтобы кошелёк:

  • подписывал счета именем холдера — ФИО агента/партнёра в реестре счетов оператора ПИН (../api/platform/list-wallet-accounts.md) и в балансах агентов руководителю агентства (../api/agency/list-agency-agent-balances.md);
  • определял текущий состав агентства для выборки балансов агентов — ростер резолвится по реплике (CompanyId = агентство актора), а не синхронным вызовом identity на read-path;
  • подписывал счёт агентства его названием (CompanyName).

Реплика наполняется двумя событиями identity с монотонными version-guard-ами (алгоритм — consumers/identity-users.md): pin.identity.users.v1 (ФИО + текущее агентство) и pin.identity.companies.v1 (название агентства). Собственных данных сущность не владеет и интеграционных событий не публикует (внутренняя реплика).

Классификация

ПолеЗначение
Контекстwallet
KindReplica (проекция из identity; наполняется только по событиям, для операций кошелька read-only)
Source of truthidentity: ФИО — user.md; название агентства — company.md
Таблицаwallet.user_display_replicas
Tenant isolationвне tenant-модели — справочник person-level (Id = UserId совпадает с identity.users.id, прямая адресация); read-path кошелька накладывает собственные record-level / scope-правила поверх (wallet-account.md)

Поля

ПолеТипОбязательностьОграничения (PK/FK/index/constraint)ИсточникОписание
UserIdGuidДаPK (user_id)identity (= users.id)Ключ реплики и key события pin.identity.user-upserted. Совпадает с HolderUserId холдерских счетов.
DisplayNamestringДаvarchar(384); допускается пустая строка при lazy-регистрацииidentity snapshot (Name)Отображаемое ФИО холдера, схлопнутое из FullNameSnapshot (INV-WLT-UDR-3). Единственная проекция ФИО (поиск/маскирование кошельку не нужны).
CompanyIdGuid?Нетindex ix_user_display_replicas_company (company_id) where company_id is not nullidentity snapshot (CompanyId)Текущее агентство агента (активная компания, ADR-0070); null — партнёр/инвестор/агент вне агентства. Ключ резолва ростера агентства.
CompanyNamestring?Нетvarchar(256)company snapshot (Name)Название текущего агентства (денормализовано на строку агента для подписи счёта агентства без второго чтения). null — компания ещё не реплицирована / холдер вне агентства.
IdentityUserVersionlong?Нетidentity (user-upserted.Version)Монотонный guard применённых snapshot-ов пользователя (INV-WLT-UDR-1); null — snapshot ещё не применялся.
IdentityCompanyVersionlong?Нетidentity (company-upserted.Version)Монотонный guard применённого snapshot-а названия текущей компании (INV-WLT-UDR-2); null — название ещё не применялось.
CreatedAtDateTimeOffsetДаconsumerПервое появление строки реплики.
UpdatedAtDateTimeOffsetДаconsumerМомент последнего применённого snapshot-а (любого из двух).

Value objects

VO не используются: ФИО хранится схлопнутой строкой DisplayName (кошелёк только отображает, не ищет и не маскирует — в отличие от messenger/domain/messenger-user.md, где ФИО разложено на лексемы под поиск/маску). Единый дом концепта «ФИО» остаётся в identity — здесь только плоская проекция для подписи.

Модели JSONB

JSONB-модели не используются.

Связи

СвязьТипКлючПравило загрузкиDelete behavior
UserEntity (identity)replica sourceUserId = identity.users.id (логическая)по событию pin.identity.users.v1— (в identity)
CompanyEntity (identity)replica sourceCompanyId = identity.companies.id (логическая)по событию pin.identity.company-upserted.v1— (в identity)
WalletAccountEntity (AgentCommission/ReferralCommission)referenceHolderUserId → user_display_replicas.user_id (логическая)при проекции HolderDisplayName счётареплика не удаляется
WalletAccountEntity (AgencyCommission)referenceподпись по CompanyName строк агентов агентства (CompanyId = HolderCompanyId)при проекции HolderDisplayName счёта агентства

Инварианты и переходы состояний

ИнвариантУсловиеГде проверяетсяОшибка
INV-WLT-UDR-1Snapshot пользователя применяется только при user-upserted.Version > IdentityUserVersion (last-write-wins по key = UserId; защита от out-of-order/replay)version-guard консюмера до примененияskip + метрика wallet_identity_sync_stale_total{source="user"}
INV-WLT-UDR-2Snapshot названия компании применяется к строке только при company-upserted.Version > IdentityCompanyVersion и CompanyId строки = компании событияversion-guard консюмера до примененияskip + метрика wallet_identity_sync_stale_total{source="company"}
INV-WLT-UDR-3DisplayName — детерминированное схлопывание FullNameSnapshot в «LastName FirstName MiddleName» (пустые части опускаются, лишние пробелы схлопываются); пустой snapshot (lazy-регистрация без ФИО) → пустая строкапроекция ФИО в консюмере
INV-WLT-UDR-4При смене CompanyId (агент сменил агентство) CompanyName берётся из уже известной строки того же нового агентства; если ни одной нет — CompanyName = null, IdentityCompanyVersion = null до ближайшего company-upsertedобработка user-upserted
INV-WLT-UDR-5Реплика read-only для операций кошелька — запись выполняет только консюмер identity; отсутствие строки на read-path не ошибка (подпись = null, клиент показывает id)отсутствие операций записи вне консюмера

Доменные и интеграционные события

Сущность не является агрегатом: доменных событий не поднимает, интеграционных Kafka-событий не публикует (внутренняя реплика; downstream реагирует на события identity).

Потребляет (наполнение реплики):

СобытиеТопикКогдаЭффект
UserUpsertedKafkaEventpin.identity.users.v1регистрация/обновление публичных полей пользователя, смена агентстваupsert DisplayName + CompanyId с guard INV-WLT-UDR-1; CompanyName — по INV-WLT-UDR-4
CompanyUpsertedKafkaEventpin.identity.companies.v1создание/переименование компанииобновление CompanyName строк агентов этой компании с guard INV-WLT-UDR-2

Событие блокировки пользователя (identity/events/user-blocked.md) и приостановки компании из тех же топиков игнорируются: блокировка на отображаемую подпись не влияет, доступ отсекает identity (JWT), а фактический учёт балансов ведёт журнал.

Контракты событий — identity/events/user-upserted.md, identity/events/company-upserted.md; алгоритм консюмера — consumers/identity-users.md.

Окно консистентности. Реплика eventual: наполнение гарантирует at-least-once доставку с ключами UserId/CompanyId и монотонными версиями. Только что зарегистрированный или переименованный холдер (агентство) получает корректную подпись через секунды (при лаге консюмера — минуты). Пока snapshot не применён, подпись = null (клиент показывает id) — это косметическая гонка на read-path, деньги и балансы она не затрагивает (баланс ведёт журнал, независимо от реплики). Смена CompanyId тоже eventual: ростер агентства сходится к текущему составу после применения user-upserted.

Индексы, хранение, безопасность

АспектЗначение
ИндексыPK btree (user_id); ix_user_display_replicas_company btree (company_id) where company_id is not null — резолв ростера агентства
Партиционирование / retentionnone (справочник до ~1 млн пользователей, ADR-0056); строка живёт, пока жив identity-пользователь; блокировка/архивация в identity реплику не удаляет (подпись остаётся для исторических счетов)
Concurrencyоптимистической блокировки нет; порядок применения — раздельные монотонные guard-ы IdentityUserVersion (key UserId) и IdentityCompanyVersion (key CompanyId), last-write-wins
Permissions / PIIреплика читается только на read-path балансов (wallet.accounts.view-all, wallet.team.view); ФИО — класс PII, в логах маскируется (инициалы); топики identity — m2m ACL Kafka (доверенный consumer). PII не переживает admin purge в identity: анонимизированный snapshot затирает DisplayName (INV-WLT-UDR-1)
Auditactor-штампов нет (реплика); аудит источника — в identity

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

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

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