fix(#243): cad_zouit fallback срабатывает когда nspd dump полностью отсутствует #248
2 changed files with 159 additions and 2 deletions
|
|
@ -146,9 +146,14 @@ def get_quarter_dump_data(
|
|||
max_age = timedelta(days=_DUMP_MAX_AGE_DAYS)
|
||||
|
||||
if row is None:
|
||||
# Дампа нет — ставим harvest в очередь
|
||||
# Дампа нет — ставим harvest в очередь и делаем fallback на cad_zouit (#243).
|
||||
harvest_triggered = _trigger_harvest(quarter)
|
||||
return make_empty_result(harvest_triggered=harvest_triggered)
|
||||
cad_zouit_overlaps: list[dict[str, Any]] = (
|
||||
_get_cad_zouit_overlaps(db, parcel_wkt) if parcel_wkt is not None else []
|
||||
)
|
||||
result = make_empty_result(harvest_triggered=harvest_triggered)
|
||||
result["nspd_zouit_overlaps"] = cad_zouit_overlaps
|
||||
return result
|
||||
|
||||
fetched_at: datetime = row[1]
|
||||
# Убедимся что timezone-aware для корректного сравнения
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@ import pytest
|
|||
from app.services.site_finder.quarter_dump_lookup import (
|
||||
EMPTY_DUMP_RESULT,
|
||||
_extract_features_by_layer,
|
||||
_get_cad_zouit_overlaps,
|
||||
_get_opportunity_parcels,
|
||||
_get_red_lines,
|
||||
_get_risk_zones,
|
||||
derive_quarter_cad,
|
||||
get_quarter_dump_data,
|
||||
make_empty_result,
|
||||
)
|
||||
|
||||
|
|
@ -402,3 +404,153 @@ def test_make_empty_result_has_tier4_keys() -> None:
|
|||
)
|
||||
def test_derive_quarter_cad(cad: str, expected: str | None) -> None:
|
||||
assert derive_quarter_cad(cad) == expected
|
||||
|
||||
|
||||
# ── #243: cad_zouit fallback when dump absent ─────────────────────────────────
|
||||
|
||||
|
||||
def _make_mock_db_no_dump_with_cad_zouit(
|
||||
cad_zouit_rows: list[tuple[Any, ...]],
|
||||
) -> MagicMock:
|
||||
"""Мок Session: nspd_quarter_dumps.first() → None; cad_zouit.fetchall() → rows."""
|
||||
db = MagicMock()
|
||||
first_result = MagicMock()
|
||||
first_result.first.return_value = None # нет дампа
|
||||
|
||||
fetchall_result = MagicMock()
|
||||
fetchall_result.fetchall.return_value = cad_zouit_rows
|
||||
|
||||
# Первый вызов execute (SELECT nspd_quarter_dumps) → first()=None
|
||||
# Второй вызов execute (SELECT cad_zouit) → fetchall()=rows
|
||||
db.execute.side_effect = [first_result, fetchall_result]
|
||||
return db
|
||||
|
||||
|
||||
def test_cad_zouit_fallback_fires_when_no_dump() -> None:
|
||||
"""#243: cad_zouit fallback срабатывает когда nspd_quarter_dumps пуст глобально.
|
||||
|
||||
Ожидаем: nspd_zouit_overlaps непустой, source='cad_zouit'.
|
||||
"""
|
||||
cad_zouit_rows: list[tuple[Any, ...]] = [
|
||||
# (type_zone, category_name, name_by_doc, reg_numb_border, id)
|
||||
("Охранная зона трубопровода", "Трубопровод", "ГВС ул. Ленина", "66-66-00/123", 42),
|
||||
]
|
||||
db = _make_mock_db_no_dump_with_cad_zouit(cad_zouit_rows)
|
||||
|
||||
wkt = "POLYGON((60 56,61 56,61 57,60 57,60 56))"
|
||||
result = get_quarter_dump_data(db, "66:41:0603016:194", wkt)
|
||||
|
||||
overlaps: list[dict[str, Any]] = result["nspd_zouit_overlaps"]
|
||||
assert len(overlaps) == 1
|
||||
assert overlaps[0]["source"] == "cad_zouit"
|
||||
assert overlaps[0]["type_zone"] == "Охранная зона трубопровода"
|
||||
assert overlaps[0]["name"] == "ГВС ул. Ленина"
|
||||
# dump.available остаётся False (нет дампа)
|
||||
assert result["nspd_dump"]["available"] is False
|
||||
|
||||
|
||||
def test_cad_zouit_fallback_no_dump_no_parcel_wkt() -> None:
|
||||
"""#243: dump=None + parcel_wkt=None → overlaps пустой, не вызываем cad_zouit."""
|
||||
db = MagicMock()
|
||||
first_result = MagicMock()
|
||||
first_result.first.return_value = None
|
||||
db.execute.return_value = first_result
|
||||
|
||||
result = get_quarter_dump_data(db, "66:41:0603016:194", None)
|
||||
|
||||
assert result["nspd_zouit_overlaps"] == []
|
||||
# cad_zouit SELECT не должен вызываться (только один execute для dump lookup)
|
||||
assert db.execute.call_count == 1
|
||||
|
||||
|
||||
def test_cad_zouit_fallback_empty_when_no_overlaps() -> None:
|
||||
"""#243: dump=None + cad_zouit возвращает 0 строк → nspd_zouit_overlaps пуст."""
|
||||
db = _make_mock_db_no_dump_with_cad_zouit([])
|
||||
|
||||
wkt = "POLYGON((60 56,61 56,61 57,60 57,60 56))"
|
||||
result = get_quarter_dump_data(db, "66:41:0603016:194", wkt)
|
||||
|
||||
assert result["nspd_zouit_overlaps"] == []
|
||||
|
||||
|
||||
def test_get_cad_zouit_overlaps_returns_correct_schema() -> None:
|
||||
"""_get_cad_zouit_overlaps: поля group_key/layer/source/type_zone корректны."""
|
||||
rows: list[tuple[Any, ...]] = [
|
||||
# (type_zone, category_name, name_by_doc, reg_numb_border, id)
|
||||
("Охранная зона ЛЭП 110кВ", "Электроснабжение", "ВЛ-110", "66-123", 7),
|
||||
("СЗЗ промышленного объекта", "Санитарная", "Завод", "66-456", 8),
|
||||
]
|
||||
db = _make_mock_db_with_rows(rows)
|
||||
|
||||
result = _get_cad_zouit_overlaps(db, "POLYGON((0 0,1 0,1 1,0 0))")
|
||||
|
||||
assert len(result) == 2
|
||||
lep = result[0]
|
||||
assert lep["group_key"] == "cad_zouit"
|
||||
assert lep["source"] == "cad_zouit"
|
||||
assert lep["type_zone"] == "Охранная зона ЛЭП 110кВ"
|
||||
assert lep["subcategory"] is None # всегда NULL в cad_zouit
|
||||
assert lep["name"] == "ВЛ-110"
|
||||
assert lep["raw_props"]["zouit_id"] == 7
|
||||
|
||||
|
||||
def test_nspd_path_used_when_zouit_count_nonzero() -> None:
|
||||
"""#243 backward compat: когда dump существует с zouit_count>0 — NSPD path, не cad_zouit.
|
||||
|
||||
Мок: row.zouit_count=3, fetchall возвращает NSPD features.
|
||||
Проверяем source='nspd-quarter-dump' (не cad_zouit).
|
||||
"""
|
||||
# Строка дампа: все поля через MagicMock
|
||||
from datetime import UTC, datetime
|
||||
|
||||
dump_row = MagicMock()
|
||||
dump_row.__getitem__ = lambda self, i: [
|
||||
"66:41:0603016", # quarter_cad
|
||||
datetime(2026, 1, 1, tzinfo=UTC), # fetched_at_utc (свежий)
|
||||
100, # total_features
|
||||
None, # harvest_error
|
||||
1, # territorial_zones_count
|
||||
3, # zouit_count > 0 → NSPD path
|
||||
0, # engineering_count
|
||||
0, # risks_count
|
||||
0, # opportunity_count
|
||||
0, # red_lines_count
|
||||
][i]
|
||||
|
||||
nspd_zouit_rows: list[tuple[Any, ...]] = [
|
||||
# (layer, props)
|
||||
("zouit_engineering", {"name": "ЛЭП", "subcategory": 17}),
|
||||
]
|
||||
|
||||
db = MagicMock()
|
||||
first_result = MagicMock()
|
||||
first_result.first.return_value = dump_row
|
||||
|
||||
# Последующие вызовы execute (spatial queries) — возвращаем пустые кроме zouit
|
||||
zouit_result = MagicMock()
|
||||
zouit_result.fetchall.return_value = nspd_zouit_rows
|
||||
|
||||
empty_result = MagicMock()
|
||||
empty_result.fetchall.return_value = []
|
||||
empty_result.first.return_value = None
|
||||
|
||||
# [0]=dumps lookup, [1]=zoning(first), [2]=zouit(fetchall), [3+]=others
|
||||
db.execute.side_effect = [
|
||||
first_result,
|
||||
empty_result, # _get_zoning first()
|
||||
zouit_result, # _get_zouit_overlaps fetchall()
|
||||
empty_result, # _get_engineering_nearby fetchall()
|
||||
empty_result, # _get_risk_zones fetchall()
|
||||
empty_result, # _get_opportunity_parcels fetchall()
|
||||
empty_result, # _get_red_lines fetchall()
|
||||
]
|
||||
|
||||
result = get_quarter_dump_data(
|
||||
db, "66:41:0603016:194", "POLYGON((60 56,61 56,61 57,60 57,60 56))"
|
||||
)
|
||||
|
||||
overlaps: list[dict[str, Any]] = result["nspd_zouit_overlaps"]
|
||||
assert len(overlaps) == 1
|
||||
assert overlaps[0]["source"] == "nspd-quarter-dump"
|
||||
# dump.available=True т.к. свежий дамп
|
||||
assert result["nspd_dump"]["available"] is True
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue