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

GET /api/developer/crm/statistics — Статистика ЛК застройщика

Сводная статистика личного кабинета застройщика по его сделкам: воронка по системным группам этапов, разбивка по статусам, суммы, временные ряды по периодам и конверсия «заявка → продажа». Read-only агрегация агрегата DealEntity с фильтрами по проекту/зданию/периоду. Разграничение — TenantId = DeveloperCompanyId (ADR-0052): застройщик видит только агрегаты своих сделок. View-статистика витрины (показы/просмотры объектов недвижимости, воронка визитов) здесь не считается — это отдельный контекст dev-stats на ClickHouse (ADR-0057). Мутаций и событий не производит.

Назначение

Отдать менеджеру застройщика числовую сводку для дашборда ЛК: сколько сделок в каждой группе воронки, сколько активных/продаж/отказов, суммы стоимостей объектов недвижимости, помесячную динамику создания сделок и продаж, а также конверсию заявка→продажа — за выбранный период и, опционально, по конкретному проекту/зданию.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (Менеджер застройщика); Менеджер ПИН — platform-scope
Auth policy / Permissionscrm.deals.view (JWT identity) — статистика читает свои сделки
Scope (RBAC)организация (застройщик) — TenantId = DeveloperCompanyId
Record-levelне применяется: агрегаты по всем сделкам своего TenantId (лиды/клиентскую базу агентства застройщик не видит)
Tenant isolationTenantId в каждом запросе (= DeveloperCompanyId); разграничение применяется автоматически
Auditread (не sensitive): результат — только счётчики/суммы, PII клиентов не возвращается и не пишется в логи
Доступ

Разграничение — только по TenantId = DeveloperCompanyId (ADR-0052): застройщик получает агрегаты по всем сделкам своего tenant без record-level фильтра. Лиды и клиентскую базу агентства (владельца заявки) застройщик не видит — доступны только сделки, созданные в его tenant.

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired, != Guid.Empty (DeveloperCompanyId)Tenant isolation
ProjectIdqueryGuid?Нет!= Guid.Empty; несуществующий/чужой → пустые агрегаты (scope+filter), без 404
BuildingIdqueryGuid?Нет!= Guid.Empty; здание/литер проекта
FromqueryDateTimeOffset?Нетнижняя граница окна по CreatedAt; default — начало месяца 12 мес. назад
ToqueryDateTimeOffset?Нетверхняя граница (эксклюзивно); default — текущий момент; From <= To; окно <= 3 лет

Окно [From, To) задаёт и фильтр «за период», и диапазон помесячных бакетов Periods. ProjectId/BuildingId — комбинируемые предикаты (AND) поверх окна.

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

Единый container-model: агрегаты ограниченной кардинальности (7 групп воронки, десятки месячных бакетов), пагинация не нужна (ср. list-steps.md). Три именованные части: сводка, воронка, помесячные ряды. Все суммы — MoneyView (Amount, CurrencyCode, Formatted); enum — строкой. «Дом» полей сделки — deal.md.

DeveloperStatisticsModel (container)

ПолеТипОбязательностьИсточникОписание
SummaryDeveloperStatisticsSummaryModelДаагрегацияИтоговые счётчики, суммы и конверсия за окно.
FunnelIReadOnlyList<DeveloperFunnelStageModel>ДаGROUP BY StepGroupВоронка по 7 системным группам; группы без сделок — с нулями (полная шкала).
PeriodsIReadOnlyList<DeveloperPeriodBucketModel>ДаGROUP BY месяц CreatedAtПомесячная динамика в окне [From, To); месяцы без сделок — с нулями.

DeveloperStatisticsSummaryModel

