# Trade-In MVP — Roadmap «Максимум»

> Решение 19.05 вечером: убираем привязку к срокам, делаем максимально хорошо.
> План структурирован по архитектурным слоям, не по дням. Катимся сверху-вниз —
> каждый следующий слой требует, чтобы предыдущий был стабилен.

## Видение продукта

**Что есть у Брусники:** 4-стр PDF с медианой цен, диапазоном, аналогами и сравнением «их обмен vs самопродажа». Один white-label (Брусника). Один продукт.

**Что мы делаем поверх:**

1. **Парсим больше источников** (6 vs ~5 у Брусники): Авито, Циан, Дом.Клик, Я.Недвижимость, Н1, Дом.РФ
2. **Кликабельные ссылки** на каждый аналог — Брусника не даёт
3. **Real-time freshness** — индикатор «обновлено N минут назад» на каждом источнике
4. **Объяснимый confidence** — `high (12 аналогов, разброс ±8%)` вместо «качество данных может быть не очень»
5. **POI окружение** — школы / метро / магазины с decay по расстоянию (переиспользуем код gendesign Site Finder)
6. **Тренд цен** — график за 6/24 мес. в этом районе
7. **Map view** — карта с точками аналогов + heatmap
8. **Multi-tenant white-label** — PRINZIP / Практика / любой бренд через query param
9. **JSON API + iframe widget** — для интеграции в чужие CRM
10. **Похожие сделки в том же доме** — точные совпадения если есть
11. **AI narrative summary** — короткое описание квартиры от LLM
12. **Прогноз срока продажи** — median days_on_market по выборке
13. **Score breakdown explainability** — почему именно эта цена (как Site Finder)
14. **Photo upload** на cover PDF
15. **Shareable URL** + QR-код в PDF

---

## Слой 1 — Core data layer (фундамент)

Без него ничего не работает. Каждый последующий слой строится поверх.

### 1.1 Postgres schema
- `listings` — объявления (id, source, source_url, lat, lon, address, rooms, area, floor, total_floors, year_built, house_type, repair_state, price, price_per_m2, photo_urls jsonb, listing_date, scraped_at, dedup_hash, raw_payload jsonb, geom GEOMETRY(Point, 4326))
- `deals` — фактические сделки (тот же набор + deal_date, source = 'rosreestr' пока)
- `geocode_cache` — address → lat/lon кэш (избегаем повторных Yandex hits)
- `audit_log` — каждый POST /estimate (ip, user_agent, input, estimate_id, ts)
- `house_metadata` — кэш данных ЕКБ Геопортала по координатам/кадастру (этажи, год, материал, кол-во квартир)
- Индексы: GIST на geom, btree на (rooms, scraped_at), unique на dedup_hash

### 1.2 Geocoder + Suggest
- `app/services/geocoder.py` — Yandex Geocoder через httpx + кэш в `geocode_cache`
- `app/services/suggest.py` — Yandex Suggest для frontend autocomplete (proxy через backend, не светим API key в браузере)
- Fallback к Nominatim если Яндекс упал / лимит

### 1.3 Scraper framework
- `app/services/scrapers/base.py` — общий BaseScraper
- Retry через `tenacity` (3 попытки, exp backoff)
- Rotation: пул из 5-10 User-Agent + Accept-Language
- Sleep 5-10 sec между запросами (без бана)
- Поддержка Playwright если scrapy не справляется (Циан иногда JS-render требует)
- Дедупликация: `dedup_hash = sha256(source + source_url + price)` уникальный
- Каждый scraper возвращает list[ScrapedLot] — единая Pydantic схема

---

## Слой 2 — Парсеры источников

### 2.1 Авито (приоритет 1 — стартуем с него)
- URL: `/ekaterinburg/kvartiry/prodam?radius=N&geoCoords=lat,lon&list_view=1`
- Парс HTML через `selectolax` (быстрее BeautifulSoup)
- Поля: address, rooms, area, floor/total_floors, price, listing_date, photo, deeplink
- Edge case: «частный риэлтор» vs «агентство» — фильтруем спам

### 2.2 Циан ✅ ДЕПЛОЕН
- URL: `https://ekaterinburg.cian.ru/cat.php?...`
- ВАЖНО: Циан банит httpx по TLS fingerprint → `curl_cffi(impersonate="chrome120")`
- 2025+ верстка БЕЗ `__NEXT_DATA__` — парсим DOM по `data-mark` атрибутам:
  `OfferTitle`, `MainPrice`, `Description`; адрес из множественных `data-name="GeoLabel"`
