fix(tradein-api): add PlacementHistoryEntry schema + None checks

Follow-up to PR #528 review-bot Medium findings:
- /placement-history endpoint typed via new PlacementHistoryEntry schema
  (OpenAPI + Pydantic validation, consistent with other endpoints)
- Replace truthy 'if target.lat and target.lon' with 'is not None'
  (defensive against lat=0.0 in future region expansion)
This commit is contained in:
lekss361 2026-05-24 17:25:32 +03:00
parent 7dc11e4339
commit 72334a7558
2 changed files with 27 additions and 5 deletions

View file

@ -21,6 +21,7 @@ from app.schemas.trade_in import (
HouseInfoForEstimate,
IMVBenchmarkResponse,
PhotoMeta,
PlacementHistoryEntry,
TradeInEstimateInput,
)
from app.services.exporters.trade_in_pdf import generate_trade_in_pdf
@ -418,7 +419,7 @@ def get_estimate_houses(
# Path 2: geo-nearby (any source, 500м radius)
nearby: list[Any] = []
if target.lat and target.lon:
if target.lat is not None and target.lon is not None:
nearby = list(
db.execute(
text(
@ -453,11 +454,11 @@ def get_estimate_houses(
]
@router.get("/estimate/{estimate_id}/placement-history")
@router.get("/estimate/{estimate_id}/placement-history", response_model=list[PlacementHistoryEntry])
def get_estimate_placement_history(
estimate_id: UUID,
db: Annotated[Session, Depends(get_db)],
) -> list[dict]:
) -> list[PlacementHistoryEntry]:
"""Историческая продажная активность по дому(ам) target estimate.
Возвращает rows из house_placement_history для всех houses связанных с
@ -490,7 +491,7 @@ def get_estimate_placement_history(
).all()
house_ids = [r.id for r in rows]
if not house_ids and target.lat and target.lon:
if not house_ids and target.lat is not None and target.lon is not None:
# Geo fallback: 100м (tight radius чтобы не смешать соседние дома)
rows = db.execute(
text(
@ -528,7 +529,7 @@ def get_estimate_placement_history(
{"house_ids": house_ids},
).mappings().all()
return [dict(r) for r in history]
return [PlacementHistoryEntry(**dict(r)) for r in history]
@router.get("/estimate/{estimate_id}/imv-benchmark", response_model=IMVBenchmarkResponse)

View file

@ -146,6 +146,27 @@ class IMVBenchmarkResponse(BaseModel):
diff_pct: float | None = None # (our - imv) / imv * 100
class PlacementHistoryEntry(BaseModel):
"""Запись истории размещения лота в доме (`house_placement_history`)."""
id: int
source: str
house_id: int | None = None
ext_item_id: str
title: str | None = None
rooms: int | None = None
area_m2: float | None = None
floor: int | None = None
total_floors: int | None = None
start_price: int | None = None
start_price_date: date | None = None
last_price: int | None = None
last_price_date: date | None = None
removed_date: date | None = None
exposure_days: int | None = None
notes: str | None = None
# ── In-app scheduler (Stage 4e) ─────────────────────────────────────────────