GET /api/agent/crm/statistics — Статистика агентства/агента
Статистика ЛК агента и руководителя агентства: активные сделки, показатели за период (from/to), успех/провал,
суммы завершённых сделок, число фиксаций и лидов, комиссии из rewards. Read-only агрегация по своим сущностям
(DealEntity, FixationRequestEntity,
LeadEntity) плюс сводка комиссий из контекста rewards (владелец расчёта — rewards).
Мутаций и событий не производит. Переключатель «свои / всё агентство» — scope=agent|agency.
Отдать одной моделью сводку воронки за выбранный период для дашборда ЛК: агенту — по себе (Agent), руководителю
агентства — по себе или по всему агентству/конкретному агенту (Agency).
Отчёт agency-centric
Метрика заякорена на текущийAgencyCompanyId актора и считается по record-level снапшотам атрибуции
OriginAgencyCompanyId/OriginAgentUserId — то есть только в границах текущего агентства и не
агрегирует работу агента в других компаниях (ADR-0061 п.5). Личная user-centric статистика агента за
всю карьеру (агрегация по AgentUserId сквозь все компании) — отдельный метод rewards
get-my-agent-performance.
crm.deals.view (читает также свои лиды/фиксации того же агентства)
Scope (RBAC)
организация (агентство): якорь — AgencyCompanyId из claims
Record-level
Agent — только свои (OriginAgentUserId = actor); Agency — всё агентство (опц. фильтр OriginAgentUserId), доступно только Руководителю
Tenant isolation
По OriginAgencyCompanyId (record-level). deals/fixations живут в tenant застройщика (ADR-0052) — чтения идут по явному предикату OriginAgencyCompanyId (не по авто-tenant-фильтру); leads тенантятся по TenantId = AgencyCompanyId штатно
Audit
read (не sensitive): суммы/комиссии в логи не пишутся
Дом полей — контекст rewards (расчёт/выплата комиссий); здесь — только форма проекции, полученной по
read-порту rewards. Money (owned VO) — контракт в deal.md.
Контекст и доступ. Операцию выполняет агент/руководитель агентства (permission crm.deals.view; иначе
CRM_STATISTICS_FORBIDDEN); якорь — AgencyCompanyId из claims.
Проверка входа и предусловий.
формат: Scope — известное значение enum (unknown → ошибка валидации); Period.From <= Period.To при
обоих заданных;
scope/принадлежность: Scope=Agency разрешён только Руководителю — иначе CRM_STATISTICS_SCOPE_FORBIDDEN;
при Scope=AgentOriginAgentUserId принудительно = actor.
Что читаем. Общий record-level предикат: OriginAgencyCompanyId = query.AgencyCompanyId (для leads —
TenantId = query.AgencyCompanyId) (+ OriginAgentUserId = effective при Agent/фильтре). Метрики —
агрегирующие проекции с группировкой по Status и (для сделок)
суммой цены; период по CreatedAt/DecidedAt/CompletedAt/CancelledAt, Active — без периода. Для
deals/fixations выборка кросс-tenant по застройщику идёт по явному предикату OriginAgencyCompanyId. Комиссии:
read-порт rewardsGetAgencyCommissionSummary(AgencyCompanyId, effectiveAgentUserId?, From, To) (timeout
3 c, retry 1); таймаут/ошибка → Commissions = null (деградация, не ошибка).
Маппинг. Свернуть grouped-строки в под-модели: ActiveCount = Σ по {Application,Booking,Sale};
CompletedCount/CancelledCount — по терминальным; CompletedAmount = Money(Σ price по Completed, CurrencyId = RUB) — суммирование корректно, пока все сделки в одной валюте (RUB-only, ADR-0048);
SuccessRate = Completed/(Completed+Cancelled) или null. Фиксации/лиды — по своим строкам. Commissions
— из ответа порта или null.
Результат. Сводная модель (Scope, AgencyCompanyId, OriginAgentUserId, Period, Deals, Fixations,
Leads, Commissions, GeneratedAt) → HTTP 200. Пустая воронка — 200 с нулевыми счётчиками (не 404).
Freshness — GeneratedAt; допустима микро-рассинхронизация между агрегатами и данными rewards.
Новые optional поля/метрики — additive (без breaking); удаление/переименование/смена типа поля или значения
enum (StatisticsScope) — breaking (migration + rollout). Unknown enum во входе — ValidationError. Формулы
метрик стабильны; изменение семантики — breaking.
Logs: agency-stats requested (AgencyCompanyId, Scope, OriginAgentUserId, период, rewardsAvailable; суммы/
комиссии — не в логи); metrics: crm_agency_stats_total{scope,result}, latency p95 (< 1 c, SLO чтений README),
crm_rewards_port_failures_total; traces — сквозной trace через traceparent; dashboard pin-crm; runbook —
docs/06-services/crm/runbooks/.
Happy path (агент). Given агент и Scope=Agent (default); When GET; Then 200 с метриками только по
ActorUserId; при доступном rewards — заполнен Commissions.
Всё агентство. Given руководитель и Scope=Agency; When GET; Then 200 по всему AgencyCompanyId (или
по одному OriginAgentUserId, если задан фильтр).
Scope запрещён. Given рядовой агент и Scope=Agency; When GET; Then 403 CRM_STATISTICS_SCOPE_FORBIDDEN.
Валидация периода. Given Period.From > Period.To; When GET; Then 400 VALIDATION_FAILED.
Деградация rewards. Given rewards недоступен (timeout read-порта); When GET; Then 200 с Commissions = null (деградация, не ошибка) — клиент показывает прочерк.
Пустая воронка. Given нет данных за период; When GET; Then 200 с нулевыми счётчиками (не 404).
Нет прав. Given отсутствует permission crm.deals.view; When GET; Then 403 CRM_STATISTICS_FORBIDDEN.
Деление на ноль. Given CompletedCount + CancelledCount == 0; When GET; Then SuccessRate = null.