feat(trade-in): TI-1 mock /trade-in/estimate endpoint + Pydantic + SQL migration #316

Merged
lekss361 merged 1 commit from feat/ti-1-mock-estimate into main 2026-05-17 16:45:07 +00:00
Owner

Summary

Wave A для issue #314 TradeIn MVP. Mock backend, чтобы frontend (Wave B TI-3) мог стартовать integration пока objective_lots data не готов (issue #307 OBJ-1/2 ещё в работе).

  • New POST /api/v1/trade-in/estimate — realistic ЕКБ mock prices by rooms/floor/repair
  • GET /api/v1/trade-in/estimate/{estimate_id} — fetch saved estimate by UUID (для future PDF endpoint TI-2)
  • Каждый POST persists row в trade_in_estimates с full input snapshot + output JSON, TTL 24h
  • New backend/app/schemas/trade_in.py: TradeInEstimateInput, AnalogLot, AggregatedEstimate
  • New data/sql/115_trade_in_estimates.sql: table + 2 indexes + CHECK constraint on confidence

Mock price bands (ЕКБ 2026)

Type Median Range ₽/m²
Studio ~25m² 6.5M 5.8–7.5M 260k
1-room ~40m² 9.0M 8.0–10.5M 225k
2-room ~60m² 12.5M 11–14M 208k
3-room ~80m² 17.0M 15–19M 213k

Adjustments: ±5% floor (1st/top floor discount), ±10% repair_state. Генерит 7–10 analog listings + 3–5 actual_deals с реальными ЕКБ адресами (Малышева/Куйбышева/Восточная).

TI-1b (next PR — after OBJ-1/2 merge)

_mock_estimate() будет заменена на real SQL aggregation из objective_lots + rosreestr_deals. GET endpoint и DB table уже production-ready.

psycopg v3 compliance

Все SQL используют CAST(:x AS uuid) / CAST(:x AS jsonb) — никаких :x::type после bind params.

Test plan

  • POST /api/v1/trade-in/estimate {"address":"ул. Малышева 10","area_m2":42,"rooms":1,"floor":5,"total_floors":17} → 200 + AggregatedEstimate JSON
  • Apply data/sql/115_trade_in_estimates.sql → verify table + indexes
  • Подтвердить row inserted в trade_in_estimates после POST
  • GET /api/v1/trade-in/estimate/{id} returns same shape with 200

Part of issue #314 (Wave A — TI-1 mock).

## Summary Wave A для issue #314 TradeIn MVP. Mock backend, чтобы frontend (Wave B TI-3) мог стартовать integration пока objective_lots data не готов (issue #307 OBJ-1/2 ещё в работе). - New `POST /api/v1/trade-in/estimate` — realistic ЕКБ mock prices by rooms/floor/repair - `GET /api/v1/trade-in/estimate/{estimate_id}` — fetch saved estimate by UUID (для future PDF endpoint TI-2) - Каждый POST persists row в `trade_in_estimates` с full input snapshot + output JSON, TTL 24h - New `backend/app/schemas/trade_in.py`: `TradeInEstimateInput`, `AnalogLot`, `AggregatedEstimate` - New `data/sql/115_trade_in_estimates.sql`: table + 2 indexes + CHECK constraint on confidence ## Mock price bands (ЕКБ 2026) | Type | Median | Range | ₽/m² | |------|--------|-------|------| | Studio ~25m² | 6.5M | 5.8–7.5M | 260k | | 1-room ~40m² | 9.0M | 8.0–10.5M | 225k | | 2-room ~60m² | 12.5M | 11–14M | 208k | | 3-room ~80m² | 17.0M | 15–19M | 213k | Adjustments: ±5% floor (1st/top floor discount), ±10% repair_state. Генерит 7–10 analog listings + 3–5 actual_deals с реальными ЕКБ адресами (Малышева/Куйбышева/Восточная). ## TI-1b (next PR — after OBJ-1/2 merge) `_mock_estimate()` будет заменена на real SQL aggregation из `objective_lots` + `rosreestr_deals`. GET endpoint и DB table уже production-ready. ## psycopg v3 compliance Все SQL используют `CAST(:x AS uuid)` / `CAST(:x AS jsonb)` — никаких `:x::type` после bind params. ## Test plan - [ ] `POST /api/v1/trade-in/estimate` `{"address":"ул. Малышева 10","area_m2":42,"rooms":1,"floor":5,"total_floors":17}` → 200 + AggregatedEstimate JSON - [ ] Apply `data/sql/115_trade_in_estimates.sql` → verify table + indexes - [ ] Подтвердить row inserted в `trade_in_estimates` после POST - [ ] `GET /api/v1/trade-in/estimate/{id}` returns same shape with 200 Part of issue #314 (Wave A — TI-1 mock).
lekss361 added 1 commit 2026-05-17 16:38:03 +00:00
lekss361 merged commit 29dbbee2ac into main 2026-05-17 16:45:07 +00:00
Author
Owner

