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

UC-PRC-002. Расчёт доходности объекта недвижимости с выбором субъекта (юр/физ) и налогового режима

Назначение

Калькулятор доходности на странице /lots/[id]/analytics: пользователь выбирает тип субъекта (юр/физ лицо) и налоговый режим (УСН-6 / УСН-15 / ОСНО), а система серверно строит InvestorMonth[0..HorizonMonths] + AnalyticsSeries (gross-profit/ebitda/price-per-meter) + AsideMetrics для каждого месяца. Модели и формулы — ../domain/investor-series-models.md. Контекст invest-analytics.

Metadata

ПолеЗначение
IDUC-PRC-002
Контекстinvest-analytics
Типосновной
Статусdraft

Актор

ПолеЗначение
РольИнвестор / Агент / Менеджер ПИН (по токену) или Гость по реферальной ссылке (реф-cookie): страница readOnly, но переключатели entity/tax доступны — расчёт read-only и не привилегирован (../domain/tax-entity-params.md)
Permissions / scopeinvest-analytics.series.view (public, выдаётся ролям каталога; реф-гость — тот же гейт токен/реф-cookie, что и /lots)
Tenantне применяется: коэффициенты платформенные (ADR-0052 п.3), результат публичен
Record-levelне применяется; PII отсутствует — параметры entity/tax не персистятся

Предусловие

  • chessboard internal API доступен и отдаёт параметры объекта недвижимости (BasePrice — эффективная цена с учётом каталожных оверрайдов, Area, PricePerMeter, GroupId, DeliveryDate — строка срока сдачи корпуса).
  • Seed-миграция создала singleton invest_analytics.model_config (../domain/economic-model-config.md); при отсутствии строки расчёт использует hard-coded дефолты (fallback последнего уровня).
  • Опционально: PropertyObjectTypeConfig по GroupId объекта недвижимости и его помесячные оверрайды настроены админом (UC-PRC-003, UC-PRC-004).
  • Пользователь прошёл гейт каталога (JWT или реф-cookie).

Триггер

