feat(tradein): insufficient_data flag on AggregatedEstimate (#697) #734
2 changed files with 62 additions and 1 deletions
|
|
@ -9,7 +9,7 @@ from datetime import date, datetime
|
|||
from typing import Any, Literal
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, computed_field
|
||||
|
||||
|
||||
class TradeInEstimateInput(BaseModel):
|
||||
|
|
@ -132,6 +132,18 @@ class AggregatedEstimate(BaseModel):
|
|||
confidence: Literal["low", "medium", "high"]
|
||||
confidence_explanation: str | None = None # «Найдено 15 аналогов, разброс ±7%»
|
||||
n_analogs: int
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def insufficient_data(self) -> bool:
|
||||
"""#697: нет пригодной оценки — комплов/якоря не нашлось → headline median=0.
|
||||
|
||||
Фронт по этому флагу рендерит явное «недостаточно данных», а не буквальный
|
||||
«0 ₽». Производное от median_price_rub (==0 ⇔ нет оценки), поэтому корректно
|
||||
во всех конструкторах AggregatedEstimate автоматически (вкл. rehydrate на GET).
|
||||
"""
|
||||
return self.median_price_rub <= 0
|
||||
|
||||
period_months: int # 24
|
||||
analogs: list[AnalogLot] # top 5-10 listings
|
||||
actual_deals: list[AnalogLot] # реальные продажи last 12 mo
|
||||
|
|
|
|||
49
tradein-mvp/backend/tests/test_insufficient_data_flag.py
Normal file
49
tradein-mvp/backend/tests/test_insufficient_data_flag.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"""#697 — AggregatedEstimate.insufficient_data computed flag.
|
||||
|
||||
median=0 (нет комплов/якоря) → insufficient_data=True, чтобы фронт показал
|
||||
«недостаточно данных», а не «0 ₽». Производное от median_price_rub.
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import UTC, datetime
|
||||
from uuid import uuid4
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
|
||||
|
||||
from app.schemas.trade_in import AggregatedEstimate
|
||||
|
||||
|
||||
def _estimate(median: int) -> AggregatedEstimate:
|
||||
return AggregatedEstimate(
|
||||
estimate_id=uuid4(),
|
||||
median_price_rub=median,
|
||||
range_low_rub=0 if median == 0 else median - 100,
|
||||
range_high_rub=0 if median == 0 else median + 100,
|
||||
median_price_per_m2=0 if median == 0 else 150_000,
|
||||
confidence="low" if median == 0 else "medium",
|
||||
n_analogs=0 if median == 0 else 12,
|
||||
period_months=12,
|
||||
analogs=[],
|
||||
actual_deals=[],
|
||||
expires_at=datetime(2026, 6, 1, tzinfo=UTC),
|
||||
)
|
||||
|
||||
|
||||
def test_insufficient_when_median_zero() -> None:
|
||||
assert _estimate(0).insufficient_data is True
|
||||
|
||||
|
||||
def test_sufficient_when_median_positive() -> None:
|
||||
assert _estimate(6_900_000).insufficient_data is False
|
||||
|
||||
|
||||
def test_flag_serialized_in_model_dump() -> None:
|
||||
dump = _estimate(0).model_dump()
|
||||
assert dump["insufficient_data"] is True
|
||||
dump2 = _estimate(5_000_000).model_dump()
|
||||
assert dump2["insufficient_data"] is False
|
||||
|
||||
|
||||
def test_negative_median_is_insufficient() -> None:
|
||||
# защита от случайного отрицательного headline.
|
||||
assert _estimate(-1).insufficient_data is True
|
||||
Loading…
Add table
Reference in a new issue