feat(trade-in): MVP оценщика вторички на Objective ground truth (P0, UI track) #314

Closed
opened 2026-05-17 16:28:29 +00:00 by lekss361 · 0 comments
Owner

Severity: P0

Контекст: после OBJ-3 (Site Finder migration на Objective, issue #307) у нас 247k квартир Свердл с известной ценой → можно сделать MVP TradeIn Estimator без скрапа 7 внешних API.

Этот issue — frontend + thin backend layer для запуска /trade-in страницы. Изначально был OBJ-4 в #307, выделено для разделения data-only vs UI треков.

Главный отличие от изначального TradeIn brief

Vault code/patterns/TradeIn_Estimator_Brief_May17.md писал:

Нужны 7 внешних API: Циан / Авито / ДомКлик / Yandex / Дом.РФ / Restate / N1
Effort: 2-3 недели Phase 1 MVP

Реальность для Свердл:

  • objective_lots 247k квартир с ценой (81% coverage)
  • Период 10 лет (2016-2026)
  • Ипотека через bank_name (после OBJ-1)
  • Источник = достаточно для ground truth

MVP за 5 дней, не за 2-3 недели.

⚠️ Не редактировать code/patterns/TradeIn_Estimator_Brief_May17.md — это «полная стратегия» для после-пилотных регионов где Objective не покрывает. Создать новый vault doc code/patterns/TradeIn_Sverdl_MVP_May17.md с упрощённой архитектурой.

Зависимости

  • #307 OBJ-1 (bank_name mapping) — нужно для отображения ипотеки в отчёте
  • #307 OBJ-2 (massive mapping backfill) — нужно для full coverage по адресам ЕКБ
  • #307 OBJ-3 (Site Finder migration) — параллельно, не блокирует

Можно стартовать после OBJ-1 + OBJ-2 (Wave 1-2 в #307 = ~1 неделя).

Scope

A) Backend (~1d)

backend/app/api/v1/trade_in.py — новый файл:

POST /api/v1/trade-in/estimate

  • Input: {address, area_m2, rooms, floor, total_floors, year_built, house_type, repair_state, has_balcony}
  • Geocode address → lat/lon (Yandex Geocoder или dadata)
  • Query objective_lots с фильтрами:
    SELECT * FROM objective_lots
    WHERE district = :district  -- из geocode
      AND ABS(area_pd - :area) < 5
      AND rooms_int = :rooms
      AND price_per_m2_rub IS NOT NULL
      AND contract_date >= NOW() - INTERVAL '24 months'
    ORDER BY ABS(area_pd - :area), contract_date DESC
    LIMIT 100
    
  • Aggregate: {median, p25, p75, n_analogs, period}
  • Также analogs WHERE status='free' для текущих listings
  • Также actual_deals WHERE is_sold=true для реальных сделок
  • Return AggregatedEstimate Pydantic schema

POST /api/v1/trade-in/{estimate_id}/pdf

  • Reuse backend/app/services/exporters/layout_tz_pdf.py pattern (WeasyPrint)
  • Template — 4 страницы как у Брусника.Обмен (см. code/patterns/TradeIn_Estimator_Brief_May17.md §1)

DB migration data/sql/NN_trade_in_estimates.sql:

CREATE TABLE trade_in_estimates (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  address text, lat float8, lon float8,
  area_m2 numeric, rooms int, floor int, ...,
  median_price numeric, range_low numeric, range_high numeric,
  confidence text,
  analogs jsonb, actual_deals jsonb,
  created_at timestamptz DEFAULT now(),
  expires_at timestamptz DEFAULT now() + interval '24 hours'
);

B) Frontend (~2d)

frontend/src/app/trade-in/page.tsx — новая страница:

Layout (per UI Brief design tokens):

  • Слева sticky форма (~360px): address autocomplete + 8 fields + кнопка «Оценить»
  • Справа result area:
    • Hero card: median price + range + n_analogs + confidence
    • 4 секции отчёта (cover / listings / actual deals / trade-in cost) — как Брусника.Обмен PDF
    • Кнопка «Скачать PDF»

Компоненты frontend/src/components/trade-in/:

  • <EstimateForm/> — форма ввода
  • <EstimateProgress/> — индикатор «Считаем...» (опционально, SQL быстрый)
  • <EstimateResult/> — hero + 4 секции
  • <PriceRangeBar/> — штриховой бар диапазона цен (как в Брусника PDF)
  • <AnalogsTable/> — таблица 5-10 аналогов с фото / адресом / ценой / экспозицией

Не делать сейчас:

  • Manager CRM dashboard /admin/trade-in (отложить)
  • White-label theming (отложить)
  • External API integration (Cian/Avito) — только Свердл через Objective

