GET /api/developer/crm/statistics — Статистика ЛК застройщика
Сводная статистика личного кабинета застройщика по его сделкам: воронка по системным группам этапов,
разбивка по статусам, суммы, временные ряды по периодам и конверсия «заявка → продажа». Read-only агрегация
агрегата DealEntity с фильтрами по проекту/зданию/периоду.
Разграничение — TenantId = DeveloperCompanyId (ADR-0052): застройщик видит только
агрегаты своих сделок. View-статистика витрины (показы/просмотры объектов недвижимости, воронка визитов) здесь не
считается — это отдельный контекст dev-stats на ClickHouse
(ADR-0057). Мутаций и событий не
производит.
Назначение
Отдать менеджеру застройщика числовую сводку для дашборда ЛК: сколько сделок в каждой группе воронки, сколько активных/продаж/отказов, суммы стоимостей объектов недвижимости, помесячную динамику создания сделок и продаж, а также конверсию заявка→продажа — за выбранный период и, опционально, по конкретному проекту/зданию.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | developer |
| Feature group | statistics |
| Статус | draft |
| Документ | docs/06-services/crm/api/developer/get-statistics.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Менеджер застройщика); Менеджер ПИН — platform-scope |
| Auth policy / Permissions | crm.deals.view (JWT identity) — статистика читает свои сделки |
| Scope (RBAC) | организация (застройщик) — TenantId = DeveloperCompanyId |
| Record-level | не применяется: агрегаты по всем сделкам своего TenantId (лиды/клиентскую базу агентства застройщик не видит) |
| Tenant isolation | TenantId в каждом запросе (= DeveloperCompanyId); разграничение применяется автоматически |
| Audit | read (не sensitive): результат — только счётчики/суммы, PII клиентов не возвращается и не пишется в логи |
Разграничение — только по TenantId = DeveloperCompanyId (ADR-0052): застройщик получает агрегаты по
всем сделкам своего tenant без record-level фильтра. Лиды и клиентскую базу агентства (владельца заявки)
застройщик не видит — доступны только сделки, созданные в его tenant.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/developer/crm/statistics |
| Success status | 200 |
| Idempotency header | не применяется (read-only, безопасный метод) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required, != Guid.Empty (DeveloperCompanyId) | Tenant isolation |
ProjectId | query | Guid? | Нет | != Guid.Empty; несуществующий/чужой → пустые агрегаты (scope+filter), без 404 | — |
BuildingId | query | Guid? | Нет | != Guid.Empty; здание/литер проекта | — |
From | query | DateTimeOffset? | Нет | нижняя граница окна по CreatedAt; default — начало месяца 12 мес. назад | — |
To | query | DateTimeOffset? | Нет | верхняя граница (эксклюзивно); 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)
| Поле | Тип | Обязательность | Источник | Описание |
|---|---|---|---|---|
Summary | DeveloperStatisticsSummaryModel | Да | агрегация | Итоговые счётчики, суммы и конверсия за окно. |
Funnel | IReadOnlyList<DeveloperFunnelStageModel> | Да | GROUP BY StepGroup | Воронка по 7 системным группам; группы без сделок — с нулями (полная шкала). |
Periods | IReadOnlyList<DeveloperPeriodBucketModel> | Да | GROUP BY месяц CreatedAt | Помесячная динамика в окне [From, To); месяцы без сделок — с нулями. |
DeveloperStatisticsSummaryModel
| Поле | Тип | Обязательность | Источник | Описание |
|---|---|---|---|---|
TotalDeals | int | Да | count(*) окна | Всего сделок, созданных в окне (после фильтров). |
ActiveDeals | int | Да | computed | Нетерминальные: Status ∉ {Completed, Cancelled}. |
SalesCount | int | Да | computed | Дошедшие до продажи: группы {Contract, Payment, Registration, Completed} (DealStatus.Sale/Completed). |
CompletedDeals | int | Да | computed | Status = Completed (продажа завершена). |
CancelledDeals | int | Да | computed | Status = Cancelled (отказы). |
TotalAmount | MoneyView | Да | sum(Price.Amount) окна | Сумма стоимостей объектов недвижимости всех сделок окна. |
SalesAmount | MoneyView | Да | computed | Сумма стоимостей объектов недвижимости сделок-продаж (SalesCount). |
RequestToSaleConversion | decimal | Да | computed | Конверсия заявка→продажа = SalesCount / TotalDeals, 0..1; 0 при TotalDeals = 0. |
DeveloperFunnelStageModel (снимок воронки по группе этапов)
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Group | PipelineStepGroup | Да | DealEntity.StepGroup (enum строкой) | — | Системная группа: Application/Booking/Contract/Payment/Registration/Completed/Cancelled (pipeline-step.md). |
MappedStatus | DealStatus | Да | маппинг группа→статус (enum строкой) | — | Соответствующий статус сделки (deal.md, таблица групп). |
DealsCount | int | Да | count(*) где StepGroup = Group | — | Число сделок в группе (текущее положение на доске). |
Amount | MoneyView | Да | sum(Price.Amount) группы | — | Сумма стоимостей объектов недвижимости в группе. |
DeveloperPeriodBucketModel (помесячный бакет)
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
PeriodStart | DateOnly | Да | date_trunc('month', CreatedAt) (UTC) | — | Первый день месяца-бакета. |
DealsCreated | int | Да | count(*) бакета | — | Сделок создано в месяце. |
SalesCount | int | Да | computed | — | Из них дошли до продажи (Status ∈ {Sale, Completed}, cohort по CreatedAt). |
Amount | MoneyView | Да | sum(Price.Amount) бакета | — | Сумма стоимостей объектов недвижимости, созданных в месяце. |
Воронка — снимок текущего положения сделок по группам (не исторический проход по этапам). MappedStatus
— денормализация из StepGroup (без join на pipeline_steps; deal.md).
Алгоритм
Операция только читает — шагов записи, событий и транзакции нет.
- Контекст и доступ. Выполняет менеджер застройщика (
User) в своём tenant (TenantId = DeveloperCompanyId); требуется permissioncrm.deals.view, иначеCRM_STATISTICS_FORBIDDEN. Параметры окна и фильтров —ProjectId?/BuildingId?/From?/To?. - Проверка входа. Дефолты окна проставляются до валидации:
To ??= now,From ??= начало месяца (To − 12 мес.). Затем:From <= To; окноTo − From <= 3 лет(ограничение числа бакетов);ProjectId/BuildingId != Guid.Emptyпри заданных — иначеVALIDATION_FAILED. СуществованиеProjectId/BuildingIdне проверяется — это фильтры агрегации (несуществующий/чужой → нулевые агрегаты, без 404). Scope — только tenant (застройщик видит агрегаты своих сделок); record-level не применяется. - Что читаем. Сделки застройщика в окне
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не нужен).
- воронка — группировка по
- Бизнес-правила и маппинг. Переходов состояний нет.
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. - Результат.
DeveloperStatisticsModel { Summary, Funnel, Periods }→ HTTP200. Пустой период/проект без сделок —200с нулевыми агрегатами (полная шкала групп и месяцев), не 404.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none (read-only) |
| External calls | none (агрегация по своей схеме crm; Price/ProjectId/BuildingId — snapshot из chessboard на момент создания сделки) |
| Cache / projections / search | none (прямой GROUP BY в PostgreSQL); view-статистика витрины — контекст dev-stats/ClickHouse (ADR-0057), здесь не читается |
| Notifications | none |
Ошибки и failure modes
Один код VALIDATION_FAILED покрывает три независимых условия входа: обратный период (From > To), окно
шире 3 лет и Guid.Empty в ProjectId/BuildingId — детали см. в таблице ниже.
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | From > To, окно > 3 лет, Guid.Empty в ProjectId/BuildingId | «Проверьте параметры запроса» | no retry |
UNAUTHORIZED | 401 | нет/просрочен JWT | «Требуется авторизация» | no retry |
CRM_STATISTICS_FORBIDDEN | 403 | нет 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; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/(будущий путь).
Acceptance criteria
- Given у застройщика есть сделки в окне
[From, To), When менеджер запрашивает статистику без фильтров, Then200соSummary(счётчики/суммы/конверсия),Funnel(полная шкала из 7 системных групп, отсутствующие — с нулями) иPeriods(непрерывный помесячный ряд, месяцы без сделок — с нулями). - Given в окне/по фильтру сделок нет, When GET, Then
200с нулевыми агрегатами и полной шкалой групп/месяцев (не404, не пустой ответ). - Given
TotalDeals = 0, When вычисляется конверсия, ThenRequestToSaleConversion = 0(без деления на ноль). - Given
ProjectId/BuildingIdнесуществующий или чужой, When GET, Then200с нулевыми агрегатами (это фильтр, а не сущность — без404). - Given
From > Toили окно> 3 лет, When GET, Then400 VALIDATION_FAILED. - Given параметры окна не переданы, When GET, Then дефолт:
To = now,From = начало месяца (To − 12 мес.); результат — за этот период. - Given актор без permission
crm.deals.view, When GET, Then403 CRM_STATISTICS_FORBIDDEN.
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: 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.