fix(sf-02): добавить site_status/ready_dt в /analyze competitors + sort строящиеся first #276

Merged
lekss361 merged 1 commit from fix/sf-02-competitors-status-analyze into main 2026-05-17 10:35:47 +00:00
Owner

Summary

SF FixList #2 — competitors в /analyze теперь различают строящиеся/сданные.

SQL в /analyze (parcels.py:1337) не выбирал site_status, ready_dt из domrf_kn_objects; ORDER BY flat_count DESC ставил сданные ЖК (например ПИК Космонавтов 11 × 4 сданных 2020-2025) в топ.

Изменения

  • backend/app/api/v1/parcels.py:1337 — SELECT добавил site_status, ready_dt
  • ORDER BY заменён на CASE site_status WHEN 'Строящиеся' THEN 0 ELSE 1 END, distance_m ASC
  • backend/app/schemas/parcel.pyCompetitor extended fields
  • backend/tests/api/v1/test_analyze_competitors_status.py — 5 new tests (fields, values, sort, regression)
  • Vault fixes/Fix_SF_Competitors_Status_May17.md

Test plan

  • 5/5 tests pass + ruff clean
  • After deploy: /analyze для cad с конкурентами → response.competitors[] имеет site_status + строящиеся первыми

Related: epic #271 item #2 (Wave 1)

## Summary SF FixList #2 — competitors в `/analyze` теперь различают строящиеся/сданные. SQL в `/analyze` (`parcels.py:1337`) не выбирал `site_status`, `ready_dt` из `domrf_kn_objects`; `ORDER BY flat_count DESC` ставил сданные ЖК (например ПИК Космонавтов 11 × 4 сданных 2020-2025) в топ. ## Изменения - `backend/app/api/v1/parcels.py:1337` — SELECT добавил `site_status, ready_dt` - ORDER BY заменён на `CASE site_status WHEN 'Строящиеся' THEN 0 ELSE 1 END, distance_m ASC` - `backend/app/schemas/parcel.py` — `Competitor` extended fields - `backend/tests/api/v1/test_analyze_competitors_status.py` — 5 new tests (fields, values, sort, regression) - Vault `fixes/Fix_SF_Competitors_Status_May17.md` ## Test plan - [x] 5/5 tests pass + ruff clean - [ ] After deploy: `/analyze` для cad с конкурентами → `response.competitors[]` имеет `site_status` + строящиеся первыми Related: epic #271 item #2 (Wave 1)
lekss361 added 1 commit 2026-05-17 10:31:29 +00:00
SQL в /analyze не выбирал site_status, ready_dt из domrf_kn_objects;
ORDER BY flat_count DESC ставил сданные ЖК в топ.

Closes (epic part) #271 item 2
Author
Owner

Deep Code Review — verdict APPROVE

Files: 2 (P1: parcels.py, P3: новый test) · +126/-1 · mergeable=true · base=15e75c20

Проверено

  • SQL (parcels.py:1337-1354): site_status, ready_dt добавлены в SELECT; колонки существуют в domrf_kn_objects (text / date). JOIN не добавлен — оба поля доступны из CTE latest_obj o (SELECT *). Ambiguity нет (single source).
  • ORDER BY: CASE site_status WHEN 'Строящиеся' THEN 0 ELSE 1 END, distance_m ASCNULL / Сданные / Проблемные → bucket 1, distance_m ASC как tie-breaker. Корректно.
  • psycopg v3 / CAST: правок binding не делал, :: syntax не вводил — clean.
  • Pydantic Competitor: site_status: str | None = None, ready_dt: dt.date | None = None — backward compat OK, default None.
  • Cross-file: downstream consumers competitor_rows (parcels.py:2070,2120) бьют через dict(c) — новые ключи просто пробросятся, ничего не сломается. _score_parcel(competitor_rows=...) использует len() — sort change не влияет.
  • Existing tests: test_velocity.py:335 обращается к sample_competitors[0] от другого механизма (/competitors endpoint, не /analyze) — не задеваем.
  • Vault: fixes/Fix_SF_Competitors_Status_May17.md создан .

