gendesign/tradein-mvp/backend/app/schemas/trade_in.py
lekss361 08cf615ad7
Some checks failed
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-backend (push) Successful in 48s
Deploy Trade-In / build-frontend (push) Has been cancelled
feat(tradein/cian): surface valuation chart + rent + change-pct on /trade-in (#530)
2026-05-24 14:32:54 +00:00

180 lines
7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Pydantic schemas for Trade-In Estimator.
POST /api/v1/trade-in/estimate → AggregatedEstimate
"""
from __future__ import annotations
from datetime import date, datetime
from typing import Any, Literal
from uuid import UUID
from pydantic import BaseModel, Field
class TradeInEstimateInput(BaseModel):
address: str = Field(min_length=3, max_length=500)
area_m2: float = Field(gt=10, lt=500)
rooms: int = Field(ge=0, le=10) # 0 = студия
floor: int = Field(ge=1, le=100)
total_floors: int = Field(ge=1, le=100)
year_built: int | None = Field(default=None, ge=1800, le=2100)
house_type: Literal["panel", "brick", "monolith", "monolith_brick", "other"] | None = None
repair_state: Literal["needs_repair", "standard", "good", "excellent"] | None = None
has_balcony: bool | None = None
# CRM-поля (#395) — операционные, на расчёт оценки не влияют
ownership_type: str | None = Field(default=None, max_length=100)
has_mortgage: bool | None = None
client_name: str | None = Field(default=None, max_length=200)
client_phone: str | None = Field(default=None, max_length=50)
class AnalogLot(BaseModel):
address: str
area_m2: float
rooms: int
floor: int | None
total_floors: int | None
price_rub: int
price_per_m2: int
listing_date: date | None
days_on_market: int | None
photo_url: str | None = None
# ── Новые поля (Слой 5.2 — clickable links) ──
source: str | None = None # 'avito' / 'cian' / 'domklik' / 'rosreestr'
source_url: str | None = None # ссылка на оригинальное объявление / сделку
distance_m: int | None = None # расстояние до целевой квартиры в метрах
class CianChartPoint(BaseModel):
"""Одна точка 7-месячного chart Cian Valuation Calculator."""
date: str
price: float
class CianValuationSummary(BaseModel):
"""Cian Valuation Calculator данные для UI.
Источник: external_valuations table (source='cian_valuation').
Заполняется только при successful Cian Calculator call в estimator.py.
"""
sale_price_rub: int | None = None
rent_price_rub: int | None = None
chart: list[CianChartPoint] = Field(default_factory=list)
chart_change_pct: float | None = None
chart_change_direction: Literal["increase", "decrease", "neutral"] | None = None
class AggregatedEstimate(BaseModel):
estimate_id: UUID
median_price_rub: int
range_low_rub: int
range_high_rub: int
median_price_per_m2: int
confidence: Literal["low", "medium", "high"]
confidence_explanation: str | None = None # «Найдено 15 аналогов, разброс ±7%»
n_analogs: int
period_months: int # 24
analogs: list[AnalogLot] # top 5-10 listings
actual_deals: list[AnalogLot] # реальные продажи last 12 mo
expires_at: datetime
# ── Дополнительные метаданные ──
target_address: str | None = None # geocoded full address
target_lat: float | None = None
target_lon: float | None = None
sources_used: list[str] = Field(default_factory=list) # ['avito', 'cian', 'rosreestr']
data_freshness_minutes: int | None = None # сколько минут назад был самый свежий парсинг
est_days_on_market: int | None = None # прогноз срока продажи (медиана по аналогам)
cian_valuation: CianValuationSummary | None = None
# ── Параметры оценённой квартиры — нужны, чтобы восстановить карточку
# при открытии оценки по ссылке (?id=), когда формы-инпута уже нет ──
area_m2: float | None = None
rooms: int | None = None
floor: int | None = None
total_floors: int | None = None
year_built: int | None = None
house_type: str | None = None
repair_state: str | None = None
has_balcony: bool | None = None
class PhotoMeta(BaseModel):
"""Метаданные фото квартиры (#394). Содержимое отдаётся отдельным эндпоинтом."""
id: UUID
filename: str | None = None
content_type: str
size_bytes: int | None = None
uploaded_at: datetime
# ── Stage 4a response schemas ────────────────────────────────────────────────
class HouseInfoForEstimate(BaseModel):
"""Summary информации о доме целевой квартиры (для GET /estimate/{id}/houses)."""
house_id: int | None = None
source: str | None = None # 'avito' / 'derived' / 'cian_newbuilding' / etc.
ext_house_id: str | None = None
address: str | None = None
short_address: str | None = None
lat: float | None = None
lon: float | None = None
year_built: int | None = None
total_floors: int | None = None
house_type: str | None = None
passenger_elevators: int | None = None
cargo_elevators: int | None = None
has_concierge: bool | None = None
closed_yard: bool | None = None
has_playground: bool | None = None
parking_type: str | None = None
developer_name: str | None = None
rating: float | None = None
reviews_count: int | None = None
raw_characteristics: list[dict] = Field(default_factory=list)
class IMVBenchmarkResponse(BaseModel):
"""Avito IMV benchmark для UI (GET /estimate/{id}/imv-benchmark)."""
available: bool # есть ли IMV для этого estimate
cache_key: str | None = None
recommended_price: int | None = None
lower_price: int | None = None
higher_price: int | None = None
market_count: int | None = None
fetched_at: datetime | None = None
# comparison vs our estimate
our_median_price: int | None = None
diff_pct: float | None = None # (our - imv) / imv * 100
# ── In-app scheduler (Stage 4e) ─────────────────────────────────────────────
class ScheduleConfig(BaseModel):
"""Текущая конфигурация schedule для source."""
id: int
source: str
enabled: bool
window_start_hour: int = Field(ge=0, le=23)
window_end_hour: int = Field(ge=0, le=23)
default_params: dict[str, Any] = Field(default_factory=dict)
last_run_id: int | None = None
last_run_at: str | None = None # ISO
next_run_at: str | None = None # ISO
updated_at: str | None = None # ISO
class ScheduleConfigUpdate(BaseModel):
"""PUT body для update_schedule endpoint."""
enabled: bool = True
window_start_hour: int = Field(default=2, ge=0, le=23)
window_end_hour: int = Field(default=5, ge=0, le=23)
default_params: dict[str, Any] = Field(default_factory=dict)