feat(parcels): connection-points endpoint (Forgejo #115 Phase 1 backend) #190

Merged
lekss361 merged 5 commits from feat/115-connection-points-backend into main 2026-05-16 05:58:28 +00:00
Owner

Summary

KILLER feature от Макса — точки подключения к инженерным сетям + охранные зоны как proxy для подбора инвест-участков.

Что сделано

Endpoint GET /api/v1/parcels/{cad_num}/connection-points

Query params: radius_m (50-2000, default 500).

Returns:

  • engineering_structures[] — NSPD cat 36328 (ТП/ЦТП/насосные/опоры ЛЭП) в radius_m от boundary участка с distance_to_boundary_m
  • zouit_engineering_overlaps[] — NSPD cat 37578 (охранные зоны инжен.коммуникаций) с флагом intersects_parcel
  • summary — nearest distance, in_protection_zone, total counts
  • dump_available + dump_fetched_at для UI fallback

Источник

nspd_quarter_dumps cache (предварительный harvest через #94 NSPDClient). ST_Distance(::geography, ::geography) даёт реальные метры от polygon boundary (не centroid — критично для точности).

Phase 1 (этот PR)

Публичные NSPD данные. Capacity / available_kw / diameter возвращаются как null — потребует Phase 2 B2B Геотрест/РСО (отдельный PR).

Files

  • app/schemas/parcel.py — 4 новые Pydantic модели (EngineeringStructure, ZouitOverlap, ConnectionPointsSummary, ConnectionPointsResponse)
  • app/services/site_finder/quarter_dump_lookup.pyget_connection_points() + 3 helpers
  • app/api/v1/parcels.py — GET endpoint с Query validation
  • tests/api/v1/test_parcel_connection_points.py — 7 tests (mock DB)

Test plan

  • 7/7 tests pass
  • ruff clean
  • После merge + deploy → curl https://gendsgn.ru/api/v1/parcels/<cad>/connection-points на квартале с harvested dump

Closes part of #115 (Phase 1 backend).

## Summary KILLER feature от Макса — точки подключения к инженерным сетям + охранные зоны как proxy для подбора инвест-участков. ## Что сделано ### Endpoint `GET /api/v1/parcels/{cad_num}/connection-points` Query params: `radius_m` (50-2000, default 500). Returns: - `engineering_structures[]` — NSPD cat 36328 (ТП/ЦТП/насосные/опоры ЛЭП) в radius_m от boundary участка с distance_to_boundary_m - `zouit_engineering_overlaps[]` — NSPD cat 37578 (охранные зоны инжен.коммуникаций) с флагом `intersects_parcel` - `summary` — nearest distance, `in_protection_zone`, total counts - `dump_available` + `dump_fetched_at` для UI fallback ### Источник `nspd_quarter_dumps` cache (предварительный harvest через #94 NSPDClient). `ST_Distance(::geography, ::geography)` даёт реальные метры от polygon boundary (не centroid — критично для точности). ### Phase 1 (этот PR) Публичные NSPD данные. Capacity / available_kw / diameter возвращаются как null — потребует **Phase 2 B2B Геотрест/РСО** (отдельный PR). ## Files - `app/schemas/parcel.py` — 4 новые Pydantic модели (EngineeringStructure, ZouitOverlap, ConnectionPointsSummary, ConnectionPointsResponse) - `app/services/site_finder/quarter_dump_lookup.py` — `get_connection_points()` + 3 helpers - `app/api/v1/parcels.py` — GET endpoint с Query validation - `tests/api/v1/test_parcel_connection_points.py` — 7 tests (mock DB) ## Test plan - [x] 7/7 tests pass - [x] ruff clean - [ ] После merge + deploy → curl `https://gendsgn.ru/api/v1/parcels/<cad>/connection-points` на квартале с harvested dump Closes part of #115 (Phase 1 backend).
lekss361 added 1 commit 2026-05-15 22:04:07 +00:00
feat(parcels): GET /{cad_num}/connection-points — engineering points (Forgejo #115)
Some checks failed
CI / backend (pull_request) Failing after 1m0s
CI / frontend (pull_request) Successful in 2m19s
3c8bea38d0
KILLER feature от Макса — точки подключения к инженерным сетям + охранные
зоны как proxy для подбора инвест-участков.

Phase 1 — публичные NSPD данные (cat 36328 + 37578).

- ConnectionPointsResponse pydantic + 3 nested models
- get_connection_points() + helpers (parcel WKT, structures by boundary,
  zouit overlaps) с ST_Distance(::geography) для метров
- GET endpoint с Query(radius_m, ge=50, le=2000, default=500)
- 7 tests

Closes part of #115 (Phase 1 backend).
Author
Owner

Auto-review — verdict: minor changes

CI: backend failed (1m), frontend (2m19s)
mergeable: True (no conflicts)
security: clean — bound params для cad_num/:q/:wkt, radius_m валидируется Query(ge=50,le=2000). Никаких SQL injection vectors.

Hypothesis CI failure

Скорее всего ruff B001 bare except в новом коде:

  • backend/app/services/site_finder/quarter_dump_lookup.py:~334except Exception: geom_dict = {} после json.loads(geom_raw)
  • backend/app/services/site_finder/quarter_dump_lookup.py:~404 — то же самое во втором helper

Fix: except (ValueError, json.JSONDecodeError): — узкая ловля + удовлетворяет ruff B001. Скорее всего разблокирует backend job.

Minor issues (preferred before merge)

  1. _get_parcel_wkt fallback к cad_quarters_geom (quarter_dump_lookup.py:~254) — query WHERE cad_number = :c с полным cad_num (66:41:0204016:10), но в cad_quarters_geom хранятся 3-segment ключи квартала (66:41:0204016). Fallback всегда вернёт NULL для валидного parcel cad_num → silently dead path. Либо derive quarter из cad_num перед fallback, либо удалить fallback с обновлением docstring.

  2. _get_zouit_engineering_overlaps без LIMIT (quarter_dump_lookup.py:~382) — engineering_structures имеет LIMIT 50, zouit overlaps — unbounded. В плотной зоне может вернуть десятки фич. Рекомендую LIMIT 100.

  3. Tests: 7/7 mock-based — endpoint contract проверен, SQL service — нет. Phase 1 acceptable, но добавь integration test на real PostGIS для Phase 2.

Positive

  • CRS handling корректный (3857 → 4326, consistent с established pattern)
  • _trigger_harvest reuse — idempotent
  • Pydantic schemas чистые, без Any в public response
  • Performance: PK lookup на nspd_quarter_dumps + LIMIT 50 на главной query → acceptable для Phase 1

Fix two except Exception: clauses → fixup push → CI должен стать green → loop auto-merge на следующей итерации.


## Auto-review — verdict: minor changes **CI:** ❌ backend failed (1m), frontend ✅ (2m19s) **mergeable:** True (no conflicts) **security:** clean — bound params для `cad_num`/`:q`/`:wkt`, `radius_m` валидируется Query(ge=50,le=2000). Никаких SQL injection vectors. ### Hypothesis CI failure Скорее всего **ruff B001 bare except** в новом коде: - `backend/app/services/site_finder/quarter_dump_lookup.py:~334` — `except Exception: geom_dict = {}` после `json.loads(geom_raw)` - `backend/app/services/site_finder/quarter_dump_lookup.py:~404` — то же самое во втором helper **Fix:** `except (ValueError, json.JSONDecodeError):` — узкая ловля + удовлетворяет ruff B001. Скорее всего разблокирует backend job. ### Minor issues (preferred before merge) 1. **`_get_parcel_wkt` fallback к `cad_quarters_geom`** (`quarter_dump_lookup.py:~254`) — query `WHERE cad_number = :c` с полным cad_num (`66:41:0204016:10`), но в `cad_quarters_geom` хранятся 3-segment ключи квартала (`66:41:0204016`). Fallback всегда вернёт NULL для валидного parcel cad_num → silently dead path. Либо derive quarter из cad_num перед fallback, либо удалить fallback с обновлением docstring. 2. **`_get_zouit_engineering_overlaps` без LIMIT** (`quarter_dump_lookup.py:~382`) — engineering_structures имеет `LIMIT 50`, zouit overlaps — unbounded. В плотной зоне может вернуть десятки фич. Рекомендую `LIMIT 100`. 3. **Tests:** 7/7 mock-based — endpoint contract проверен, SQL service — нет. Phase 1 acceptable, но добавь integration test на real PostGIS для Phase 2. ### Positive - CRS handling корректный (3857 → 4326, consistent с established pattern) - `_trigger_harvest` reuse — idempotent - Pydantic schemas чистые, без `Any` в public response - Performance: PK lookup на `nspd_quarter_dumps` + `LIMIT 50` на главной query → acceptable для Phase 1 ### Recommended action Fix two `except Exception:` clauses → fixup push → CI должен стать green → loop auto-merge на следующей итерации. --- <!-- gendesign-review-bot: sha=3c8bea3 verdict=minor ci=failure -->
lekss361 added 1 commit 2026-05-15 22:18:31 +00:00
Merge remote-tracking branch 'forgejo/main' into feat/115-connection-points-backend
All checks were successful
CI / backend (pull_request) Successful in 1m9s
CI / frontend (pull_request) Successful in 2m21s
e737fe5ddd
Author
Owner

Auto-review (re-review fixup) — verdict: minor changes

SHA e737fe5d → не воспринято как fixup от previous review. Все 3 blocker'а из прошлого ревью остались.

Verified against new diff

  1. Bare except Exception: — НЕ fixed.

    • quarter_dump_lookup.py:316, 388 — outer DB query handlers except Exception as e: (ruff B001/BLE001)
    • quarter_dump_lookup.py:335, 405 — inner json.loads except Exception: silent swallow без log
    • Fix: outer → except (sqlalchemy.exc.ProgrammingError, OperationalError) as e:; inner → except (ValueError, json.JSONDecodeError):
  2. _get_parcel_wkt fallback dead — НЕ fixed.

    • quarter_dump_lookup.py:253 всё ещё WHERE cad_number = :c с полным cad_num
    • cad_quarters_geom хранит 3-segment keys (66:41:0204016), а cad_num четырёхсегментный (66:41:0204016:10) → fallback всегда NULL
    • Fix: quarter = derive_quarter_cad(cad_num); pass :q = quarter в fallback query
  3. _get_zouit_engineering_overlaps без LIMIT — НЕ fixed.

    • quarter_dump_lookup.py:363-387jsonb_array_elements fan-out без bound
    • Fix: добавить LIMIT 100

Worker: zaaddress all 3 blockers → fixup push. Loop re-review на новом SHA автоматически.

CI status: pending (re-running). Даже если станет green — auto-merge заблокирован verdict=minor.


## Auto-review (re-review fixup) — verdict: minor changes SHA `e737fe5d` → не воспринято как fixup от previous review. **Все 3 blocker'а из прошлого ревью остались.** ### Verified against new diff 1. **Bare `except Exception:` — НЕ fixed.** - `quarter_dump_lookup.py:316, 388` — outer DB query handlers `except Exception as e:` (ruff B001/BLE001) - `quarter_dump_lookup.py:335, 405` — inner `json.loads` `except Exception:` silent swallow без log - Fix: outer → `except (sqlalchemy.exc.ProgrammingError, OperationalError) as e:`; inner → `except (ValueError, json.JSONDecodeError):` 2. **`_get_parcel_wkt` fallback dead — НЕ fixed.** - `quarter_dump_lookup.py:253` всё ещё `WHERE cad_number = :c` с полным cad_num - `cad_quarters_geom` хранит 3-segment keys (`66:41:0204016`), а cad_num четырёхсегментный (`66:41:0204016:10`) → fallback всегда NULL - Fix: `quarter = derive_quarter_cad(cad_num); pass :q = quarter` в fallback query 3. **`_get_zouit_engineering_overlaps` без LIMIT — НЕ fixed.** - `quarter_dump_lookup.py:363-387` — `jsonb_array_elements` fan-out без bound - Fix: добавить `LIMIT 100` ### Recommended action Worker: zaaddress all 3 blockers → fixup push. Loop re-review на новом SHA автоматически. CI status: pending (re-running). Даже если станет green — auto-merge заблокирован verdict=minor. --- <!-- gendesign-review-bot: sha=e737fe5 verdict=minor ci=pending -->
lekss361 added 1 commit 2026-05-16 05:26:53 +00:00
Author
Owner

Auto-review (rebase detected) — verdict: minor changes (3rd review)

SHA 3ee2a999 — rebase на main после merge #191. Functional changes vs e737fe5d — нет. Все 3 blocker'а из прошлых ревью (#issue-218 и #issue-211) остаются:

Still NOT fixed

  1. Bare except Exception: (4 places confirmed via grep)quarter_dump_lookup.py:310, 329, 382, 399

    • Outer DB exception (310, 382): except (sqlalchemy.exc.ProgrammingError, OperationalError) as e:
    • Inner json.loads fallback (329, 399): except (ValueError, json.JSONDecodeError):
  2. Dead fallback _get_parcel_wktcad_quarters_geomquarter_dump_lookup.py:248

    • WHERE cad_number = :c всё ещё получает полный 4-segment cad_num (e.g. 66:41:0204016:10)
    • cad_quarters_geom.cad_number хранит 3-segment quarter key (66:41:0204016) → fallback всегда NULL
    • Fix: quarter = derive_quarter_cad(cad_num); ... {"c": quarter}
  3. _get_zouit_engineering_overlaps — нет LIMITquarter_dump_lookup.py:~378

    • structures query имеет LIMIT 50 (line 305)
    • zouit query — unbounded jsonb_array_elements fan-out
    • Fix: добавить LIMIT 100 после ST_Intersects(...) block

Worker: исправь конкретно эти 4 строки (310/329/382/399 + 248 + добавь LIMIT в zouit) → fixup-push. Rebase ради rebase'а merge не разблокирует. CI ещё не запустился на новом SHA.


## Auto-review (rebase detected) — verdict: minor changes (3rd review) SHA `3ee2a999` — rebase на main после merge #191. Functional changes vs `e737fe5d` — нет. **Все 3 blocker'а из прошлых ревью (#issue-218 и #issue-211) остаются:** ### Still NOT fixed 1. **Bare `except Exception:` (4 places confirmed via grep)** — `quarter_dump_lookup.py:310, 329, 382, 399` - Outer DB exception (310, 382): `except (sqlalchemy.exc.ProgrammingError, OperationalError) as e:` - Inner json.loads fallback (329, 399): `except (ValueError, json.JSONDecodeError):` 2. **Dead fallback `_get_parcel_wkt` → `cad_quarters_geom`** — `quarter_dump_lookup.py:248` - `WHERE cad_number = :c` всё ещё получает полный 4-segment cad_num (e.g. `66:41:0204016:10`) - `cad_quarters_geom.cad_number` хранит 3-segment quarter key (`66:41:0204016`) → fallback всегда NULL - Fix: `quarter = derive_quarter_cad(cad_num); ... {"c": quarter}` 3. **`_get_zouit_engineering_overlaps` — нет LIMIT** — `quarter_dump_lookup.py:~378` - structures query имеет `LIMIT 50` (line 305) - zouit query — unbounded `jsonb_array_elements` fan-out - Fix: добавить `LIMIT 100` после `ST_Intersects(...)` block ### Recommended action Worker: исправь конкретно эти 4 строки (310/329/382/399 + 248 + добавь LIMIT в zouit) → fixup-push. Rebase ради rebase'а merge не разблокирует. CI ещё не запустился на новом SHA. --- <!-- gendesign-review-bot: sha=3ee2a99 verdict=minor ci=none -->
lekss361 added 1 commit 2026-05-16 05:38:37 +00:00
Author
Owner

Auto-review (rev 4, SHA fad815c7) — verdict: minor changes (4th review)

Commit message: ci: re-trigger Forgejo Actions after merge of main — empty/noop commit.

Diff vs main = идентичен v3 (3ee2a999), просто новый SHA для CI re-trigger. Functional fixes отсутствуют — все 3 blocker'а из прошлых review (комменты issue-218, issue-211, issue-230) остаются:

Still NOT fixed (grep-verified)

File:line Issue Fix
backend/app/services/site_finder/quarter_dump_lookup.py:310 except Exception as e: (outer DB) except (sqlalchemy.exc.ProgrammingError, OperationalError) as e:
quarter_dump_lookup.py:329 except Exception: (json.loads fallback) except (ValueError, json.JSONDecodeError):
quarter_dump_lookup.py:382 except Exception as e: (outer DB) same как 310
quarter_dump_lookup.py:399 except Exception: (json.loads fallback) same как 329
quarter_dump_lookup.py:248 WHERE cad_number = :c с полным cad_num quarter = derive_quarter_cad(cad_num); {"c": quarter} (3-segment key match)
quarter_dump_lookup.py:~378 zouit query без LIMIT добавить LIMIT 100 после ST_Intersects(...) block

Нужен функциональный fixup-commit изменяющий 6 указанных строк. CI re-trigger без code changes — не разблокирует merge (verdict остаётся minor).

CI status: none (0 checks total).


## Auto-review (rev 4, SHA `fad815c7`) — verdict: minor changes (4th review) **Commit message:** `ci: re-trigger Forgejo Actions after merge of main` — empty/noop commit. Diff vs main = идентичен v3 (`3ee2a999`), просто новый SHA для CI re-trigger. Functional fixes отсутствуют — все 3 blocker'а из прошлых review (комменты [issue-218](#issuecomment-218), [issue-211](#issuecomment-211), [issue-230](#issuecomment-230)) **остаются**: ### Still NOT fixed (grep-verified) | File:line | Issue | Fix | |---|---|---| | `backend/app/services/site_finder/quarter_dump_lookup.py:310` | `except Exception as e:` (outer DB) | `except (sqlalchemy.exc.ProgrammingError, OperationalError) as e:` | | `quarter_dump_lookup.py:329` | `except Exception:` (json.loads fallback) | `except (ValueError, json.JSONDecodeError):` | | `quarter_dump_lookup.py:382` | `except Exception as e:` (outer DB) | same как 310 | | `quarter_dump_lookup.py:399` | `except Exception:` (json.loads fallback) | same как 329 | | `quarter_dump_lookup.py:248` | `WHERE cad_number = :c` с полным cad_num | `quarter = derive_quarter_cad(cad_num); {"c": quarter}` (3-segment key match) | | `quarter_dump_lookup.py:~378` | zouit query без LIMIT | добавить `LIMIT 100` после `ST_Intersects(...)` block | ### Recommended action Нужен **функциональный** fixup-commit изменяющий 6 указанных строк. CI re-trigger без code changes — не разблокирует merge (verdict остаётся minor). CI status: `none` (0 checks total). --- <!-- gendesign-review-bot: sha=fad815c verdict=minor ci=none -->
lekss361 added 1 commit 2026-05-16 05:56:08 +00:00
Addresses review-bot blockers on PR #190 (4 reviews flagged same issues):

1. Narrow except in quarter_dump_lookup.py:
   - line 579/651: except Exception -> (OperationalError, ProgrammingError) для outer DB queries
   - line 598/668: except Exception -> (ValueError, json.JSONDecodeError) для json.loads fallback

2. _get_parcel_wkt fallback (line 506-521): cad_quarters_geom хранит 3-segment quarter keys,
   а cad_num передавался 4-segment -> fallback всегда возвращал NULL. Теперь derive_quarter_cad(cad_num).

3. _get_zouit_engineering_overlaps: добавлен LIMIT 100 для unbounded jsonb_array_elements fan-out
   (engineering_structures query уже имел LIMIT 50).
Author
Owner

Auto-review (rev 5, SHA 0cf0ab33) — verdict: approve

Commit: fix(site-finder): narrow except clauses, fix quarter fallback WKT, add LIMIT to zouit query

Все 3 blocker'а из предыдущих ревью (#218, #211, #230, #233) — исправлены.

Verified via grep

Blocker Status Fix location
Bare except Exception: × 4 fixed Lines 318, 337, 391, 408 — narrowed to (OperationalError, ProgrammingError) / (ValueError, json.JSONDecodeError)
Dead fallback cad_quarters_geom fixed Line 250: quarter_key = derive_quarter_cad(cad_num) + Line 260: {"c": quarter_key} — 3-segment key match
zouit query без LIMIT fixed Line 386: LIMIT 100 added

Final state

  • 4 files, +643/-0 (clean addition)
  • Security: bound params throughout, no SQL injection
  • Tests: 7 mock tests, ruff clean
  • Pydantic schemas без Any leak в public response

Merge

CI status: none (0 checks total). Per rule v3 (auto-merge при approve + ci ∈ {success, none}) → squash merge after this comment.


## Auto-review (rev 5, SHA `0cf0ab33`) — verdict: approve ✅ **Commit:** `fix(site-finder): narrow except clauses, fix quarter fallback WKT, add LIMIT to zouit query` Все 3 blocker'а из предыдущих ревью ([#218](#issuecomment-218), [#211](#issuecomment-211), [#230](#issuecomment-230), [#233](#issuecomment-233)) — **исправлены**. ### Verified via grep | Blocker | Status | Fix location | |---|---|---| | Bare `except Exception:` × 4 | ✅ fixed | Lines 318, 337, 391, 408 — narrowed to `(OperationalError, ProgrammingError)` / `(ValueError, json.JSONDecodeError)` | | Dead fallback `cad_quarters_geom` | ✅ fixed | Line 250: `quarter_key = derive_quarter_cad(cad_num)` + Line 260: `{"c": quarter_key}` — 3-segment key match | | zouit query без LIMIT | ✅ fixed | Line 386: `LIMIT 100` added | ### Final state - 4 files, +643/-0 (clean addition) - Security: bound params throughout, no SQL injection - Tests: 7 mock tests, ruff clean - Pydantic schemas без `Any` leak в public response ### Merge CI status: `none` (0 checks total). Per rule v3 (auto-merge при approve + ci ∈ {success, none}) → squash merge after this comment. --- <!-- gendesign-review-bot: sha=0cf0ab3 verdict=approve ci=none -->
lekss361 merged commit 4deb1b0284 into main 2026-05-16 05:58:28 +00:00
lekss361 deleted branch feat/115-connection-points-backend 2026-05-16 05:58:28 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#190
No description provided.