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

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.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areaagent
Feature groupstatistics
Статусdraft
Документdocs/06-services/crm/api/agent/get-statistics.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (Агент или Руководитель агентства)
Auth policy / Permissionscrm.deals.view (читает также свои лиды/фиксации того же агентства)
Scope (RBAC)организация (агентство): якорь — AgencyCompanyId из claims
Record-levelAgent — только свои (OriginAgentUserId = actor); Agency — всё агентство (опц. фильтр OriginAgentUserId), доступно только Руководителю
Tenant isolationПо OriginAgencyCompanyId (record-level). deals/fixations живут в tenant застройщика (ADR-0052) — чтения идут по явному предикату OriginAgencyCompanyId (не по авто-tenant-фильтру); leads тенантятся по TenantId = AgencyCompanyId штатно
Auditread (не sensitive): суммы/комиссии в логи не пишутся

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/agent/crm/statistics
Success status200
Idempotency headerне применяется (безопасный GET)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
AgencyCompanyIdidentity claimsGuidДаRequired (агентство актора)Record-level якорь
ActorUserIdidentity claimsGuidДаRequiredRecord-level
ScopequeryStatisticsScopeНетenum строкой Agent/Agency (default Agent); Agency — только Руководитель, иначе CRM_STATISTICS_SCOPE_FORBIDDEN
OriginAgentUserIdqueryGuid?Неттолько при Scope=Agency (фильтр по агенту-инициатору); при Agent игнорируется (принудительно = actor)
PeriodqueryNullableRange<DateTimeOffset>Нет{From, To}; From <= To при обоих; From=null — с начала времён, To=null — по now

Enum StatisticsScope

ЗначениеОтображениеОписание
AgentСвоиПоказатели только по ActorUserId (режим по умолчанию, доступен всем).
AgencyВсё агентствоПоказатели по всему AgencyCompanyId (опц. по одному OriginAgentUserId); только Руководитель.

Модель ответа AgencyStatisticsModel

Именованный контейнер: сводные под-модели (без коллекций). Поля-метрики — производные (computed) агрегации; дом полей исходных сущностей — в domain-доках (здесь только форма отчёта).

ПолеТипОбязательностьИсточникMaskingОписание
ScopeStatisticsScope (enum→string)Даecho применённого режимаAgent/Agency.
AgencyCompanyIdGuidДаclaimsАгентство отчёта.
OriginAgentUserIdGuid?НетAgentactor; Agency → фильтр или nullАгент-инициатор отчёта; null — по всему агентству.
PeriodNullableRange<DateTimeOffset>Нетecho {From, To} (To — эффективный или now)Период отчёта.
DealsDealStatisticsModelДаагрегация crm.dealsСделки.
FixationsFixationStatisticsModelДаагрегация crm.fixation_requestsФиксации.
LeadsLeadStatisticsModelДаагрегация crm.leadsЛиды.
CommissionsCommissionSummaryModel?Нетrewardsсуммы не в логиКомиссии; nullrewards недоступен (деградация, не ошибка).
GeneratedAtDateTimeOffsetДаnowМомент расчёта (freshness).

DealStatisticsModel

Проекция DealEntity (статусы DealStatus, VO Money).

ПолеТипОбязательностьИсточникОписание
ActiveCountintДаStatus ∈ {Application,Booking,Sale} на момент запросаАктивные сделки (нетерминальные).
CreatedCountintДаCreatedAt ∈ [Period.From, Period.To]Заведено сделок за период.
CompletedCountintДаStatus=Completed, CompletedAt ∈ [Period.From, Period.To]Успех (продажа завершена).
CancelledCountintДаStatus=Cancelled, CancelledAt ∈ [Period.From, Period.To]Провал (отказ).
CompletedAmountMoney (owned VO)ДаSUM(Deal.Price) по Completed за периодСумма завершённых (Amount + CurrencyId — справочник валют handbook; RUB-only, ADR-0048).
SuccessRatedecimal?НетCompleted/(Completed+Cancelled)Доля успеха; null, если знаменатель 0.

FixationStatisticsModel

Проекция FixationRequestEntity (статусы FixationStatus).

