fix(tradein): rehydrate POST-only fields on GET /estimate/{id} (#696) #710

Merged
bot-reviewer merged 1 commit from feat/696-get-estimate-rehydrate into main 2026-05-30 11:05:53 +00:00
Collaborator

Summary

  • GET /estimate/{id} (shared-link, PDF reopen, ?id= restore) возвращал price_trend / last_scraped_at / avito_imv / dkp_corridor как null — house_id не ререзолвился в GET-хендлере, derived-поля терялись. POST их возвращает.
  • Пересчитываем поля в хендлере из персистированного состояния (без DDL):
    • target_house_id ререзолвится по адресу (tradein_normalize_short_addr) + geo-fallback 100м (паттерн из placement-history/house-analytics, вынесен в _resolve_target_house_id);
    • price_trend / avito_imv (display-only anchor) / dkp_corridor — через существующие estimator-хелперы;
    • last_scraped_at = created_at − data_freshness_minutes (оба поля персистятся; эквивалентно max(scraped_at) на момент POST).
  • Всё best-effort → None при отсутствии данных (без регрессий).
  • Out of scope (#696 их не называет, но тоже POST-only на GET): cian_valuation, est_days_on_market — потенциальный follow-up.

Test plan

  • ruff check — clean (line-100)
  • pytest tests/test_estimate_idor.py — 23 passed (включая новый test_get_estimate_rehydrates_post_only_fields, проверяющий что 4 поля surface'ятся)
  • pytest tests/test_api_avito_stage4a.py — 5 passed (no regression)
  • smoke: открыть оценку по ?id= / shared-link → тренд-чарт, IMV-маркер, ДКП-коридор, метка свежести присутствуют

Closes #696

## Summary - GET `/estimate/{id}` (shared-link, PDF reopen, `?id=` restore) возвращал `price_trend` / `last_scraped_at` / `avito_imv` / `dkp_corridor` как **null** — house_id не ререзолвился в GET-хендлере, derived-поля терялись. POST их возвращает. - Пересчитываем поля в хендлере из персистированного состояния (**без DDL**): - `target_house_id` ререзолвится по адресу (`tradein_normalize_short_addr`) + geo-fallback 100м (паттерн из placement-history/house-analytics, вынесен в `_resolve_target_house_id`); - `price_trend` / `avito_imv` (display-only anchor) / `dkp_corridor` — через существующие estimator-хелперы; - `last_scraped_at` = `created_at − data_freshness_minutes` (оба поля персистятся; эквивалентно `max(scraped_at)` на момент POST). - Всё best-effort → None при отсутствии данных (без регрессий). - Out of scope (#696 их не называет, но тоже POST-only на GET): `cian_valuation`, `est_days_on_market` — потенциальный follow-up. ## Test plan - [x] `ruff check` — clean (line-100) - [x] `pytest tests/test_estimate_idor.py` — 23 passed (включая новый `test_get_estimate_rehydrates_post_only_fields`, проверяющий что 4 поля surface'ятся) - [x] `pytest tests/test_api_avito_stage4a.py` — 5 passed (no regression) - [ ] smoke: открыть оценку по `?id=` / shared-link → тренд-чарт, IMV-маркер, ДКП-коридор, метка свежести присутствуют Closes #696
bot-backend added 1 commit 2026-05-30 10:33:46 +00:00
POST /estimate возвращает price_trend, last_scraped_at, avito_imv и
dkp_corridor, но GET /{id} (shared-link, PDF reopen, ?id= restore) отдавал
их null — house_id не ререзолвился в GET-хендлере, derived-поля терялись.

Пересчитываем в хендлере из персистированного состояния (DDL не требуется):
- target_house_id ререзолвится по address (tradein_normalize_short_addr) с
  geo-fallback 100м — тот же паттерн, что в placement-history/house-analytics;
- price_trend / avito_imv (display-only anchor) / dkp_corridor — через
  существующие estimator-хелперы _fetch_price_trend / _fetch_house_imv_anchor /
  _fetch_dkp_corridor;
- last_scraped_at реконструируется как created_at − data_freshness_minutes
  (оба поля персистятся; эквивалентно max(scraped_at) на момент POST).

Всё best-effort: None при отсутствии данных — без регрессий относительно
прежнего поведения. cian_valuation / est_days_on_market тоже POST-only на GET
(вне scope #696 — потенциальный follow-up).

Closes #696
bot-reviewer approved these changes 2026-05-30 11:05:48 +00:00
bot-reviewer left a comment
Collaborator

APPROVE — staff+ review, no blocking issues.

Correctness-critical checks all PASS:

  • timedelta imported (from datetime import UTC, date, datetime, timedelta) — no NameError.
  • Estimator helper signatures match call sites exactly: _fetch_price_trend(db, target_house_id=), _fetch_dkp_corridor(db, address=, rooms=, area=), _fetch_house_imv_anchor(db, target_house_id=, rooms=, area=).
  • All rehydrated columns SELECTed (created_at added in this PR; address/lat/lon/area_m2/rooms/data_freshness_minutes already present).
  • SQL safe: bound params only, no :x::type CAST trap (CAST(:id AS uuid) correct), no injection. ::geography casts are on column/expression, consistent with existing geo-fallback in this file.
  • avito_imv / price_trend / dkp_corridor construction mirrors POST exactly.

Advisory follow-ups (non-blocking, doc-only):

  • M1 last_scraped_at = created_at − freshness is a sub-minute approximation of POST's exact max(scraped_at). Fine for the "обновлено DD.MM HH:MM" UI; worth a one-line comment so it isn't mistaken for exact.
  • M2 GET passes row.address (raw) to _fetch_dkp_corridor while POST passes geo.full_address (DaData-normalized). Near-always same street; DKP is advisory + graceful-None, so low risk.
  • M4 test stubs the whole app.services.estimator module, so _resolve_target_house_id and the real helper signatures are NOT exercised — a real-DB integration test would be needed to cover house_id resolution. Acceptable for unit scope; established pattern in this file.

Merging.

<!-- gendesign-review-bot: sha=5b13e51 verdict=approve --> ✅ **APPROVE** — staff+ review, no blocking issues. Correctness-critical checks all PASS: - `timedelta` imported (`from datetime import UTC, date, datetime, timedelta`) — no NameError. - Estimator helper signatures match call sites exactly: `_fetch_price_trend(db, target_house_id=)`, `_fetch_dkp_corridor(db, address=, rooms=, area=)`, `_fetch_house_imv_anchor(db, target_house_id=, rooms=, area=)`. - All rehydrated columns SELECTed (`created_at` added in this PR; address/lat/lon/area_m2/rooms/data_freshness_minutes already present). - SQL safe: bound params only, no `:x::type` CAST trap (`CAST(:id AS uuid)` correct), no injection. `::geography` casts are on column/expression, consistent with existing geo-fallback in this file. - avito_imv / price_trend / dkp_corridor construction mirrors POST exactly. Advisory follow-ups (non-blocking, doc-only): - **M1** `last_scraped_at = created_at − freshness` is a sub-minute approximation of POST's exact `max(scraped_at)`. Fine for the "обновлено DD.MM HH:MM" UI; worth a one-line comment so it isn't mistaken for exact. - **M2** GET passes `row.address` (raw) to `_fetch_dkp_corridor` while POST passes `geo.full_address` (DaData-normalized). Near-always same street; DKP is advisory + graceful-None, so low risk. - **M4** test stubs the whole `app.services.estimator` module, so `_resolve_target_house_id` and the real helper signatures are NOT exercised — a real-DB integration test would be needed to cover house_id resolution. Acceptable for unit scope; established pattern in this file. Merging.
bot-reviewer merged commit 466774dbd8 into main 2026-05-30 11:05:53 +00:00
bot-reviewer deleted branch feat/696-get-estimate-rehydrate 2026-05-30 11:05:53 +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#710
No description provided.