fix(tradein/ui): показывать цену по сделкам Росреестра вместо «недостаточно данных» #2630

Merged
lekss361 merged 1 commit from fix/tradein-v2-deals-fallback-visible into main 2026-08-02 13:21:53 +00:00
Owner

Проблема

tradein-mvp/frontend/src/app/v2/page.tsx:567 считало insufficient = estimate.insufficient_data || estimate.n_analogs === 0. Второе условие ложное: backend может (и, после расширения deals-фолбэка в PR #2629, регулярно это делает) вернуть честную цену median_price_rub > 0, посчитанную по сделкам Росреестра (ДКП), даже когда листинговых аналогов-объявлений ноль. v2 всё равно рисовал «НЕДОСТАТОЧНО ДАННЫХ» — 23 существующие оценки по Серову уже застряли в этом состоянии. v1 и PDF-экспорт показывают этот же случай правильно (gate только на insufficient_data).

Изменения

  1. page.tsxinsufficient сужен до единственного честного признака бэкенда: estimate.insufficient_data (@computed_field = median_price_rub <= 0). Условие n_analogs === 0 убрано.
  2. mappers.ts (mapResultPanel) — новый флаг dealsOnlyPrice = n_analogs === 0 && !insufficient_data. Когда он true, заголовочная карточка «РЕКОМЕНДОВАННАЯ ЦЕНА» меняет вторую строку заголовка и подпись ₽/м² с «В ОБЪЯВЛЕНИИ / по объявлениям» на «ПО СДЕЛКАМ РОСРЕЕСТРА / по сделкам Росреестра» + добавлена явная note-строка под карточкой (не выдаёт цену за оценку по объявлениям — честность в обе стороны).
  3. SourcesView.tsx — таблицы «объявления» и «фактические сделки» больше не рисуют пустой заголовок без строк при нулевой выборке: при rows.length === 0 вместо DataTable рендерится EmptyTableNote с честным текстом (для таблицы объявлений — с подсказкой, что цена посчитана по сделкам Росреестра, когда это так). Соответствующие footer-подписи («Показано 0 из 0 …») скрыты как избыточные при пустой таблице.
  4. M1 аудита #2583confidence_explanation бэкенда нигде не читался в v2. Добавлен в ResultMeta/SummaryQuality (types.ts) + mapResultPanel/mapSummary (mappers.ts) + вывод: hover-tooltip на «ДОСТОВЕРНОСТЬ» в ResultPanel.tsx (тот же паттерн title=, что уже используется в HeroBar для location-index disclaimer) и видимая строка в блоке «КАЧЕСТВО ДАННЫХ» в ObjectSummary.tsx (там больше места, эта строка — более вероятное место, где пользователь реально прочитает «почему такая достоверность»).

Радиус на карте (M1, второй пункт) — расследовано, НЕ исправлено в этом PR

Проверил схему AggregatedEstimateOut (tradein-mvp/backend/app/schemas/trade_in.py) — бэкенд не отдаёт фактический радиус, использованный при поиске аналогов/сделок (base_radius_m/fallback_radius_m в estimator.py — внутренние переменные, не поле ответа). Круг на карте в ParamsPanel.tsx управляется исключительно клиентским селектором РАДИУС (RADIUS_OPTIONS, дефолт «Авто» → превью 1000 м), а не реальным радиусом расширения на бэкенде. Чтобы честно нарисовать фактический радиус (например, «расширили до 2 км» для Первоуральска), backend должен сначала добавить это поле в ответ — выношу отдельной задачей, не стал её выдумывать/чинить в обход контракта.

Out of scope (по границам задачи): analog_tier/address_precision/manual_review_recommended из той же M1-находки не выведены в этом PR — явно не запрошены текстом задачи (только confidence_explanation + расследование радиуса), оставляю как последующий заход.

Три состояния (как теперь выглядит каждое)

Состояние Условие Что видит пользователь
Полноценная оценка !insufficient_data, n_analogs > 0 Как раньше: «РЕКОМЕНДОВАННАЯ ЦЕНА В ОБЪЯВЛЕНИИ», обычные карточки/таблицы аналогов
Оценка по сделкам (новое) !insufficient_data, n_analogs === 0 Реальная цена и диапазон рендерятся (раньше — «недостаточно данных»); заголовок карточки — «РЕКОМЕНДОВАННАЯ ЦЕНА ПО СДЕЛКАМ РОСРЕЕСТРА», note поясняет источник; таблица «объявления» — честный empty-state вместо пустых заголовков
Реально нет данных insufficient_data (median_price_rub<=0) Без изменений: «НЕДОСТАТОЧНО ДАННЫХ» (InsufficientPanel)

Границы

  • Backend/estimator.py/схемы не тронуты.
  • v1 и PDF-экспорт не тронуты.
  • Логика расчёта цены не менялась — только отображение существующих полей ответа.

Test plan

  • npx tsc --noEmit -p tsconfig.json — чисто
  • next lint — чисто (только 2 pre-existing warning в нетронутых файлах: CianAuthBanner, pct)
  • Юнит-тестов для этой страницы в репо нет (frontend без test-раннера) — визуальную проверку на проде после мержа сделает main-сессия через playwright (RouteGuard локально без бэкенда рендерит null — известная ловушка проекта)

Refs #2583

## Проблема `tradein-mvp/frontend/src/app/v2/page.tsx:567` считало `insufficient = estimate.insufficient_data || estimate.n_analogs === 0`. Второе условие ложное: backend может (и, после расширения deals-фолбэка в PR #2629, регулярно это делает) вернуть честную цену `median_price_rub > 0`, посчитанную по сделкам Росреестра (ДКП), даже когда листинговых аналогов-объявлений ноль. v2 всё равно рисовал «НЕДОСТАТОЧНО ДАННЫХ» — 23 существующие оценки по Серову уже застряли в этом состоянии. v1 и PDF-экспорт показывают этот же случай правильно (gate только на `insufficient_data`). ## Изменения 1. **`page.tsx`** — `insufficient` сужен до единственного честного признака бэкенда: `estimate.insufficient_data` (`@computed_field` = `median_price_rub <= 0`). Условие `n_analogs === 0` убрано. 2. **`mappers.ts` (`mapResultPanel`)** — новый флаг `dealsOnlyPrice = n_analogs === 0 && !insufficient_data`. Когда он true, заголовочная карточка «РЕКОМЕНДОВАННАЯ ЦЕНА» меняет вторую строку заголовка и подпись `₽/м²` с «В ОБЪЯВЛЕНИИ / по объявлениям» на «ПО СДЕЛКАМ РОСРЕЕСТРА / по сделкам Росреестра» + добавлена явная note-строка под карточкой (не выдаёт цену за оценку по объявлениям — честность в обе стороны). 3. **`SourcesView.tsx`** — таблицы «объявления» и «фактические сделки» больше не рисуют пустой заголовок без строк при нулевой выборке: при `rows.length === 0` вместо `DataTable` рендерится `EmptyTableNote` с честным текстом (для таблицы объявлений — с подсказкой, что цена посчитана по сделкам Росреестра, когда это так). Соответствующие footer-подписи («Показано 0 из 0 …») скрыты как избыточные при пустой таблице. 4. **M1 аудита #2583** — `confidence_explanation` бэкенда нигде не читался в v2. Добавлен в `ResultMeta`/`SummaryQuality` (types.ts) + `mapResultPanel`/`mapSummary` (mappers.ts) + вывод: hover-tooltip на «ДОСТОВЕРНОСТЬ» в `ResultPanel.tsx` (тот же паттерн `title=`, что уже используется в HeroBar для location-index disclaimer) и видимая строка в блоке «КАЧЕСТВО ДАННЫХ» в `ObjectSummary.tsx` (там больше места, эта строка — более вероятное место, где пользователь реально прочитает «почему такая достоверность»). ## Радиус на карте (M1, второй пункт) — расследовано, НЕ исправлено в этом PR Проверил схему `AggregatedEstimateOut` (`tradein-mvp/backend/app/schemas/trade_in.py`) — бэкенд **не отдаёт** фактический радиус, использованный при поиске аналогов/сделок (`base_radius_m`/`fallback_radius_m` в `estimator.py` — внутренние переменные, не поле ответа). Круг на карте в `ParamsPanel.tsx` управляется исключительно клиентским селектором РАДИУС (`RADIUS_OPTIONS`, дефолт «Авто» → превью 1000 м), а не реальным радиусом расширения на бэкенде. Чтобы честно нарисовать фактический радиус (например, «расширили до 2 км» для Первоуральска), backend должен сначала добавить это поле в ответ — выношу отдельной задачей, не стал её выдумывать/чинить в обход контракта. Out of scope (по границам задачи): `analog_tier`/`address_precision`/`manual_review_recommended` из той же M1-находки не выведены в этом PR — явно не запрошены текстом задачи (только `confidence_explanation` + расследование радиуса), оставляю как последующий заход. ## Три состояния (как теперь выглядит каждое) | Состояние | Условие | Что видит пользователь | |---|---|---| | **Полноценная оценка** | `!insufficient_data`, `n_analogs > 0` | Как раньше: «РЕКОМЕНДОВАННАЯ ЦЕНА В ОБЪЯВЛЕНИИ», обычные карточки/таблицы аналогов | | **Оценка по сделкам (новое)** | `!insufficient_data`, `n_analogs === 0` | Реальная цена и диапазон рендерятся (раньше — «недостаточно данных»); заголовок карточки — «РЕКОМЕНДОВАННАЯ ЦЕНА ПО СДЕЛКАМ РОСРЕЕСТРА», note поясняет источник; таблица «объявления» — честный empty-state вместо пустых заголовков | | **Реально нет данных** | `insufficient_data` (`median_price_rub<=0`) | Без изменений: «НЕДОСТАТОЧНО ДАННЫХ» (`InsufficientPanel`) | ## Границы - Backend/estimator.py/схемы не тронуты. - v1 и PDF-экспорт не тронуты. - Логика расчёта цены не менялась — только отображение существующих полей ответа. ## Test plan - [x] `npx tsc --noEmit -p tsconfig.json` — чисто - [x] `next lint` — чисто (только 2 pre-existing warning в нетронутых файлах: `CianAuthBanner`, `pct`) - [ ] Юнит-тестов для этой страницы в репо нет (frontend без test-раннера) — визуальную проверку на проде после мержа сделает main-сессия через playwright (RouteGuard локально без бэкенда рендерит null — известная ловушка проекта) Refs #2583
lekss361 added 1 commit 2026-08-02 13:08:14 +00:00
fix(tradein/ui): показывать цену по сделкам Росреестра вместо «недостаточно данных»
All checks were successful
CI / changes (pull_request) Successful in 8s
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-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 Trade-In / frontend-checks (pull_request) Successful in 1m2s
9ba788f3a9
v2/page.tsx считало insufficient=true как только n_analogs===0, даже когда
median_price_rub>0 — backend уже отдаёт честную цену по deals-фолбэку
(PR #2629), но v2 всё равно рисовал «недостаточно данных» (23 оценки по
Серову, v1/PDF-экспорт показывали этот же случай правильно). Сужено до
единственного честного признака backend'а — estimate.insufficient_data
(= median_price_rub<=0). Когда цена есть, а листинговых аналогов нет,
результат теперь явно подписан «по сделкам Росреестра» вместо ложного
«в объявлении», и таблица аналогов не рисует пустой заголовок без строк.

Заодно (M1 аудита #2583): confidence_explanation бэкенда нигде не
читался в v2 — выведен рядом с «ДОСТОВЕРНОСТЬ» (tooltip в ResultPanel +
видимая строка в ObjectSummary).
lekss361 merged commit 964b62d27d into main 2026-08-02 13:21:53 +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#2630
No description provided.