ПолеТипОбязательностьИсточникОписание
TotalDealsintДаcount(*) окнаВсего сделок, созданных в окне (после фильтров).
ActiveDealsintДаcomputedНетерминальные: Status ∉ {Completed, Cancelled}.
SalesCountintДаcomputedДошедшие до продажи: группы {Contract, Payment, Registration, Completed} (DealStatus.Sale/Completed).
CompletedDealsintДаcomputedStatus = Completed (продажа завершена).
CancelledDealsintДаcomputedStatus = Cancelled (отказы).
TotalAmountMoneyViewДаsum(Price.Amount) окнаСумма стоимостей объектов недвижимости всех сделок окна.
SalesAmountMoneyViewДаcomputedСумма стоимостей объектов недвижимости сделок-продаж (SalesCount).
RequestToSaleConversiondecimalДаcomputedКонверсия заявка→продажа = SalesCount / TotalDeals, 0..1; 0 при TotalDeals = 0.

DeveloperFunnelStageModel (снимок воронки по группе этапов)

ПолеТипОбязательностьИсточникMaskingОписание
GroupPipelineStepGroupДаDealEntity.StepGroup (enum строкой)Системная группа: Application/Booking/Contract/Payment/Registration/Completed/Cancelled (pipeline-step.md).
MappedStatusDealStatusДамаппинг группа→статус (enum строкой)Соответствующий статус сделки (deal.md, таблица групп).
DealsCountintДаcount(*) где StepGroup = GroupЧисло сделок в группе (текущее положение на доске).
AmountMoneyViewДаsum(Price.Amount) группыСумма стоимостей объектов недвижимости в группе.

DeveloperPeriodBucketModel (помесячный бакет)

ПолеТипОбязательностьИсточникMaskingОписание
PeriodStartDateOnlyДаdate_trunc('month', CreatedAt) (UTC)Первый день месяца-бакета.
DealsCreatedintДаcount(*) бакетаСделок создано в месяце.
SalesCountintДаcomputedИз них дошли до продажи (Status ∈ {Sale, Completed}, cohort по CreatedAt).
AmountMoneyViewДаsum(Price.Amount) бакетаСумма стоимостей объектов недвижимости, созданных в месяце.

Воронка — снимок текущего положения сделок по группам (не исторический проход по этапам). MappedStatus — денормализация из StepGroup (без join на pipeline_steps; deal.md).

Алгоритм

Операция только читает — шагов записи, событий и транзакции нет.

  1. Контекст и доступ. Выполняет менеджер застройщика (User) в своём tenant (TenantId = DeveloperCompanyId); требуется permission crm.deals.view, иначе CRM_STATISTICS_FORBIDDEN. Параметры окна и фильтров — ProjectId?/BuildingId?/From?/To?.
  2. Проверка входа. Дефолты окна проставляются до валидации: To ??= now, From ??= начало месяца (To − 12 мес.). Затем: From <= To; окно To − From <= 3 лет (ограничение числа бакетов); ProjectId/BuildingId != Guid.Empty при заданных — иначе VALIDATION_FAILED. Существование ProjectId/BuildingId не проверяется — это фильтры агрегации (несуществующий/чужой → нулевые агрегаты, без 404). Scope — только tenant (застройщик видит агрегаты своих сделок); record-level не применяется.
  3. Что читаем. Сделки застройщика в окне CreatedAt >= From AND CreatedAt < To, при заданных — дополнительно по ProjectId и BuildingId (предикаты AND). Разграничение по TenantId = DeveloperCompanyId и отсечение soft-deleted применяются автоматически. Поверх одного набора данных (единый snapshot) считаются две группировки:
    • воронка — группировка по StepGroup: число сделок и сумма Price.Amount в каждой группе;
    • периоды — группировка по месяцу CreatedAt: создано сделок, из них продаж (Status ∈ {Sale, Completed}), сумма Price.Amount. Опорный индекс — (tenant_id, status, created_at desc) (deal.md, «Индексы»); StepGroup — денормализованная колонка (join на pipeline_steps не нужен).
  4. Бизнес-правила и маппинг. Переходов состояний нет. Funnel дополняется недостающими группами с нулями (полная шкала 7 групп по Group.Order); Periods — недостающими месяцами окна с нулями (непрерывный ряд). Summary вычисляется из воронки: TotalDeals = Σ DealsCount; SalesCount = Σ групп {Contract, Payment, Registration, Completed}; CompletedDeals/CancelledDeals — соответствующие группы; ActiveDeals = TotalDeals − CompletedDeals − CancelledDeals; TotalAmount = Σ Amount; SalesAmount = Σ амаунтов sale-групп; RequestToSaleConversion = TotalDeals > 0 ? SalesCount / TotalDeals : 0. MappedStatus — по таблице групп (pipeline-step.md). Enum — строкой, суммы — MoneyView.
  5. Результат. DeveloperStatisticsModel { Summary, Funnel, Periods } → HTTP 200. Пустой период/проект без сделок — 200 с нулевыми агрегатами (полная шкала групп и месяцев), не 404.