Merged via deep-code-reviewer — verdict APPROVE.

Что проверено:

  • psycopg v3: CAST(:id AS uuid) и CAST(:analogs AS jsonb) / CAST(:actual_deals AS jsonb) — корректно во всех queries, нет ::type после bind params
  • json.dumps(..., ensure_ascii=False) для jsonb binds — правильно
  • Migration 115_ идёт после 114_ — sequential , BEGIN/COMMIT + IF NOT EXISTS — idempotent
  • CHECK constraint confidence IN ('low','medium','high') матчит Pydantic Literal
  • Индексы: created_at DESC + expires_at — полезно для TTL queries
  • Router зарегистрирован с prefix /api/v1/trade-in в main.py
  • Изолированный модуль, без impact на existing endpoints

Minor flags для TI-1b (не блокирующее):

  1. TTL cleanup mechanism: GET фильтрует expires_at > NOW() (app-side), но row не удаляется из таблицы — нужен pg_cron job DELETE FROM trade_in_estimates WHERE expires_at < NOW() - interval '7 days' или подобное, иначе таблица растёт неограниченно
  2. Cross-field validation: TradeInEstimateInput принимает floor=50, total_floors=5 без ошибки — добавить model_validator на floor <= total_floors
  3. Mock determinism: _gen_analogs() детерминирован (seed=42+rooms+n), но n_analogs = random.randint(7, 10) использует global random без seed → разные размеры списков для same input. Если нужна полная детерминированность POST same input → same response — seed'и тоже
  4. _confidence(): всегда возвращает high/medium, никогда low (хотя Literal допускает) — можно опустить до low для exotic rooms/area
  5. Dead columns в SQL: lat, lon в таблице, но не в Pydantic schema — будут всегда NULL до TI-1b

Wave B (TI-3 frontend integration) разблокирован.

Test plan для post-deploy verify:

  • Apply data/sql/115_trade_in_estimates.sql через deploy.yml auto-apply
  • curl -X POST /api/v1/trade-in/estimate -d '{"address":"ул. Малышева 10","area_m2":42,"rooms":1,"floor":5,"total_floors":17}' → 200 + AggregatedEstimate
  • GET /api/v1/trade-in/estimate/{id} с returned UUID → same shape
  • Verify row в trade_in_estimates через SQL
Merged via deep-code-reviewer — verdict APPROVE. **Что проверено:** - psycopg v3: `CAST(:id AS uuid)` и `CAST(:analogs AS jsonb)` / `CAST(:actual_deals AS jsonb)` — корректно во всех queries, нет `::type` после bind params - `json.dumps(..., ensure_ascii=False)` для jsonb binds — правильно - Migration `115_` идёт после `114_` — sequential ✅, BEGIN/COMMIT + `IF NOT EXISTS` — idempotent - CHECK constraint `confidence IN ('low','medium','high')` матчит Pydantic Literal - Индексы: `created_at DESC` + `expires_at` — полезно для TTL queries - Router зарегистрирован с prefix `/api/v1/trade-in` в `main.py` - Изолированный модуль, без impact на existing endpoints **Minor flags для TI-1b (не блокирующее):** 1. **TTL cleanup mechanism**: GET фильтрует `expires_at > NOW()` (app-side), но row не удаляется из таблицы — нужен `pg_cron` job `DELETE FROM trade_in_estimates WHERE expires_at < NOW() - interval '7 days'` или подобное, иначе таблица растёт неограниченно 2. **Cross-field validation**: `TradeInEstimateInput` принимает `floor=50, total_floors=5` без ошибки — добавить `model_validator` на `floor <= total_floors` 3. **Mock determinism**: `_gen_analogs()` детерминирован (seed=42+rooms+n), но `n_analogs = random.randint(7, 10)` использует global `random` без seed → разные размеры списков для same input. Если нужна полная детерминированность POST same input → same response — seed'и тоже 4. **`_confidence()`**: всегда возвращает `high`/`medium`, никогда `low` (хотя Literal допускает) — можно опустить до `low` для exotic rooms/area 5. **Dead columns** в SQL: `lat`, `lon` в таблице, но не в Pydantic schema — будут всегда NULL до TI-1b **Wave B (TI-3 frontend integration) разблокирован.** **Test plan для post-deploy verify:** - Apply `data/sql/115_trade_in_estimates.sql` через deploy.yml auto-apply - `curl -X POST /api/v1/trade-in/estimate -d '{"address":"ул. Малышева 10","area_m2":42,"rooms":1,"floor":5,"total_floors":17}'` → 200 + AggregatedEstimate - `GET /api/v1/trade-in/estimate/{id}` с returned UUID → same shape - Verify row в `trade_in_estimates` через SQL
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#316
No description provided.