from typing import Any, Literal from pydantic import BaseModel, Field class ConceptInput(BaseModel): """Stage 1a — input contract. Frozen interface for frontend codegen.""" parcel_geojson: dict[str, Any] = Field( ..., description="GeoJSON Polygon of the parcel (WGS84 / EPSG:4326)" ) housing_class: Literal["econom", "comfort", "business"] = "comfort" target_floors: int = Field(9, ge=1, le=30) development_type: Literal["spot", "mid_rise", "high_rise"] = "mid_rise" land_cost_rub: float | None = Field( None, ge=0, description="Optional land cost for financial model" ) class TEAP(BaseModel): """Технико-экономические показатели.""" built_area_sqm: float total_floor_area_sqm: float # Нежилое (коммерция/офисы 1-го этажа) = GFA × нормативная доля по классу # (см. ``_OFFICE_SHARE_OF_GFA`` в teap.py). Вырезается из GFA до расчёта жилой — # жилая ужимается ровно на эту площадь, total_floor_area_sqm не меняется. Additive # поле (default 0.0) → backward-compat: 0.0 = нежилое не моделировалось/не нужно. office_area_sqm: float = 0.0 residential_area_sqm: float apartments_count: int density: float parking_spaces: int class FinancialModel(BaseModel): """Static developer P&L + monthly-DCF investment metrics (PR-1/PR-3, эпик #1881). Full static cost cascade + VAT + profit tax → net profit + ROI (PR-1). On top of that static P&L, PR-3 lays the cascade onto a **monthly cashflow schedule** (default phase norms — ПИР → СМР → продажи) and runs a real DCF: NPV, IRR (bisection over the monthly cashflow) and *undiscounted* cumulative payback. See ``compute_financial`` docstring for the schedule assumptions and the налоги/рассрочка phasing choices. ``irr`` is a **real** annualised DCF IRR when ``irr_is_proxy is False``; for a degenerate cashflow with no sign change (e.g. always-loss) the bisection cannot bracket a root, so we fall back to the PR-1 annualised-ROI proxy and flag ``irr_is_proxy=True``. VAT is a documented simplification (see docstring), not exact НК РФ mechanics. ``levered_irr`` — IRR на собственные средства (equity cashflow при LTC): тот же DCF-IRR + roi-proxy fallback, но по equity-потоку графика долга. При положительном леверидже выше unlevered ``irr``; вырожденные случаи → 0.0 + ``levered_irr_is_proxy``. The legacy summary fields (``revenue_rub`` / ``cost_rub`` / ``gross_margin_rub`` / ``irr``) are kept for backward-compat; the new fields expose the full cascade, the net P&L and the DCF metrics. """ # ── Legacy summary (backward-compat) ─────────────────────────────────────── revenue_rub: float cost_rub: float gross_margin_rub: float irr: float # ── Revenue breakdown ────────────────────────────────────────────────────── revenue_residential_rub: float revenue_parking_rub: float # Выручка нежилого (коммерция/офисы 1-го этажа) = office_area_sqm × цена нежилого # (см. ``_OFFICE_SALE_PRICE_PER_SQM`` в financial.py). Чисто ADDITIVE выручка: # СМР нежилого уже сидит в ``construction_rub`` (нежилая GFA входит в total_floor_area # → construction = total_floor_area × ставка уже её покрывает; double-count'а нет). # Нежилое ОБЛАГАЕТСЯ НДС (в отличие от льготы жилья ст.149 НК) — его value-added # входит в базу НДС. Default 0.0 → backward-compat (нежилое не моделировалось). revenue_office_rub: float = 0.0 # ── Cost cascade ─────────────────────────────────────────────────────────── construction_rub: float pir_rub: float networks_rub: float developer_services_rub: float contingency_rub: float marketing_rub: float land_rub: float # ── БДР / taxes → net profit ─────────────────────────────────────────────── vat_rub: float profit_before_tax_rub: float profit_tax_rub: float net_profit_rub: float # ── Metrics ──────────────────────────────────────────────────────────────── roi: float # net profit / total cost margin_pct: float # net profit / revenue irr_is_proxy: bool = True # frontend caveat: True → IRR is the ROI-proxy, not DCF # ── DCF / investment metrics (PR-3, эпик #1881) ──────────────────────────── # NPV дисконтированного помесячного cashflow по ставке ``discount_rate_used``. npv_rub: float = 0.0 # Окупаемость (мес) по НЕдисконтированному накопительному cashflow, линейная # интерполяция дробного месяца. ``None`` — проект не окупается на горизонте. payback_months: float | None = None # Годовая ставка дисконтирования, фактически применённая в NPV (норматив). discount_rate_used: float = 0.0 # График фаз/продаж — типовые нормативные ДОПУЩЕНИЯ (не график конкретного # проекта). True → UI/PDF показывают caveat «график — типовое допущение». RANK 1: # False, когда окно продаж построено по реальной рыночной скорости абсорбции # района (а не дефолт-30-мес). schedule_is_default: bool = True # RANK 1: реализованное окно продаж (sales_end − sales_start), мес — для UI/PDF. # Драйвится рыночной абсорбцией района, когда ``schedule_is_default is False``; # иначе дефолт-норматив (с эскроу-привязкой к завершению стройки). ``None`` — # финмодель не строилась/окно неизвестно. sales_duration_months: float | None = None # ── Financing overlay (PR-5, эпик #1881) ─────────────────────────────────── # OVERLAY поверх unlevered cashflow: кредит покрывает кассовый разрыв, проценты # капитализируются (compound). Unlevered NPV/IRR (headline) НЕ затронуты. financing_enabled: bool = False # True когда overlay посчитан annual_rate_used: float = 0.0 # _CREDIT_RATE_ANNUAL (норматив, отдельно от дисконта) peak_debt_rub: float = 0.0 # «задолженность по кредиту» Excel — пик долга total_interest_rub: float = 0.0 # «поток по финдеятельности» Excel — Σ процентов net_profit_after_financing_rub: float = 0.0 # net_profit − total_interest # honest-флаг: 100% покрытие разрыва, compound %, эскроу не моделируется точно. financing_is_simplified: bool = True # Levered (equity) IRR: доходность на собственные средства (equity cashflow при LTC). # При положительном леверидже > unlevered ``irr``. Считается DCF-IRR'ом equity-потока # того же графика долга, с тем же roi-proxy fallback'ом, что и unlevered IRR. levered_irr: float = 0.0 # IRR на собственные средства (equity cashflow при LTC) # honest-флаг: True → levered_irr — roi-proxy (вырожденный equity-поток / нет equity # под риском при LTC>=1 / финансирование выключено), не настоящий DCF-IRR. levered_irr_is_proxy: bool = True # True → levered_irr — roi-proxy, не DCF # ── Price calibration (PR-2, эпик #1881) ─────────────────────────────────── # Цена продажи жилья, руб/кв.м, фактически использованная в выручке. Либо # калиброванная по рынку (Objective / district reference), либо норматив класса. # ``price_source`` honest-flag для UI/PDF: не выдавать норматив за рынок. price_per_sqm_used: float price_is_calibrated: bool = False # Значения: "objective_district_median" | "district_reference" | "class_norm". price_source: str = "class_norm" class ConceptVariant(BaseModel): strategy: Literal["max_area", "max_insolation", "balanced"] buildings_geojson: dict[str, Any] teap: TEAP financial: FinancialModel class ConceptOutput(BaseModel): variants: list[ConceptVariant]