diff --git a/backend/app/services/cadastre/bulk_harvest.py b/backend/app/services/cadastre/bulk_harvest.py index 40e7dcae..d8530681 100644 --- a/backend/app/services/cadastre/bulk_harvest.py +++ b/backend/app/services/cadastre/bulk_harvest.py @@ -406,7 +406,21 @@ def upsert_parcel(db: Session, feature: NSPDBulkFeature, source: str = "search") return raw_opts = feature.properties.get("options") or {} - geom_json = json.dumps(feature.geometry) if feature.geometry else None + # cad_parcels.geom — geometry(Polygon, 4326). NSPD изредка возвращает Point + # для участков без detailed boundary — INSERT fail'нит на schema check. + # Сохраняем raw_props (есть координаты), geom=NULL. Не блокирует Phase 1. + geom = feature.geometry or {} + geom_type = geom.get("type") if isinstance(geom, dict) else None + if geom_type in ("Polygon", "MultiPolygon"): + geom_json: str | None = json.dumps(feature.geometry) + else: + if geom_type: + logger.info( + "upsert_parcel: cad_num=%s имеет geometry type=%s (не Polygon) — geom=NULL", + cad_num, + geom_type, + ) + geom_json = None quarter_cad = opts.quarter_cad_number or _derive_quarter_cad(cad_num) db.execute( @@ -880,7 +894,20 @@ def upsert_zouit(db: Session, feature: NSPDBulkFeature, source: str = "search") ) return - geom_json = json.dumps(feature.geometry) if feature.geometry else None + # cad_zouit.geom — geometry(MultiPolygon, 4326). Polygon допустим (ST_Multi + # обернёт в SQL), но Point/LineString → INSERT fail. Фильтруем как в upsert_parcel. + geom = feature.geometry or {} + geom_type = geom.get("type") if isinstance(geom, dict) else None + if geom_type in ("Polygon", "MultiPolygon"): + geom_json: str | None = json.dumps(feature.geometry) + else: + if geom_type: + logger.info( + "upsert_zouit: reg=%s имеет geometry type=%s (не Polygon) — geom=NULL", + reg_numb_border, + geom_type, + ) + geom_json = None db.execute( text(""" diff --git a/backend/tests/services/test_cadastre_bulk.py b/backend/tests/services/test_cadastre_bulk.py index 5431bfe6..fb4c8472 100644 --- a/backend/tests/services/test_cadastre_bulk.py +++ b/backend/tests/services/test_cadastre_bulk.py @@ -923,6 +923,85 @@ def test_upsert_quarter_geom_sql_uses_cast_not_double_colon() -> None: assert "CAST(:geom AS text)" in sql +def test_upsert_parcel_skips_geom_for_point_geometry() -> None: + """NSPD изредка возвращает Point для ЗУ — cad_parcels.geom strict Polygon, + INSERT fail'нул бы. Фильтр на Python level → geom=NULL, raw_props сохранён. + """ + from app.services.cadastre.bulk_harvest import upsert_parcel + + point_parcel = NSPDBulkFeature.model_validate( + { + "id": 99, + "type": "Feature", + "geometry": {"type": "Point", "coordinates": [6753892.91, 7747664.31]}, + "properties": { + "category": 36368, + "options": { + "cad_num": "66:41:0104002:18", + "quarter_cad_number": "66:41:0104002", + "declared_area": 200.0, + "readable_address": "г Екатеринбург, ул Тестовая", + }, + }, + } + ) + + db = MagicMock() + upsert_parcel(db, point_parcel, source="search") + + # db.execute должен быть вызван (parcel НЕ skipped, только geom=NULL) + db.execute.assert_called_once() + call_params = db.execute.call_args[0][1] + assert call_params["cad_num"] == "66:41:0104002:18" + # geom параметр должен быть None + assert call_params["geom"] is None + + +def test_upsert_parcel_keeps_polygon_geometry() -> None: + """Polygon geometry для ЗУ — обычный happy path.""" + from app.services.cadastre.bulk_harvest import upsert_parcel + + parcel = _make_parcel_feature() # Polygon + db = MagicMock() + upsert_parcel(db, parcel, source="search") + + db.execute.assert_called_once() + call_params = db.execute.call_args[0][1] + assert call_params["geom"] is not None + assert '"type": "Polygon"' in call_params["geom"] + + +def test_upsert_zouit_skips_geom_for_linestring() -> None: + """ЗОУИТ schema MultiPolygon — LineString не валиден, geom=NULL.""" + from app.services.cadastre.bulk_harvest import upsert_zouit + + line_zouit = NSPDBulkFeature.model_validate( + { + "id": 1, + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [[6090000, 7590000], [6090100, 7590100]], + }, + "properties": { + "category": 36940, + "options": { + "reg_numb_border": "66-0.1-2.1-99", + "type_zone": "Linear", + "registration_date": "2020-01-01", + }, + }, + } + ) + + db = MagicMock() + upsert_zouit(db, line_zouit, source="search") + + db.execute.assert_called_once() + call_params = db.execute.call_args[0][1] + assert call_params["geom"] is None + + def test_upsert_quarter_geom_custom_width_bounds() -> None: """min_width_m и max_width_m пробрасываются как параметры SQL (не hardcoded).""" db = _mock_db_execute_scalar(None)