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