ПолеТипОбязательностьИсточникОписание
TotalCountintДаCreatedAt ∈ [Period.From, Period.To]Подано фиксаций за период.
PendingCountintДаStatus=Pending на момент запросаОжидают решения.
ApprovedCountintДаStatus ∈ {FixedUnique,FixedNonUnique}, DecidedAt ∈ [Period.From, Period.To]Одобрено за период.
RejectedCountintДаStatus=Rejected, DecidedAt ∈ [Period.From, Period.To]Отклонено за период.

LeadStatisticsModel

Проекция LeadEntity (статусы LeadStatus).

ПолеТипОбязательностьИсточникОписание
TotalCountintДаCreatedAt ∈ [Period.From, Period.To]Заведено лидов за период.
ActiveCountintДаStatus=Active на момент запросаАктивные лиды.

CommissionSummaryModel

Дом полей — контекст rewards (расчёт/выплата комиссий); здесь — только форма проекции, полученной по read-порту rewards. Money (owned VO) — контракт в deal.md.

ПолеТипОбязательностьИсточникОписание
AccruedAmountMoneyДаrewardsНачислено за период.
PaidAmountMoneyДаrewardsВыплачено за период.
PendingAmountMoneyДаrewardsВ ожидании выплаты.

Алгоритм

  1. Контекст и доступ. Операцию выполняет агент/руководитель агентства (permission crm.deals.view; иначе CRM_STATISTICS_FORBIDDEN); якорь — AgencyCompanyId из claims.
  2. Проверка входа и предусловий.
    • формат: Scope — известное значение enum (unknown → ошибка валидации); Period.From <= Period.To при обоих заданных;
    • scope/принадлежность: Scope=Agency разрешён только Руководителю — иначе CRM_STATISTICS_SCOPE_FORBIDDEN; при Scope=Agent OriginAgentUserId принудительно = actor.
  3. Что читаем. Общий record-level предикат: OriginAgencyCompanyId = query.AgencyCompanyId (для leadsTenantId = query.AgencyCompanyId) (+ OriginAgentUserId = effective при Agent/фильтре). Метрики — агрегирующие проекции с группировкой по Status и (для сделок) суммой цены; период по CreatedAt/DecidedAt/CompletedAt/CancelledAt, Active — без периода. Для deals/fixations выборка кросс-tenant по застройщику идёт по явному предикату OriginAgencyCompanyId. Комиссии: read-порт rewards GetAgencyCommissionSummary(AgencyCompanyId, effectiveAgentUserId?, From, To) (timeout 3 c, retry 1); таймаут/ошибка → Commissions = null (деградация, не ошибка).
  4. Маппинг. Свернуть grouped-строки в под-модели: ActiveCount = Σ по {Application,Booking,Sale}; CompletedCount/CancelledCount — по терминальным; CompletedAmount = Money(Σ price по Completed, CurrencyId = RUB) — суммирование корректно, пока все сделки в одной валюте (RUB-only, ADR-0048); SuccessRate = Completed/(Completed+Cancelled) или null. Фиксации/лиды — по своим строкам. Commissions — из ответа порта или null.
  5. Результат. Сводная модель (Scope, AgencyCompanyId, OriginAgentUserId, Period, Deals, Fixations, Leads, Commissions, GeneratedAt) → HTTP 200. Пустая воронка — 200 с нулевыми счётчиками (не 404). Freshness — GeneratedAt; допустима микро-рассинхронизация между агрегатами и данными rewards.

Диаграмма

Побочные эффекты

ТипДетали
Интеграционные событияnone (read-only)
Внешние вызовыrewards read-порт GetAgencyCommissionSummary (timeout 3 c, retry 1; сбой → Commissions = null)
Проекции / поискnone (прямые агрегаты PostgreSQL по индексам ix_deals_origin_agency_company_id/ix_deals_origin_agent_user_id deals, ix_fixation_requests_origin_agency_company_id, ix_leads_tenant_id)
Уведомленияnone

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400неизвестный Scope; Period.From > Period.To«Проверьте параметры запроса»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
CRM_STATISTICS_FORBIDDEN403нет permission crm.deals.view«Недостаточно прав»no retry
CRM_STATISTICS_SCOPE_FORBIDDEN403рядовой агент запросил Scope=Agency (всё агентство)«Просмотр статистики агентства доступен руководителю»no retry
Деградация rewards — не ошибка

rewards недоступен (timeout/сбой read-порта) — не ошибка: 200 с Commissions = null (клиент показывает прочерк).

Совместимость и наблюдаемость

  • Новые 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/.

Acceptance Criteria

  • 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.

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

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

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