fix(sf-16): derive price_per_m2 from price_rub in _norm_flat + backfill SQL #298

Merged
lekss361 merged 1 commit from fix/sf-16-malevich-prices into main 2026-05-17 14:32:24 +00:00
Owner

Audit findings

Metric Value
obj_id=64701 (Малевич new) total flats 498
has price_per_m2 0
has price_rub 0 (не только price_per_m2!)
has total_area 498 (100%)
Global price_per_m2 NULL (Apr–May) 377 752 flats / 666 objects
Flats with both price fields populated 849 (0.2%), все status='free'
Малевич sale_graph price_avg (Jan 2026) 170 568 ₽/m² — данные есть, но не в /table endpoint

Root cause: DOM.РФ /portal-kn/api/sales/portal/table возвращает price: null для большинства объектов. Цены на уровне квартиры — в отдельном catalog-квартир endpoint, ещё не scraped.

Маппинг scraper-а корректен. Для Малевич этот PR не решает проблему (price_rub тоже NULL) — нужен новый scraper в follow-up (BUG #22 будет добавлен в FixList).

Changes

  • backend/app/services/scrapers/domrf_kn.py — defensive derive price_per_m2 = round(price_rub / total_area, 2) в _norm_flat() когда API даёт price_rub но не pricePerSquareMeter. Помогает для 849 partial-response cases (другие objs).
  • data/sql/107_backfill_price_per_m2.sql — idempotent UPDATE для historical 849 rows.
  • Vault: fixes/Fix_SF_Malevich_Prices_May17.md

Test plan

  • After deploy: SELECT COUNT(*) FROM domrf_kn_flats WHERE price_per_m2 IS NOT NULL → +849 (с 849 → ~1698)
  • obj_id=64701 — НЕ изменится (price_rub тоже NULL — нужен catalog scraper)

Follow-up (НЕ в этом PR)

BUG #22 — новый scraper domrf_catalog.py для извлечения per-flat price из catalog endpoint. Закроет Малевич + другие 666 objects с NULL prices.

Related: epic #271 item #16 · Wave 3 (P2) · last item списка 1-20 (BUG #22 будет добавлен)

## Audit findings | Metric | Value | |---|---| | obj_id=64701 (Малевич new) total flats | 498 | | has price_per_m2 | **0** | | **has price_rub** | **0** (не только price_per_m2!) | | has total_area | 498 (100%) | | Global price_per_m2 NULL (Apr–May) | 377 752 flats / 666 objects | | Flats with both price fields populated | 849 (0.2%), все `status='free'` | | Малевич sale_graph `price_avg` (Jan 2026) | 170 568 ₽/m² — данные есть, но не в `/table` endpoint | **Root cause**: DOM.РФ `/portal-kn/api/sales/portal/table` возвращает `price: null` для большинства объектов. Цены на уровне квартиры — в отдельном **catalog-квартир** endpoint, ещё **не scraped**. Маппинг scraper-а корректен. Для Малевич этот PR **не решает** проблему (price_rub тоже NULL) — нужен новый scraper в follow-up (**BUG #22** будет добавлен в FixList). ## Changes - `backend/app/services/scrapers/domrf_kn.py` — defensive derive `price_per_m2 = round(price_rub / total_area, 2)` в `_norm_flat()` когда API даёт `price_rub` но не `pricePerSquareMeter`. Помогает для **849 partial-response cases** (другие objs). - `data/sql/107_backfill_price_per_m2.sql` — idempotent UPDATE для historical 849 rows. - Vault: `fixes/Fix_SF_Malevich_Prices_May17.md` ## Test plan - [ ] After deploy: `SELECT COUNT(*) FROM domrf_kn_flats WHERE price_per_m2 IS NOT NULL` → +849 (с 849 → ~1698) - [ ] obj_id=64701 — НЕ изменится (price_rub тоже NULL — нужен catalog scraper) ## Follow-up (НЕ в этом PR) **BUG #22** — новый scraper `domrf_catalog.py` для извлечения per-flat price из catalog endpoint. Закроет Малевич + другие 666 objects с NULL prices. Related: epic #271 item #16 · Wave 3 (P2) · **last item списка 1-20** (BUG #22 будет добавлен)
lekss361 added 1 commit 2026-05-17 14:29:22 +00:00
Audit confirmed: DOM.RF /portal/table API returns price=null for 99.7% of flats
(both price_rub and price_per_m2 are NULL). Root cause is API limitation — per-flat
pricing lives in a separate catalog endpoint not yet scraped (BUG #22).

Adds defensive fallback in _norm_flat(): when price_rub is present but
pricePerSquareMeter is absent, derive price_per_m2 = round(price_rub / total_area, 2).
Covers partial API responses from any future endpoint that omits the computed rate.

Adds 107_backfill_price_per_m2.sql for historical rows (~849 affected).
lekss361 merged commit bfe16ae71b into main 2026-05-17 14:32:24 +00:00
Author
Owner

Merged via deep-code-reviewer — verdict APPROVE.

Проверено:

  • _norm_flat() derive logic: guard total_area > 0 + приоритет API pricePerSquareMeter сохранён. Defensive only.
  • data/sql/107_*.sql: price_rub::numeric / total_area — inline cast на column expression, не CAST trap. BEGIN/COMMIT + WHERE-guards = idempotent.
  • Schema match: price_per_m2 numeric (без явной precision) → ROUND(..., 2) совместим. price_rub bigint, total_area numeric → деление промотируется в numeric (::numeric cast явно). OK.
  • 107_ sequential после 106_backfill_obj_class_from_ai_description (#293 merged). Correct.
  • Vault fixes/Fix_SF_Malevich_Prices_May17.md присутствует, корректно документирует root cause + BUG #22 follow-up.

Smoke после deploy:

SELECT COUNT(*) FROM domrf_kn_flats WHERE price_per_m2 IS NOT NULL;
-- Ожидание: ~1698 (с 849 → +849 backfilled)

SELECT COUNT(*) FROM domrf_kn_flats WHERE obj_id = 64701 AND price_per_m2 IS NOT NULL;
-- Ожидание: 0 (Малевич — price_rub тоже NULL, не изменится)

Reminder: BUG #22 (catalog-квартир scraper domrf_catalog.py) — обязательный follow-up для закрытия Малевич + 666 объектов с NULL ценами. Текущий PR покрывает только 0.2% partial-response cases.

Merged via deep-code-reviewer — verdict APPROVE. **Проверено:** - `_norm_flat()` derive logic: guard `total_area > 0` + приоритет API `pricePerSquareMeter` сохранён. Defensive only. - `data/sql/107_*.sql`: `price_rub::numeric / total_area` — inline cast на column expression, не CAST trap. `BEGIN/COMMIT` + WHERE-guards = idempotent. - Schema match: `price_per_m2 numeric` (без явной precision) → `ROUND(..., 2)` совместим. `price_rub bigint`, `total_area numeric` → деление промотируется в numeric (`::numeric` cast явно). OK. - `107_` sequential после `106_backfill_obj_class_from_ai_description` (#293 merged). Correct. - Vault `fixes/Fix_SF_Malevich_Prices_May17.md` присутствует, корректно документирует root cause + BUG #22 follow-up. **Smoke после deploy:** ```sql SELECT COUNT(*) FROM domrf_kn_flats WHERE price_per_m2 IS NOT NULL; -- Ожидание: ~1698 (с 849 → +849 backfilled) SELECT COUNT(*) FROM domrf_kn_flats WHERE obj_id = 64701 AND price_per_m2 IS NOT NULL; -- Ожидание: 0 (Малевич — price_rub тоже NULL, не изменится) ``` **Reminder:** BUG #22 (catalog-квартир scraper `domrf_catalog.py`) — обязательный follow-up для закрытия Малевич + 666 объектов с NULL ценами. Текущий PR покрывает только 0.2% partial-response cases.
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#298
No description provided.