feat(sf): вшить parcel_reservations в ИРД-блок analyze (#1062, #1091)
All checks were successful
CI / changes (push) Successful in 8s
CI / changes (pull_request) Successful in 7s
CI / backend-tests (push) Successful in 6m30s
CI / backend-tests (pull_request) Successful in 6m27s

This commit is contained in:
bot-backend 2026-06-07 14:01:03 +03:00
parent f5aa7ed191
commit d3aa396ceb
3 changed files with 55 additions and 1 deletions

View file

@ -2994,7 +2994,7 @@ def analyze_parcel(
if settings.enable_ird_analyze:
try:
result_payload["ird"] = build_ird_analyze_block(
db, geom_wkt, centroid_lon, centroid_lat
db, geom_wkt, centroid_lon, centroid_lat, cad_num
)
except Exception:
logger.warning(

View file

@ -29,6 +29,7 @@ from sqlalchemy.orm import Session
from app.services.scrapers.ekb_geoportal_client import EKBGeoportalClient
from app.services.site_finder.ird_overlay_lookup import parcel_ird_overlaps
from app.services.site_finder.planning_lookup import parcel_planning_overlaps
from app.services.site_finder.reservation_lookup import parcel_reservations
from app.services.site_finder.zone_regulation import get_or_fetch_zone_regulation
logger = logging.getLogger(__name__)
@ -75,6 +76,7 @@ def build_ird_analyze_block(
geom_wkt: str | None,
lon: float | None,
lat: float | None,
cad_num: str | None = None,
*,
client: EKBGeoportalClient | None = None,
) -> dict[str, Any]:
@ -84,6 +86,8 @@ def build_ird_analyze_block(
db: сессия.
geom_wkt: геометрия участка WKT (EPSG:4326) для DB-overlay ``ird_overlays``.
lon/lat: centroid участка (EPSG:4326) для geoportal-резолва функц.зоны/КРТ/регламента.
cad_num: кадастровый номер ЗУ для lookup изъятия/резервирования (м.136 land_reservation).
None reservations=[].
client: переиспользуемый EKBGeoportalClient (для тестов/одного TLS-пула). По умолчанию
создаётся с укороченным timeout.
@ -91,6 +95,7 @@ def build_ird_analyze_block(
dict: ``ird_overlaps``/``ird_by_kind`` (DB-overlay, incl opportunity_*),
``opportunity_overlaps`` (подмножество layer_kind='opportunity_*'),
``planning_projects`` (ППТ/ПМТ overlap, DB, м.134 #1085),
``reservations`` (list, изъятие/резервирование из land_reservation по cad_num, #1091/#1062),
``functional_zone`` (генплан, dict|None), ``krt`` (list), ``zone_regulation`` (dict|None).
Любой источник деградирует независимо блок всегда well-formed.
"""
@ -102,6 +107,7 @@ def build_ird_analyze_block(
"ird_by_kind": overlay.get("ird_by_kind", {}),
"opportunity_overlaps": opportunity,
"planning_projects": parcel_planning_overlaps(db, geom_wkt),
"reservations": parcel_reservations(db, cad_num),
"functional_zone": None,
"krt": [],
"zone_regulation": None,

View file

@ -48,6 +48,7 @@ _PLANNING = [{"project_type": "ppt", "doc_status_name": "действующий"
def _patch_overlay(monkeypatch: Any, overlay: dict[str, Any]) -> None:
monkeypatch.setattr(ird_analyze, "parcel_ird_overlaps", lambda db, wkt: overlay)
monkeypatch.setattr(ird_analyze, "parcel_planning_overlaps", lambda db, wkt: list(_PLANNING))
monkeypatch.setattr(ird_analyze, "parcel_reservations", lambda db, cad_num: [])
def test_full_block_assembly(monkeypatch: Any) -> None:
@ -191,3 +192,50 @@ def test_session_not_shared_between_threads(monkeypatch: Any) -> None:
assert (
zone_reg_thread[0] is main_thread
), "zone_regulation должен исполняться в основном потоке, а не в фоновом (Session safety)"
# ---------------------------------------------------------------------------
# Reservation tests (#1091, #1062)
# ---------------------------------------------------------------------------
_RESERVATION_ROW = {
"reservation_kind": "изъятие",
"basis_act": "Постановление Правительства",
"act_number": "12-ПП",
"act_date": "2024-03-01",
"purpose": "дорожное строительство",
"doc_url": "https://example.com/doc.pdf",
}
def test_reservations_wired_to_block(monkeypatch: Any) -> None:
"""cad_num передан → parcel_reservations возвращает запись → ключ reservations в block."""
_patch_overlay(monkeypatch, _OVERLAY)
monkeypatch.setattr(
ird_analyze,
"parcel_reservations",
lambda db, cad_num: [_RESERVATION_ROW] if cad_num == "66:41:0101001:123" else [],
)
monkeypatch.setattr(ird_analyze, "get_or_fetch_zone_regulation", lambda *a, **kw: None)
client = _FakeClient(fz=[], krt=[])
block = ird_analyze.build_ird_analyze_block(
object(), "POINT(60 56)", None, None, "66:41:0101001:123", client=client
)
assert "reservations" in block
assert len(block["reservations"]) == 1
assert block["reservations"][0]["reservation_kind"] == "изъятие"
def test_reservations_none_cad_num_returns_empty(monkeypatch: Any) -> None:
"""cad_num=None → reservations == [] (parcel_reservations graceful, ключ присутствует)."""
_patch_overlay(monkeypatch, {"ird_overlaps": [], "ird_by_kind": {}})
monkeypatch.setattr(ird_analyze, "get_or_fetch_zone_regulation", lambda *a, **kw: None)
client = _FakeClient(fz=[], krt=[])
block = ird_analyze.build_ird_analyze_block(object(), None, None, None, None, client=client)
assert "reservations" in block
assert block["reservations"] == []