gendesign/backend/app/schemas/parcel.py
lekss361 f3d85f4d7a
All checks were successful
CI / backend (pull_request) Successful in 1m12s
CI / frontend (pull_request) Successful in 2m14s
feat(parcels): POST /{cad_num}/competitors — active competitors in radius (Forgejo #112)
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.
2026-05-16 01:16:12 +03:00

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