UC-STA-005. Экспорт отчёта статистики (Excel / PDF) с аудитом
Назначение
Застройщик выгружает отчёт по статистике внимания к своей недвижимости за период (сводка / ряд / разбивки по
иерархии / воронка) в Excel (XLSX), CSV или PDF. Выгрузка — аудируемая операция: фиксируется
актор + фильтр + формат + время (обязательное требование права dev-stats.export, см.
README.md §Безопасность). Данные не мутируются — читаются те же агрегаты, что и в метриках;
единственный побочный эффект помимо файла — audit-запись. Контракт операции —
dev-stats-export.md; модель фильтра —
dev-stats-filter.md. Контекст — dev-stats (ADR-0057).
Metadata
| Поле | Значение |
|---|---|
| ID | UC-STA-005 |
| Контекст | dev-stats (см. docs/06-services/dev-stats/) |
| Тип | основной |
| Статус | draft |
Актор
| Роль | Permissions / scope | Tenant | Record-level |
|---|---|---|---|
Девелопер (застройщик) | dev-stats.export (сильнее dev-stats.view); scope строго = свой DeveloperId | TenantId = DeveloperId = CompanyEntity.Id (ADR-0052) | не применяется — выгружаются только агрегаты (не персоны) |
Выгрузка возможна лишь для своего DeveloperId (fail-closed); отсутствие dev-stats.export при наличии
dev-stats.view → 403 (A3) — просмотр в UI разрешён, экспорт нет.
Предусловие
- Агрегаты property_object_view_daily / funnel_daily наполнены на глубину периода (в пределах хранения 3 года).
- Аналитическая витрина
pin_dev_statsдоступна; агрегаты отражают события апстрима с задержкой ≤ 5 мин (freshness) — выгрузка показывает состояние агрегатов на момент генерации, а не «живой» real-time срез (детали окна согласованности — § «Межсервисная согласованность и повторные вызовы»). - Токен застройщика содержит право
dev-stats.export. - Доступен платформенный audit-канал (структурированный security/audit-канал) для фиксации выгрузки.
- Число одновременно выполняемых выгрузок застройщика не превышает лимит контракта операции (иначе — A4).
Триггер
| Поле | Значение |
|---|---|
| Источник | UI (кнопка «Экспорт» на дашборде с текущим фильтром) → API call |
| Триггерящий элемент | панель дашборда /developer/stats → GET /api/v1/dev-stats/export?format=xlsx |
Основной Поток
Полный контракт операции — dev-stats-export.md; ниже — сценарий выгрузки с аудитом.
-
Ввод. Застройщик на дашборде (с текущим
DevStatsFilterModel) жмёт «Экспорт», выбираетformat(Xlsx/Csv/Pdf) и состав секций (summary/timeseries/breakdowns/funnel). -
Запрос. Экспорт за период/срез с выбранными форматом и секциями;
DeveloperId— из токена. -
Проверка входа и авторизация. По порядку, при первом нарушении — соответствующий отказ:
- право: требуется
dev-stats.export(fail-closed, A3); одногоdev-stats.viewнедостаточно →FORBIDDEN; - scope:
DeveloperId = актор(значение берётся из токена, не из UI); клиентский чужойDeveloperId→FORBIDDEN; - период:
DateFrom/DateToобязательны,DateFrom ≤ DateTo, окно ≤ срока хранения (3 года) → иначеVALIDATION_ERROR; - формат и секции:
format ∈ ExportFormat(Xlsx/Csv/Pdf),sections⊆ допустимого набора (summary/timeseries/breakdowns/funnel) → иначеVALIDATION_ERROR; - лимит параллелизма: если у застройщика уже идёт предельное число выгрузок →
ExportConcurrencyLimit(A4) до чтения витрины.
Существование id-фильтров иерархии не проверяется: неизвестный
ProjectId/BuildingId/… даёт пустой результат (валидный «пустой» отчёт, A1), а не ошибку (см. dev-stats-filter.md). - право: требуется
-
Чтение секций. Экспорт переиспользует чтения метрик:
property_object_view_dailyдляsummary/timeseries/breakdowns,funnel_dailyдляfunnel(тот же срез(DeveloperId, ProjectId, …)+ затронутые месячные партиции). Крупные секции (разбивка по объектам за длинный период) читаются потоково/постранично, чтобы не держать весь набор в памяти. -
Сборка отчёта (устойчивая к дублям). Счётчики секций считаются так же, как в UC-STA-001/UC-STA-002/ UC-STA-004 — устойчиво к дублям. Собирается типизированный
ExportReportModel(набор секций: сводка, ряд, разбивки, воронка). Человекочитаемые названия узлов иерархии берутся из реплик-измеренийdim_*; при отставании репликиchessboardстрока использует последнюю известную версию названия, а при её отсутствии — id узла (деградация, не ошибка — A5). -
Рендер файла. По
format:Xlsx/CsvиPdfформируются из одногоExportReportModel(табличный layout сводки/рядов/воронки). Результат — байтовый поток + имяdev-stats_{DeveloperId}_{DateFrom}_{DateTo}.{ext}. -
Аудит + выдача. Пишется обязательная audit-запись в платформенный audit-канал:
ActorUserId,DeveloperId, сериализованныйDevStatsFilterModel,Format,Sections,RowCount,RequestedAt, идентификатор трассировкиTraceId. Ответ — HTTP200с телом файла (ExportFileResult) иContent-Disposition: attachment; filename=…(пустой набор → A1; крупная выгрузка → A2).
Диаграмма
Межсервисная согласованность и повторные вызовы
Что питает выгрузку (входящее, асинхронно). Экспорт не вызывает соседние сервисы синхронно — он читает
только собственные агрегаты dev-stats, наполняемые заранее батч-консюмерами:
- просмотры/сессии/действия — из событий
pin.user-activity.{property-object-view-ended, session-ended, action-tracked}.v1→ факты иproperty_object_view_daily; - этапы воронки — из
pin.favorites.favorite-added.v1,pin.selections.selection-item-added.v1,pin.crm.{lead-created, booking-created, booking-paid, deal-created, deal-completed}.v1→funnel_daily; - названия иерархии — из
pin.chessboard.*-upserted.v1→ репликиdim_*.
Гарантия свежести цепочки — ≤ 5 мин (событие → отражение в агрегате). Идемпотентность ingestion по
FactId/SourceEventId и бизнес-ключам этапов означает: повтор апстрим-события не задваивает счётчики в
файле.
Что предоставляет выгрузка (исходящее). Ничего в домен: dev-stats — листовая аналитика, экспорт
не мутирует факты/агрегаты/измерения и не публикует доменных или интеграционных (Kafka) событий.
Единственный побочный эффект — синхронная audit-запись в платформенный security/audit-канал; она должна
успешно записаться до выдачи файла (fail-closed на аудит).
Окно eventual-consistency (что видит пользователь). Файл — снимок состояния агрегатов на момент
генерации. Просмотр/лид/сделка, случившиеся секунды назад, могут ещё не попасть в выгрузку (лаг до ~5 мин);
нижние этапы воронки могут отставать от верхних (апстрим crm догоняет очередь). Пользователь получает
данные, актуальные «на момент генерации», а не real-time.
Повторные вызовы и восстановление. Операция read-only и без ключа идемпотентности: повтор безопасен, но каждая выгрузка — самостоятельное аудируемое обращение, поэтому N повторов дают N audit-записей (так и задумано — фиксируется каждый факт доступа). Две одинаковые выгрузки могут дать разные числа, если между ними агрегаты продвинулись — это ожидаемо. Транзиентный отказ (витрина/рендер) безопасно ретраить (нет частичного доменного состояния); отказ audit-канала не должен молча терять аудит — выгрузка откатывается, файл не отдаётся, клиент повторяет позже.
Альтернативные Потоки
A1. Пустой набор данных
- За период/срез нет строк агрегата.
- Файл всё равно формируется (заголовки секций + нулевая сводка / пустые таблицы); audit-запись пишется —
HTTP
200(факт выгрузки аудируется независимо от объёма данных).
Завершается: застройщик получает валидный «пустой» отчёт.
A2. Крупная выгрузка
- Оценённый объём секций (например, разбивка по объектам за годы) превышает порог синхронной генерации.
- Чтение идёт потоково/постранично, рендер — по мере набора строк; при превышении жёсткого лимита
строк/размера —
PAYLOAD_TOO_LARGE(413) с рекомендацией сузить фильтр. Асинхронная генерация с последующей ссылкой на файл — additive/deferred (не в v1).
Возвращается к основному потоку на шаг 6 (рендер потоком).
A3. Нет права dev-stats.export
- Актор имеет
dev-stats.view(видит дашборд), но неdev-stats.export. - Операция завершается
FORBIDDEN(403) до чтения витрины; попытка экспорта может быть зафиксирована в audit-канале какdenied(опционально) — данные не выгружаются.
A4. Лимит одновременных выгрузок
- У застройщика уже выполняется предельное число одновременных выгрузок (ограничение контракта операции — dev-stats-export.md §Лимиты).
- Новый запрос отклоняется
ExportConcurrencyLimit(429) до чтения витрины; попытка может быть зафиксирована в audit-канале какdenied. Клиент повторяет после завершения текущих выгрузок — состояние не меняется.
A5. Устаревшее/неполное измерение иерархии
- Реплика
dim_*отстаёт (консюмерpin.chessboard.*-upserted.v1лагает) или строка узла ещё не реплицирована. - Строки отчёта используют последнюю известную версию названия узла, а при её отсутствии — id узла как
fallback: файл формируется, счётчики корректны, audit-запись пишется штатно. Это деградация, не ошибка —
HTTP
200.
Возвращается к основному потоку на шаг 6 (рендер с деградированными названиями).
Постусловие
- Мутаций данных нет (read-only): факты/агрегаты/измерения не изменяются, доменные и интеграционные (Kafka) события не публикуются.
- Сформирован файл (XLSX/CSV/PDF) с агрегатами строго в scope
DeveloperId; персональные данные зрителей не раскрыты (только типы/счётчики). - Записана audit-запись выгрузки (
актор + DeveloperId + фильтр + формат + время + rowCount) — обязательное требованиеdev-stats.export; при отказе audit-канала запись не теряется, а вся операция откатывается (файл не отдаётся). - Файл — снимок состояния агрегатов на момент генерации (freshness ≤ 5 мин к живым событиям апстрима); активность последних минут может быть ещё не отражена. Повторная выгрузка тем же фильтром порождает новую независимую audit-запись и может дать другие числа, если агрегаты продвинулись.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
VALIDATION_ERROR | 400 | Нет дат / DateFrom > DateTo; неизвестный format/sections | Показать ошибку параметров |
FORBIDDEN | 403 | Нет dev-stats.export; DeveloperId ≠ актор (A3) | Скрыть экспорт, показать причину |
PAYLOAD_TOO_LARGE | 413 | Превышен лимит строк/размера синхронной выгрузки (A2) | Предложить сузить фильтр/период |
ExportConcurrencyLimit | 429 | Превышен лимит одновременных выгрузок застройщика (A4) | Повторить после завершения текущих выгрузок |
| — (не ошибка) | 200 | Пустой набор (A1) или устаревшее измерение иерархии (A5) | Валидный отчёт + аудит |
5xx (ProblemDetails) | 500/503 | Отказ аналитической витрины / рендер-движка / audit-канала | Ретрай позже (аудит не должен теряться — при сбое канала экспорт откатывается) |
Acceptance Criteria
- AC1 (право на экспорт). Given актор с
dev-stats.view, но безdev-stats.export, When он запрашивает экспорт, Then операция завершается403до чтения витрины (A3); при наличииdev-stats.export— выгрузка проходит. - AC2 (обязательный аудит). Given корректный запрос экспорта, When выгрузка успешна, Then в
audit-канале есть запись с полями
актор + DeveloperId + фильтр + format + время + rowCount. - AC3 (аудит не теряется). Given недоступный audit-канал, When выполняется экспорт, Then операция откатывается и файл не отдаётся (аудит обязателен).
- AC4 (единая модель форматов). Given один фильтр, When запрошены
Xlsx,CsvиPdf, Then все три формата строятся из одногоExportReportModelи содержат согласованные данные. - AC5 (изоляция и устойчивость к дублям). Given застройщик, When он экспортирует, Then читаются
только агрегаты его
DeveloperId(чужойDeveloperId→403), а повтор апстрим-события не задваивает счётчики в файле. - AC6 (крупная выгрузка). Given фильтр на длинный период/широкую разбивку, When оценённый объём
превышает лимит синхронной генерации, Then чтение идёт потоково/постранично, а превышение жёсткого
лимита →
413(A2). - AC7 (пустой набор). Given срез без строк агрегата, When выполняется экспорт, Then формируется
валидный «пустой» типизированный (
ExportReportModel) файл и пишется audit-запись (A1). - AC8 (лимит параллелизма). Given застройщик с предельным числом идущих выгрузок, When он
запрашивает ещё одну, Then она отклоняется
ExportConcurrencyLimit(429) до чтения витрины (A4). - AC9 (снимок и свежесть). Given просмотр/лид, случившийся секунды назад, When выполняется экспорт, Then файл отражает состояние агрегатов на момент генерации (лаг ≤ 5 мин) — свежайшая активность может ещё не попасть, что ожидаемо.
- AC10 (деградация названий иерархии). Given отстающую реплику
dim_*, When выполняется экспорт, Then строки используют последнее известное название или id-fallback, файл отдаётся с200, а не с ошибкой (A5). - AC11 (нет PII). Given любой успешный экспорт, When открывают файл, Then в нём нет
ФИО/контактов/
UserId/AnonymousId/SessionId— только агрегаты и типы зрителей.
Наблюдаемость и НФТ
- Аудит (обязателен). Каждая выгрузка — успешная и отклонённая (
denied) — оставляет записьExportSensitive(актор +DeveloperId+ сериализованный фильтр + формат +rowCount+ время); запись не теряется (сбой канала → откат экспорта). - Метрики. Длительность генерации, число строк, размер файла, счётчик ошибок с причиной
(
validation/forbidden/too-large/concurrency/store/render/audit) — для мониторинга нагрузки и доли отказов. - Логи. Этапы
started/generated/downloaded/failedс актором, форматом,rowCountи сериализованным фильтром; без raw-id зрителей (UserId/AnonymousId/SessionId) и без ФИО/контактов. - Трейс. Сквозной
correlationId/TraceId,tenantId = DeveloperId. - НФТ. Нагрузка низкая (ADR-0056): выгрузки редки, число одновременных на застройщика ограничено (A4).
Синхронная генерация в пределах лимита строк — секунды; крупные секции читаются потоково/постранично, чтобы
не держать весь набор в памяти; превышение жёсткого лимита →
413(A2). Доступ — толькоdev-stats.exportв scope собственногоDeveloperId(fail-closed).
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (2): UC-STA-001. Застройщик смотрит статистику проекта (период + проект), UC-STA-004. Воронка конверсии «просмотр → сделка»
- index.md (1): DevStats Service (Pin.DevStats)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- Контракт операции: dev-stats-export.md; композиция ЛК — dev-stats-overview.md.
- Домены: dev-stats-filter.md,
response-models.md (
ExportReportModel/ExportFileResult/ExportFormat). - Источники секций: property-object-view-daily-rollup.md, funnel-daily-rollup.md; карта областей/безопасность — README.md.
- Смежные use-cases: UC-STA-001 (сводка/ряд), UC-STA-002 (разбивки), UC-STA-004 (воронка).
- Каталог permission: identity/domain/permission.md
(
dev-stats.view/dev-stats.export). - ADR: 0057 dev-stats/ClickHouse, 0056 simplicity-first, 0052 tenancy.
Персистентный запрос-трейл выгрузок (аналитический export_audit / audit-store identity) —
additive/deferred; на старте достаточно структурированной audit-записи в security-канал.