4 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
73c4c487ed |
fix(mera/b2c): житель Серова получал «вы вне области», а подсказки игнорировали выбранный город
All checks were successful
CI / changes (pull_request) Successful in 10s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m6s
CI Trade-In / backend-tests (pull_request) Successful in 4m50s
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Successful in 48s
Два дефекта, найденных прогоном сценария глазами посетителя на живом домене. ## 1. Город предлагали выбрать, но отвечать по нему не умели Дропдаун на сайте (`OBLAST_CITIES`, city-registry.ts) и списки покрытия (`COVERAGE_GREEN/YELLOW_CITIES`, trade_in.py) — одно множество, записанное в двух местах. Они разошлись в обе стороны: предлагали, но не отвечали: Серов отвечали, но не предлагали: Берёзовский, Среднеуральск, Ревда Житель Серова выбирал СВОЙ город из НАШЕГО дропдауна и получал: «Этот адрес вне области, по которой мы собираем данные. Сейчас это Свердловская область: Екатеринбург целиком и ещё несколько городов вокруг.» Про город в той же самой области. Серов при этом покрыт данными: 363 активных объявления в радиусе 15 км, все свежие (замер по проде). Поэтому добавлен в жёлтый тир, а не убран из дропдаунa; три недостающих города добавлены на фронт. Шапка city-registry.ts этот риск прямо предсказывала — «перед добавлением 7-го города сверить оба списка вручную, теста на это пока нет». Теперь тест есть: бэкендовый сьют читает TS-реестр и требует РАВЕНСТВА множеств. Плюс проверка, что у каждого города с порогом есть центроид, — иначе порог мёртвый, город по координатам не резолвится. ## 2. Подсказки не слушались выбранного города `city_hint` доезжает до геокодера, но на выдачу не влияет: его смотрит только екатеринбургский кадастровый тир (как признак «речь не про ЕКБ, тир пропускаем»), а DaData-тир ограничен регионом целиком и хинта не принимает. Замер: выбран Серов, введено «Ленина 1» → первой подсказкой «Невьянский р-н, пгт Верх-Нейвинский». Человек выбирает верхний вариант и считает чужой дом — ровно баг #2576, ради которого город и спрашивают. Публичная ручка теперь подставляет город в саму строку запроса. Проверено на проде: «Серов Ленина 1» даёт серовскую выдачу целиком. Для Екатеринбурга подстановка безвредна — три разных адреса дали тот же результат с префиксом и без, поэтому правило одно на все города, без исключения для основного трафика. Чинится в публичной ручке, а не в геокодере: там от `city_hint` зависит поведение закрытого контура (`target_city_ambiguous`). ## Фикстура теста `_FAR_AWAY_CITY` стояла в 21 км от центра Серова и работала как «далеко от всех» лишь потому, что Серов не был поддержан. Переехала в Тавду — 271 км до ближайшего центроида. ## Мутации убрать Серов из покрытия (состояние прода) → падает сверка списков не подставлять город в строку → падает проверка ручки откат → 21 passed Плюс backend 75 passed, vitest 56 passed, tsc, lint, build, isolation guard. `city-registry.ts` добавлен в paths-фильтр БЭКЕНДОВОГО лэйна: сверку списков делает бэкендовый тест, и без этой строки правка одного лишь дропдауна её бы не запускала — то есть ровно тот путь, которым списки и разошлись. |
||
|
|
37e738c802 |
fix(tradein/coverage): resolve city by coordinates, not sweep-context city_hint
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m34s
Повторная проверка /coverage закрыла оба MAJOR из #2894, но выявила три новых дефекта: 1. Город больше не резолвится из моды listings.city найденной когорты — эта колонка хранит город SWEEP-контекста скрейпера (миграция 196), не геокод адреса объявления. Замер на проде: 90/90 строк в радиусе 1000м вокруг Берёзовского имеют city='Екатеринбург', 74/74 вокруг Ревды — city='Первоуральск'. Города-спутники из COVERAGE_GREEN/YELLOW_CITIES были физически недостижимы. Город теперь резолвится детерминированно по lat/lon запроса — ближайший центроид из статичной константы (8 городов, рядом с ручкой, не в БД — comment объясняет почему) в пределах 25 км. city_hint остаётся в схеме (фронт его шлёт для соседних ручек), но чисто информационный — на порог/статус не влияет. 2. test_max_age_outlier_days_passed_to_sql проверял подстроку, которая встречается в SQL дважды (count и percentile_cont) — мутация «убрать FILTER у percentile_cont, оставив у count» проходила зелёной. Добавлен живой поведенческий тест (вставляет когорту + выброс days_on_market=4000, проверяет что медиана не сдвигается) — ловит эту мутацию (подтверждено: median 8→9 при мутации). 3. _live_session() вызывался в pytest.mark.skipif на этапе сбора тестов и создавал никогда не закрываемый Session, плюс дублировался в теле теста. Заменено на _live_db_available() (open+close голого connection) для skipif и pytest-фикстуру live_session с гарантированным close/dispose. 4. Nit: пустая когорта в поддерживаемом городе отдавала status=not_covered вместе с ненулевым threshold — противоречило докстрингу CoverageProbeResponse.threshold ("0, когда порог неприменим"). threshold теперь всегда 0 при not_covered, независимо от причины. |
||
|
|
3e9af2fdef |
fix(tradein/coverage): sync cohort with paid estimator, honest age medians
Independent review found two MAJOR defects in POST /api/v1/trade-in/coverage: MAJOR-1: the probe cohort WHERE clause was missing three predicates present in estimator._COMMON_WHERE / Tier W (novostroyki guard, geo_precision != 'city', price_rub > 0) — the free probe could answer "ok" at points where the paid estimator's own 1000m radius tier sees zero real analogs. Prod example: 56.868904/60.837955, 2 rooms, 50 m2 gave n_listings=22/status=ok while the estimator's cohort at the same radius was 0 (all 54 rows were novostroyki). Added the three predicates verbatim from estimator.py, plus both a static SQL-text regression test and a real-Postgres integration test (skip_allowlist.txt, same _live_session() pattern as test_gar_flats_loader) that inserts novostroyka/geo_precision=city/price=0 rows and asserts they are not counted. MAJOR-2: median_listing_age_days was computed from days_on_market, which on prod is populated almost exclusively by one source (yandex) — thin cohorts produced a "median" over 1-2 listings. Added n_with_age to the response (honest count of listings the median is based on); median is now null below COVERAGE_MIN_AGE_SAMPLES=5, and values above COVERAGE_MAX_AGE_DAYS=365 (near -certainly dead listings, per prod: 15% of fresh yandex rows exceed 365d, max 4261d) are excluded as outliers before the percentile is computed. MINOR: city_hint was trusted at face value and echoed back verbatim — a client could pass city_hint="Екатеринбург" with coordinates in Серов and get threshold=8/status=ok. _resolve_coverage_city now prioritizes the SQL cohort's mode city (ground truth) over the client hint, falling back to hint only when the cohort is empty (where status is forced not_covered anyway). Unmatched cities no longer echo the raw client string in the city field. |
||
|
|
d0105470f4 |
feat(tradein/coverage): бесплатная проба покрытия для лендинга МЕРА (#2894)
POST /api/v1/trade-in/coverage — до оплаты пользователь видит только n похожих объявлений в радиусе 1000м и медианный возраст листинга, без единой цены. Один SQL (радиус GIST + rooms + area ±15% + freshness 14д + тот же дедуп/cap- канон, что у estimator._fetch_analogs), ноль внешних вызовов, ноль записей. Пороги ok/thin/not_covered — константы рядом с ручкой (зелёные города >=8, жёлтые >=12, остальные всегда not_covered). Поле median_listing_age_days (не "срок продажи" — возраст активного объявления, цензурированная выборка). RBAC не тронут — путь остаётся закрытым, открытие анонимного периметра вынесено в #2895. |