Acceptance criteria

  • /trade-in page работает, форма валидируется
  • Backend endpoint возвращает корректный AggregatedEstimate за <500ms (без внешних API!)
  • PDF генерится <3 sec, открывается в Adobe Reader / Chrome
  • 3 sample адреса ЕКБ (1-комн / 2-комн / 3-комн) дают реалистичную оценку ±15% от рыночной
  • confidence='high' если ≥30 analogs в радиусе 1км
  • Mobile responsive (см. code/patterns/UI_Brief_GenDesign_May17.md §10)
  • Color tokens из UI Brief (нет inline hex)
  • Lucide icons (нет emoji)

Out of scope (отложить в Phase 2 / 3)

  • External scrapers (Cian/Avito/ДомКлик/Yandex/Restate/N1) — для регионов вне Свердл
  • Manager CRM dashboard (/admin/trade-in)
  • White-label theming (логотип девелопера)
  • Mortgage calculator integration
  • Sales pipeline integration с CRM PRINZIP

Effort

Sub Что Effort
TI-1 Backend /trade-in/estimate endpoint + Pydantic + SQL aggregation 1d
TI-2 Backend /trade-in/{id}/pdf + WeasyPrint template 1d
TI-3 Frontend <EstimateForm/> + <EstimateResult/> 1.5d
TI-4 PDF style match Брусника.Обмен (4 страницы) 0.5d
TI-5 Smoke test 3 адресов + acceptance 1d
Total ~5d

Parallelism

Если есть 2 worker'а — backend (TI-1, TI-2) и frontend (TI-3, TI-4) параллельно в worktree. Sequential merge.

References

  • vault code/patterns/Objective_Data_Source_May17.md — источник цен (objective_lots)
  • vault code/patterns/TradeIn_Estimator_Brief_May17.md — full strategy (НЕ редактировать)
  • vault code/patterns/UI_Brief_GenDesign_May17.md — design tokens
  • vault code/patterns/SiteFinder_Mockup_Migration_Plan_May17.md — frontend redesign (паттерны)
  • Issue #307 — data-only Objective activation (parent track)
  • Issue #295 — weighted AVG MV (prerequisite через цепочку)
  • repo backend/app/services/exporters/layout_tz_pdf.py — reference WeasyPrint Cyrillic

Discovered: 2026-05-17 split из #307 для разделения data/UI tracks.

