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

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

ПолеЗначение
IDUC-STA-005
Контекстdev-stats (см. docs/06-services/dev-stats/)
Типосновной
Статусdraft

Актор

РольPermissions / scopeTenantRecord-level
Девелопер (застройщик)dev-stats.export (сильнее dev-stats.view); scope строго = свой DeveloperIdTenantId = DeveloperId = CompanyEntity.Id (ADR-0052)не применяется — выгружаются только агрегаты (не персоны)

Выгрузка возможна лишь для своего DeveloperId (fail-closed); отсутствие dev-stats.export при наличии dev-stats.view403 (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/statsGET /api/v1/dev-stats/export?format=xlsx

Основной Поток

Полный контракт операции — dev-stats-export.md; ниже — сценарий выгрузки с аудитом.

  1. Ввод. Застройщик на дашборде (с текущим DevStatsFilterModel) жмёт «Экспорт», выбирает format (Xlsx/Csv/Pdf) и состав секций (summary/timeseries/breakdowns/funnel).

  2. Запрос. Экспорт за период/срез с выбранными форматом и секциями; DeveloperId — из токена.

  3. Проверка входа и авторизация. По порядку, при первом нарушении — соответствующий отказ:

    • право: требуется dev-stats.export (fail-closed, A3); одного dev-stats.view недостаточно → FORBIDDEN;
    • scope: DeveloperId = актор (значение берётся из токена, не из UI); клиентский чужой DeveloperIdFORBIDDEN;
    • период: 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).

  4. Чтение секций. Экспорт переиспользует чтения метрик: property_object_view_daily для summary/timeseries/breakdowns, funnel_daily для funnel (тот же срез (DeveloperId, ProjectId, …) + затронутые месячные партиции). Крупные секции (разбивка по объектам за длинный период) читаются потоково/постранично, чтобы не держать весь набор в памяти.

  5. Сборка отчёта (устойчивая к дублям). Счётчики секций считаются так же, как в UC-STA-001/UC-STA-002/ UC-STA-004 — устойчиво к дублям. Собирается типизированный ExportReportModel (набор секций: сводка, ряд, разбивки, воронка). Человекочитаемые названия узлов иерархии берутся из реплик-измерений dim_*; при отставании реплики chessboard строка использует последнюю известную версию названия, а при её отсутствии — id узла (деградация, не ошибка — A5).

  6. Рендер файла. По format: Xlsx/Csv и Pdf формируются из одного ExportReportModel (табличный layout сводки/рядов/воронки). Результат — байтовый поток + имя dev-stats_{DeveloperId}_{DateFrom}_{DateTo}.{ext}.

  7. Аудит + выдача. Пишется обязательная audit-запись в платформенный audit-канал: ActorUserId, DeveloperId, сериализованный DevStatsFilterModel, Format, Sections, RowCount, RequestedAt, идентификатор трассировки TraceId. Ответ — HTTP 200 с телом файла (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}.v1funnel_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. Пустой набор данных

  1. За период/срез нет строк агрегата.
  2. Файл всё равно формируется (заголовки секций + нулевая сводка / пустые таблицы); audit-запись пишется — HTTP 200 (факт выгрузки аудируется независимо от объёма данных).

Завершается: застройщик получает валидный «пустой» отчёт.

A2. Крупная выгрузка

  1. Оценённый объём секций (например, разбивка по объектам за годы) превышает порог синхронной генерации.
  2. Чтение идёт потоково/постранично, рендер — по мере набора строк; при превышении жёсткого лимита строк/размера — PAYLOAD_TOO_LARGE (413) с рекомендацией сузить фильтр. Асинхронная генерация с последующей ссылкой на файл — additive/deferred (не в v1).

Возвращается к основному потоку на шаг 6 (рендер потоком).

A3. Нет права dev-stats.export

  1. Актор имеет dev-stats.view (видит дашборд), но не dev-stats.export.
  2. Операция завершается FORBIDDEN (403) до чтения витрины; попытка экспорта может быть зафиксирована в audit-канале как denied (опционально) — данные не выгружаются.

A4. Лимит одновременных выгрузок

  1. У застройщика уже выполняется предельное число одновременных выгрузок (ограничение контракта операции — dev-stats-export.md §Лимиты).
  2. Новый запрос отклоняется ExportConcurrencyLimit (429) до чтения витрины; попытка может быть зафиксирована в audit-канале как denied. Клиент повторяет после завершения текущих выгрузок — состояние не меняется.

A5. Устаревшее/неполное измерение иерархии

  1. Реплика dim_* отстаёт (консюмер pin.chessboard.*-upserted.v1 лагает) или строка узла ещё не реплицирована.
  2. Строки отчёта используют последнюю известную версию названия узла, а при её отсутствии — 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_ERROR400Нет дат / DateFrom > DateTo; неизвестный format/sectionsПоказать ошибку параметров
FORBIDDEN403Нет dev-stats.export; DeveloperId ≠ актор (A3)Скрыть экспорт, показать причину
PAYLOAD_TOO_LARGE413Превышен лимит строк/размера синхронной выгрузки (A2)Предложить сузить фильтр/период
ExportConcurrencyLimit429Превышен лимит одновременных выгрузок застройщика (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 (чужой DeveloperId403), а повтор апстрим-события не задваивает счётчики в файле.
  • 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).

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

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

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

Персистентный запрос-трейл выгрузок (аналитический export_audit / audit-store identity) — additive/deferred; на старте достаточно структурированной audit-записи в security-канал.