- Multi-room scrape: `fetch_around_multi_room()` по rooms=1/2/3/4 → ~108 unique lots
- Cron каждый час: `45 * * * * /opt/tradein-mvp/deploy/cron-scrape.sh cian fast`

### 2.3 Росреестр сделки
- Дамп из prod gendesign `rosreestr_deals` (6.83M строк, partitioned по кварталам)
- SSH туннель → `pg_dump --table rosreestr_deals --where "region_code = 66"` → restore локально
- Регулярный refresh: weekly Celery task

### 2.4 Дом.Клик
- API: `https://api.domclick.ru/...` (публичный, без auth, JSON)
- Поля: те же + ипотечные опции (мы пока игнорим)

### 2.5 Я.Недвижимость ✅ ДЕПЛОЕН
- URL: `https://realty.yandex.ru/ekaterinburg/kupit/kvartira/?rgid=240970&offerType=SELL`
- State в `<script id="initial_state_script">` JSON; path `state.map.offers.points`
- DATE_DESC возвращает одинаковые 20 лотов независимо от geoLat/geoLon
- Multi-room (`STUDIO/1/2/3/PLUS_4`) даёт 5× expansion → ~100 unique/anchor
- Deep mode (5 rooms × 3 sorts × 2 pages = 30 запросов / ~150s) → ~300 unique/anchor
- EXACT coords из `offer.location.point` (precision: EXACT) — без anchor jitter
- Cron каждые 3 часа: `0 */3 * * * /opt/tradein-mvp/deploy/cron-scrape.sh yandex deep`

### 2.6 Н1.ru (региональный, Урал)
- HTML scrape, очень простая структура
- Сильнее представлен в ЕКБ чем федералы

### 2.7 Дом.РФ (наш.дом.рф)
- API `kn-api` уже есть в gendesign (Playwright scraper)
- Только новостройки (ДДУ) — для вторички не критично, опционально

### 2.8 ЕКБ Геопортал (citymap.ekburg.ru)
- WMS/WFS API для зданий
- По lat/lon отдаёт: этажность, год постройки, материал, кол-во квартир, кадастровый номер
- Кэш в `house_metadata`

---

## Слой 3 — Aggregation model

### 3.1 Замена `_mock_estimate()` на реальный SQL
```sql
SELECT
  percentile_cont(0.5)  WITHIN GROUP (ORDER BY price_per_m2) AS median_ppm2,
  percentile_cont(0.25) WITHIN GROUP (ORDER BY price_per_m2) AS q1_ppm2,
  percentile_cont(0.75) WITHIN GROUP (ORDER BY price_per_m2) AS q3_ppm2,
  COUNT(*) AS n_analogs
FROM listings
WHERE ST_DWithin(geom, ST_MakePoint(:lon, :lat)::geography, 800)
  AND rooms = :rooms
  AND area BETWEEN :area * 0.85 AND :area * 1.15
  AND scraped_at > NOW() - interval '14 days'
```

### 3.2 Confidence score (объяснимый)
- `n_analogs ≥ 10 AND IQR/median < 0.15` → `high` («Найдено 12 аналогов, разброс цены ±8%»)
- `n_analogs ≥ 5 OR IQR/median < 0.25` → `medium`
- иначе → `low` («Только 3 аналога в радиусе 800м, рекомендуем проверить ручной поиск»)

### 3.3 Outlier detection
- Tukey fence: цена вне `[Q1 - 1.5*IQR, Q3 + 1.5*IQR]` → исключаем
- Логи: сколько отфильтровали

### 3.4 House match (приоритетные аналоги)
- Если есть листинг с тем же кадастровым номером дома → отдельно показываем как «в этом же доме»
- Соседние дома (тот же квартал) — следующий приоритет

### 3.5 Price trend
- Group by month, last 6/12/24 мес.
- Linear regression slope → "цены растут на N% / год"

### 3.6 Прогноз days_on_market
- Median + percentile по выборке
- «Аналогичные квартиры продаются за 30-90 дней»

---

## Слой 4 — Дифференциация (фишки выше Брусники)

### 4.1 POI окружение
- Reuse `app/services/poi_score.py` из gendesign
- Категории: school / kindergarten / metro_stop / bus_stop / shop_mall / shop_supermarket / park / hospital / pharmacy
- Distance decay (gendesign формула)
- Bonus к привлекательности района (не к цене, отдельно)

