fix(sf-05): clamp sold_pct_of_supply на 100% + is_oversold flag #274

Merged
lekss361 merged 1 commit from fix/sf-05-sold-pct-normalize into main 2026-05-17 09:04:22 +00:00
Owner

Summary

SF FixList #5sold_pct clamp 100% + warning flag.

sold_pct = sum_deals_24mo / supply_count_snapshot * 100 использует несопоставимые окна (24 мес сделок vs текущий snapshot supply) → значения >100% (например 199%) для активно продаваемых маленьких форматов.

Изменения

backend/app/services/site_finder/best_layouts.py:349:

  • sold_pct = min(round(raw, 1), 100.0) — clamp на 100%
  • Новое field is_oversold: bool = raw > 100 (UI показывает warning badge)

schemas/parcel.py (или соседний) — extended response model.

Tests — мок что raw=199 → returned sold_pct=100, is_oversold=true.

Backward compat

sold_pct оставлено как имя — frontend не ломается. is_oversold — opt-in новое поле.

Future work (Option B)

Полная нормализация sum_deals_in_window / avg_supply_in_window — отдельный issue если понадобится (требует rolling avg supply snapshot которого может не быть в схеме).

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

## Summary SF FixList #5 — `sold_pct` clamp 100% + warning flag. `sold_pct = sum_deals_24mo / supply_count_snapshot * 100` использует несопоставимые окна (24 мес сделок vs текущий snapshot supply) → значения >100% (например 199%) для активно продаваемых маленьких форматов. ## Изменения `backend/app/services/site_finder/best_layouts.py:349`: - `sold_pct = min(round(raw, 1), 100.0)` — clamp на 100% - Новое field `is_oversold: bool` = `raw > 100` (UI показывает warning badge) `schemas/parcel.py` (или соседний) — extended response model. Tests — мок что raw=199 → returned `sold_pct=100`, `is_oversold=true`. ## Backward compat `sold_pct` оставлено как имя — frontend не ломается. `is_oversold` — opt-in новое поле. ## Future work (Option B) Полная нормализация `sum_deals_in_window / avg_supply_in_window` — отдельный issue если понадобится (требует rolling avg supply snapshot которого может не быть в схеме). Related: epic #271 item #5 (Wave 1)
lekss361 added 2 commits 2026-05-17 09:01:01 +00:00
Audit 2026-05-17: objective_corpus_room_month содержал активные сделки
для Малевич (98/12m) и Ньютон парк (30/12m), но записей в
objective_complex_mapping не было → mv_layout_velocity пропускал оба
проекта в velocity calculations.

Добавлены маппинги:
- Малевич → domrf_obj_id 64701 (Строящиеся, 244кв, сдача 2027-09)
- Ньютон парк → domrf_obj_id 47390 (Сданные, 411кв, крупнейший corpus)

ON CONFLICT DO NOTHING — идемпотентно. REFRESH mv_layout_velocity
выполняется вручную post-deploy (runbooks/Refresh_MV_Layout_Velocity.md).

Closes (epic part) #271 item 4
Symptom: sold_pct_of_supply = sum_deals_24mo / supply_count_snapshot * 100
yields >100% (e.g. 199%) for fast-selling small formats due to incompatible
time windows (24-month deals vs point-in-time supply snapshot).

Option A (hotfix): clamp to 100.0, expose is_oversold=True for UI badge.

- best_layouts.py: compute sold_pct_raw, clamp with min(..., 100.0),
  set is_oversold = raw > 100%
- parcel.py TopLayoutRow: add is_oversold: bool field
- best-layouts.ts TopLayoutRow: add is_oversold: boolean
- BestLayoutsBlock.tsx: show warn badge ">100%" when is_oversold=True
- tests: two new cases — raw 199% clamps + is_oversold=True;
  raw 50% passes through + is_oversold=False

Closes (epic part) #271 item 5
lekss361 merged commit ac1da79946 into main 2026-05-17 09:04:22 +00:00
Author
Owner

Merged via deep-code-reviewer — verdict APPROVE.

