gendesign/backend/tests/test_layout_tz_html.py
bot-backend fc6542416d
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 2m50s
Deploy / build-worker (push) Successful in 5m25s
Deploy / deploy (push) Successful in 2m27s
feat(exporters): §4.2 supply-only блок в ТЗ-PDF (#2177) (#2260)
2026-07-03 06:38:23 +00:00

130 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests для build_layout_tz_html — §4.2 «По предложению (без темпа продаж)».
Проверяют HTML-разметку ТЗ (наличие блоков, RU-микрокопию, форматирование ₽/м²)
БЕЗ WeasyPrint: `build_layout_tz_html` — PURE-функция (строит строку HTML), поэтому
эти тесты гоняются и на macOS/Windows, где native GTK/Pango отсутствуют (в отличие
от test_layout_tz_pdf.py, который зависит от write_pdf → native libs).
Ключевой сценарий: §4.2 блок рендерится, когда `supply_only_layouts` есть, и
graceful ОТСУТСТВУЕТ (без ошибок), когда список пуст (старые снапшоты / нет матча).
"""
import datetime as dt
from app.schemas.parcel import (
BestLayoutsResponse,
LayoutDataQuality,
LayoutTzMixRow,
LayoutTzRecommendation,
SupplyOnlyLayoutRow,
TopLayoutRow,
)
from app.services.exporters.layout_tz_pdf import build_layout_tz_html
def _base_response(supply_only: list[SupplyOnlyLayoutRow] | None = None) -> BestLayoutsResponse:
return BestLayoutsResponse(
top_layouts=[
TopLayoutRow(
rank=1,
room_bucket="1",
area_bin="25-40",
signature="1__25-40",
competitor_obj_ids=[1234],
competitor_count=1,
total_sold_in_window=67,
velocity_per_month=8.4,
avg_price_per_m2_rub=148000.0,
avg_area_m2=38.5,
supply_units_in_radius=312,
sold_pct_of_supply=21.5,
is_oversold=False,
),
],
recommendation_for_tz=LayoutTzRecommendation(
rationale_text="Test rationale текст",
mix=[LayoutTzMixRow(room_bucket="1", pct=100, abs_units=100, avg_target_area_m2=38.5)],
weighted_avg_price_per_m2_rub=152000.0,
based_on_obj_count=5,
based_on_total_deals=107,
data_window_start=dt.date(2026, 2, 1),
data_window_end=dt.date(2026, 5, 1),
),
data_quality=LayoutDataQuality(
objects_with_velocity_data=5,
objects_total_in_radius=8,
raw_objects_total=14,
velocity_coverage_pct=62.5,
confidence="medium",
),
supply_only_layouts=supply_only or [],
)
def _render(response: BestLayoutsResponse) -> str:
return build_layout_tz_html(
response,
cad_num="66:41:0204016:10",
radius_km=1.0,
time_window="last_quarter",
)
def test_supply_only_block_renders_when_present() -> None:
"""§4.2: блок «По предложению (без темпа продаж)» + строки supply_only_layouts."""
resp = _base_response(
[
SupplyOnlyLayoutRow(
project_name="ЖК Тестовый",
room_bucket="2",
area_bin="50-65",
lots_count=17,
median_price_m2=132500.0,
median_area_m2=58.3,
),
]
)
html = _render(resp)
# Заголовок + caveat про «без темпа продаж» / objective_lots.
assert "По предложению (без темпа продаж)" in html
assert "objective_lots" in html or "Объектив" in html
# Данные строки: комплекс, тип, лоты, медиана ₽/м², ср. площадь.
assert "ЖК Тестовый" in html
assert "17" in html
assert "132 500 ₽/м²" in html # тыс-разделитель — неразрывный/обычный пробел
assert "58.3 м²" in html
def test_supply_only_block_absent_when_empty() -> None:
"""Graceful: пустой supply_only_layouts (старый снапшот) → секции НЕТ, не падаем."""
html = _render(_base_response([]))
assert "По предложению (без темпа продаж)" not in html
# Основной документ всё равно собран.
assert "Техническое задание на проектирование" in html
def test_supply_only_null_price_renders_dash() -> None:
"""median_price_m2=None (цены не прошли price-sanity) → «—», без ошибок."""
resp = _base_response(
[
SupplyOnlyLayoutRow(
project_name="ЖК Без цен",
room_bucket="studio",
area_bin="<25",
lots_count=4,
median_price_m2=None,
median_area_m2=None,
),
]
)
html = _render(resp)
assert "По предложению (без темпа продаж)" in html
assert "ЖК Без цен" in html
assert "<td>—</td>" in html
def test_top_layouts_block_still_present() -> None:
"""§4.2-блок аддитивен: топ продаж конкурентов не потеснён."""
html = _render(_base_response([]))
assert "Топ планировок конкурентов по продажам" in html
assert "Рекомендуемая структура квартирографии" in html