Stage 3 of AvitoScraper_v2.
- _get_or_fetch_imv_cached(db, **params) — TTL 24h по cache_key (sha256 of address+params)
* Cache HIT: SELECT avito_imv_evaluations WHERE cache_key AND fetched_at > NOW() - 24h
* Cache MISS: evaluate_via_imv + save_imv_evaluation (2 HTTP requests to Avito)
* Graceful: на любой error → return None, estimator продолжает без IMV
- В estimate_quality после aggregation:
* Map house_type/repair_state → Avito IMV формат (_IMV_HOUSE_TYPE_MAP, _IMV_REPAIR_MAP)
* Skip IMV если payload без required fields (rooms/area/floor/total_floors/house_type/repair)
* Append 'avito_imv' в sources_used (saved + returned)
- После INSERT estimate: link estimate_id в avito_imv_evaluations row (для analytics joining)
- Logging: imv=<price> в final estimate log line
- pytest offline (4 tests): house_type_map / repair_map / cache_miss_fetch / fetch_failure_graceful
* anyio.run() вместо pytest-asyncio (не установлен в tradein-mvp venv)
* DATABASE_URL dummy через os.environ.setdefault (Settings fail-fast без него)
IMV — ON-DEMAND only (per estimator call), НЕ в cron-scrape.
Refs: AvitoScraper_v2_Implementation_Plan Stage 3.
AggregatedEstimate не нёс параметры целевой квартиры (площадь, этаж,
комнаты, год, тип дома). HeroSummary брал их из формы-инпута — у
свежей оценки работало, но при открытии оценки по ссылке (?id=, из
Истории) формы нет → «Сводка» показывала «ЭТАЖ 0/0, Площадь 0 м²».
Параметры теперь проходят сквозь весь стек:
- AggregatedEstimate + TS-тип получили area_m2/rooms/floor/
total_floors/year_built/house_type/repair_state/has_balcony;
- estimate_quality заполняет их из payload, get_estimate — из строки
trade_in_estimates (колонки там уже есть);
- HeroSummary читает из estimate с откатом на input.
Заодно в TS-тип добавлен пропущенный est_days_on_market.
У Avito-объявлений «координаты» = якорь cron-скрейпа ± jitter (DOM
Avito реальных coords не отдаёт). Фейковые точки кучкуются плотными
кластерами на 5 якорях ЕКБ — tier-1 радиус (1 км) набирался почти
целиком из них: оценка для центра давала sources_used=['avito'],
18 аналогов все Avito, при 1400 живых cian/yandex/n1 в БД.
_fetch_analogs теперь исключает source='avito'. Трёхтирный fallback
по радиусу доходит до реальных cian/yandex/n1 с настоящей геопривязкой
— оценка считается по корректным аналогам.
Доп. поля для трейд-ин менеджера: тип собственности, ипотека/
обременение, имя и телефон клиента. На расчёт оценки не влияют —
сохраняются в trade_in_estimates вместе с записью.
- data/sql/008_crm_fields.sql — 4 колонки в trade_in_estimates.
- TradeInEstimateInput + estimator INSERT — приём и сохранение.
- EstimateForm — секция «CRM — для менеджера» (4 поля).
Closes#395
Когда пользователь не указал год постройки / тип дома, estimator
подтягивает их из OpenStreetMap (Overpass API) — building:levels,
start_date, building:material. Улучшает house-match аналогов (#6).
- app/services/house_metadata.py — Overpass-провайдер + кэш в таблице
house_metadata (TTL 30 дней, lookup по близости координат, 40 м).
- estimator.estimate_quality — обогащает target_year / target_house_type
перед _fetch_analogs. Best-effort: ошибка OSM → оценка без обогащения.
OSM по ЕКБ: building:levels населён хорошо, год постройки — частично.
Closes#392
Второй баг из #385 (#388 закрыл только алиас distance_m). В
_fetch_analogs ORDER BY использует :target_year и :target_house_type.
Когда год постройки / тип дома не заданы, они приходят NULL — psycopg
шлёт их без типа, PostgreSQL не может определить тип параметра →
psycopg.errors.AmbiguousParameter ($8) → 500 на каждый estimate.
Фикс: явный CAST(:target_year AS integer) и CAST(:target_house_type
AS text). Проверено на прод-БД через psycopg3: без CAST падает с
AmbiguousParameter, с CAST — успешно (7 строк).
Регрессия #385 (#6 house-match): ORDER BY использует SELECT-алиас
distance_m внутри арифметического выражения. PostgreSQL подставляет
алиасы в ORDER BY только голым термом — внутри выражения имя ищется
как колонка listings.distance_m, которой нет → "column does not
exist" → 500 на каждый геокодящийся адрес. Инлайнили ST_Distance()
вместо алиаса.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#6: аналоги сортируются по relevance — расстояние + близость года
постройки + совпадение типа дома (соразмерные квартиры из встречи Птицы).
#7: медиана корректируется коэффициентом по состоянию ремонта
(требует ремонта 0.92 ... евроремонт 1.08).