### 4.2 Map view
- Leaflet с пинами на каждый аналог
- Цветовая heatmap цен по м²
- Кликабельные popup'ы

### 4.3 AI narrative summary
- LLM call (Claude / GPT) → 2-3 предложения о квартире
- Промпт: «На основе следующих данных опиши квартиру: ...»
- Пример: «Двушка на Малышева в кирпичном доме 1985 года, требует ремонта. Цена в верхнем квартиле по району из-за центра.»

### 4.4 White-label
- Query param `?brand=prinzip|praktika|generic`
- В DB табличка `brands` (slug, logo_url, primary_color, footer_text)
- Header + PDF cover читают из brand

### 4.5 Shareable + QR
- URL `/trade-in?id=<uuid>` ✓ done
- В PDF cover — QR-код этого URL (через `segno` lib)

### 4.6 Photo upload
- `<input type="file" multiple accept="image/*">`
- Upload в `static/photos/<estimate_id>/`
- Первая фото → cover PDF

---

## Слой 5 — UX

### 5.1 Address autocomplete
- Yandex Suggest (через backend proxy)
- Debounced 300ms
- Показывать full address с городом

### 5.2 Кликабельные analogs
- В `AnalogsTable.tsx` — `<a href={listing.source_url} target="_blank">`
- Логотип источника рядом (Авито/Циан/...)

### 5.3 Loading states
- `EstimateProgress.tsx` уже есть скелет — заполнить:
  - «Геокодируем адрес...»
  - «Парсим Циан (12 объявлений)...»
  - «Парсим Авито (28 объявлений)...»
  - «Считаем медиану...»
  - «Готово!»

### 5.4 Empty / error states
- 0 аналогов в 800м → fallback 2 км + warning
- Адрес не распознан → suggest альтернативы
- Парсер упал → grace degradation (показываем что есть)

### 5.5 Mobile + iPad
- Sticky form слева → переход в полноэкранный modal на mobile
- Tap-friendly buttons (min 44px)
- iPad: 2-column layout сохраняется

### 5.6 Dark mode (низкий приоритет)
- CSS vars уже подготовлены — добавить `@media (prefers-color-scheme: dark)` overrides

---

## Слой 6 — PDF

### 6.1 Кликабельные ссылки
- WeasyPrint поддерживает `<a href>` — просто добавить в шаблон
- В таблице аналогов адрес делаем линком

### 6.2 White-label header
- Logo + название бренда читается из `brands` таблицы
- PDF cover: бренд-цвет accent

### 6.3 График цен (стр. 3)
- Render chart как SVG через Python (`matplotlib` → SVG) или send to backend chart-renderer
- Линия median + диапазон Q1-Q3 + точки сделок

### 6.4 Карта на cover
- Static map image от Yandex Static Maps API
- Пин на квартире + circle радиуса поиска

### 6.5 QR-code
- `segno.make(shareable_url).save(buffer)` → embed в HTML template

### 6.6 Freshness footer
- «Данные собраны 19.05.2026 23:15. Источники: Авито (47), Циан (38), Росреестр сделки (12). Confidence: high.»

---

## Слой 7 — Auth + Analytics + Monitoring

(отложено из MVP, добавим перед demo Геныча/Тёмы)

### 7.1 Простой password gate
- Middleware: проверка cookie/header с shared secret
- Set-cookie на 30 дней при правильном вводе
- HTML form `/login` с одним полем «Пароль»

### 7.2 Audit log
- Каждый POST /estimate → INSERT в `audit_log` (ip, ua, input, output uuid)
- Каждый GET /estimate/{id}/pdf → INSERT
- Каждый click на оригинал листинга — если хотим точно (frontend → POST /analytics/event)

### 7.3 Analytics dashboard
- `/admin/analytics` — простая страница: топ-адреса, статистика по дням, conversion (form_open → form_submit → pdf_download)
- Или PostHog self-hosted (если хочется UI без писательства)

### 7.4 Rate limiting
- slowapi (FastAPI-friendly) — 10 POST /estimate per minute per IP

### 7.5 Monitoring
- Backend logs → файл + опционально Loki
- Health checks Caddy перепингует backend каждые 30 сек

---

## Слой 8 — Deployment & Production

### 8.1 Smoke test
- `make smoke-test` bash: 5 курлов на разные адреса ЕКБ → проверка HTTP 200 + confidence != low
- Запускать в CI и перед каждым деплоем

