feat(tradein): insufficient_data flag on AggregatedEstimate (#697)
When no comps/anchor are found the estimate returns median=0/range 0-0, and the frontend can render a literal «0 ₽». Add a computed boolean insufficient_data (= median_price_rub <= 0) so the UI shows an explicit «недостаточно данных» state instead. Implemented as a Pydantic @computed_field derived from median_price_rub — correct automatically across all four AggregatedEstimate construction sites (main estimate, _empty_estimate, and both GET-rehydration paths), no DB column, no risk of per-site drift. Additive/serialized; existing fields untouched. Refs #697
This commit is contained in:
parent
066fab11d3
commit
01cc7057cd
2 changed files with 62 additions and 1 deletions
|
|
@ -9,7 +9,7 @@ from datetime import date, datetime
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, computed_field
|
||||||
|
|
||||||
|
|
||||||
class TradeInEstimateInput(BaseModel):
|
class TradeInEstimateInput(BaseModel):
|
||||||
|
|
@ -132,6 +132,18 @@ class AggregatedEstimate(BaseModel):
|
||||||
confidence: Literal["low", "medium", "high"]
|
confidence: Literal["low", "medium", "high"]
|
||||||
confidence_explanation: str | None = None # «Найдено 15 аналогов, разброс ±7%»
|
confidence_explanation: str | None = None # «Найдено 15 аналогов, разброс ±7%»
|
||||||
n_analogs: int
|
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
|
period_months: int # 24
|
||||||
analogs: list[AnalogLot] # top 5-10 listings
|
analogs: list[AnalogLot] # top 5-10 listings
|
||||||
actual_deals: list[AnalogLot] # реальные продажи last 12 mo
|
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