ПолеЗначение
ИсточникUI (открытие вкладки / переключатель) + API call
Триггерящий элементСтраница /lots/[id]/analytics: первичная загрузка (дефолты entity=legal, tax=usn-6) и каждое переключение «юр/физ лицо» / «УСН-6 / УСН-15 / ОСНО»; слайдер месяца запросов не порождает — фронт листает готовый MetricsByMonth[i]

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

  1. Пользователь открывает вкладку аналитики; фронт вызывает GET property-objects/{propertyObjectId}/investor-series?entity=legal&tax=usn-6 (дефолты).

  2. Система проверяет вход:

    • PropertyObjectId не пустой;
    • entity ∈ {legal, individual}, tax ∈ {usn-6, usn-15, osno}; отсутствующий параметр → дефолт (legal, usn-6); неизвестное значение → ошибка INVEST_ANALYTICS_INVALID_TAX_PARAMS (HTTP 400), молчаливый откат к дефолту запрещён (../domain/tax-entity-params.md);
    • Individual + любой tax — не ошибка: tax принимается и игнорируется в ставке.
  3. Система собирает входы расчёта (порядок фиксирован):

    • а) объект недвижимости: m2m-вызов chessboard (../../chessboard/api/public/property-objects/get-property-object.md) → BasePrice, Area, PricePerMeter, GroupId, DeliveryDate; объект не найден → INVEST_ANALYTICS_PROPERTY_OBJECT_NOT_FOUND (404, A2); сервис недоступен → 503 (A3);
    • б) singleton EconomicModelConfig по well-known Id — при отсутствии строки применяются дефолты кода (таблица дефолтов в ../domain/economic-model-config.md);
    • в) PropertyObjectTypeConfig по GroupId объекта недвижимости — может быть не настроен (отсутствие допустимо);
    • г) помесячные оверрайды типа по GroupId — полный список (единицы–десятки строк), проекция в MonthOverrideValues;
    • д) параметры ипотеки: purchase-config crm (MortgageAnnualRate, MortgageMonths); недоступен/не настроен → fallback-константы 21 / 240 (A5), расчёт не блокируется;
    • е) историческая часть серии price-per-meter: помесячная агрегация фактов цен из ClickHouse (argMax(NewPricePerMeter, (EventTime, IngestionTime)) до AnchorTime); < 2 фактов или CH недоступен → synthetic-fallback (A4).
  4. Система выполняет расчёт (движок — ../domain/investor-series-models.md):

    • разрешение эффективных коэффициентов: для каждого из 9 полей независимо тип → модель → дефолт кода (первое not-null конечное) → InvestorCoefficients;
    • regimeTaxRate по (EntityKind, TaxRegime): Individual → 0.13; Legal: Usn6 → 0.06, Usn15 → 0.15, Osno → 0.43 (константы модели);
    • AnchorTime = первый день текущего месяца; DeliveryTime — парсинг DeliveryDate (ISO / «N кв. YYYY» / месяц-год / год → правила дат дома; не распарсилось → null);
    • цикл i = 0..HorizonMonths: marketM2, dailyRent, gainM2, налоги (ovr(i).*TaxMonthly × 12 замещает regimeTaxRate поточечно — INV-CALC-5), monthRevenueM2 (0 до DeliveryTime — INV-CALC-3), cumRevenueM2, ebitdaM2, правило positiveOr для абсолютных оверрайдов (INV-CALC-2) → 18 полей InvestorMonth[i];
    • AnalyticsSeries: gross-profit (на м², все месяцы), ebitda (на м², только месяцы с Ebitda > 0), price-per-meter = историческая часть (шаг 3.е) + прогноз round2(MarketPriceM2) от AnchorTime;
    • MortgageCalculation (аннуитет) — одна величина на все срезы;
    • AsideMetrics для каждого месяца (21 поле — таблица дома, включая RoiPerYear, Growth12mPercent, CreditPayment); при HorizonMonths < 12 RoiPerYear считается по доступным месяцам 1..min(12, HorizonMonths), а Growth12mPercent у краевых срезов = 0 (нет ни i+12, ни i−12) — см. «Граничные случаи серии» дома;
    • финальный guard: не-конечное → 0 (INV-CALC-1); длины Months.Count == MetricsByMonth.Count == HorizonMonths + 1 (INV-CALC-4).

    Расчёт ничего не персистит (без кэша и без сохранения результатов, ADR-0056).

  5. Система возвращает { months[], series {gross-profit, ebitda, price-per-meter}, metricsByMonth[], isSyntheticHistory } (camelCase, дефисные ключи серий); поле CumulativeEbitda сохранено (дубль Ebitda, совместимость).

  6. Фронт рисует графики и правую панель AsideMetrics (мобильный calc-block); слайдер месяца листает metricsByMonth[i] локально.

  7. Пользователь переключает «физ лицо» → фронт повторяет шаги 1–5 с entity=individual (меняются только налоговые поля и производные: AppreciationTax, EbitdaTax, GrossProfit, ROI, TaxRatePercent).

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

A1. Неизвестное значение entity/tax

  1. Ручной вызов API с tax=ausn → 400 INVEST_ANALYTICS_INVALID_TAX_PARAMS (шаг 2). Фронт таких значений не шлёт. Сценарий завершается.

A2. Объект недвижимости не найден в chessboard

  1. Параметры объекта недвижимости не найдены → 404 INVEST_ANALYTICS_PROPERTY_OBJECT_NOT_FOUND; фронт показывает 404-страницу карточки. Сценарий завершается.

A3. chessboard недоступен

  1. m2m-вызов падает (timeout/5xx) → 503 INVEST_ANALYTICS_PROPERTY_OBJECT_SOURCE_UNAVAILABLE (расчёт по объекту недвижимости недоступен). Ретраев внутри запроса нет — фронт показывает плейсхолдер с retry. История цен (UC-PRC-001) продолжает работать независимо.

