fix(tradein/estimate): не блокировать оценку — расширять подбор и честно предупреждать #2823

Merged
lekss361 merged 1 commit from fix/tradein-estimate-never-block into main 2026-08-10 15:42:23 +00:00
Owner

Инцидент

Первый платящий заказчик не смог оценить квартиру: студия 23.1 м² в Екатеринбурге (ул Академика Парина, 46/5) → экран «НЕДОСТАТОЧНО ДАННЫХ».

Адрес геокодировался корректно (fias_exact, conf=0.95). Причина в другом: в UI не было варианта «Студия», и студия схлопывалась в rooms=1 (initRoomsLabel: if (rooms <= 1) return "1"). Оценщик отбирает аналоги строгим совпадением комнатности.

Замер на проде, радиус 2 км, площадь 17–29 м², активные:

Комнатность Всего Свежих (14 дней)
Студии (rooms=0) 327 259
1-комнатные (rooms=1) 10 4

Найдено 4 → сработал гейт #oblast-E (n=4 < 5 → suppressing listings-derived median) → median=0 → блок.

Проверка прямым запросом на проде: тот же адрес с rooms=034 аналога, медиана 3 765 452 ₽, confidence medium. Данные были на месте, их отсекал фильтр.

Summary

  • «Студия» добавлена в выбор комнатности, маппинг rooms=0 в обе стороны — корневая причина инцидента.
  • Каскад послаблений при нехватке аналогов, каждый шаг только при n < 5: комнатность (студия↔1-комн, иначе ±1) → свежесть 14→60 дней → новостройки → радиус 3/5 км. Радиус не расширяется за пределы явно выбранного пользователем (контракт #2044).
  • Медиана больше не обнуляется. Правило выбора headline:
    • n >= 5 → медиана по объявлениям;
    • тонкая выборка + живой коридор ДКП → сделки (сохраняет смысл #oblast-E: 3 шумных объявления не должны перебивать 54 реальные сделки);
    • тонкая выборка без коридора → реальная медиана по объявлениям;
    • «Недостаточно данных» — только при реальном нуле.
  • Новые поля ответа relaxations: string[] и reliability: ok|low|very_low (аддитивные, с дефолтами). Красный баннер над оценкой + предупреждение и сноски в PDF.
  • Счётчики перестали брать сырой n_analogs там, где рядом список карточек (PDF, v1 HeroSummary/ListingsCard, v2 SourcesMap/mappers) — иначе печаталось «0 шт.» над непустой таблицей. Места, где n_analogs означает базу расчёта, не тронуты.
  • extract_street_name: сокращения с необязательной точкой. DaData — основной источник адресов — отдаёт ул Академика Парина без точки, улица не извлекалась → пустые блоки «сделки по улице» и «продажи vs объявления» на любом адресе из DaData.

Test plan

  • Полный бэкенд-прогон: 4174 passed, 18 skipped (venv проекта, прогнан дважды независимо)
  • ruff check + ruff format — чисто; pre-commit хуки зелёные
  • tsc --noEmit, eslint, npm run build — чисто
  • Новые тесты: студии подтягиваются при rooms=1 + малой площади; репро Серова (3 объявления + 54 сделки) снова идёт по сделкам; PDF не печатает «0 шт.» над непустой таблицей; разбор улицы на прод-строке из DaData
  • Два прохода code-review в свежем контексте, блокер по PDF найден и закрыт
  • Post-deploy: реальный рендер PDF на relaxed-сценарии в контейнере (локально WeasyPrint без GTK не собирается) + smoke на исходном адресе заказчика
## Инцидент Первый платящий заказчик не смог оценить квартиру: студия 23.1 м² в Екатеринбурге (ул Академика Парина, 46/5) → экран «НЕДОСТАТОЧНО ДАННЫХ». Адрес геокодировался корректно (`fias_exact`, conf=0.95). Причина в другом: **в UI не было варианта «Студия»**, и студия схлопывалась в `rooms=1` (`initRoomsLabel`: `if (rooms <= 1) return "1"`). Оценщик отбирает аналоги строгим совпадением комнатности. Замер на проде, радиус 2 км, площадь 17–29 м², активные: | Комнатность | Всего | Свежих (14 дней) | |---|---|---| | Студии (`rooms=0`) | **327** | 259 | | 1-комнатные (`rooms=1`) | 10 | **4** | Найдено 4 → сработал гейт `#oblast-E` (`n=4 < 5 → suppressing listings-derived median`) → median=0 → блок. Проверка прямым запросом на проде: тот же адрес с `rooms=0` → **34 аналога, медиана 3 765 452 ₽, confidence medium**. Данные были на месте, их отсекал фильтр. ## Summary - **«Студия»** добавлена в выбор комнатности, маппинг `rooms=0` в обе стороны — корневая причина инцидента. - **Каскад послаблений** при нехватке аналогов, каждый шаг только при `n < 5`: комнатность (студия↔1-комн, иначе ±1) → свежесть 14→60 дней → новостройки → радиус 3/5 км. Радиус не расширяется за пределы явно выбранного пользователем (контракт #2044). - **Медиана больше не обнуляется.** Правило выбора headline: - `n >= 5` → медиана по объявлениям; - тонкая выборка + живой коридор ДКП → сделки (сохраняет смысл `#oblast-E`: 3 шумных объявления не должны перебивать 54 реальные сделки); - тонкая выборка без коридора → реальная медиана по объявлениям; - «Недостаточно данных» — только при реальном нуле. - **Новые поля ответа** `relaxations: string[]` и `reliability: ok|low|very_low` (аддитивные, с дефолтами). Красный баннер над оценкой + предупреждение и сноски в PDF. - **Счётчики** перестали брать сырой `n_analogs` там, где рядом список карточек (PDF, v1 `HeroSummary`/`ListingsCard`, v2 `SourcesMap`/`mappers`) — иначе печаталось «0 шт.» над непустой таблицей. Места, где `n_analogs` означает базу расчёта, не тронуты. - **`extract_street_name`**: сокращения с необязательной точкой. DaData — основной источник адресов — отдаёт `ул Академика Парина` без точки, улица не извлекалась → пустые блоки «сделки по улице» и «продажи vs объявления» на **любом** адресе из DaData. ## Test plan - [x] Полный бэкенд-прогон: **4174 passed, 18 skipped** (venv проекта, прогнан дважды независимо) - [x] `ruff check` + `ruff format` — чисто; pre-commit хуки зелёные - [x] `tsc --noEmit`, `eslint`, `npm run build` — чисто - [x] Новые тесты: студии подтягиваются при `rooms=1` + малой площади; репро Серова (3 объявления + 54 сделки) снова идёт по сделкам; PDF не печатает «0 шт.» над непустой таблицей; разбор улицы на прод-строке из DaData - [x] Два прохода code-review в свежем контексте, блокер по PDF найден и закрыт - [ ] Post-deploy: реальный рендер PDF на relaxed-сценарии в контейнере (локально WeasyPrint без GTK не собирается) + smoke на исходном адресе заказчика
lekss361 added 1 commit 2026-08-10 15:35:47 +00:00
fix(tradein/estimate): не блокировать оценку — расширять подбор и честно предупреждать
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Successful in 1m3s
CI Trade-In / backend-tests (pull_request) Successful in 3m59s
fde784f015
Прод-инцидент у первого заказчика: студия 23.1 м² в Екатеринбурге
(ул Академика Парина, 46/5) получала экран «НЕДОСТАТОЧНО ДАННЫХ».

Корневая причина — в UI не было варианта «Студия», и студия схлопывалась
в rooms=1 (initRoomsLabel). Оценщик ищет аналоги строгим совпадением
комнатности → в радиусе 2 км нашлось 4 объявления вместо 327 доступных
студий (rooms=0), после чего гейт #oblast-E подавил медиану.
Тот же адрес с rooms=0 даёт 34 аналога и медиану 3 765 452 ₽.

Что сделано:
- «Студия» добавлена в выбор комнатности, маппинг rooms=0 в обе стороны.
- Каскад послаблений при нехватке аналогов: комнатность (студия↔1-комн,
  иначе ±1) → свежесть 14→60 дней → новостройки → радиус 3/5 км.
  Каждый шаг только при n<5, применённые послабления отдаются наружу
  в relaxations и показываются пользователю.
- Медиана больше не обнуляется. Правило выбора headline:
  n>=5 → объявления; тонкая выборка + живой коридор ДКП → сделки
  (сохраняет смысл #oblast-E); тонкая без коридора → реальная медиана
  по объявлениям. «Недостаточно данных» — только при реальном нуле.
- Новые поля ответа relaxations / reliability (ok|low|very_low), красный
  баннер над оценкой, предупреждение и сноски в PDF-отчёте.
- Счётчики перестали брать сырой n_analogs там, где рядом список карточек
  (PDF, v1 HeroSummary/ListingsCard, v2 SourcesMap/mappers) — иначе
  печаталось «0 шт.» над непустой таблицей.
- extract_street_name: сокращения с необязательной точкой. DaData отдаёт
  «ул Академика Парина» без точки → улица не извлекалась → пустые блоки
  «сделки по улице» и «продажи vs объявления» на любом адресе из DaData.
lekss361 merged commit 9d9457f67d into main 2026-08-10 15:42:23 +00:00
lekss361 deleted branch fix/tradein-estimate-never-block 2026-08-10 15:42:23 +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#2823
No description provided.