Диаграмма

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

ТипДетали
Integration eventsnone (read-only)
External callsnone (агрегация по своей схеме crm; Price/ProjectId/BuildingId — snapshot из chessboard на момент создания сделки)
Cache / projections / searchnone (прямой GROUP BY в PostgreSQL); view-статистика витрины — контекст dev-stats/ClickHouse (ADR-0057), здесь не читается
Notificationsnone

Ошибки и failure modes

Ошибки

Один код VALIDATION_FAILED покрывает три независимых условия входа: обратный период (From > To), окно шире 3 лет и Guid.Empty в ProjectId/BuildingId — детали см. в таблице ниже.

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400From > To, окно > 3 лет, Guid.Empty в ProjectId/BuildingId«Проверьте параметры запроса»no retry
UNAUTHORIZED401нет/просрочен JWT«Требуется авторизация»no retry
CRM_STATISTICS_FORBIDDEN403нет permission crm.deals.view«Недостаточно прав»no retry
Пустой результат — не ошибка

Несуществующий/чужой ProjectId/BuildingId и период без сделок — не 404: ответ 200 с нулевыми агрегатами и полной шкалой групп/месяцев (см. Acceptance criteria).

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

  • Новые optional поля ответа/фильтры — additive (без breaking); удаление/переименование/смена типа поля или значения enum DealStatus/PipelineStepGroup — breaking (migration + rollout). Unknown enum во входе — ValidationError (не «тихий» фильтр).
  • Logs: developer statistics computed (TenantId, окно, набор фильтров, TotalDeals; PII/суммы отдельных сделок в логи не пишутся); metrics: crm_developer_statistics_total{result}, latency p95 (< 1 c, SLO агрегаций); traces — OTel, correlation через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь).

Acceptance criteria

  • Given у застройщика есть сделки в окне [From, To), When менеджер запрашивает статистику без фильтров, Then 200 со Summary (счётчики/суммы/конверсия), Funnel (полная шкала из 7 системных групп, отсутствующие — с нулями) и Periods (непрерывный помесячный ряд, месяцы без сделок — с нулями).
  • Given в окне/по фильтру сделок нет, When GET, Then 200 с нулевыми агрегатами и полной шкалой групп/месяцев (не 404, не пустой ответ).
  • Given TotalDeals = 0, When вычисляется конверсия, Then RequestToSaleConversion = 0 (без деления на ноль).
  • Given ProjectId/BuildingId несуществующий или чужой, When GET, Then 200 с нулевыми агрегатами (это фильтр, а не сущность — без 404).
  • Given From > To или окно > 3 лет, When GET, Then 400 VALIDATION_FAILED.
  • Given параметры окна не переданы, When GET, Then дефолт: To = now, From = начало месяца (To − 12 мес.); результат — за этот период.
  • Given актор без permission crm.deals.view, When GET, Then 403 CRM_STATISTICS_FORBIDDEN.

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

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

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

  • Сущности: deal.md (дом полей, DealStatus, Money, индекс (tenant_id,status,created_at desc)), pipeline-step.md (PipelineStepGroup, маппинг группа→статус).
  • Смежные операции: list-deals.md (детальный список сделок с теми же фильтрами), list-steps.md (доска воронки); события — deal-created.md, deal-completed.md.
  • Контекст: README; tenancy — ADR-0052; view-статистика витрины — ADR-0057.