A4. ClickHouse недоступен или < 2 фактов по объекту недвижимости

  1. Историческая часть price-per-meter строится синтетикой: годовые точки 01-01 с 2022 по прошлый год, экстраполяция назад от marketM2(0) фактором annualFactor = marketM2(12)/marketM2(0); пустой список, если серия короче 13 месяцев или значения ≤ 0 (формула — дом серий). Ответ помечается isSyntheticHistory = true.
  2. Feature flag invest-analytics.synthetic-history-fallback выключен → историческая часть просто пустая (только прогноз), ошибки нет. Возврат в основной поток на шаг 4.

A5. Purchase-config crm недоступен / не настроен

  1. MortgageAnnualRate = 21, MortgageMonths = 240 (fallback-константы); AsideMetrics.CreditPayment считается по ним. Возврат на шаг 4; деградация не сигналится пользователю (модельная величина).

A6. Singleton model_config отсутствует (до первого сохранения админом)

  1. Все коэффициенты — hard-coded дефолты кода (MarketPremium 0.1, PriceGrowthMonthly 0.02, DailyRentM2 800, RentGrowthMonthly 0.01, ExpenseShare 0.4, Occupancy 0.6, HorizonMonths 60). Возврат на шаг 4. В штатном рантайме недостижимо — seed-миграция создаёт строку.

Постусловие

  • Состояние системы не изменилось (read-only): PG/CH/outbox не мутированы, результат не кэширован и не сохранён.
  • Пользователь видит серии и сводку, соответствующие выбранным entity/tax и актуальной трёхуровневой модели; правки админа (UC-PRC-003/004) видны немедленно — со следующего запроса.

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

КодHTTPУсловиеПоведение клиента
INVEST_ANALYTICS_INVALID_TAX_PARAMS400Неизвестное значение entity/tax (A1)Исправить параметры; фронт шлёт только известные
ValidationError400PropertyObjectId пустИсправить запрос
Unauthorized401Нет JWT и нет реф-cookieAuth-модалка (гейт каталога)
INVEST_ANALYTICS_PROPERTY_OBJECT_NOT_FOUND404Объект недвижимости отсутствует в chessboard (A2)404-страница карточки
INVEST_ANALYTICS_PROPERTY_OBJECT_SOURCE_UNAVAILABLE503chessboard internal API недоступен (A3)Плейсхолдер + retry с backoff

Недоступность ClickHouse (A4) и crm (A5) ошибками не являются — graceful degradation.

События

Сценарий (read-only расчёт) событий не публикует; produced-события контекста — ../events/README.md.

Acceptance Criteria

#КритерийПроверка
AC1Ответ детерминирован по входу; ключи camelCase, серии gross-profit/ebitda/price-per-meter, cumulativeEbitda присутствуетgolden-тест ответа на фиксированном входе
AC2Переключение entity/tax меняет ставку по таблице (0.13/0.06/0.15/0.43); Individual игнорирует tax без ошибкипараметризованный тест 4 комбинаций + individual&tax=osno
AC3Months.Count == MetricsByMonth.Count == HorizonMonths + 1; до DeliveryTime аренда/EBITDA = 0 и точек ebitda нетunit-тесты INV-CALC-3/4
AC4Помесячные оверрайды применяются поточечно: абсолюты только при > 0 (positiveOr), налоговые × 12 замещают ставку режимаunit-тесты INV-CALC-2/5
AC5Ни одно поле результата не NaN/Infinity при нулевых BasePrice/Areaunit-тест INV-CALC-1 (крайние входы)
AC6≥ 2 фактов в CH → историческая часть из реальных точек, isSyntheticHistory=false; < 2 или CH down → синтетика/пусто по feature flagинтеграционный тест A4
AC7Недоступный chessboard → 503; недоступный crm → расчёт с константами 21/240тесты A3/A5
AC8Реф-гость получает расчёт и может менять entity/tax (страница readOnly не блокирует чтение)e2e-тест реф-cookie
AC9HorizonMonths < 12: RoiPerYear по доступному хвосту 1..min(12,Horizon), Growth12mPercent краевых = 0, серия длины HorizonMonths+1, поля не NaNunit-тест малого горизонта

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

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

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