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

UC-PRC-005. Скачивание PDF-отчёта «Скачать расчёт» (контракт; реализация отложена)

Назначение

Описывает сценарий кнопки «Скачать расчёт» на странице аналитики объекта недвижимости: авторизованный пользователь получает PDF-документ с расчётом доходности (параметры объекта недвижимости, серии, сводка AsideMetrics, дисклеймер). Настоящий документ фиксирует контракт; реализация отложена: до включения фичи endpoint отвечает 501, а кнопка на фронте скрыта feature-флагом. Генерация PDF — синхронная, без сохранения файла (см. Постусловие). Связан с контекстом invest-analytics.

Metadata

ПолеЗначение
IDUC-PRC-005
Контекстinvest-analytics (см. docs/06-services/invest-analytics/)
Типосновной (контракт зафиксирован, реализация отложена)
Статусdraft

Актор

ПолеЗначение
РольИнвестор / Агент / Менеджер ПИН — только авторизованный пользователь (JWT). Реф-гость (readOnly-режим страницы) отчёт не скачивает: кнопка скрыта, API отвечает 401 — персонализированный экспортный документ доступен после входа; просмотр расчёта на странице гостю остаётся доступен (UC-PRC-002)
Permissions / scopeinvest-analytics.series.view (тот же расчёт, что UC-PRC-002) + проверка экспортного доступа в API layer (../domain/price-history-fact.md, «Security»: export permissions проверяются в API layer)
TenantНе применяется — расчёт платформенный, результат персонально не тарифицируется
Record-levelНе применяется; PII в отчёте нет (объектные данные + модельные величины)

Предусловие

  • Все предусловия UC-PRC-002 (chessboard, singleton модели, гейт) — отчёт строится на том же расчётном движке.
  • Feature flag invest-analytics.analytics-report включён (до реализации — выключен: фронт скрывает кнопку, API отвечает 501, см. A1).
  • Генератор отчётов (reporting) доступен в образе сервиса (шаблон отчёта задеплоен вместе с релизом).

Триггер

ПолеЗначение
ИсточникUI button + API call
Триггерящий элементКнопка «Скачать расчёт» на странице аналитики объекта недвижимости (десктоп — правая панель; мобайл — calc-block)

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

Целевое поведение после реализации (сейчас — A1):

  1. Пользователь настраивает калькулятор (entity, tax, месяц слайдера) и нажимает «Скачать расчёт».
  2. Фронт вызывает POST property-objects/{propertyObjectId}/analytics-report (../api/developers/create-analytics-report.md, будущий док) с телом:
    • entity (legal|individual, дефолт legal) и tax (usn-6|usn-15|osno, дефолт usn-6) — как в UC-PRC-002;
    • selectedMonthIndex (int, 0..HorizonMonths, опционально; дефолт HorizonMonths) — месяц слайдера, чья сводка AsideMetrics выносится на титульный блок отчёта. POST (а не GET) — из-за тела запроса; побочных эффектов вызов не имеет.
  3. Формирование отчёта:
    • Доступ: только JWT (реф-cookie не проходит → 401). Идемпотентность не требуется: повтор даёт эквивалентный файл (расчёт детерминирован по входу и текущему месяцу, INV-CALC-6), состояния нет.
    • Проверка входа: правила UC-PRC-002 шага 3.2 (PropertyObjectId, enum entity/tax — неизвестное → INVEST_ANALYTICS_INVALID_TAX_PARAMS); дополнительно selectedMonthIndex >= 0; верхняя граница сверяется после расчёта (> HorizonMonthsValidationError, A5); feature flag выключен → INVEST_ANALYTICS_REPORT_NOT_IMPLEMENTED (501, A1).
    • Загрузка: полностью повторяет UC-PRC-002 шаг 3.3 (объект недвижимости из chessboard, три уровня модели из PostgreSQL, ипотека из crm с fallback 21/240, история из ClickHouse с synthetic-fallback) — единый расчётный сервис, дублирования логики нет.
    • Расчёт и сборка модели: расчёт InvestorSeriesResult (движок UC-PRC-002); сборка модели отчёта AnalyticsReportModel:
      • шапка: наименование объекта недвижимости, номер, проект/корпус, Area, BasePrice, PricePerMeter (из ответа chessboard), дата формирования (таймзона платформы), выбранные entity/tax (человекочитаемо: «Юридическое лицо, УСН 6%»);
      • титульный блок: AsideMetrics[selectedMonthIndex] — все 21 поле сводки (прогнозная стоимость, ROI, EBITDA, аренда, налоги, CreditPayment, …);
      • графики: три серии AnalyticsSeries (gross-profit, ebitda, price-per-meter) — рендер изображениями средствами генератора отчётов; пометка «история синтетическая», если isSyntheticHistory = true;
      • таблица помесячной серии InvestorMonth[] (18 колонок — дом ../domain/investor-series-models.md);
      • обязательный дисклеймер: «расчёт не является налоговой консультацией и индивидуальной инвестиционной рекомендацией; ставки — модельные допущения платформы» (../domain/tax-entity-params.md, «Безопасность»).
    • Рендер: PDF формируется синхронно, бюджет ≤ 5 с; файл не сохраняется ни в БД, ни в storage — отдаётся потоком (повторный запрос — повторная генерация; simplicity-first ADR-0056). Рендер — всё-или-ничего: при сбое или превышении бюджета отдаётся ошибка (A4), а не усечённый файл.
    • Результат: HTTP 200, Content-Type: application/pdf, Content-Disposition: attachment; filename="pin-analytics-{propertyObjectId}-{yyyyMMdd}.pdf"; маскирование не требуется (PII в отчёте нет).
  4. Браузер скачивает файл; страница остаётся в текущем состоянии.

