gendesign/tradein-mvp
bot-backend 4a6820b8f7
All checks were successful
CI / changes (pull_request) Successful in 9s
CI Trade-In / changes (pull_request) Successful in 9s
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) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m48s
feat(tradein/estimator): switch core valuation call sites to scraper_kit (#2337)
Group E4 (final, highest-risk step of scraper_kit migration epic #2277):
estimator.py and house_imv_backfill.py's avito_imv/cian_valuation/
yandex_valuation call sites now import from scraper_kit.providers.* instead
of app.services.scrapers.*, following the exact wiring proven safe by E1/E2/E3
(#2334/#2335/#2336):

- estimator.py's avito IMV (_get_or_fetch_imv_cached, both call sites) and
  house_imv_backfill.py's _process_one_house: add config=RealScraperConfig()
  — kit's evaluate_via_imv silently drops the configured proxy without it.
- estimator.py's cian valuation (Stage 9): add config=RealScraperConfig() —
  mandatory kwarg on the kit function (TypeError if omitted).
- estimator.py's yandex valuation: add config=RealScraperConfig() (mandatory)
  and delay_provider=get_scraper_delay — without it kit silently falls back
  to a hardcoded 5.0s throttle instead of the DB-configured anti-ban delay.
- All exception classes imported consistently from the same kit module as
  evaluate_via_imv (not just the function) — mixing legacy/kit exception
  classes would break `except IMVAddressNotFoundError` etc. via identity
  mismatch (caught by an existing test that assumed the legacy class,
  fixed alongside).

Observability: both cian_valuation and yandex_valuation graceful-degradation
except-blocks upgraded from logger.warning to logger.exception. GlitchTip's
LoggingIntegration listens at event_level=ERROR (main.py/scheduler_main.py) —
a WARNING never reaches GlitchTip as an event regardless of exc_info, so a
future config-wiring mistake at these call sites needs ERROR level to be
visible in monitoring.

house_imv_backfill.py: RealScraperConfig is imported lazily inside
_process_one_house (not at module level) to avoid a circular import —
app.services.scraper_adapters imports backfill_house_imv/
process_houses_imv_batch from this module at module level. Verified via
direct import in both orders plus a full `app.main` import.

Also fixes a stale docstring claiming process_houses_imv_batch is "not wired
into scheduler" — it is, via scrape_pipeline.py's run_avito_city_sweep.

Test updates: 2 pre-existing tests (test_backfill_wave2.py) mocked the legacy
IMVAddressNotFoundError/IMVEvaluation/IMVGeo classes, now updated to import
from scraper_kit to match the production exception identity. Added
config=/delay_provider= regression-guard asserts to the relevant estimator
and backfill tests, mirroring the existing #2306 cian_price_history pattern.

Legacy app/services/scrapers/{avito_imv,cian_valuation,yandex_valuation}.py
are untouched and still imported by cian_history_backfill.py's valuation
block (separately scoped, not touched here) — revert is a clean single-commit
revert, no schema/data migration involved.
2026-07-04 02:40:34 +03:00
..
backend feat(tradein/estimator): switch core valuation call sites to scraper_kit (#2337) 2026-07-04 02:40:34 +03:00
browser feat(proxy-pool): P4 — browser dynamic pool-proxy (acquire→/fetch→relaunch) + orchestration wiring, ship-dark (#2164) 2026-07-02 19:20:29 +00:00
deploy chore(tradein): полное выключение источника n1 — миграция 165 + вычистка backend/ops (#2204) 2026-07-03 10:08:56 +03:00
docs feat(tradein): канонический матч ГАР↔houses (0%→41% yield) + tradein_canon_addr + --match-only (#2063) 2026-06-28 15:50:07 +00:00
frontend fix(tradein/v2): подключить реальные данные location-coef в LocationDrawer + HeroBar (#2317) (#2341) 2026-07-03 22:47:40 +00:00
ops fix(sf): psql stdin-bootstrap пароля gendesign_reader + COALESCE-protect внешних id (review fixup, #976) 2026-06-12 11:26:40 +03:00
packages/scraper-kit feat(tradein/scraper-kit): port domclick_detail + ekb_geoportal_client to kit (#2307) 2026-07-04 00:05:07 +03:00
scripts chore(tradein): полное выключение источника n1 — миграция 165 + вычистка backend/ops (#2204) 2026-07-03 10:08:56 +03:00
.dockerignore chore(scraper-kit): docker-context wiring + restore backend workspace dep (#2137) 2026-07-02 14:21:38 +00:00
.env.example feat(tradein-imv): route IMV backfill через BrowserFetcher.fetch_json за dormant-флагом (#915 Stage 3) (#1916) 2026-06-26 09:54:28 +00:00
.gitignore feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in) 2026-05-21 00:25:39 +03:00
DEPLOY.md fix(tradein): SSRF host-allowlist на admin scrape + удалён dead ADMIN_TOKEN (Refs #756) (#778) 2026-05-30 16:27:19 +00:00
docker-compose.prod.yml fix(tradein/scraper-kit): wire browser proxy-pool into yandex/cian SERP + persist activation flags (#2160) (#2268) 2026-07-03 06:36:56 +00:00
docker-compose.yml chore(scraper-kit): docker-context wiring + restore backend workspace dep (#2137) 2026-07-02 14:21:38 +00:00
Makefile feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in) 2026-05-21 00:25:39 +03:00
pyproject.toml chore(scraper-kit): scaffold internal package + uv workspace, build-safe (#2128) 2026-07-02 14:06:07 +00:00
README.md feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in) 2026-05-21 00:25:39 +03:00
uv.lock ci(tradein): pre-merge гейт — backend pytest + frontend type-check/lint (#2208) (#2220) 2026-07-02 19:56:22 +00:00

Trade-In MVP

Локальный standalone-форк фичи Trade-In Estimator из проекта gendesign — оценка выкупной стоимости квартиры на вторичном рынке по аналогам и реальным сделкам. Layout повторяет PDF-отчёт «Брусника.Обмен» (см. docs/).

Что это и откуда взято

Источник Что Где
gendesign/main PR #316 TI-1 mock endpoint + Pydantic + SQL migration backend/
gendesign/main PR #317 TI-3 Next.js страница + 5 компонентов + hooks frontend/
gendesign/main PR #319 TI-2 PDF export 4 страницы (как у Брусники) backend/app/services/exporters/
gendesign/main PR #283 статичный tradein.html mockup (для Геныча) frontend/public/tradein.html
Встреча 19.05.2026 («Птица») требования к MVP оценки вторички docs/PTITSA_MEETING_2026-05-19.pdf
PDF Брусники EКБ-2485 референс layout-а отчёта docs/BRUSNIKA_REFERENCE_EKB-2485.pdf

Быстрый старт

make up              # build + up весь стек (caddy + frontend + backend + postgres)
open http://localhost:8080

Откроется / → автоматически редирект на /trade-in. Заполняешь форму (адрес/площадь/комнаты/этаж/...), нажимаешь «Оценить» — backend возвращает mock-оценку, фронт показывает median + диапазон цен + список аналогов.

Проверка backend напрямую:

make test-estimate
# или вручную:
curl -sS -X POST http://localhost:8080/api/v1/trade-in/estimate \
  -H 'Content-Type: application/json' \
  -d '{"address":"ул. Малышева, 1","area_m2":54,"rooms":2,"floor":5,"total_floors":17}' \
  | python3 -m json.tool

OpenAPI документация: http://localhost:8000/docs

Для сравнения макет vs реальная фича:

Структура

tradein-mvp/
├── docker-compose.yml         # caddy + frontend + backend + postgres
├── Makefile                   # удобные команды (up/down/logs/test-estimate)
├── deploy/
│   └── Caddyfile              # local reverse-proxy на http://localhost:8080
├── backend/                   # FastAPI + WeasyPrint
│   ├── Dockerfile
│   ├── pyproject.toml
│   ├── app/
│   │   ├── main.py            # FastAPI entry — только trade-in router
│   │   ├── core/
│   │   │   ├── config.py      # минимальный pydantic-settings
│   │   │   └── db.py          # SQLAlchemy engine + get_db
│   │   ├── api/v1/
│   │   │   └── trade_in.py    # 3 endpoint'а: POST /estimate, GET /estimate/{id}, GET /estimate/{id}/pdf
│   │   ├── schemas/
│   │   │   └── trade_in.py    # Pydantic: TradeInEstimateInput / AnalogLot / AggregatedEstimate
│   │   └── services/exporters/
│   │       └── trade_in_pdf.py  # WeasyPrint → 4-страничный PDF (cover/listings/deals/offer)
│   └── data/sql/
│       └── 001_trade_in_estimates.sql  # CREATE TABLE; применяется при первом старте postgres
├── frontend/                  # Next.js 15 + React 19 + TanStack Query
│   ├── Dockerfile
│   ├── package.json
│   ├── next.config.ts         # rewrites /api/* → backend
│   ├── tsconfig.json
│   ├── src/
│   │   ├── app/
│   │   │   ├── layout.tsx
│   │   │   ├── page.tsx       # redirect → /trade-in
│   │   │   ├── globals.css
│   │   │   ├── providers.tsx  # QueryClientProvider
│   │   │   └── trade-in/
│   │   │       └── page.tsx
│   │   ├── components/trade-in/
│   │   │   ├── EstimateForm.tsx       # форма ввода (sticky 360px)
│   │   │   ├── EstimateProgress.tsx   # индикатор «Парсим Циан → Авито → ...»
│   │   │   ├── EstimateResult.tsx     # карточка результата
│   │   │   ├── PriceRangeBar.tsx      # визуализация диапазона цен (как у Брусники)
│   │   │   └── AnalogsTable.tsx       # таблица аналогов
│   │   ├── lib/
│   │   │   ├── api.ts                 # apiFetch + HTTPError
│   │   │   ├── sessionId.ts           # X-Session-Id из localStorage
│   │   │   └── trade-in-api.ts        # useEstimateMutation + useEstimate hooks
│   │   └── types/
│   │       └── trade-in.ts            # TS типы зеркалят Pydantic schemas
│   └── public/
│       └── tradein.html              # статичный mockup от 17.05 (для side-by-side review)
└── docs/
    ├── BRUSNIKA_REFERENCE_EKB-2485.pdf  # эталон layout-а
    └── PTITSA_MEETING_2026-05-19.pdf    # AI-протокол встречи с требованиями

API

POST /api/v1/trade-in/estimate — оценить квартиру.

Запрос:

{
  "address": "ул. Малышева, 1, кв. 5, Екатеринбург",
  "area_m2": 54.0,
  "rooms": 2,
  "floor": 5,
  "total_floors": 17,
  "year_built": 1985,
  "house_type": "panel",
  "repair_state": "good",
  "has_balcony": true
}

Ответ:

{
  "estimate_id": "...uuid...",
  "median_price_rub": 13125000,
  "range_low_rub": 11550000,
  "range_high_rub": 14700000,
  "median_price_per_m2": 243056,
  "confidence": "high",
  "n_analogs": 8,
  "period_months": 24,
  "analogs": [ {"address": "...", "area_m2": 56, "price_rub": 12700000, ...} ],
  "actual_deals": [ ... ],
  "expires_at": "2026-05-20T22:48:00Z"
}

GET /api/v1/trade-in/estimate/{id} — получить сохранённую оценку (TTL 24ч) GET /api/v1/trade-in/estimate/{id}/pdf — скачать 4-страничный PDF (cover / listings / deals / offer)

Что внутри _mock_estimate() (текущая реализация)

Формула:

price = base_price_by_rooms × floor_factor × repair_factor
Поле Значения
Базовая цена ЕКБ 2026 студия 6.5M (260K/м²) · 1к 9.0M (225K/м²) · 2к 12.5M (208K/м²) · 3к 17.0M (213K/м²)
floor_factor 1-й этаж = ×0.95, последний = ×0.97, остальные = ×1.00
repair_factor needs_repair = ×0.90, standard = ×1.00, good = ×1.05, excellent = ×1.10
confidence 1-3 комнаты = high, остальные = medium
Улицы аналогов реальные центральные ЕКБ (Малышева, Куйбышева, 8 Марта, Белинского, пр. Ленина, Толмачёва, Радищева, Мамина-Сибиряка, Луначарского, Первомайская)

Каждая оценка сохраняется в trade_in_estimates с TTL 24 часа — UUID можно использовать для shareable links и PDF-экспорта.

Roadmap — что доделать

Phase 1 — заменить mock на реальные данные (TODO TI-1b из gendesign)

Сейчас _mock_estimate() возвращает хардкод. На встрече Птица 19.05 решили:

  • источники: Циан, Авито, Дом.Клик, Я.Недвижимость, Н1, Дом РФ
  • Объектив НЕ использовать на вторичке (он про первичку/ДДУ)
  • Росреестр для исторических сделок (квартал глубины)
  • картография ЕКБ для проверки этажности/года/планировок

Phase 2 — то что обсуждали на встрече

Задача Из протокола Птицы
Парольный вход + учёт пользователей + аналитика 0:23:44, 0:25:54
Доступ только Геныч / Загайнов / Паша (НЕ Рожкова) 0:25:50, 0:22:35
PDF-отчёт под паролем 0:08:39
Real-time парсинг ≥1/час чтобы ловить быстрые продажи 0:40:19
MVP к понедельнику 25.05.2026 0:26:12
Демо для девелопера в четверг 28.05.2026 0:18:08

Phase 3 — следующие продукты (упоминалось на встрече)

  1. Птица — анализ участков + расселение домов (≥20% квартир дома в продаже → подсветить можно расселять)
  2. Расселение как сервис — следствие #1 и Птицы

См. полный протокол: docs/PTITSA_MEETING_2026-05-19.pdf.

Как это связано с прод gendesign

Аспект Прод (gendsgn.ru) Этот MVP
URL https://gendsgn.ru/trade-in http://localhost:8080/trade-in
Backend shared FastAPI /api/v1/trade-in/* то же самое, standalone
Frontend Next.js 15 в большом monorepo тот же код, standalone
Postgres 84 таблицы, 6.83M ДДУ partitioned только trade_in_estimates (1 таблица)
Caddy TLS + 5 доменов + reverse-proxy local :8080 без TLS
Что отрезано site-finder, analytics, generative, scraper, OSM, NSPD, sentry, celery, redis, playwright всё это — кроме trade-in

Важно: эти два инстанса полностью изолированы. Локальный backend пишет в свой Postgres контейнер (порт 5433), не трогает прод. Можно сломать локально что угодно — прод не пострадает.

Лицензия

Internal use only. Forked from gendesign monorepo (private).