TASK A (#29 G2): add parcel_meta to analyze response - New ParcelMeta Pydantic schema in app/schemas/parcel.py - SELECT from cad_parcels WHERE cad_num=:c in analyze_parcel() (step 9f) - Returns permitted_use_established_by_document, land_record_category_type, land_record_subtype, cost_value; None when row absent - Tests: test_analyze_parcel_meta.py (found + not-found cases) TASK B (#232 G3): cad_zouit fallback in _get_zouit_overlaps - When nspd_quarter_dumps has zouit_count==0, fall back to ST_Intersects query on cad_zouit (3483 rows, GIST indexed) - Overlaps tagged with source='cad_zouit'; format compatible with NSPD path - gate_verdict.py: BLOCKER_TYPE_ZONE_KEYWORDS tuple for keyword-based classification (охранная зона / трубопровод / электр / газ -> blocker; СЗЗ -> warning); NSPD subcategory path preserved backward-compat - Tests: 6 new test cases in test_gate_verdict.py covering cad_zouit path and backward-compat for NSPD subcategory path Updated db.execute call sequence in test_analyze_*.py (index shift +1 at pos 10).
207 lines
9.3 KiB
Python
207 lines
9.3 KiB
Python
"""Tests for gate_verdict aggregator — pure function, no DB."""
|
||
|
||
from app.services.site_finder.gate_verdict import (
|
||
compute_gate_verdict,
|
||
is_residential_zone,
|
||
)
|
||
|
||
# ── is_residential_zone unit tests ────────────────────────────────────────────
|
||
|
||
|
||
def test_residential_zone_code_zh2():
|
||
assert is_residential_zone("Ж-2", None) is True
|
||
|
||
|
||
def test_residential_zone_code_zh5():
|
||
assert is_residential_zone("Ж5", None) is True
|
||
|
||
|
||
def test_residential_zone_name_fallback():
|
||
assert is_residential_zone(None, "Жилая зона смешанной застройки") is True
|
||
|
||
|
||
def test_non_residential_zone_code():
|
||
assert is_residential_zone("П-1", "Производственная П-1") is False
|
||
|
||
|
||
def test_non_residential_zone_recreation():
|
||
assert is_residential_zone("Р-2", "Рекреационная Р-2") is False
|
||
|
||
|
||
# ── compute_gate_verdict ──────────────────────────────────────────────────────
|
||
|
||
|
||
def test_no_dump_returns_unknown():
|
||
"""Если nspd_dump отсутствует или available=False → unknown + no_data."""
|
||
verdict = compute_gate_verdict(None, None, None, None)
|
||
assert verdict["can_build_mkd"] == "unknown"
|
||
assert verdict["source"] == "no_data"
|
||
assert verdict["verdict_label"] == "Нужна проверка"
|
||
assert any(w["code"] == "NO_NSPD_DUMP" for w in verdict["warnings"])
|
||
assert verdict["checks_performed"] == []
|
||
|
||
|
||
def test_no_dump_available_false():
|
||
verdict = compute_gate_verdict(None, None, None, {"available": False})
|
||
assert verdict["can_build_mkd"] == "unknown"
|
||
assert verdict["source"] == "no_data"
|
||
|
||
|
||
def test_residential_zone_no_overlaps_can_build():
|
||
"""Жилая зона + нет ЗОУИТ + есть сети → Можно."""
|
||
nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая зона Ж-2"}
|
||
verdict = compute_gate_verdict(nspd_zoning, [], [{"name": "ТП-5"}], {"available": True})
|
||
assert verdict["can_build_mkd"] is True
|
||
assert verdict["verdict_label"] == "Можно"
|
||
assert verdict["blockers"] == []
|
||
assert verdict["source"] == "nspd_dump"
|
||
|
||
|
||
def test_production_zone_blocks():
|
||
"""Производственная зона → blocker PZZ_NOT_RESIDENTIAL → Нельзя."""
|
||
nspd_zoning = {"zone_code": "П-1", "zone_name": "Производственная П-1"}
|
||
verdict = compute_gate_verdict(nspd_zoning, [], [], {"available": True})
|
||
assert verdict["can_build_mkd"] is False
|
||
assert verdict["verdict_label"] == "Нельзя"
|
||
assert any(b["code"] == "PZZ_NOT_RESIDENTIAL" for b in verdict["blockers"])
|
||
|
||
|
||
def test_zouit_subcategory_17_blocks():
|
||
"""ЗОУИТ subcategory=17 (охранная зона ЛЭП) блокирует даже жилую зону."""
|
||
nspd_zoning = {"zone_code": "Ж-3", "zone_name": "Жилая"}
|
||
overlaps = [{"subcategory": 17, "name": "ЛЭП 110кВ", "layer": "37578"}]
|
||
verdict = compute_gate_verdict(nspd_zoning, overlaps, [], {"available": True})
|
||
assert verdict["can_build_mkd"] is False
|
||
assert any("ZOUIT_OVERLAP_SUB17" == b["code"] for b in verdict["blockers"])
|
||
assert "ЛЭП 110кВ" in verdict["blockers"][0]["detail"]
|
||
|
||
|
||
def test_zouit_non_blocker_subcategory_is_warning():
|
||
"""ЗОУИТ с subcategory != 17 → warning, не blocker."""
|
||
nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая"}
|
||
overlaps = [{"subcategory": 5, "name": "Придорожная полоса", "layer": "12345"}]
|
||
verdict = compute_gate_verdict(nspd_zoning, overlaps, [{"name": "ТП-1"}], {"available": True})
|
||
assert verdict["can_build_mkd"] is True
|
||
assert verdict["verdict_label"] == "С ограничениями"
|
||
assert any("ZOUIT_SUB5" == w["code"] for w in verdict["warnings"])
|
||
assert verdict["blockers"] == []
|
||
|
||
|
||
def test_no_engineering_warning_not_blocker():
|
||
"""Отсутствие инженерных сетей в 200м — предупреждение, не блокер."""
|
||
nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая"}
|
||
verdict = compute_gate_verdict(nspd_zoning, [], [], {"available": True})
|
||
assert verdict["can_build_mkd"] is True # warnings не блокируют
|
||
assert any(w["code"] == "NO_ENGINEERING_NEARBY" for w in verdict["warnings"])
|
||
assert verdict["blockers"] == []
|
||
|
||
|
||
def test_stale_dump_marks_source_partial():
|
||
"""stale=True → source='nspd_dump_partial'."""
|
||
nspd_zoning = {"zone_code": "Ж-1", "zone_name": "Жилая"}
|
||
dump = {"available": True, "stale": True}
|
||
verdict = compute_gate_verdict(nspd_zoning, [], [{"name": "ТП-3"}], dump)
|
||
assert verdict["source"] == "nspd_dump_partial"
|
||
|
||
|
||
def test_checks_performed_list():
|
||
"""checks_performed содержит все 3 проверки при наличии данных."""
|
||
nspd_zoning = {"zone_code": "Ж-1", "zone_name": "Жилая"}
|
||
verdict = compute_gate_verdict(nspd_zoning, [], [], {"available": True})
|
||
assert len(verdict["checks_performed"]) == 3
|
||
assert "ПЗЗ зональность" in verdict["checks_performed"]
|
||
assert "ЗОУИТ пересечения" in verdict["checks_performed"]
|
||
|
||
|
||
def test_pzz_unknown_warning_when_no_zoning():
|
||
"""Если nspd_zoning=None при наличии dump — PZZ_UNKNOWN warning."""
|
||
verdict = compute_gate_verdict(None, [], [{"name": "ТП-1"}], {"available": True})
|
||
assert any(w["code"] == "PZZ_UNKNOWN" for w in verdict["warnings"])
|
||
assert verdict["can_build_mkd"] is True
|
||
|
||
|
||
# ── cad_zouit fallback path (#232) ────────────────────────────────────────────
|
||
|
||
|
||
def test_cad_zouit_ohranaya_zona_truboprovodov_blocks():
|
||
"""cad_zouit overlap с 'охранная зона трубопроводов' → ZOUIT_CAD_BLOCKER → Нельзя."""
|
||
nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая"}
|
||
overlaps = [
|
||
{
|
||
"source": "cad_zouit",
|
||
"type_zone": "Охранная зона трубопроводов",
|
||
"layer": "Охранная зона трубопроводов",
|
||
"name": "Газопровод высокого давления",
|
||
}
|
||
]
|
||
verdict = compute_gate_verdict(nspd_zoning, overlaps, [], {"available": True})
|
||
assert verdict["can_build_mkd"] is False
|
||
assert verdict["verdict_label"] == "Нельзя"
|
||
assert any(b["code"] == "ZOUIT_CAD_BLOCKER" for b in verdict["blockers"])
|
||
|
||
|
||
def test_cad_zouit_elektr_blocks():
|
||
"""cad_zouit overlap с 'электр' substring в type_zone → blocker."""
|
||
nspd_zoning = {"zone_code": "Ж-3", "zone_name": "Жилая"}
|
||
overlaps = [
|
||
{
|
||
"source": "cad_zouit",
|
||
"type_zone": "Охранная зона электросетевого объекта",
|
||
"layer": "Охранная зона электросетевого объекта",
|
||
"name": "ЛЭП 110 кВ",
|
||
}
|
||
]
|
||
verdict = compute_gate_verdict(nspd_zoning, overlaps, [], {"available": True})
|
||
assert verdict["can_build_mkd"] is False
|
||
assert any(b["code"] == "ZOUIT_CAD_BLOCKER" for b in verdict["blockers"])
|
||
|
||
|
||
def test_cad_zouit_szz_is_warning_not_blocker():
|
||
"""cad_zouit overlap с 'СЗЗ' → ZOUIT_CAD_SZZ warning, не blocker."""
|
||
nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая"}
|
||
overlaps = [
|
||
{
|
||
"source": "cad_zouit",
|
||
"type_zone": "Санитарно-защитная зона предприятия",
|
||
"layer": "Санитарно-защитная зона предприятия",
|
||
"name": "СЗЗ завода",
|
||
}
|
||
]
|
||
verdict = compute_gate_verdict(nspd_zoning, overlaps, [{"name": "ТП-1"}], {"available": True})
|
||
assert verdict["can_build_mkd"] is True
|
||
assert verdict["verdict_label"] == "С ограничениями"
|
||
assert any(w["code"] == "ZOUIT_CAD_SZZ" for w in verdict["warnings"])
|
||
assert verdict["blockers"] == []
|
||
|
||
|
||
def test_cad_zouit_other_is_warning():
|
||
"""cad_zouit overlap без blocker/СЗЗ keyword → ZOUIT_CAD_OTHER warning."""
|
||
nspd_zoning = {"zone_code": "Ж-2", "zone_name": "Жилая"}
|
||
overlaps = [
|
||
{
|
||
"source": "cad_zouit",
|
||
"type_zone": "Зона публичного сервитута",
|
||
"layer": "Зона публичного сервитута",
|
||
"name": None,
|
||
}
|
||
]
|
||
verdict = compute_gate_verdict(nspd_zoning, overlaps, [{"name": "ТП-1"}], {"available": True})
|
||
assert verdict["can_build_mkd"] is True
|
||
assert any(w["code"] == "ZOUIT_CAD_OTHER" for w in verdict["warnings"])
|
||
assert verdict["blockers"] == []
|
||
|
||
|
||
def test_nspd_subcategory_path_backward_compat():
|
||
"""NSPD dump path (source != 'cad_zouit') продолжает работать через subcategory."""
|
||
nspd_zoning = {"zone_code": "Ж-3", "zone_name": "Жилая"}
|
||
overlaps = [
|
||
{
|
||
"source": "nspd-quarter-dump",
|
||
"subcategory": 17,
|
||
"name": "ЛЭП 110кВ",
|
||
"layer": "37578",
|
||
}
|
||
]
|
||
verdict = compute_gate_verdict(nspd_zoning, overlaps, [], {"available": True})
|
||
assert verdict["can_build_mkd"] is False
|
||
assert any(b["code"] == "ZOUIT_OVERLAP_SUB17" for b in verdict["blockers"])
|