From d9b92f19f4372e24e75e85ea50ab0e9ede9b9d93 Mon Sep 17 00:00:00 2001 From: lekss361 Date: Sun, 7 Jun 2026 11:09:26 +0000 Subject: [PATCH] =?UTF-8?q?feat(sf):=20=D0=B2=D1=88=D0=B8=D1=82=D1=8C=20pa?= =?UTF-8?q?rcel=5Freservations=20=D0=B2=20=D0=98=D0=A0=D0=94-=D0=B1=D0=BB?= =?UTF-8?q?=D0=BE=D0=BA=20analyze=20(#1127)=20Co-authored-by:=20lekss361?= =?UTF-8?q?=20=20Co-committed-by:=20lekss361=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/parcels.py | 2 +- .../app/services/site_finder/ird_analyze.py | 6 +++ backend/tests/services/test_ird_analyze.py | 48 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/backend/app/api/v1/parcels.py b/backend/app/api/v1/parcels.py index db03e037..d99173a4 100644 --- a/backend/app/api/v1/parcels.py +++ b/backend/app/api/v1/parcels.py @@ -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( diff --git a/backend/app/services/site_finder/ird_analyze.py b/backend/app/services/site_finder/ird_analyze.py index 41a82922..2a705ab4 100644 --- a/backend/app/services/site_finder/ird_analyze.py +++ b/backend/app/services/site_finder/ird_analyze.py @@ -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, diff --git a/backend/tests/services/test_ird_analyze.py b/backend/tests/services/test_ird_analyze.py index 197aece7..fc55b9d3 100644 --- a/backend/tests/services/test_ird_analyze.py +++ b/backend/tests/services/test_ird_analyze.py @@ -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"] == []