UC-RWD-006. Агент смотрит личную статистику за всю карьеру (user-centric)
Назначение
Описывает сценарий чтения агентом личной user-centric статистики (ADR-0061 п.5, ADR-0070 п.6):
пожизненные метрики — одобренные фиксации, закрытые/сорванные сделки, начисленная и выплаченная комиссия,
достижения — агрегируются суммированием строк проекции по AgentUserId сквозь все компании и
сохраняются при выходе/смене агентства. Проекция AgentPerformanceEntity
имеет композитный ключ (AgentUserId, CompanyId) (строка на агентство, мульти-членство); display-статус
членства (InAgency/Independent) и достижения — производные на чтении. Ключевой инвариант:
независимый агент (вышедший из агентства, UC-IDN-010) продолжает видеть накопленные метрики — они не
привязаны к текущему активному AgencyCompanyId. Связан с контекстом rewards.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-RWD-006 |
| Контекст | rewards (см. docs/06-services/rewards/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Агент (в агентстве или независимый) — self-действие |
| Permissions / scope | rewards.performance.view-own; роль agent в scope Platform (независимый) или Organization (в агентстве) |
| Tenant | не применяется — проекция AgentPerformanceEntity вне tenant (платформенный scope агента, ADR-0061 п.5, ADR-0070 п.6); CompanyId — разрез строки, не tenant |
| Record-level | AgentUserId из sub (только свои строки проекции) |
Предусловие
- Учётка агента активна; роль
agentдействует (в любом scope). - Строки проекции
AgentPerformanceEntityнаполнены consumer-ами (одна/ несколько по числу агентств; может не быть ни одной у новичка — тогда нули, не ошибка).
Триггер
| Поле | Значение |
|---|---|
| Источник | UI button |
| Триггерящий элемент | ЛК агента → «Профиль / Мои достижения» |
Основной Поток
- Агент открывает экран личного профиля/достижений.
- Фронт вызывает
GET /api/v1/my/agent-performance(../api/agent/get-my-agent-performance.md). - Система читает личную проекцию:
AgentUserIdберётся из claims (не из query), проверяется permissionrewards.performance.view-own. Все строки проекцииAgentPerformanceEntityагента читаются поAgentUserId(композитный ключ(AgentUserId, CompanyId), вне tenant — платформенный scope агента) и агрегируются на чтении (SUMметрик). - Возвращается
AgentPerformanceModel:FixationsApprovedCount,DealsWonCount,DealsLostCount,CommissionAccruedTotal,CommissionPaidTotal(суммы по строкам), производныйMembershipStatus(InAgency/Independent),ActiveAgencyCompanyIds,Achievements(вычислены над агрегатом),First/LastActivityAt,GeneratedAt = now; опционально — разбивкаPerAgency(приIncludePerAgency=true). Чтение не меняет состояние и событий не публикует. Нет строк → нули +Independent+ пустой список достижений (200, не 404).
Альтернативные Потоки
A1. Независимый агент после выхода из агентства
- Агент вышел из агентства (UC-IDN-010):
identityопубликовалagent-membership-changed (Left); agent-membership-changed.md перевёл строку этого агентства вMembershipStatus = Left(прочие строки не тронуты). Если активных строк не осталось — производныйMembershipStatus = Independent,ActiveAgencyCompanyIds = []. - Чтение агрегирует все строки агента и возвращает те же накопленные
FixationsApprovedCount/DealsWonCount/CommissionAccruedTotalи т.д. — метрики user-centric,SUMпо строкамAgentUserId, при выходе строки не обнуляются (ADR-0061 п.5, ADR-0070 п.6). Историческая привязка сделок/лидов/фиксаций вcrmне пересчитана (снапшоты иммутабельны, п.4).
A2. Агент вступил в другое агентство (в т.ч. одновременно с прежним)
agent-membership-changed (Active)c новымCompanyIdсоздаёт/активирует строку нового агентства → если у агента есть ≥ 1 активная строка, производныйMembershipStatus = InAgency,ActiveAgencyCompanyIdsсодержит все активныеCompanyId(с мульти-членством их может быть несколько, ADR-0070). Накопленные метрики прежних строк сохраняются; новые сделки/фиксации ложатся в строку своего агентства.
A3. Новичок без активности
- Ни одной строки проекции →
200с нулевыми счётчиками и пустыми достижениями (не 404); UI показывает «Пока нет данных».
A4. Проекция отстаёт (лаг consumer-ов)
- Значения могут отставать на лаг (< 5 мин SLO);
LastActivityAt/GeneratedAtотражают свежесть. Не ошибка; при подозрении на рассинхрон — rebuild проекции (runbook).
Постусловие
- Состояние системы не изменилось (read-only): мутаций/событий нет.
- Агент получил пожизненные метрики и достижения, консистентные с его историей вне зависимости от текущего агентства.
Согласованность и межсервисное взаимодействие
- Ожидает на входе. Строки проекции
AgentPerformanceEntityпо паре(AgentUserId, CompanyId)уже наполнены consumer-ами из долговечных Kafka-топиков:pin.rewards.commission-accrual-created.v1/-status-changed.v1(сделки и деньги),pin.crm.fixation-approved.v1(одобренные фиксации),pin.identity.agent-membership-changed.v1(членство). Чтение опирается на гарантии наполнения: события одного агрегата приходят по ключу (Idначисления /AgentUserId) упорядоченно в рамках партиции; апдейты идемпотентны (двойная доставка не удваивает счётчик, AC4); membership применяется с monotonic-guard поLastMembershipVersionстроки — переставленная доставка старого перехода отбрасывается (AC3), затрагивая лишь строку своего агентства. Историюcrm(снапшотыAgencyCompanyId/AgentUserIdсделок и фиксаций) чтение не запрашивает — только их последствия-события, снапшоты иммутабельны (ADR-0061 п.4). - Предоставляет на выходе. Ничего не публикует и не мутирует (read-only). Отдаёт агрегат
AgentPerformanceModel(SUMстрок) строго поAgentUserIdиз claims — чужой id (из query) игнорируется (AC6). - Окно eventual-consistency. Значения отражают состояние проекции на момент чтения; штатный лаг
наполнения < 5 мин (SLO, A4).
GeneratedAt/LastActivityAt— маркеры свежести для UI. Только что закрытая сделка или одобренная фиксация может ещё не попасть в счётчик — не ошибка; повторное чтение после применения события покажет актуальные значения. - Поведение при отсутствии/устаревании данных. Нет строк (новичок без событий) → нули +
Independent+ пустой список достижений (200, не404, A3). Устаревшее membership-состояние строки (лаг identity-события) → накопленные метрики корректны, производныйMembershipStatus/ActiveAgencyCompanyIdsотражают прежнее состояние строк до применения перехода (A1/A2). Метрики при выходе/переходе агента между агентствами не обнуляются — агрегируются по неизменномуAgentUserIdсквозь строки (ADR-0061 п.5, ADR-0070 п.6, AC1/AC2). - Восстановление. Проекция производна и перестраиваема: снос строк и реплей перечисленных топиков с offset 0 через выделенную consumer-group даёт те же значения (fold детерминирован, AC8). Чтение во время rebuild может вернуть частичные/нулевые значения — операционное окно, покрывается runbook (A4).
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
UNAUTHORIZED | 401 | Нет/просрочен JWT | «Требуется авторизация» |
FORBIDDEN | 403 | Нет permission rewards.performance.view-own | «Недостаточно прав» |
Пустая проекция — не ошибка (200 с нулями).
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
| — (потребление) | pin.rewards.commission-accrual-created.v1 / -status-changed.v1, pin.crm.fixation-approved.v1, pin.identity.agent-membership-changed.v1 | Наполняют проекцию (асинхронно, до чтения) | consumer-ы rewards (проекция) |
Само чтение интеграционных событий не публикует. Наполнение — consumer-ами agent-performance-projection.md, crm-fixation-approved.md, agent-membership-changed.md.
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Метрики агрегируются SUM по строкам агента (AgentUserId) сквозь все компании (не по текущему агентству) | тест: сделки в двух разных агентствах суммируются в личной сводке |
| AC2 | После выхода из агентства (строка → Left) метрики строки сохраняются (A1) | тест: leave → чтение возвращает прежние счётчики |
| AC3 | Производный MembershipStatus/ActiveAgencyCompanyIds отражают последние membership-события строк (guard по Version строки) | тест join→leave→join, переставленная доставка |
| AC4 | Одобренная фиксация увеличивает FixationsApprovedCount строки ровно один раз (идемпотентность) | двойная доставка fixation-approved |
| AC5 | Завершённая сделка → DealsWonCount+CommissionAccruedTotal; выплата → CommissionPaidTotal; отмена (DealCancelled) → реверс + DealsLostCount | тесты по каждому переходу |
| AC6 | Агент видит только свои строки (AgentUserId из claims, не из query) | попытка запроса чужого id игнорируется |
| AC7 | Ни одной строки → 200 с нулями (A3) | новичок без событий |
| AC8 | Проекция перестраиваема реплеем событий (fold детерминирован) | rebuild из runbook → те же значения |
| AC9 | Устаревшее membership-состояние строки не искажает накопленные метрики (лаг identity-события) | подать метрики, затем Left с задержкой → счётчики прежние, статус Independent |
| AC10 | Мульти-членство: две активные строки → MembershipStatus = InAgency, ActiveAgencyCompanyIds содержит оба CompanyId (INV-AP5) | тест: агент в двух агентствах одновременно |
| AC11 | IncludePerAgency=true → PerAgency из построчных метрик; по умолчанию — только агрегат | тест разбивки по агентствам |
Наблюдаемость и НФТ
- Логи (без секретов/PII).
agent-performance-read(agentUserIdиз claims,membershipStatus,generatedAt,result = ok|empty). Суммы (CommissionAccruedTotal/CommissionPaidTotal) — класс Sensitive, в логи не пишутся; раскладки по сделкам иInvestorUserIdв проекции отсутствуют (только агрегаты). Попытка запроса чужого id логируется как факт (id — из claims), без содержимого чужой проекции. - Метрики.
rewards_agent_performance_read_total{result=ok|empty};rewards_agent_performance_read_duration(p95);rewards_agent_performance_projection_lag(лаг наполнения consumer-ами, alert > 5 мин — SLO свежести, A4); доля пустых чтений (empty) — индикатор новичков. - Трейсы. Span приёма → чтение строк проекции по префиксу ключа
AgentUserId+ агрегация; сквознойtraceparent. Внешних синхронных вызовов нет — чтение локальной проекции. - НФТ (нагрузка/латентность). Read p95 < 300 мс (SLO сервиса); чтение строк агента по индексу
ix_agent_performance_agent(обычно 1–2 строки на агента, ≤ 20k агентов, ADR-0056) + агрегация на чтении; проекция вне tenant — фильтр строго поAgentUserIdизsub. - Доступ/безопасность.
rewards.performance.view-own;AgentUserIdберётся изsub, не из query (fail closed на чужую проекцию, AC6); независимый агент (scopePlatform) читает свою проекцию так же, как агент в агентстве (scopeOrganization); суммы Sensitive не логируются.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Projection: AgentPerformanceEntity
- API (1): GET /api/v1/my/agent-performance — Моя личная статистика (агент, user-centric)
- Use Cases (1): UC-RWD-017. Уволенный агент: судьба заработанных комиссий
- Консюмеры (2): Consumer: CRM Fixation Approved, Consumer: Identity Agent Membership Changed
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API метод: ../api/agent/get-my-agent-performance.md.
- Проекция: ../domain/agent-performance.md.
- Consumers-источники: ../consumers/agent-performance-projection.md, ../consumers/crm-fixation-approved.md, ../consumers/agent-membership-changed.md.
- Agency-centric параллель: crm
get-statistics.md. - Связанные UC: UC-IDN-010 (выход из агентства, сохранение метрик), UC-RWD-003 (создание начисления).
- Решение: ADR-0061 (п.4, п.5),
ADR-0070 (п.6 —
композитный ключ
(AgentUserId, CompanyId), мульти-членство).