Что проверено:

  • Clamp logic min(raw, 100.0) корректна, no zero division (guarded supply_count > 0).
  • is_oversold = raw > 100.0 — строго >, ровно 100% не флагается. OK.
  • Backward compat: sold_pct_of_supply имя сохранено; is_oversold без default но всегда сериализуется backend'ом.
  • TS types updated синхронно (best-layouts.ts).
  • Frontend badge — XSS-safe (> escaped), CSS tokens из дизайн-системы.
  • Tests покрывают raw=199 → clamp + flag, raw=50 → pass-through.
  • Vault: fixes/Fix_SF_SoldPct_Clamp_May17.md уже создан до merge.

Bundle bonus (SF-04):

  • data/sql/103_seed_prinzip_mappings.sqlBEGIN/COMMIT + ON CONFLICT DO NOTHING idempotent.
  • 2 row seed (Малевич, Ньютон парк) — минимальный blast radius.
  • REFRESH MATERIALIZED VIEW CONCURRENTLY mv_layout_velocity нужно прогнать вручную post-deploy (runbook Refresh_MV_Layout_Velocity.md).

Замечания (не блокирующее, follow-up):

  • Schema Competitor получила site_status / ready_dt (Optional) — это scope из SF-02, не упомянуто в title PR. Бэкенд пока эти поля не заполняет (вернёт None). Нужен follow-up SF-02 PR который заполнит их в SQL competitors query.
  • Edge case raw == 100.0 exactly в тестах не покрыт — low priority.

Что проверить после деплоя:

  • POST /api/v1/parcels/{cad}/best-layouts на участке с активно-продаваемым маленьким форматом → должен вернуть sold_pct_of_supply=100 + is_oversold=true.
  • UI badge >100% появляется в колонке sold_pct в BestLayoutsBlock.
  • Прогнать REFRESH MATERIALIZED VIEW CONCURRENTLY mv_layout_velocity для подхвата seed mapping'ов.
Merged via deep-code-reviewer — verdict APPROVE. **Что проверено:** - Clamp logic `min(raw, 100.0)` корректна, no zero division (guarded `supply_count > 0`). - `is_oversold = raw > 100.0` — строго `>`, ровно 100% не флагается. OK. - Backward compat: `sold_pct_of_supply` имя сохранено; `is_oversold` без default но всегда сериализуется backend'ом. - TS types updated синхронно (`best-layouts.ts`). - Frontend badge — XSS-safe (`>` escaped), CSS tokens из дизайн-системы. - Tests покрывают raw=199 → clamp + flag, raw=50 → pass-through. - Vault: `fixes/Fix_SF_SoldPct_Clamp_May17.md` уже создан до merge. **Bundle bonus (SF-04):** - `data/sql/103_seed_prinzip_mappings.sql` — `BEGIN/COMMIT` + `ON CONFLICT DO NOTHING` idempotent. - 2 row seed (Малевич, Ньютон парк) — минимальный blast radius. - `REFRESH MATERIALIZED VIEW CONCURRENTLY mv_layout_velocity` нужно прогнать вручную post-deploy (runbook `Refresh_MV_Layout_Velocity.md`). **Замечания (не блокирующее, follow-up):** - Schema `Competitor` получила `site_status` / `ready_dt` (Optional) — это scope из SF-02, не упомянуто в title PR. Бэкенд пока эти поля не заполняет (вернёт `None`). Нужен follow-up SF-02 PR который заполнит их в SQL competitors query. - Edge case `raw == 100.0 exactly` в тестах не покрыт — low priority. **Что проверить после деплоя:** - `POST /api/v1/parcels/{cad}/best-layouts` на участке с активно-продаваемым маленьким форматом → должен вернуть `sold_pct_of_supply=100` + `is_oversold=true`. - UI badge `>100%` появляется в колонке sold_pct в `BestLayoutsBlock`. - Прогнать `REFRESH MATERIALIZED VIEW CONCURRENTLY mv_layout_velocity` для подхвата seed mapping'ов.
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#274
No description provided.