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

POST /api/admin/v1/investor-preview — Расчёт серий доходности по произвольным параметрам (admin preview / growth-chart)

Назначение

Серверный расчёт помесячных серий доходности по произвольным параметрам объекта недвижимости (не по реальному PropertyObjectId): InvestorMonth[] + AnalyticsSeries (графики gross-profit/ebitda/price-per-meter) + AsideMetrics для каждого месяца. Питает admin-кнопку «Предпросмотр» и growth-chart — проверку эффекта правок экономической модели (put-model-config.md). Использует тот же расчётный движок, что и public ../public/get-property-object-investor-series.md (UC-PRC-002), c отличиями входа: параметры объекта недвижимости приходят из тела запроса, а chessboard/crm/ClickHouse не вызываются. Wire-модель ответа идентична public-серии. Формулы и инварианты расчёта — дом ../../domain/investor-series-models.md.

Metadata

ПолеЗначение
Сервис/контекстinvest-analytics
API areaadmin (ADR-0053)
Feature groupadmin-economic-model
Статусdraft
Документdocs/06-services/invest-analytics/api/admin/investor-preview.md

Актор и доступ

ПроверкаЗначение
Actor typeOperator(Admin) — администратор платформы (доступ через admin API, m2m client-credentials, клиент pin-admin-system)
Auth policy / Permissionspolicy AdminSystemOnly; каталожное соответствие — permission invest-analytics.model-config.view (read-only расчёт доступен и без .manage)
Scope (RBAC)платформа
Tenant isolationне применяется — вход платформенный, request не tenanted (ADR-0052 п.3)
Auditnone (read-only расчёт; экономическая модель наружу не раскрывается — уходят только вычисленные серии)

HTTP-контракт

ПолеЗначение
MethodPOST
Success status200
Route/api/admin/v1/investor-preview
Idempotency headerне применяется — read-only расчёт, результат не персистится; повтор детерминирован (INV-CALC-6)
Correlationсквозной trace через traceparent; X-Admin-User-Id — обязателен

Входные данные / параметры запроса

Параметры объекта недвижимости — из тела (имитируют реальный объект недвижимости). Коэффициенты — опциональный полный набор из девяти nullable-полей: переданное значение имитирует уровень PropertyObjectTypeConfig, не переданное разрешается из EconomicModelConfig → дефолт кода. JSON тела — camelCase.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
BasePricebody basePricedecimalДа>= 0, руб; конечное число
Areabody areadecimalДа> 0, м²; конечное число
PricePerMeterbody pricePerMeterdecimalДа> 0, руб/м²; конечное число
Coefficientsbody coefficientsInvestorCoefficientsInput?Нет9 nullable-полей, диапазоны INV-MC2 при not null (дом economic-model-config.md)
MonthOverridesbody monthOverridesIReadOnlyList<MonthOverrideInput>Нет (может быть пуст)схема MonthOverrideValues: (year,month) + nullable-поля; INV-MO3 диапазоны + INV-MO5 нет дублей (year,month) (property-object-type-month-override.md)
Entitybody entityEntityKindНетwire legal/individual; default legal; неизвестное → INVEST_ANALYTICS_INVALID_TAX_PARAMS (tax-entity-params.md)
Taxbody taxTaxRegimeНетwire usn-6/usn-15/osno; default usn-6; при Entity = Individual принимается и игнорируется в ставке (не ошибка)
AdminUserIdheader X-Admin-User-IdGuidДа!= Guid.Emptyнаблюдаемость (не PII)

InvestorCoefficientsInput — девять nullable-полей marketPremium?, priceGrowthMonthly?, dailyRentM2?, rentGrowthMonthly?, expenseShare?, occupancy?, horizonMonths?, appreciationTaxMonthly?, ebitdaTaxMonthly? (семантика/единицы/диапазоны — дом economic-model-config.md). MonthOverrideInput — проекция MonthOverrideValues один-в-один (investor-series-models.md).

Модель ответа InvestorSeriesResponse

Та же wire-модель, что у ../public/get-property-object-investor-series.md (дом полей — там; JSON camelCase, ключи серий дословно gross-profit/ebitda/price-per-meter). Здесь — только дельта значений admin-режима:

ПолеТипЗначение в admin-previewОписание
monthsIReadOnlyList<InvestorMonthModel>всегда полный список (HorizonMonths + 1 строк, INV-CALC-4)Admin всегда видит помесячную детализацию (урезания реф-гостя нет).
seriesAnalyticsSeriesModelтри серииGrowth-chart рисует gross-profit/ebitda/price-per-meter.
metricsByMonthIReadOnlyList<AsideMetricsModel>все поля заполненыНалоговая детализация и creditPayment видны (admin — полный расчёт).
readOnlyboolвсегда falseAdmin — полный расчёт (не урезанный режим реф-гостя).
isSyntheticHistoryboolвсегда trueРеального PropertyObjectId нет — историческая часть price-per-meter синтетическая (ClickHouse не вызывается).

Алгоритм