## Severity: P0 **Контекст**: после OBJ-3 (Site Finder migration на Objective, issue #307) у нас 247k квартир Свердл с известной ценой → можно сделать MVP TradeIn Estimator **без скрапа 7 внешних API**. Этот issue — **frontend + thin backend layer** для запуска `/trade-in` страницы. Изначально был OBJ-4 в #307, выделено для разделения **data-only** vs **UI** треков. ## Главный отличие от изначального TradeIn brief Vault `code/patterns/TradeIn_Estimator_Brief_May17.md` писал: > Нужны 7 внешних API: Циан / Авито / ДомКлик / Yandex / Дом.РФ / Restate / N1 > Effort: 2-3 недели Phase 1 MVP **Реальность для Свердл**: - `objective_lots` 247k квартир с ценой (81% coverage) - Период 10 лет (2016-2026) - Ипотека через `bank_name` (после OBJ-1) - Источник = достаточно для ground truth **MVP за 5 дней**, не за 2-3 недели. ⚠️ **Не редактировать `code/patterns/TradeIn_Estimator_Brief_May17.md`** — это «полная стратегия» для после-пилотных регионов где Objective не покрывает. Создать **новый** vault doc `code/patterns/TradeIn_Sverdl_MVP_May17.md` с упрощённой архитектурой. ## Зависимости - **#307 OBJ-1** (bank_name mapping) — нужно для отображения ипотеки в отчёте - **#307 OBJ-2** (massive mapping backfill) — нужно для full coverage по адресам ЕКБ - **#307 OBJ-3** (Site Finder migration) — параллельно, не блокирует Можно стартовать после OBJ-1 + OBJ-2 (Wave 1-2 в #307 = ~1 неделя). ## Scope ### A) Backend (~1d) `backend/app/api/v1/trade_in.py` — новый файл: **`POST /api/v1/trade-in/estimate`** - Input: `{address, area_m2, rooms, floor, total_floors, year_built, house_type, repair_state, has_balcony}` - Geocode address → lat/lon (Yandex Geocoder или dadata) - Query `objective_lots` с фильтрами: ```sql SELECT * FROM objective_lots WHERE district = :district -- из geocode AND ABS(area_pd - :area) < 5 AND rooms_int = :rooms AND price_per_m2_rub IS NOT NULL AND contract_date >= NOW() - INTERVAL '24 months' ORDER BY ABS(area_pd - :area), contract_date DESC LIMIT 100 ``` - Aggregate: `{median, p25, p75, n_analogs, period}` - Также analogs `WHERE status='free'` для текущих listings - Также actual_deals `WHERE is_sold=true` для реальных сделок - Return `AggregatedEstimate` Pydantic schema **`POST /api/v1/trade-in/{estimate_id}/pdf`** - Reuse `backend/app/services/exporters/layout_tz_pdf.py` pattern (WeasyPrint) - Template — 4 страницы как у Брусника.Обмен (см. `code/patterns/TradeIn_Estimator_Brief_May17.md` §1) **DB migration** `data/sql/NN_trade_in_estimates.sql`: ```sql CREATE TABLE trade_in_estimates ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), address text, lat float8, lon float8, area_m2 numeric, rooms int, floor int, ..., median_price numeric, range_low numeric, range_high numeric, confidence text, analogs jsonb, actual_deals jsonb, created_at timestamptz DEFAULT now(), expires_at timestamptz DEFAULT now() + interval '24 hours' ); ``` ### B) Frontend (~2d) `frontend/src/app/trade-in/page.tsx` — новая страница: **Layout** (per UI Brief design tokens): - Слева sticky форма (~360px): address autocomplete + 8 fields + кнопка «Оценить» - Справа result area: - Hero card: median price + range + n_analogs + confidence - 4 секции отчёта (cover / listings / actual deals / trade-in cost) — как Брусника.Обмен PDF - Кнопка «Скачать PDF» **Компоненты** `frontend/src/components/trade-in/`: - `<EstimateForm/>` — форма ввода - `<EstimateProgress/>` — индикатор «Считаем...» (опционально, SQL быстрый) - `<EstimateResult/>` — hero + 4 секции - `<PriceRangeBar/>` — штриховой бар диапазона цен (как в Брусника PDF) - `<AnalogsTable/>` — таблица 5-10 аналогов с фото / адресом / ценой / экспозицией **Не делать сейчас**: - ❌ Manager CRM dashboard `/admin/trade-in` (отложить) - ❌ White-label theming (отложить) - ❌ External API integration (Cian/Avito) — только Свердл через Objective ## Acceptance criteria - [ ] `/trade-in` page работает, форма валидируется - [ ] Backend endpoint возвращает корректный AggregatedEstimate за <500ms (без внешних API!) - [ ] PDF генерится <3 sec, открывается в Adobe Reader / Chrome - [ ] 3 sample адреса ЕКБ (1-комн / 2-комн / 3-комн) дают реалистичную оценку ±15% от рыночной - [ ] confidence='high' если ≥30 analogs в радиусе 1км - [ ] Mobile responsive (см. `code/patterns/UI_Brief_GenDesign_May17.md` §10) - [ ] Color tokens из UI Brief (нет inline hex) - [ ] Lucide icons (нет emoji) ## Out of scope (отложить в Phase 2 / 3) - External scrapers (Cian/Avito/ДомКлик/Yandex/Restate/N1) — для регионов вне Свердл - Manager CRM dashboard (`/admin/trade-in`) - White-label theming (логотип девелопера) - Mortgage calculator integration - Sales pipeline integration с CRM PRINZIP ## Effort | Sub | Что | Effort | |---|---|---| | TI-1 | Backend `/trade-in/estimate` endpoint + Pydantic + SQL aggregation | 1d | | TI-2 | Backend `/trade-in/{id}/pdf` + WeasyPrint template | 1d | | TI-3 | Frontend `<EstimateForm/>` + `<EstimateResult/>` | 1.5d | | TI-4 | PDF style match Брусника.Обмен (4 страницы) | 0.5d | | TI-5 | Smoke test 3 адресов + acceptance | 1d | | **Total** | | **~5d** | ## Parallelism Если есть 2 worker'а — backend (TI-1, TI-2) и frontend (TI-3, TI-4) параллельно в worktree. Sequential merge. ## References - vault `code/patterns/Objective_Data_Source_May17.md` — источник цен (`objective_lots`) - vault `code/patterns/TradeIn_Estimator_Brief_May17.md` — full strategy (НЕ редактировать) - vault `code/patterns/UI_Brief_GenDesign_May17.md` — design tokens - vault `code/patterns/SiteFinder_Mockup_Migration_Plan_May17.md` — frontend redesign (паттерны) - Issue #307 — data-only Objective activation (parent track) - Issue #295 — weighted AVG MV (prerequisite через цепочку) - repo `backend/app/services/exporters/layout_tz_pdf.py` — reference WeasyPrint Cyrillic Discovered: 2026-05-17 split из #307 для разделения data/UI tracks.
lekss361 added the
ux
enhancement
labels 2026-05-17 16:28:29 +00:00
Sign in to join this conversation.
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#314
No description provided.