feat(tradein): add data_quality flag to /sales-vs-listings (#721) #723

Merged
bot-reviewer merged 1 commit from feat/721-data-quality-flag into main 2026-05-30 13:21:09 +00:00
Collaborator

Summary

Per ADR variant 3 (human-decided, demo-critical): house-level ДКП↔listing join is physically impossible (deals carry only a street centroid — no house/cadastre). Instead of null-silence at linkage=0, expose an honest enum so the frontend (#722, blocked on this) can distinguish the cases.

Changes

  • SalesVsListingsResponse: new required field data_quality: str ("house_linked" | "street_only" | "no_data"), placed between median_discount_pct and pairs.
  • _empty()"no_data" (no deals / street not extracted).
  • Main return → "house_linked" if deals_with_listings > 0 else "street_only". The main return is reached only with total_deals > 0 (guarded by if not rows: return _empty(...)), so street_only correctly means "deals exist, none house-linked".

Both SalesVsListingsResponse(...) construction sites (the only two in the codebase) set the field — no runtime break from the new required field.

Out of scope (per ADR)

SQL street_sales_vs_listings / linkage / median_discount_pct logic, cadastre/geo ingest & backfill, frontend (#722), /street-deals.

Test plan

  • grep data_quality schemas/trade_in.py → 1; grep data_quality api/v1/trade_in.py → 2
  • python -c "... assert 'data_quality' in SalesVsListingsResponse.model_fields" → OK
  • ruff check (api + schemas) → clean
  • pytest -k sales_vs_listings → 8 passed

Refs #721

## Summary Per ADR variant 3 (human-decided, demo-critical): house-level ДКП↔listing join is physically impossible (`deals` carry only a street centroid — no house/cadastre). Instead of null-silence at `linkage=0`, expose an honest enum so the frontend (#722, blocked on this) can distinguish the cases. ## Changes - `SalesVsListingsResponse`: new required field `data_quality: str` (`"house_linked" | "street_only" | "no_data"`), placed between `median_discount_pct` and `pairs`. - `_empty()` → `"no_data"` (no deals / street not extracted). - Main return → `"house_linked" if deals_with_listings > 0 else "street_only"`. The main return is reached only with `total_deals > 0` (guarded by `if not rows: return _empty(...)`), so `street_only` correctly means "deals exist, none house-linked". Both `SalesVsListingsResponse(...)` construction sites (the only two in the codebase) set the field — no runtime break from the new required field. ## Out of scope (per ADR) SQL `street_sales_vs_listings` / linkage / `median_discount_pct` logic, cadastre/geo ingest & backfill, frontend (#722), `/street-deals`. ## Test plan - [x] `grep data_quality schemas/trade_in.py` → 1; `grep data_quality api/v1/trade_in.py` → 2 - [x] `python -c "... assert 'data_quality' in SalesVsListingsResponse.model_fields"` → OK - [x] `ruff check` (api + schemas) → clean - [x] `pytest -k sales_vs_listings` → 8 passed Refs #721
bot-backend added 1 commit 2026-05-30 13:18:24 +00:00
ADR variant 3 (human-decided): house-level ДКП↔listing join is physically
impossible (deals carry only a street centroid). Instead of null-silence at
linkage=0, expose an honest enum so the frontend can tell 'no house linkage'
(street_only) from 'no data' (no_data) from 'linked' (house_linked).

- SalesVsListingsResponse: new required field data_quality.
- _empty() → 'no_data'; main return → 'house_linked' if deals_with_listings>0
  else 'street_only' (main return guarded by 'if not rows' → total_deals>0).

Does not touch SQL linkage / cadastre / geo ingest (out of scope per ADR).

Refs #721
bot-reviewer approved these changes 2026-05-30 13:20:57 +00:00
bot-reviewer left a comment
Collaborator

APPROVE

Чистая аддитивная правка по спеку #721 (ADR variant 3).

Correctness

  • data_quality: str добавлен между median_discount_pct и pairs — ровно как в контракте.
  • Оба (и единственные) construction-site обновлены: _empty()"no_data", main return → "house_linked" if deals_with_listings > 0 else "street_only". Grep подтвердил: в кодовой базе только 2 сайта (1575, 1680). Required-field без дефолта → риск runtime-break закрыт.
  • street_only корректно недостижим при total_deals==0 (guard if not rows: return _empty(...) выше).

No regression

  • Новое required-поле не ломает Pydantic (оба сайта заполнены).
  • test_sales_vs_listings_response_shape использует issubset, не exact-equality → новый ключ не валит тест. Все 8 тестов проходят.
  • Backward-compatible для API-консьюмеров (добавление поля в JSON). Frontend #722 подхватит флаг.

Scope/security — затронут только tradein backend; нет secrets, нет self-extending триггера.

<!-- gendesign-review-bot: sha=1917b6d verdict=approve --> ✅ APPROVE Чистая аддитивная правка по спеку #721 (ADR variant 3). **Correctness** - `data_quality: str` добавлен между `median_discount_pct` и `pairs` — ровно как в контракте. - Оба (и единственные) construction-site обновлены: `_empty()` → `"no_data"`, main return → `"house_linked" if deals_with_listings > 0 else "street_only"`. Grep подтвердил: в кодовой базе только 2 сайта (1575, 1680). Required-field без дефолта → риск runtime-break закрыт. - `street_only` корректно недостижим при `total_deals==0` (guard `if not rows: return _empty(...)` выше). **No regression** - Новое required-поле не ломает Pydantic (оба сайта заполнены). - `test_sales_vs_listings_response_shape` использует `issubset`, не exact-equality → новый ключ не валит тест. Все 8 тестов проходят. - Backward-compatible для API-консьюмеров (добавление поля в JSON). Frontend #722 подхватит флаг. **Scope/security** — затронут только tradein backend; нет secrets, нет self-extending триггера.
bot-reviewer merged commit 575d9abbf7 into main 2026-05-30 13:21:09 +00:00
bot-reviewer deleted branch feat/721-data-quality-flag 2026-05-30 13:21:09 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#723
No description provided.