Расчётный движок — тот же, что в UC-PRC-002; отличия — на шаге чтения (единственное чтение — платформенная модель).

  1. Контекст и доступ. Администратор платформы через admin API (policy AdminSystemOnly, право .view); scope — платформа; X-Admin-User-Id → контекст наблюдаемости.
  2. Проверка входа. BasePrice >= 0, Area > 0, PricePerMeter > 0; переданные коэффициенты — INV-MC2; оверрайды — INV-MO3 (диапазоны) + INV-MO5 (нет дублей (year,month)); entity/tax — неизвестное значение → INVEST_ANALYTICS_INVALID_TAX_PARAMS (400, молчаливый откат к дефолту запрещён); отсутствующие entity/tax → дефолты legal/usn-6. Существование сущностей по id не проверяется (реального объекта недвижимости нет).
  3. Что читаем. Единственное чтение — платформенная модель EconomicModelConfig по well-known-id, только для НЕ переданных коэффициентов; при отсутствии строки — дефолты кода (таблица в economic-model-config.md). Уровень PropertyObjectTypeConfig в preview не выбирается (переданные коэффициенты имитируют этот уровень). chessboard/crm/ClickHouse не вызываются: параметры объекта недвижимости — из тела; ипотека = константы 21 % / 240 мес; историческая часть price-per-meter синтетическая, IsSyntheticHistory = true.
  4. Бизнес-правила и расчёт (детерминированный, чистый, без I/O — INV-CALC-6):
    1. Каскад коэффициентов (per-field): для каждого из девяти полей — переданное значение → EconomicModelConfig.поле → дефолт кода.
    2. Даты: AnchorTime = первый день текущего месяца сервера (yyyy-MM-01); DeliveryTime = null → аренда/EBITDA считаются с месяца 0 (inOperation(i) всегда true).
    3. Налог режима: regimeTaxRate по (Entity,Tax) — Individual0.13; Legal: Usn60.06, Usn150.15, Osno0.43 (tax-entity-params.md).
    4. Словарь оверрайдов: overrideMap[(Year, Month)] → MonthOverrideValues из тела.
    5. Помесячный цикл i = 0..HorizonMonths (HorizonMonths + 1 итераций) — формулы таблицы InvestorMonth дома моделей (правила positiveOr / ?? / × 12; INV-CALC-2/3/5).
    6. Ипотека: аннуитет (S = BasePrice, r = 21/100/12, Months = 240) — одна величина для всех срезов.
    7. AsideMetrics для каждого i — 21 поле по таблице дома (все заполнены, readOnly = false).
    8. AnalyticsSeries: gross-profit (все месяцы), ebitda (только точки > 0), price-per-meter = синтетическая история + прогнозные round2(marketM2(i)).
    9. Финальный guard INV-CALC-1: любое не-конечное число результата → 0.
  5. Результат. Wire-модель серии (camelCase, ключи серий gross-profit/ebitda/price-per-meter); readOnly = false, isSyntheticHistory = true. Результат не персистится, повтор детерминирован.

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Area <= 0 / BasePrice < 0 / PricePerMeter <= 0; коэффициенты вне INV-MC2; оверрайды вне INV-MO3; дубль (year,month) INV-MO5Ошибка валидации запросаno retry
INVEST_ANALYTICS_INVALID_TAX_PARAMS400Неизвестное значение entity/taxНедопустимые параметры расчётаno retry
UNAUTHORIZED401Нет m2m-авторизации admin APIТребуется авторизация admin APIno retry
FORBIDDEN403Клиент не pin-admin-system / нет permission .viewДоступ запрещёнno retry

Совместимость и наблюдаемость

  • Контракт ответа — тот же, что у public ../public/get-property-object-investor-series.md; единственное отличие — вход = произвольные параметры, а не PropertyObjectId. Additive-поля — без breaking; удаление/переименование/смена типа — breaking (migration + rollout). Unknown enum на входе → 400.
  • Logs: admin-investor-preview-calculated (adminUserId, entity, tax, horizon, elapsed; PII нет). Metrics: invest_analytics_admin_operations_total{operation="investor-preview"}, latency histogram. Traces: span приёма → span расчёта; correlation через сквозной trace traceparent.

Acceptance Criteria

  • Given валидные basePrice/area/pricePerMeter и полный набор коэффициентов в теле, When администратор шлёт POST, Then 200 с months из HorizonMonths + 1 строк, readOnly = false, isSyntheticHistory = true; переданные коэффициенты использованы напрямую (уровень PropertyObjectTypeConfig не выбирается).
  • Given коэффициенты в теле не переданы, When POST, Then каждое поле разрешается по каскаду EconomicModelConfig → дефолт кода; chessboard/crm/ClickHouse не вызываются, ипотека = константы 21 %/240 мес.
  • Given неизвестное значение entity/tax, When POST, Then 400 INVEST_ANALYTICS_INVALID_TAX_PARAMS (молчаливый откат к дефолту запрещён).
  • Given entity/tax отсутствуют, When POST, Then приняты дефолты legal/usn-6 (не ошибка); при entity = individual переданный tax игнорируется в ставке (ставка 0.13), не ошибка.
  • Given коэффициент вне INV-MC2 или оверрайд вне INV-MO3 или дубль (year, month) (INV-MO5) или area <= 0/basePrice < 0/pricePerMeter <= 0, When POST, Then 400 ValidationError.
  • Given идентичное тело запроса послано дважды, When POST, Then результат побайтово одинаков (детерминизм INV-CALC-6; ничего не персистится).
  • Given любое промежуточное не-конечное число в расчёте, When POST, Then оно заменяется на 0 (финальный guard INV-CALC-1).

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

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

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