### 8.2 Backup strategy
- Cron pg_dump → S3 daily (или local если S3 нет)
- Test restore раз в неделю

### 8.3 Production deploy
- Опция A: deploy в существующий gendesign monorepo (просто merge изменений TI-2/TI-3 + новые парсеры) → выкатить на gendsgn.ru/trade-in
- Опция B: отдельный subdomain `tradein.gendsgn.ru` через отдельный docker stack на том же VPS
- Опция C: совсем отдельный VPS / отдельная репа в Forgejo

Решим когда дойдём — пока локально.

### 8.4 Multi-tenant infrastructure
- White-label через subdomain: `prinzip.tradein.gendsgn.ru` / `praktika.tradein.gendsgn.ru`
- Caddy wildcard cert + tenant detection из Host header

---

## Слой 9 — Advanced (после core готов)

### 9.1 ML-модель цены
- XGBoost regression на исторических сделках Росреестра
- Features: rooms, area, floor/total_floors, year, distance_to_center, POI score
- Сравнение с simple median: насколько лучше / стабильнее

### 9.2 iframe widget
- `<iframe src="https://tradein.gendsgn.ru/embed?brand=prinzip&width=400">`
- Минимальная форма + результат
- Для интеграции в чужие CRM

### 9.3 JSON API для интеграций
- API key auth
- POST /api/v2/trade-in/estimate с rate limits
- Документация Swagger

### 9.4 Push notifications
- «В вашем районе 2-к продалась за 11.5M — на 8% ниже вашей оценки»
- WebPush или Telegram bot

### 9.5 PRINZIP CRM integration
- API call в их 102K заявок
- Авто-оценка по конверту заявки

### 9.6 Птица integration
- Подсветка домов где ≥20% квартир в Циан/Авито
- Карта расселения + потенциальная стоимость участка

### 9.7 Multi-region
- После ЕКБ — Пермь, Челябинск, Новосибирск, Москва, СПб
- Yandex Geocoder покрывает всё

---

## Что уже есть (стартовая позиция)

✅ Backend: FastAPI + `/estimate` (mock) + `/estimate/{id}` + PDF endpoint + Pydantic schemas
✅ Frontend: Next.js page + 5 компонентов (EstimateForm/Result/Progress/PriceRangeBar/AnalogsTable)
✅ Infra: docker-compose (caddy + frontend + backend + postgres) запущен на localhost:8080
✅ Postgres: таблица `trade_in_estimates` создаётся автоматом
✅ PDF: WeasyPrint 4-page (cover/listings/deals/offer) работает
✅ Shareable URL: `/trade-in?id=<uuid>` ✓
✅ Reference docs: PDF Брусники + PDF Птицы в `docs/`

## Что сделано ночью 19→20 мая 2026

### Слой 1 — Core data layer
- ✅ **1.1** PostgreSQL schema: listings + deals + geocode_cache + audit_log + house_metadata + brands + PostGIS GIST индексы + auto-update geom trigger
- ✅ **1.2** Geocoder service (Yandex когда дашь key + Nominatim fallback) с кэшем 90 дней. Live тест: «ул. Малышева 30 Екатеринбург» → (56.8333, 60.5945), confidence exact
- ✅ **1.3** Scraper framework — BaseScraper async context + tenacity retry + 5 ротируемых User-Agent + ScrapedLot единая Pydantic схема + save_listings с дедуп

### Слой 2 — Парсеры
- ✅ **2.1 Авито парсер** — код готов (`scrapers/avito.py`), 2 стратегии (mobile API + HTML scrape). С маки через V2Box банится (302/429) — заработает с VPS Beget.
- 💾 **Seed данные** — 120 листингов + 60 сделок на 10 центральных улицах ЕКБ, реалистичное распределение (12 студий / 30 1к / 45 2к / 25 3к / 8 4к) с jitter цен и координат для тестирования

### Слой 3 — Aggregation
- ✅ **3.1** SQL aggregation поверх listings + deals: PostGIS `ST_DWithin` 800м (fallback 2 км) + комнаты ± 0 + площадь ±15% + Tukey IQR outlier filter + median/Q1/Q3 + confidence high/medium/low с человекочитаемым объяснением

### Слой 5 + 6 — Дифференциация vs Брусники
- ✅ **5.2** Frontend AnalogsTable теперь:
  - **Source badges** цветные (avito/cian/domklik)
  - **Distance column** (167м / 628м / 1.2км)
  - **Кликабельные строки** — открывают оригинальное объявление в новой вкладке
  - **Confidence explanation плашка** под медианой «Найдено 15 аналогов, разброс ±7%»
  - **Source pills** + **freshness indicator** «● обновлено N мин назад»
