- FastAPI backend: PostGIS estimator + 3 scrapers (Avito/Cian/Yandex)
- Next.js 15 frontend: tradein.html mockup design, basePath=/trade-in
- WeasyPrint PDF (Брусника-style 4-page report)
- Address autocomplete с typo-tolerance + 6 EKB presets
- Изолированный docker stack gendesign-tradein (отдельная postgres БД)
- Caddy inline routes: gendsgn.ru/trade-in/* и /trade-in/api/v1/*
- Forgejo Actions: .forgejo/workflows/deploy-tradein.yml (shell-based GHCR login)
- Триггер только по paths: tradein-mvp/** (не пересекается с deploy.yml)
- Образы: ghcr.io/lekss361/gendesign-tradein-{backend,frontend}:latest
Первый запуск на сервере (вручную, один раз):
- создать /opt/gendesign/tradein-mvp/.env.runtime (postgres pwd, contact email)
- docker network create gendesign_shared (если нет)
- docker compose -p gendesign-tradein up -d
- docker compose -p gendesign exec caddy caddy reload
196 lines
10 KiB
Markdown
196 lines
10 KiB
Markdown
# Trade-In MVP
|
||
|
||
Локальный standalone-форк фичи **Trade-In Estimator** из проекта gendesign — оценка выкупной стоимости квартиры на вторичном рынке по аналогам и реальным сделкам. Layout повторяет PDF-отчёт «Брусника.Обмен» (см. [docs/](docs/)).
|
||
|
||
## Что это и откуда взято
|
||
|
||
| Источник | Что | Где |
|
||
|---|---|---|
|
||
| `gendesign/main` PR [#316](https://git.gendsgn.ru/lekss361/gendesign/pulls/316) TI-1 | mock endpoint + Pydantic + SQL migration | `backend/` |
|
||
| `gendesign/main` PR [#317](https://git.gendsgn.ru/lekss361/gendesign/pulls/317) TI-3 | Next.js страница + 5 компонентов + hooks | `frontend/` |
|
||
| `gendesign/main` PR [#319](https://git.gendsgn.ru/lekss361/gendesign/pulls/319) TI-2 | PDF export 4 страницы (как у Брусники) | `backend/app/services/exporters/` |
|
||
| `gendesign/main` PR [#283](https://git.gendsgn.ru/lekss361/gendesign/pulls/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` |
|
||
|
||
## Быстрый старт
|
||
|
||
```bash
|
||
make up # build + up весь стек (caddy + frontend + backend + postgres)
|
||
open http://localhost:8080
|
||
```
|
||
|
||
Откроется `/` → автоматически редирект на `/trade-in`. Заполняешь форму (адрес/площадь/комнаты/этаж/...), нажимаешь «Оценить» — backend возвращает mock-оценку, фронт показывает median + диапазон цен + список аналогов.
|
||
|
||
Проверка backend напрямую:
|
||
```bash
|
||
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 реальная фича:
|
||
- макет: <http://localhost:8080/preview/tradein.html>
|
||
- фича: <http://localhost:8080/trade-in>
|
||
|
||
## Структура
|
||
|
||
```
|
||
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` — оценить квартиру.
|
||
|
||
Запрос:
|
||
```json
|
||
{
|
||
"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
|
||
}
|
||
```
|
||
|
||
Ответ:
|
||
```json
|
||
{
|
||
"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](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).
|