Записи состояния, доменных/интеграционных событий и транзакции сценарий не выполняет — генерация read-only. Факт скачивания попадает в user-activity штатным фронтовым трекингом действий (вне этого контекста).

Альтернативные Потоки

A1. Реализация отложена (текущее состояние)

  1. Feature flag invest-analytics.analytics-report выключен: фронт не показывает кнопку; прямой вызов API → 501 INVEST_ANALYTICS_REPORT_NOT_IMPLEMENTED.
  2. Контракт (вход/выход/ошибки этого документа) уже зафиксирован — фронт и API могут разрабатываться независимо; включение флага после реализации не меняет контракт. Сценарий завершается.

A2. Реф-гость / неавторизованный

  1. Запрос без JWT (в т.ч. с валидной реф-cookie) → 401; фронт для гостя кнопку не рендерит (readOnly-режим страницы). Гость может авторизоваться и повторить. Сценарий завершается.

A3. Ошибки расчёта

  1. Объект недвижимости не найден → 404 INVEST_ANALYTICS_PROPERTY_OBJECT_NOT_FOUND; chessboard недоступен → 503 INVEST_ANALYTICS_PROPERTY_OBJECT_SOURCE_UNAVAILABLE; неизвестный entity/tax → 400 — зеркально UC-PRC-002 (A1–A3). PDF не генерируется. Сценарий завершается.

A4. Сбой рендера PDF

  1. Генератор отчётов падает или превышает бюджет 5 с → 503 INVEST_ANALYTICS_REPORT_RENDER_FAILED + алерт (ошибки генерации — метрика ниже); расчётные API страницы не затронуты.
  2. Фронт показывает тост «не удалось сформировать отчёт, попробуйте позже». Сценарий завершается.

A5. selectedMonthIndex за пределами горизонта

  1. selectedMonthIndex > HorizonMonths (горизонт стал меньше после правки модели администратором) → 400 ValidationError; фронт повторяет с дефолтом (HorizonMonths). Возврат на шаг 2.

A6. Частичная деградация зависимостей расчёта (отчёт формируется)

  1. crm недоступен/не настроен → ипотека считается по константам 21%/240 мес (UC-PRC-002 A5); ClickHouse недоступен или < 2 фактов цены по объекту недвижимости → историческая часть серии price-per-meter строится синтетикой и отчёт помечается «история синтетическая» (isSyntheticHistory = true, UC-PRC-002 A4). Отчёт при этом генерируется — это модельные допущения, а не отказ. Фатальна только недоступность chessboard (A3, 503 — расчёт по объекту невозможен). Возврат в основной поток на шаг 3 (рендер).

A7. Модель изменена между открытием страницы и скачиванием

  1. Админ правит эконом-модель (UC-PRC-003/UC-PRC-004) после того как пользователь открыл страницу, но до нажатия «Скачать расчёт». Отчёт — свежий пересчёт на момент запроса (кэша нет), поэтому числа в PDF могут отличаться от ранее отрисованных на странице; при неизменной модели PDF детерминированно совпадает с ответом investor-series при тех же параметрах (INV-CALC-6). Это ожидаемое поведение, не ошибка; сценарий завершается штатно.

