UC-PRC-005. Скачивание PDF-отчёта «Скачать расчёт» (контракт; реализация отложена)
Назначение
Описывает сценарий кнопки «Скачать расчёт» на странице аналитики объекта недвижимости: авторизованный
пользователь получает PDF-документ с расчётом доходности (параметры объекта недвижимости, серии, сводка
AsideMetrics, дисклеймер). Настоящий документ фиксирует контракт; реализация отложена: до включения
фичи endpoint отвечает 501, а кнопка на фронте скрыта feature-флагом. Генерация PDF — синхронная, без
сохранения файла (см. Постусловие). Связан с контекстом invest-analytics.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-PRC-005 |
| Контекст | invest-analytics (см. docs/06-services/invest-analytics/) |
| Тип | основной (контракт зафиксирован, реализация отложена) |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Инвестор / Агент / Менеджер ПИН — только авторизованный пользователь (JWT). Реф-гость (readOnly-режим страницы) отчёт не скачивает: кнопка скрыта, API отвечает 401 — персонализированный экспортный документ доступен после входа; просмотр расчёта на странице гостю остаётся доступен (UC-PRC-002) |
| Permissions / scope | invest-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):
- Пользователь настраивает калькулятор (
entity,tax, месяц слайдера) и нажимает «Скачать расчёт». - Фронт вызывает
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) — из-за тела запроса; побочных эффектов вызов не имеет.
- Формирование отчёта:
- Доступ: только JWT (реф-cookie не проходит → 401). Идемпотентность не требуется: повтор даёт эквивалентный файл (расчёт детерминирован по входу и текущему месяцу, INV-CALC-6), состояния нет.
- Проверка входа: правила UC-PRC-002 шага 3.2 (
PropertyObjectId, enumentity/tax— неизвестное →INVEST_ANALYTICS_INVALID_TAX_PARAMS); дополнительноselectedMonthIndex >= 0; верхняя граница сверяется после расчёта (> HorizonMonths→ValidationError, 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 в отчёте нет).
- Браузер скачивает файл; страница остаётся в текущем состоянии.
Записи состояния, доменных/интеграционных событий и транзакции сценарий не выполняет — генерация read-only.
Факт скачивания попадает в user-activity штатным фронтовым трекингом действий (вне этого контекста).
Альтернативные Потоки
A1. Реализация отложена (текущее состояние)
- Feature flag
invest-analytics.analytics-reportвыключен: фронт не показывает кнопку; прямой вызов API → 501INVEST_ANALYTICS_REPORT_NOT_IMPLEMENTED. - Контракт (вход/выход/ошибки этого документа) уже зафиксирован — фронт и API могут разрабатываться независимо; включение флага после реализации не меняет контракт. Сценарий завершается.
A2. Реф-гость / неавторизованный
- Запрос без JWT (в т.ч. с валидной реф-cookie) → 401; фронт для гостя кнопку не рендерит (readOnly-режим страницы). Гость может авторизоваться и повторить. Сценарий завершается.
A3. Ошибки расчёта
- Объект недвижимости не найден → 404
INVEST_ANALYTICS_PROPERTY_OBJECT_NOT_FOUND;chessboardнедоступен → 503INVEST_ANALYTICS_PROPERTY_OBJECT_SOURCE_UNAVAILABLE; неизвестныйentity/tax→ 400 — зеркально UC-PRC-002 (A1–A3). PDF не генерируется. Сценарий завершается.
A4. Сбой рендера PDF
- Генератор отчётов падает или превышает бюджет 5 с → 503
INVEST_ANALYTICS_REPORT_RENDER_FAILED+ алерт (ошибки генерации — метрика ниже); расчётные API страницы не затронуты. - Фронт показывает тост «не удалось сформировать отчёт, попробуйте позже». Сценарий завершается.
A5. selectedMonthIndex за пределами горизонта
selectedMonthIndex > HorizonMonths(горизонт стал меньше после правки модели администратором) → 400ValidationError; фронт повторяет с дефолтом (HorizonMonths). Возврат на шаг 2.
A6. Частичная деградация зависимостей расчёта (отчёт формируется)
crmнедоступен/не настроен → ипотека считается по константам21%/240мес (UC-PRC-002 A5); ClickHouse недоступен или< 2фактов цены по объекту недвижимости → историческая часть серииprice-per-meterстроится синтетикой и отчёт помечается «история синтетическая» (isSyntheticHistory = true, UC-PRC-002 A4). Отчёт при этом генерируется — это модельные допущения, а не отказ. Фатальна только недоступностьchessboard(A3, 503 — расчёт по объекту невозможен). Возврат в основной поток на шаг 3 (рендер).
A7. Модель изменена между открытием страницы и скачиванием
- Админ правит эконом-модель (UC-PRC-003/UC-PRC-004) после того как пользователь открыл страницу, но до
нажатия «Скачать расчёт». Отчёт — свежий пересчёт на момент запроса (кэша нет), поэтому числа в PDF могут
отличаться от ранее отрисованных на странице; при неизменной модели PDF детерминированно совпадает с
ответом
investor-seriesпри тех же параметрах (INV-CALC-6). Это ожидаемое поведение, не ошибка; сценарий завершается штатно.
Постусловие
- Состояние системы не изменилось: расчёт и файл не персистятся, событий нет.
- Пользователь получил PDF, воспроизводящий состояние калькулятора (те же
entity/tax/месяц и та же трёхуровневая модель на момент запроса — свежий пересчёт; при правке модели после открытия страницы числа могут отличаться от ранее показанных, A7).
События
Генерация отчёта — read-only: доменных и интеграционных событий не публикует. Факт скачивания трекается
фронтом через контекст user-activity (вне границы invest-analytics).
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
ValidationError | 400 | selectedMonthIndex вне 0..HorizonMonths (A5), PropertyObjectId пустой | Повторить с корректными параметрами |
INVEST_ANALYTICS_INVALID_TAX_PARAMS | 400 | Неизвестный entity/tax | Исправить параметры |
Unauthorized | 401 | Нет JWT (реф-cookie недостаточно, A2) | Auth-модалка; после входа повторить |
INVEST_ANALYTICS_PROPERTY_OBJECT_NOT_FOUND | 404 | Объект недвижимости отсутствует в chessboard (A3) | 404-страница карточки |
INVEST_ANALYTICS_REPORT_NOT_IMPLEMENTED | 501 | Feature flag выключен — реализация отложена (A1) | Кнопка скрыта; прямые вызовы — не ретраить |
INVEST_ANALYTICS_PROPERTY_OBJECT_SOURCE_UNAVAILABLE | 503 | chessboard недоступен (A3) | Тост + retry позже |
INVEST_ANALYTICS_REPORT_RENDER_FAILED | 503 | Сбой/таймаут генератора отчётов (A4) | Тост «попробуйте позже» |
Acceptance Criteria
Проверяются при снятии отметки «реализация отложена»; до этого действует только AC1.
| # | Критерий | Проверка |
|---|---|---|
| AC1 | При выключенном флаге API отвечает 501 с кодом, кнопка на фронте отсутствует | API-тест + e2e |
| AC2 | PDF содержит шапку объекта недвижимости, сводку выбранного месяца (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: attachment | API-тест заголовков |
| AC8 | crm недоступен → 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.
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (1): UC-PRC-002. Расчёт доходности объекта недвижимости с выбором субъекта (юр/физ) и налогового режима
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- 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 (синхронная генерация без хранения).