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

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

ПолеЗначение
IDUC-RWD-006
Контекстrewards (см. docs/06-services/rewards/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольАгент (в агентстве или независимый) — self-действие
Permissions / scoperewards.performance.view-own; роль agent в scope Platform (независимый) или Organization (в агентстве)
Tenantне применяется — проекция AgentPerformanceEntity вне tenant (платформенный scope агента, ADR-0061 п.5, ADR-0070 п.6); CompanyId — разрез строки, не tenant
Record-levelAgentUserId из sub (только свои строки проекции)

Предусловие

  • Учётка агента активна; роль agent действует (в любом scope).
  • Строки проекции AgentPerformanceEntity наполнены consumer-ами (одна/ несколько по числу агентств; может не быть ни одной у новичка — тогда нули, не ошибка).

Триггер

ПолеЗначение
ИсточникUI button
Триггерящий элементЛК агента → «Профиль / Мои достижения»

Основной Поток

  1. Агент открывает экран личного профиля/достижений.
  2. Фронт вызывает GET /api/v1/my/agent-performance (../api/agent/get-my-agent-performance.md).
  3. Система читает личную проекцию: AgentUserId берётся из claims (не из query), проверяется permission rewards.performance.view-own. Все строки проекции AgentPerformanceEntity агента читаются по AgentUserId (композитный ключ (AgentUserId, CompanyId), вне tenant — платформенный scope агента) и агрегируются на чтении (SUM метрик).
  4. Возвращается AgentPerformanceModel: FixationsApprovedCount, DealsWonCount, DealsLostCount, CommissionAccruedTotal, CommissionPaidTotal (суммы по строкам), производный MembershipStatus (InAgency/Independent), ActiveAgencyCompanyIds, Achievements (вычислены над агрегатом), First/LastActivityAt, GeneratedAt = now; опционально — разбивка PerAgency (при IncludePerAgency=true). Чтение не меняет состояние и событий не публикует. Нет строк → нули + Independent + пустой список достижений (200, не 404).

Альтернативные Потоки

A1. Независимый агент после выхода из агентства

  1. Агент вышел из агентства (UC-IDN-010): identity опубликовал agent-membership-changed (Left); agent-membership-changed.md перевёл строку этого агентства в MembershipStatus = Left (прочие строки не тронуты). Если активных строк не осталось — производный MembershipStatus = Independent, ActiveAgencyCompanyIds = [].
  2. Чтение агрегирует все строки агента и возвращает те же накопленные FixationsApprovedCount/DealsWonCount/CommissionAccruedTotal и т.д. — метрики user-centric, SUM по строкам AgentUserId, при выходе строки не обнуляются (ADR-0061 п.5, ADR-0070 п.6). Историческая привязка сделок/лидов/фиксаций в crm не пересчитана (снапшоты иммутабельны, п.4).

A2. Агент вступил в другое агентство (в т.ч. одновременно с прежним)

  1. agent-membership-changed (Active) c новым CompanyId создаёт/активирует строку нового агентства → если у агента есть ≥ 1 активная строка, производный MembershipStatus = InAgency, ActiveAgencyCompanyIds содержит все активные CompanyId (с мульти-членством их может быть несколько, ADR-0070). Накопленные метрики прежних строк сохраняются; новые сделки/фиксации ложатся в строку своего агентства.

A3. Новичок без активности

  1. Ни одной строки проекции → 200 с нулевыми счётчиками и пустыми достижениями (не 404); UI показывает «Пока нет данных».

A4. Проекция отстаёт (лаг consumer-ов)

  1. Значения могут отставать на лаг (< 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УсловиеПоведение клиента
UNAUTHORIZED401Нет/просрочен JWT«Требуется авторизация»
FORBIDDEN403Нет 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)тест: агент в двух агентствах одновременно
AC11IncludePerAgency=truePerAgency из построчных метрик; по умолчанию — только агрегаттест разбивки по агентствам

Наблюдаемость и НФТ

  • Логи (без секретов/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); независимый агент (scope Platform) читает свою проекцию так же, как агент в агентстве (scope Organization); суммы Sensitive не логируются.

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

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

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