UC-STA-003. Сравнение периодов и тренд просмотров
Назначение
Застройщик сравнивает внимание к своей недвижимости в двух периодах (текущий vs предыдущий/базовый) и
смотрит тренд — временной ряд с разницей в процентах (Δ%) по ключевым мерам (просмотры / уникальные
зрители / средняя длительность). По ADR-0056 (simplicity-first) сравнение — композиция канонических
метрик: две сводки get-views-summary.md (текущее и базовое окно) +
один тренд get-views-trend.md на общий диапазон; Δ% считается при
чтении, без отдельного хранилища. Модель фильтра — dev-stats-filter.md;
источник — property_object_view_daily. Контекст — dev-stats
(ADR-0057).
Metadata
| Поле | Значение |
|---|---|
| ID | UC-STA-003 |
| Контекст | dev-stats (см. docs/06-services/dev-stats/) |
| Тип | основной |
| Статус | draft |
Актор
| Роль | Permissions / scope | Tenant | Record-level |
|---|---|---|---|
Девелопер (застройщик) | dev-stats.view; scope строго = свой DeveloperId | TenantId = DeveloperId = CompanyEntity.Id (ADR-0052) | не применяется (агрегаты) |
Оба окна и ряд запрашиваются в scope застройщика: DeveloperId = актор — обязательный префикс каждого из
трёх чтений.
Предусловие
- Агрегат property_object_view_daily наполнен на глубину обоих периодов (в пределах хранения 3 года).
- Базовый период определяется UI: авто-сдвиг окна на равную длину назад (
предыдущий период) или явныеBaselineFrom/BaselineTo. - Токен застройщика содержит право
dev-stats.view.
Триггер
| Поле | Значение |
|---|---|
| Источник | UI (переключатель «Сравнить с предыдущим периодом» на дашборде) → API call |
| Триггерящий элемент | панель периода дашборда → 2×GET /views/summary + GET /views/timeseries |
Основной Поток
Полный контракт одного окна — get-views-summary.md (см. UC-STA-001); ниже — логика сравнения.
- Ввод. Застройщик задаёт текущий период (
DateFrom/DateTo) и включает сравнение; UI выводит базовое окно (BaselineFrom/BaselineTo) авто-сдвигом на ту же длину назад либо принимает явное. Фильтр иерархии (ProjectId/BuildingId/…) — общий для обоих окон. - Запрос. Три чтения: сводка текущего окна; сводка базового окна (тот же фильтр, другие
DateFrom/DateTo); тренд на объединённый диапазон[min(Baseline,Current)…max(Baseline,Current)].DeveloperId— из токена. - Проверка входа и доступа. Обе пары дат обязательны и
From ≤ To; окна не длиннее срока хранения; длины окон согласованы UI (для честногоΔ%— равные интервалы, иначе UI помечает несопоставимость);DeveloperId = актор(fail-closed). Нарушение —VALIDATION_ERROR; чужойDeveloperId/нет права —FORBIDDEN. - Чтение окон и тренда. Каждое чтение — из
property_object_view_dailyс одним и тем же срезом(DeveloperId, ProjectId, …); окна различаются только границамиEventDate(⇒ разные месячные партиции). Ряд тренда — тот же срез с группировкой по дате (или по началу недели/месяца). - Расчёт мер и
Δ%(устойчивый к дублям). На окно —Views,UniqViewers, средняя длительность =DurationSum / Views.Δ%по мере =(current − baseline) / baseline · 100с защитой от нуля: приbaseline = 0—null(«новый», не деление на ноль). Знак и величина — на каждую меру. Подсчёт устойчив к дублям и at-least-once-доставке. - Результат. Возвращается типизированный
PeriodComparison:Current: ViewsSummary,Baseline: ViewsSummary,Deltas: ViewsDelta(ViewsDeltaPercent,UniqViewersDeltaPercent,AvgDurationDeltaPercent),Trend: список ViewsTimeseriesPoint. Интерфейс показывает две колонки сводок + бейджиΔ%(рост/падение) + линию тренда с маркером границы периодов; несопоставимые длины окон → предупреждение (A2); пустой базовый период → A1.
Диаграмма
Согласованность трёх чтений
Сравнение — три независимых read-only чтения одной витрины (без синхронных вызовов соседей). Правила согласованности:
- Оба окна берутся из одного среза
(DeveloperId, ProjectId, …)и различаются лишь границамиEventDate/партициями — фильтр иерархии и сегмента идентичен, иначеΔ%несопоставим. - Каждое чтение — снапшот витрины на свой момент; идущая параллельно ingestion может добавить строки между чтениями (окно freshness ≤ 5 мин). Для текущего окна, включающего последние минуты, значения могут дорасти при повторе — это не рассинхрон, а eventual-consistency (обычно значим только для «сегодня»).
- Ряд тренда строится на объединённом диапазоне; бакеты (
day/week/month) выравниваются к началу периода (toMonday/toStartOfMonth) — граница между базовым и текущим окном ложится на границу бакета, маркер границы рисуется на этой точке.
Альтернативные Потоки
A1. Базовый период без данных
- Чтение базового окна вернуло нули (
Views = 0). Δ%по мерам =null(«новый»/«нет базы») — HTTP200; UI показывает рост как «новое», без деления на ноль.
Завершается: сравнение отрисовано с пометкой отсутствия базы.
A2. Несопоставимые длины окон
- Длина текущего и базового окна различается (
|Current| ≠ |Baseline|). Δ%считается, но ответ помечаетIsComparable = false; UI выводит предупреждение о некорректности прямого сравнения абсолютов (рекомендует нормировать на длину/выбрать равные окна).
Возвращается к основному потоку на шаг 6 (результат с предупреждением).
A3. Сравнение одним запросом на стороне сервиса (deferred, additive)
- Целевое упрощение: одна операция сравнения с обоими окнами читает
property_object_view_dailyпо объединённому диапазону и разбивает по окну, возвращая обе сводки +Δ%за один вызов. - Отложено: на старте достаточно композиции (шаги 2–5); отдельная операция сравнения — additive, не меняет контракты существующих метрик.
A4. Базовое окно частично за пределом хранения (клиппинг)
- Авто-сдвиг базового окна уводит его начало за глубину хранения (старше 3 лет — TTL фактов).
- Часть базового окна вне TTL уже вычищена: в ней нули. Если само окно шире TTL (
To − From > 3 года) —VALIDATION_ERROR(INV-DSF-3). Если окно ≤ TTL, но начало упирается в старую границу — база считается по доступным дням; UI помечает базу как усечённую (сравнение возможно, но неполное).
Возвращается к основному потоку на шаг 6 (результат с пометкой усечения базы).
Постусловие
- Мутаций нет (read-only): агрегаты/факты/измерения не изменяются, доменные и интеграционные (Kafka) события не публикуются, аудит не пишется.
- Клиент получил две сводки,
Δ%по мерам и линию тренда в scope своегоDeveloperId.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
VALIDATION_ERROR | 400 | Отсутствует пара дат окна; From > To; окно длиннее срока хранения | Показать ошибку периодов |
FORBIDDEN | 403 | DeveloperId ≠ актор; нет dev-stats.view | Скрыть данные |
| — (не ошибка) | 200 | Базовый период пуст → Δ% = null (A1); несопоставимые окна → IsComparable=false (A2) | Пометка «новое»/предупреждение |
5xx (ProblemDetails) | 500/503 | Отказ аналитической витрины pin_dev_stats | Ретрай позже |
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Сравнение — композиция 2×summary + timeseries без отдельного хранилища (ADR-0056) | Тест: три канонических чтения дают согласованные Current/Baseline/Trend |
| AC2 | Δ% = (current − baseline)/baseline·100; baseline=0 → null (без деления на ноль) | Тест A1: пустая база → Δ%=null |
| AC3 | Оба окна читаются одним срезом, различаются лишь границами EventDate/партициями | Тест плана: смена окна не меняет срез чтения |
| AC4 | Несопоставимые длины окон помечаются IsComparable=false | Тест A2 |
| AC5 | Все чтения устойчивы к дублям и в scope DeveloperId=актор | Тест изоляции + устойчивости к дублям |
| AC6 | Тренд поддерживает granularity day/week/month на объединённом диапазоне | Тест: единый ряд с маркером границы периодов |
| AC7 | Ответ типизирован (PeriodComparison/ViewsDelta) | Ревью контракта ответа |
| AC8 | Бакеты тренда выровнены к началу периода; граница окон ложится на границу бакета | Тест: маркер границы совпадает с toMonday/toStartOfMonth |
| AC9 | Базовое окно шире TTL → VALIDATION_ERROR; частично за TTL → усечение с пометкой (A4) | Тест A4: окно за пределом хранения |
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (1): UC-STA-001. Застройщик смотрит статистику проекта (период + проект)
- index.md (1): DevStats Service (Pin.DevStats)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- Контракты метрик окна: get-views-summary.md, get-views-trend.md; композиция ЛК — dev-stats-overview.md.
- Домены: dev-stats-filter.md,
response-models.md (
PeriodComparison/ViewsDelta). - Источник: property-object-view-daily-rollup.md (меры/агрегат); карта областей — README.md.
- Смежные use-cases: UC-STA-001 (сводка/ряд одного окна), UC-STA-002 (детализация среза).
- ADR: 0057 dev-stats/ClickHouse, 0056 simplicity-first, 0052 tenancy.