- ✅ **6.1** PDF Listings/Deals таблицы: source badges + distance + кликабельные `<a href>` адреса (WeasyPrint)
- ✅ **6.5** **QR-код на cover** PDF (`segno` lib) — шёрить отчёт через QR scan
- ✅ **6.6** Freshness footer в PDF + список источников

### Слой 4 — White-label
- ✅ **4.4** Multi-tenant brands в Postgres (generic / prinzip / praktika) + `GET /api/v1/brand/{slug}` endpoint + `/pdf?brand=prinzip` query param. PDF header адаптируется (PRINZIP → чёрный header, generic → синий)

## Что НЕ сделано пока

### Слой 2 — Парсеры (отложено до prod deploy)
- 🟡 ЦИАН парсер — фреймворк готов, нужен код. Прод-IP обойдёт ban
- 🟡 Росреестр дамп из gendesign-postgres-1 (через SSH туннель)
- 🟡 ДомКлик, Я.Недвижимость, Н1, Дом.РФ

### Слой 3 — Advanced aggregation
- 🟡 House match — приоритетные аналоги в том же доме
- 🟡 Price trend regression (last 6mo slope) для «цены растут N%/год»
- 🟡 Прогноз days_on_market для «продаётся за 30-90 дней»

### Слой 4 — Доп. дифференциация
- 🟡 POI окружение (reuse gendesign) — школы/метро/магазины с decay
- 🟡 Map view (Leaflet) — пины + heatmap цен
- 🟡 AI narrative summary (LLM call) — описание квартиры
- 🟡 Photo upload на cover PDF

### Слой 5 — UX
- 🟡 Yandex Suggest autocomplete (нужен Yandex key)
- 🟡 Loading states «Парсим Циан... Авито... Расчёт...»
- 🟡 Empty state fallback
- 🟡 Mobile / iPad responsive

### Слой 6 — PDF extra
- 🟡 График цен на странице 3
- 🟡 Карта c пином на cover (Yandex Static Maps API)
- 🟡 Photo на cover (если загружено)

### Слой 7 — Auth + Analytics
- 🟡 Shared password middleware
- 🟡 Audit log INSERT с каждого POST /estimate
- 🟡 PostHog / analytics dashboard
- 🟡 Rate limiting

### Слой 8 — Deploy
- 🟡 Production deploy (gendsgn.ru/trade-in OR tradein.gendsgn.ru subdomain)
- 🟡 Backup cron pg_dump → S3
- 🟡 Smoke-test bash script
- 🟡 CI с авто-парсингом для freshness

### Слой 9 — Advanced
- 🟡 ML модель цены (XGBoost)
- 🟡 iframe widget
- 🟡 JSON API + key auth
- 🟡 PRINZIP CRM integration (102K заявок)
- 🟡 Птица integration (расселение)
- 🟡 Multi-region (Москва, СПб)

---

## Приоритет реализации

Иду сверху вниз, но внутри слоя могу параллелить независимые таски (например, 2.1 Авито и 2.2 Циан — независимые).

**Критический путь** (минимум чтобы вообще работало):
1.1 → 1.2 → 1.3 → 2.1 → 3.1 → 3.2 → 5.2 → 6.1

Всё остальное — incremental quality wins поверх.

---

## Решения 19.05.2026

- ✅ Геокодинг: **Yandex Geocoder + Suggest API**
- ✅ Источники к MVP: **Авито + Циан + Росреестр (дамп с prod)**, затем +ДомКлик/Я.Нед/Н1/Дом.РФ
- ✅ Auth: отложен в Слой 7 (добавим перед demo)
- ✅ Срок: убран, делаем максимально хорошо
- ✅ Деплой: пока локально, продакшен решим когда дойдём

## Что нужно от Антона

1. **Yandex Geocoder API key** — registration на developer.tech.yandex.ru (10 мин). Альтернатива — переиспользуем gendesign'овский если есть
2. Подтверждение SSH доступа к prod (gendesign_deploy ключ уже работает) для `pg_dump rosreestr_deals`
3. **Logo PRINZIP** (svg/png) — когда дойдём до Слоя 6.2, не блокирует старт
4. Решение по production deploy (Слой 8.3) — но это позже
