diff --git a/backend/app/api/v1/parcels.py b/backend/app/api/v1/parcels.py index 5862b6e2..db03e037 100644 --- a/backend/app/api/v1/parcels.py +++ b/backend/app/api/v1/parcels.py @@ -1379,12 +1379,8 @@ def export_parcel_forecast( if format == "docx": return Response( content=render_report_docx(run.result), - media_type=( - "application/vnd.openxmlformats-officedocument.wordprocessingml.document" - ), - headers={ - "Content-Disposition": f'attachment; filename="{base_name}.docx"' - }, + media_type=("application/vnd.openxmlformats-officedocument.wordprocessingml.document"), + headers={"Content-Disposition": f'attachment; filename="{base_name}.docx"'}, ) # pptx — байты (PowerPoint OOXML), отдельной веткой (зеркало docx early-return), @@ -1395,9 +1391,7 @@ def export_parcel_forecast( media_type=( "application/vnd.openxmlformats-officedocument.presentationml.presentation" ), - headers={ - "Content-Disposition": f'attachment; filename="{base_name}.pptx"' - }, + headers={"Content-Disposition": f'attachment; filename="{base_name}.pptx"'}, ) if format == "json": @@ -3003,7 +2997,11 @@ def analyze_parcel( db, geom_wkt, centroid_lon, centroid_lat ) except Exception: - logger.warning("ird block failed for cad=%s — analyze response unaffected", cad_num) + logger.warning( + "ird block failed for cad=%s — analyze response unaffected", + cad_num, + exc_info=True, + ) return result_payload diff --git a/backend/app/core/config.py b/backend/app/core/config.py index d325bbf4..723eb725 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -135,11 +135,11 @@ class Settings(BaseSettings): openrouteservice_api_key: str = "" # ИРД-слой в analyze (#1067 D9b «GG-форсайт»): поле `ird` в ответе analyze — - # parcel_ird_overlaps (м.132, incl opportunity) + функц.зона/КРТ (геопортал WFS) + - # ПЗЗ-регламент зоны (C8b). Default OFF: источники 2-3 — live-зависимость от внешнего - # ЕКБ-геопортала (reverse-eng, без SLA) в hot-пути analyze. Включать после прогона - # harvest на проде (B6) и заполнения zone_regulation_cache. - enable_ird_analyze: bool = False + # parcel_ird_overlaps (м.132, incl opportunity) + КРТ (геопортал WFS) + + # ПЗЗ-регламент зоны (C8b). Включён 2026-06-07 после B6-harvest + прогрева + # zone_regulation_cache (#1102). Override через env ENABLE_IRD_ANALYZE=false остаётся + # возможен. functional_zone выключен — слой пуст в геопортале ЕКБ (0 фич, #1058). + enable_ird_analyze: bool = True settings = Settings() diff --git a/backend/app/services/site_finder/ird_analyze.py b/backend/app/services/site_finder/ird_analyze.py index ca685a2a..1acecc71 100644 --- a/backend/app/services/site_finder/ird_analyze.py +++ b/backend/app/services/site_finder/ird_analyze.py @@ -33,7 +33,7 @@ from app.services.site_finder.zone_regulation import get_or_fetch_zone_regulatio logger = logging.getLogger(__name__) # Короткий timeout для geoportal-вызовов в hot-пути analyze (default клиента — 20s, слишком долго). -_GEOPORTAL_TIMEOUT_S = 8 +_GEOPORTAL_TIMEOUT_S = 4 def _functional_zone_at( @@ -105,10 +105,14 @@ def build_ird_analyze_block( "krt": [], "zone_regulation": None, } + # guard активен только в unit-тестах; в prod centroid_lon/lat всегда float + # (EKB-fallback в parcels.py). # Geoportal-источники — только при валидном centroid (network, опц.). if lon is not None and lat is not None: client = client or EKBGeoportalClient(timeout=_GEOPORTAL_TIMEOUT_S) - block["functional_zone"] = _functional_zone_at(client, lon, lat) + # Слой генплан-функц.зон пуст в геопортале ЕКБ (0 фич, 2026-06); источник = ФГИС ТП (#1058). + # Не дёргаем geoportal впустую — вернём _functional_zone_at когда появится ingest. + block["functional_zone"] = None block["krt"] = _krt_at(client, lon, lat) block["zone_regulation"] = _zone_regulation_at(db, client, lon, lat) return block diff --git a/backend/tests/api/v1/test_analyze_inline_weights.py b/backend/tests/api/v1/test_analyze_inline_weights.py index 05805746..c6ff3768 100644 --- a/backend/tests/api/v1/test_analyze_inline_weights.py +++ b/backend/tests/api/v1/test_analyze_inline_weights.py @@ -141,6 +141,19 @@ _PATCHES = [ ), patch("app.api.v1.parcels.compute_velocity", return_value=None), patch("app.api.v1.parcels.compute_gate_verdict", return_value={"verdict": "unknown"}), + # enable_ird_analyze=True by default — мокаем чтобы не делать сетевые вызовы в тестах + patch( + "app.api.v1.parcels.build_ird_analyze_block", + return_value={ + "ird_overlaps": [], + "ird_by_kind": {}, + "opportunity_overlaps": [], + "planning_projects": [], + "functional_zone": None, + "krt": [], + "zone_regulation": None, + }, + ), ] @@ -270,9 +283,9 @@ def test_inline_weights_rejects_nan() -> None: content=raw_body, headers={"Content-Type": "application/json"}, ) - assert resp.status_code == 422, ( - f"Ожидали 422 для NaN-weight, получили {resp.status_code}: {resp.text}" - ) + assert ( + resp.status_code == 422 + ), f"Ожидали 422 для NaN-weight, получили {resp.status_code}: {resp.text}" finally: app.dependency_overrides.clear() _stop_patches() diff --git a/backend/tests/api/v1/test_analyze_market_price.py b/backend/tests/api/v1/test_analyze_market_price.py index 41fa62b1..456d256d 100644 --- a/backend/tests/api/v1/test_analyze_market_price.py +++ b/backend/tests/api/v1/test_analyze_market_price.py @@ -140,6 +140,19 @@ _PATCHES = [ ), patch("app.api.v1.parcels.compute_velocity", return_value=None), patch("app.api.v1.parcels.compute_gate_verdict", return_value={"verdict": "unknown"}), + # enable_ird_analyze=True by default — мокаем чтобы не делать сетевые вызовы в тестах + patch( + "app.api.v1.parcels.build_ird_analyze_block", + return_value={ + "ird_overlaps": [], + "ird_by_kind": {}, + "opportunity_overlaps": [], + "planning_projects": [], + "functional_zone": None, + "krt": [], + "zone_regulation": None, + }, + ), ] diff --git a/backend/tests/api/v1/test_analyze_parcel_meta.py b/backend/tests/api/v1/test_analyze_parcel_meta.py index 7dfb9e76..a3fda907 100644 --- a/backend/tests/api/v1/test_analyze_parcel_meta.py +++ b/backend/tests/api/v1/test_analyze_parcel_meta.py @@ -147,6 +147,19 @@ _PATCHES = [ ), patch("app.api.v1.parcels.compute_velocity", return_value=None), patch("app.api.v1.parcels.compute_gate_verdict", return_value={"verdict": "unknown"}), + # enable_ird_analyze=True by default — мокаем чтобы не делать сетевые вызовы в тестах + patch( + "app.api.v1.parcels.build_ird_analyze_block", + return_value={ + "ird_overlaps": [], + "ird_by_kind": {}, + "opportunity_overlaps": [], + "planning_projects": [], + "functional_zone": None, + "krt": [], + "zone_regulation": None, + }, + ), ] diff --git a/backend/tests/api/v1/test_analyze_recent_permits.py b/backend/tests/api/v1/test_analyze_recent_permits.py index b935d9a2..f0ba3cd0 100644 --- a/backend/tests/api/v1/test_analyze_recent_permits.py +++ b/backend/tests/api/v1/test_analyze_recent_permits.py @@ -129,6 +129,19 @@ _PATCHES = [ ), patch("app.api.v1.parcels.compute_velocity", return_value=None), patch("app.api.v1.parcels.compute_gate_verdict", return_value={"verdict": "unknown"}), + # enable_ird_analyze=True by default — мокаем чтобы не делать сетевые вызовы в тестах + patch( + "app.api.v1.parcels.build_ird_analyze_block", + return_value={ + "ird_overlaps": [], + "ird_by_kind": {}, + "opportunity_overlaps": [], + "planning_projects": [], + "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 49f572cf..86c709fb 100644 --- a/backend/tests/services/test_ird_analyze.py +++ b/backend/tests/services/test_ird_analyze.py @@ -50,7 +50,9 @@ def _patch_overlay(monkeypatch: Any, overlay: dict[str, Any]) -> None: def test_full_block_assembly(monkeypatch: Any) -> None: - """Все источники → overlaps + split opportunity + функц.зона + КРТ + регламент.""" + """Все источники → overlaps + split opportunity + КРТ + регламент. + functional_zone всегда None: слой пуст в геопортале ЕКБ (0 фич, 2026-06) — WFS не дёргается. + """ _patch_overlay(monkeypatch, _OVERLAY) monkeypatch.setattr( ird_analyze, @@ -67,7 +69,9 @@ def test_full_block_assembly(monkeypatch: Any) -> None: "opportunity_auction", "opportunity_krt", ] - assert block["functional_zone"] == {"name": "Ж-зона"} + # functional_zone WFS выключен (слой пуст в геопортале ЕКБ, #1058) — всегда None + assert block["functional_zone"] is None + assert "functional_zone" not in [layer for layer, _, _ in client.calls] assert block["krt"] == [{"num_oks_krt": "КРТ-7"}] assert block["zone_regulation"] == {"zone_index": "Ц-1"} assert block["planning_projects"] == _PLANNING