Замечания (не блокирующие)

  • 🟡 Minor: magic string 'Строящиеся' в SQL литералом. При переименовании значения в БД (например миграция на enum/латиницу) — sort silent сломается. Acceptable: значение pинимается напрямую из DOM.РФ API, scraper его не трансформирует (audit подтверждает). Логировать в Endpoint_Parcel_Analyze.md если ещё нет.
  • 🟢 Nit: тесты mock-based — проверяют логику сортировки на Python-симуляции SQL, не реальный roundtrip. Регрессия типа «забыли поле в SELECT» ими не ловится. Smoke на проде после deploy обязателен.
  • 🟢 Perf: новый sort даёт Sort node перед LIMIT 20, но input set ≤ ~50 rows (radius 3km + DISTINCT ON) — cost negligible. Index obj_district_class_active_idx не задействован тут (нет фильтра по district/class), что OK.

Post-merge smoke

curl -X POST .../api/v1/parcels/66:41:0702048:27/analyze | jq '.competitors[0:3] | .[] | {comm_name, site_status, ready_dt, distance_m}'
# Ожидаем: первый элемент site_status='Строящиеся' (если есть в радиусе)

Follow-up (отдельный PR, не блокирующий)

  • Frontend CompetitorTable — Badge для site_status + filter tabs (Wave 1 item из SiteFinder_FixList_May17).
  • TS codegen: npm run codegen после деплоя backend → ParcelAnalysisCompetitor подхватит новые поля.

Мержу.

## Deep Code Review — verdict ✅ APPROVE **Files**: 2 (P1: `parcels.py`, P3: новый test) · **+126/-1** · mergeable=true · base=15e75c20 ### Проверено - **SQL (`parcels.py:1337-1354`)**: `site_status`, `ready_dt` добавлены в SELECT; колонки существуют в `domrf_kn_objects` (`text` / `date`). JOIN не добавлен — оба поля доступны из CTE `latest_obj o` (SELECT *). Ambiguity нет (single source). - **ORDER BY**: `CASE site_status WHEN 'Строящиеся' THEN 0 ELSE 1 END, distance_m ASC` — `NULL` / `Сданные` / `Проблемные` → bucket 1, `distance_m ASC` как tie-breaker. Корректно. - **psycopg v3 / CAST**: правок binding не делал, `::` syntax не вводил — clean. - **Pydantic `Competitor`**: `site_status: str | None = None`, `ready_dt: dt.date | None = None` — backward compat OK, default None. - **Cross-file**: downstream consumers `competitor_rows` (`parcels.py:2070,2120`) бьют через `dict(c)` — новые ключи просто пробросятся, ничего не сломается. `_score_parcel(competitor_rows=...)` использует `len()` — sort change не влияет. - **Existing tests**: `test_velocity.py:335` обращается к `sample_competitors[0]` от другого механизма (`/competitors` endpoint, не `/analyze`) — не задеваем. - **Vault**: `fixes/Fix_SF_Competitors_Status_May17.md` создан ✅. ### Замечания (не блокирующие) - 🟡 **Minor**: magic string `'Строящиеся'` в SQL литералом. При переименовании значения в БД (например миграция на enum/латиницу) — sort silent сломается. Acceptable: значение pинимается напрямую из DOM.РФ API, scraper его не трансформирует (audit подтверждает). Логировать в `Endpoint_Parcel_Analyze.md` если ещё нет. - 🟢 **Nit**: тесты mock-based — проверяют логику сортировки на Python-симуляции SQL, не реальный roundtrip. Регрессия типа «забыли поле в SELECT» ими не ловится. Smoke на проде после deploy обязателен. - 🟢 **Perf**: новый sort даёт Sort node перед LIMIT 20, но input set ≤ ~50 rows (radius 3km + DISTINCT ON) — cost negligible. Index `obj_district_class_active_idx` не задействован тут (нет фильтра по district/class), что OK. ### Post-merge smoke ```bash curl -X POST .../api/v1/parcels/66:41:0702048:27/analyze | jq '.competitors[0:3] | .[] | {comm_name, site_status, ready_dt, distance_m}' # Ожидаем: первый элемент site_status='Строящиеся' (если есть в радиусе) ``` ### Follow-up (отдельный PR, не блокирующий) - Frontend `CompetitorTable` — Badge для `site_status` + filter tabs (Wave 1 item из `SiteFinder_FixList_May17`). - TS codegen: `npm run codegen` после деплоя backend → `ParcelAnalysisCompetitor` подхватит новые поля. Мержу.
lekss361 merged commit 7a27cb857f into main 2026-05-17 10:35:47 +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#276
No description provided.