diff --git a/tradein-mvp/backend/app/schemas/trade_in.py b/tradein-mvp/backend/app/schemas/trade_in.py index 3cc7d214..8caef7c6 100644 --- a/tradein-mvp/backend/app/schemas/trade_in.py +++ b/tradein-mvp/backend/app/schemas/trade_in.py @@ -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 diff --git a/tradein-mvp/backend/tests/test_insufficient_data_flag.py b/tradein-mvp/backend/tests/test_insufficient_data_flag.py new file mode 100644 index 00000000..b37facd3 --- /dev/null +++ b/tradein-mvp/backend/tests/test_insufficient_data_flag.py @@ -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