Active competitor analysis для site-finder. Радиус 0.1-1.5km, time_window month/quarter/year, фильтр obj_class, exclude_obj_ids. Schema correctness (review fixed 4 critical): - snapshot_date (not snapshot_at) - flat_count (not flats_total/flats_sold) — flats_sold/sold_pct null - Velocity migrated domrf_kn_sale_graph -> objective_corpus_room_month - domrf_kn_flats.status (not deal_status) 9 mock tests pass. Closes part of #112 backend.
93 lines
2.3 KiB
Python
93 lines
2.3 KiB
Python
from typing import Any, Literal
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class ParcelFilter(BaseModel):
|
|
vri: list[str] | None = None
|
|
area_min_sqm: float | None = None
|
|
area_max_sqm: float | None = None
|
|
region: str | None = None
|
|
polygon_geojson: dict[str, Any] | None = None
|
|
text_search: str | None = None
|
|
|
|
|
|
class ScoringWeights(BaseModel):
|
|
distance_to_center: float = 0.4
|
|
poi_density: float = 0.3
|
|
neighborhood_price: float = 0.3
|
|
|
|
|
|
class ParcelSearchRequest(BaseModel):
|
|
filters: ParcelFilter = Field(default_factory=ParcelFilter)
|
|
weights: ScoringWeights = Field(default_factory=ScoringWeights)
|
|
limit: int = 50
|
|
|
|
|
|
class ScoreBreakdown(BaseModel):
|
|
total: float
|
|
distance_to_center: float
|
|
poi_density: float
|
|
neighborhood_price: float
|
|
|
|
|
|
class ParcelSummary(BaseModel):
|
|
id: str
|
|
cadastral_number: str
|
|
area_sqm: float
|
|
vri: str
|
|
address: str | None
|
|
score: ScoreBreakdown
|
|
|
|
|
|
class ParcelSearchResponse(BaseModel):
|
|
items: list[ParcelSummary]
|
|
total: int
|
|
|
|
|
|
class ParcelDetail(ParcelSummary):
|
|
geometry_geojson: dict[str, Any]
|
|
enrichment: dict[str, Any]
|
|
|
|
|
|
# ── Competitors endpoint schemas ──────────────────────────────────────────────
|
|
|
|
TimeWindow = Literal["last_month", "last_quarter", "last_year"]
|
|
ObjClassFilter = Literal["economy", "comfort", "business"]
|
|
|
|
|
|
class CompetitorsRequest(BaseModel):
|
|
radius_km: float = Field(default=1.0, ge=0.1, le=1.5)
|
|
time_window: TimeWindow = "last_quarter"
|
|
obj_class_filter: ObjClassFilter | None = None
|
|
exclude_obj_ids: list[int] = Field(default_factory=list)
|
|
|
|
|
|
class Competitor(BaseModel):
|
|
obj_id: int
|
|
comm_name: str | None
|
|
dev_name: str | None
|
|
obj_class: str | None
|
|
distance_m: float
|
|
lat: float
|
|
lng: float
|
|
stage: str | None
|
|
flats_total: int | None
|
|
flats_sold: int | None
|
|
sold_pct: float | None
|
|
velocity_per_month: float
|
|
avg_price_per_m2: float | None
|
|
is_active: bool
|
|
|
|
|
|
class CompetitorsSummary(BaseModel):
|
|
total_competitors: int
|
|
active_count: int
|
|
weighted_avg_velocity: float
|
|
radius_km: float
|
|
time_window: str
|
|
|
|
|
|
class CompetitorsResponse(BaseModel):
|
|
competitors: list[Competitor]
|
|
summary: CompetitorsSummary
|