"""Stage 1c tests — ТЭП and financial model arithmetic. Pure-arithmetic unit tests against known footprint geometry: GFA, residential area, apartment count, FAR, parking, revenue/cost/margin and the IRR-proxy clamp. """ from __future__ import annotations from shapely.geometry import box from app.schemas.concept import TEAP from app.services.generative import financial, teap # Один прямоугольник 20 x 10 = 200 кв.м пятна. _FOOTPRINT = box(0, 0, 20, 10) def test_teap_basic_arithmetic() -> None: result = teap.compute_teap( footprints=[_FOOTPRINT], floors=10, site_area_sqm=1000.0, housing_class="comfort", ) assert result.built_area_sqm == 200.0 # GFA = пятно * этажность. assert result.total_floor_area_sqm == 2000.0 # FAR = GFA / участок. assert result.density == 2.0 # Жилая = GFA * efficiency(comfort=0.78). assert result.residential_area_sqm == 1560.0 # Квартир = floor(жилая / avg(comfort=55)). assert result.apartments_count == int(1560.0 // 55.0) # Парковка = ceil(квартир * 1.0). assert result.parking_spaces == result.apartments_count def test_teap_class_changes_efficiency_and_lot() -> None: econ = teap.compute_teap( footprints=[_FOOTPRINT], floors=10, site_area_sqm=1000.0, housing_class="econom" ) biz = teap.compute_teap( footprints=[_FOOTPRINT], floors=10, site_area_sqm=1000.0, housing_class="business" ) # Эконом эффективнее по площади -> больше жилой при той же GFA. assert econ.residential_area_sqm > biz.residential_area_sqm # Бизнес — крупнее лот -> меньше квартир. assert biz.apartments_count < econ.apartments_count # Бизнес — выше норма парковки на квартиру. assert biz.parking_spaces / max(1, biz.apartments_count) >= 1.4 def test_teap_zero_site_area_no_division_error() -> None: result = teap.compute_teap( footprints=[_FOOTPRINT], floors=5, site_area_sqm=0.0, housing_class="comfort" ) assert result.density == 0.0 def test_teap_empty_placement_is_zeroed() -> None: result = teap.compute_teap( footprints=[], floors=9, site_area_sqm=1000.0, housing_class="comfort" ) assert result.built_area_sqm == 0.0 assert result.total_floor_area_sqm == 0.0 assert result.apartments_count == 0 assert result.parking_spaces == 0 def _teap(residential: float, gfa: float, parking: int = 10) -> TEAP: return TEAP( built_area_sqm=100.0, total_floor_area_sqm=gfa, residential_area_sqm=residential, apartments_count=10, density=1.0, parking_spaces=parking, ) def test_financial_revenue_includes_parking() -> None: t = _teap(residential=1000.0, gfa=1300.0, parking=10) model = financial.compute_financial( teap=t, housing_class="comfort", land_cost_rub=50_000_000.0 ) # revenue = жильё (1000 * 145_000) + паркинг (10 * 1_900_000). assert model.revenue_residential_rub == 1000.0 * 145_000.0 assert model.revenue_parking_rub == 10 * 1_900_000.0 assert model.revenue_rub == model.revenue_residential_rub + model.revenue_parking_rub def test_financial_cost_cascade_includes_all_lines() -> None: t = _teap(residential=1000.0, gfa=1300.0, parking=10) model = financial.compute_financial( teap=t, housing_class="comfort", land_cost_rub=50_000_000.0 ) # СМР = GFA*СМР + паркинг*себест. assert model.construction_rub == 1300.0 * 88_000.0 + 10 * 1_800_000.0 # Каждая статья каскада > 0 при ненулевых вводных. assert model.pir_rub > 0 assert model.networks_rub > 0 assert model.developer_services_rub > 0 assert model.contingency_rub > 0 assert model.marketing_rub > 0 assert model.land_rub == 50_000_000.0 # Итог затрат = сумма всех статей. expected_cost = ( model.construction_rub + model.pir_rub + model.networks_rub + model.developer_services_rub + model.contingency_rub + model.marketing_rub + model.land_rub ) assert abs(model.cost_rub - expected_cost) < 1.0 assert model.gross_margin_rub == model.revenue_rub - model.cost_rub def test_financial_net_profit_below_gross_margin_when_profitable() -> None: # Прибыльный сценарий: налоги срезают валовую маржу -> net < gross. t = _teap(residential=10_000.0, gfa=11_000.0, parking=50) model = financial.compute_financial(teap=t, housing_class="comfort", land_cost_rub=None) assert model.gross_margin_rub > 0 assert model.vat_rub > 0 assert model.profit_tax_rub > 0 assert model.net_profit_rub < model.profit_before_tax_rub < model.gross_margin_rub # ROI и маржа% согласованы с чистой прибылью (поля округлены до 4 знаков). assert model.roi == round(model.net_profit_rub / model.cost_rub, 4) assert model.margin_pct == round(model.net_profit_rub / model.revenue_rub, 4) def test_financial_no_profit_tax_on_loss() -> None: # Убыток: налог на прибыль = 0, НДС = 0; net == pbt == gross_margin. t = _teap(residential=1.0, gfa=10_000.0, parking=0) model = financial.compute_financial(teap=t, housing_class="business", land_cost_rub=None) assert model.gross_margin_rub < 0 assert model.profit_tax_rub == 0.0 assert model.vat_rub == 0.0 assert model.profit_before_tax_rub == model.gross_margin_rub assert model.net_profit_rub == model.gross_margin_rub def test_financial_parking_zero_yields_zero_parking_lines() -> None: t = _teap(residential=1000.0, gfa=1300.0, parking=0) model = financial.compute_financial(teap=t, housing_class="comfort", land_cost_rub=None) assert model.revenue_parking_rub == 0.0 # СМР без паркинговой составляющей = ровно GFA*СМР. assert model.construction_rub == 1300.0 * 88_000.0 def test_financial_land_cost_optional() -> None: t = _teap(residential=1000.0, gfa=1300.0) no_land = financial.compute_financial(teap=t, housing_class="comfort", land_cost_rub=None) with_land = financial.compute_financial( teap=t, housing_class="comfort", land_cost_rub=10_000_000.0 ) # Земля увеличивает затраты ровно на свою стоимость. assert with_land.cost_rub - no_land.cost_rub == 10_000_000.0 assert with_land.land_rub == 10_000_000.0 assert no_land.land_rub == 0.0 def test_financial_irr_proxy_clamped_and_flagged() -> None: # Огромная маржа -> irr-proxy зажат в [-1, 1]; флаг proxy выставлен. t = _teap(residential=100_000.0, gfa=1.0, parking=0) model = financial.compute_financial(teap=t, housing_class="business", land_cost_rub=None) assert -1.0 <= model.irr <= 1.0 assert model.irr_is_proxy is True def test_financial_zero_cost_no_division_error() -> None: t = _teap(residential=0.0, gfa=0.0, parking=0) model = financial.compute_financial(teap=t, housing_class="comfort", land_cost_rub=None) assert model.cost_rub == 0.0 assert model.revenue_rub == 0.0 assert model.roi == 0.0 assert model.margin_pct == 0.0 assert model.irr == 0.0 def test_financial_backward_compat_fields_present() -> None: t = _teap(residential=1000.0, gfa=1300.0) model = financial.compute_financial(teap=t, housing_class="comfort", land_cost_rub=None) # Старые summary-поля контракта остаются. assert isinstance(model.revenue_rub, float) assert isinstance(model.cost_rub, float) assert isinstance(model.gross_margin_rub, float) assert isinstance(model.irr, float) # PR-3 DCF-поля additive — присутствуют с дефолтами. assert isinstance(model.npv_rub, float) assert model.discount_rate_used == 0.15 assert model.schedule_is_default is True assert model.payback_months is None or isinstance(model.payback_months, float) # ── PR-2: рыночная калибровка цены продажи жилья ─────────────────────────────── def test_financial_no_market_price_uses_class_norm() -> None: # Без market_price_per_sqm — норматив класса comfort (145_000), флаг не калибровано. t = _teap(residential=1000.0, gfa=1300.0) model = financial.compute_financial(teap=t, housing_class="comfort", land_cost_rub=None) assert model.price_per_sqm_used == 145_000.0 assert model.price_is_calibrated is False assert model.price_source == "class_norm" # Выручка жилья считается по нормативу. assert model.revenue_residential_rub == 1000.0 * 145_000.0 def test_financial_market_price_overrides_class_norm() -> None: # market_price_per_sqm передан → используется он, флаг калибровано, source проброшен. t = _teap(residential=1000.0, gfa=1300.0) model = financial.compute_financial( teap=t, housing_class="comfort", land_cost_rub=None, market_price_per_sqm=180_000.0, price_source="objective_district_median", ) assert model.price_per_sqm_used == 180_000.0 assert model.price_is_calibrated is True assert model.price_source == "objective_district_median" # Выручка жилья считается по рыночной цене, НЕ по нормативу класса. assert model.revenue_residential_rub == 1000.0 * 180_000.0 def test_financial_market_price_does_not_calibrate_parking_or_smr() -> None: # Калибруем ТОЛЬКО цену жилья: паркинг (1.9М) и себестоимость СМР (88_000) — норма. t = _teap(residential=1000.0, gfa=1300.0, parking=10) model = financial.compute_financial( teap=t, housing_class="comfort", land_cost_rub=None, market_price_per_sqm=200_000.0, price_source="district_reference", ) # Паркинг — норматив (10 * 1_900_000), не зависит от рыночной цены жилья. assert model.revenue_parking_rub == 10 * 1_900_000.0 # СМР — норматив класса comfort (GFA*88_000 + паркинг*1_800_000). assert model.construction_rub == 1300.0 * 88_000.0 + 10 * 1_800_000.0 def test_financial_none_market_price_forces_class_norm_source() -> None: # Даже если source передан, при market_price_per_sqm=None он форсится в class_norm — # нельзя выдавать норматив за рыночные данные. t = _teap(residential=1000.0, gfa=1300.0) model = financial.compute_financial( teap=t, housing_class="comfort", land_cost_rub=None, market_price_per_sqm=None, price_source="objective_district_median", ) assert model.price_is_calibrated is False assert model.price_source == "class_norm" assert model.price_per_sqm_used == 145_000.0