"""IO contracts for the rule-based квартирография recommender. POST /api/v1/analytics/recommend/mix """ from typing import Any, Literal from pydantic import BaseModel, Field ClassLiteral = Literal["Comfort", "Comfort+", "Business", "Elite"] class RecommendMixInput(BaseModel): district_name: str = Field(..., min_length=2, max_length=80) area_total_m2: float | None = Field(default=None, ge=100, le=500_000) target_class: ClassLiteral | None = None months_window: int = Field(default=12, ge=3, le=24) # Velocity / pricing scenario knobs (live-tuned client-side; backend just # ships base coefficients so frontend can recompute without round-trips). # 0.01..3.0 = -99%..+200% к рынку. min=0.01 (а не 0) чтобы избежать # деления на 0 / pf^elasticity = ∞ при «бесплатной» цене. price_factor: float = Field(default=1.0, ge=0.01, le=3.0) target_months: int | None = Field(default=None, ge=3, le=120) class RecommendBucket(BaseModel): bucket: str share_pct: float deal_count: int area_avg_m2: float area_median_m2: float price_median_per_m2: float price_p25_per_m2: float price_p75_per_m2: float units_planned: int | None = None revenue_planned_rub: float | None = None # Velocity baseline (units/month for THIS project allocated to this bucket # at price_factor=1.0). Frontend scales by price_factor^elasticity for live # what-if recompute. velocity_per_month: float | None = None months_to_sellout: float | None = None # Success-driven mix flag (issue #25): bucket has top success_score in district is_top_success: bool = False class RecommendComparable(BaseModel): obj_id: int comm_name: str | None = None dev_name: str | None = None obj_class: str | None = None flat_count: int | None = None sold_perc: float | None = None cad_quarter: str | None = None lat: float | None = None lon: float | None = None buildings_n: int | None = None class RecommendMixOutput(BaseModel): scope: dict[str, Any] buckets: list[RecommendBucket] summary: dict[str, Any] comparables: list[RecommendComparable]