Постусловие

  • Состояние системы не изменилось: расчёт и файл не персистятся, событий нет.
  • Пользователь получил PDF, воспроизводящий состояние калькулятора (те же entity/tax/месяц и та же трёхуровневая модель на момент запроса — свежий пересчёт; при правке модели после открытия страницы числа могут отличаться от ранее показанных, A7).

События

Генерация отчёта — read-only: доменных и интеграционных событий не публикует. Факт скачивания трекается фронтом через контекст user-activity (вне границы invest-analytics).

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
ValidationError400selectedMonthIndex вне 0..HorizonMonths (A5), PropertyObjectId пустойПовторить с корректными параметрами
INVEST_ANALYTICS_INVALID_TAX_PARAMS400Неизвестный entity/taxИсправить параметры
Unauthorized401Нет JWT (реф-cookie недостаточно, A2)Auth-модалка; после входа повторить
INVEST_ANALYTICS_PROPERTY_OBJECT_NOT_FOUND404Объект недвижимости отсутствует в chessboard (A3)404-страница карточки
INVEST_ANALYTICS_REPORT_NOT_IMPLEMENTED501Feature flag выключен — реализация отложена (A1)Кнопка скрыта; прямые вызовы — не ретраить
INVEST_ANALYTICS_PROPERTY_OBJECT_SOURCE_UNAVAILABLE503chessboard недоступен (A3)Тост + retry позже
INVEST_ANALYTICS_REPORT_RENDER_FAILED503Сбой/таймаут генератора отчётов (A4)Тост «попробуйте позже»

Acceptance Criteria

Проверяются при снятии отметки «реализация отложена»; до этого действует только AC1.

#КритерийПроверка
AC1При выключенном флаге API отвечает 501 с кодом, кнопка на фронте отсутствуетAPI-тест + e2e
AC2PDF содержит шапку объекта недвижимости, сводку выбранного месяца (21 поле), три графика, таблицу серии и дисклеймерснапшот-тест структуры отчёта
AC3Числа в PDF совпадают с ответом investor-series при тех же параметрах (единый движок)сравнительный тест модели отчёта vs расчёт
AC4Реф-гость получает 401, авторизованный — файл (A2)e2e-тест двух акторов
AC5Файл не персистится: ни строки в PG/CH, ни объекта в storage после генерацииинспекция хранилищ в интеграционном тесте
AC6Сбой рендера → 503 без влияния на investor-series/price-history (A4)тест с моком генератора отчётов
AC7Имя файла — pin-analytics-{propertyObjectId}-{yyyyMMdd}.pdf, Content-Disposition: attachmentAPI-тест заголовков
AC8crm недоступен → PDF формируется с ипотекой 21%/240; ClickHouse недоступен/<2 фактов → PDF с пометкой «история синтетическая»; фатален только chessboard-down (503) — A6тесты частичной деградации с моками зависимостей
AC9Повторный запрос с теми же параметрами при неизменной модели даёт эквивалентный PDF (детерминизм, INV-CALC-6); после правки модели админом — свежие числа (A7)тест идемпотентности + тест правки модели между запросами

Наблюдаемость

  • Logs: параметры запроса (propertyObjectId, entity, tax — не PII), длительность расчёта и рендера по отдельности (для разбора, что упёрлось в бюджет 5 с); признак использованных fallback-ов (isSyntheticHistory, ипотека по константам) — для диагностики расхождений в отчёте.
  • Metrics: invest_analytics_report_generated_total, invest_analytics_report_render_seconds, invest_analytics_report_failures_total (алерт при росте).
  • Traces: сквозной трейс через движок расчёта и генератор отчётов.
  • Runbook: сбои рендера — проверка шаблона отчёта/версии генератора; расчётная часть диагностируется тестами UC-PRC-002.

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

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

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

  • API метод (дом контракта): ../api/developers/create-analytics-report.md (будущий док).
  • Движок и модели: ../domain/investor-series-models.md; расчётный сценарий — UC-PRC-002.
  • Налоги/дисклеймер: ../domain/tax-entity-params.md.
  • Генерация отчётов: docs/11-backend-sdk/09-reporting-export.md.
  • Экспортные права: ../domain/price-history-fact.md («Security»).
  • Трекинг скачивания: docs/06-services/user-activity/ (действия пользователя).
  • Решения: README «Принятые Решения» п.7 (контракт сейчас, реализация отложена), ADR-0056 (синхронная генерация без хранения).