feat(tradein/yandex): is_pro_seller из агентского блока + бэкфилл (#3063 п.2) #3100

Merged
bot-backend merged 1 commit from fix/3063-pro-seller-derive into main 2026-08-26 07:55:38 +00:00
Collaborator

Пункт 2 чеклиста #3063 (п.1 закрыт PR #3067, п.3 — отдельное решение, не здесь).

Что

Detail-обогащение Яндекса извлекало agency_name, но is_pro_seller не выводило: 4 469 активных листингов с известным агентством стояли с NULL (замер владельца 23-24.08). Признак идёт в оценщик.

Как

  • save_detail_enrichment: is_pro_seller = COALESCE(:is_pro_seller, is_pro_seller), параметр True при известном agency_name, иначе NULL — вывод только в одну сторону: отсутствие агентского блока не доказывает «частник» (п.3 задачи), в ту сторону не пишем ничего.
  • Миграция 271_listings_pro_seller_from_agency.sql: одноразовый бэкфилл существующих строк («починка разбора ≠ починка строк» — UPDATE прошлого задним числом не перезапишет). Скоуп — все источники: правило «есть имя агентства ⇒ профи» источник-независимо. Идемпотентна (IS DISTINCT FROM TRUE). Гейт lock_timeout пройден.

Проверки

  • test_3063_pro_seller_from_agency.py: агентский блок → is_pro_seller=True в параметрах UPDATE (красный на origin/main по значению — ключа нет), без блока → NULL (инвариант, COALESCE сохранит известное), форма миграции (идемпотентность, is_homeowner не тронут)
  • соседи test_yandex_detail*: 52 passed

Приёмка после деплоя: SELECT count(*) FROM listings WHERE agency_name IS NOT NULL AND is_pro_seller IS NULL → ~0 (было 4 469 только по яндексу); дальше признак поддерживается detail-обогащением.

🤖 Generated with Claude Code

Пункт 2 чеклиста #3063 (п.1 закрыт PR #3067, п.3 — отдельное решение, не здесь). ## Что Detail-обогащение Яндекса извлекало `agency_name`, но `is_pro_seller` не выводило: 4 469 активных листингов с известным агентством стояли с NULL (замер владельца 23-24.08). Признак идёт в оценщик. ## Как - `save_detail_enrichment`: `is_pro_seller = COALESCE(:is_pro_seller, is_pro_seller)`, параметр `True` при известном `agency_name`, иначе NULL — **вывод только в одну сторону**: отсутствие агентского блока не доказывает «частник» (п.3 задачи), в ту сторону не пишем ничего. - Миграция `271_listings_pro_seller_from_agency.sql`: одноразовый бэкфилл существующих строк («починка разбора ≠ починка строк» — UPDATE прошлого задним числом не перезапишет). Скоуп — все источники: правило «есть имя агентства ⇒ профи» источник-независимо. Идемпотентна (`IS DISTINCT FROM TRUE`). Гейт lock_timeout пройден. ## Проверки - `test_3063_pro_seller_from_agency.py`: агентский блок → `is_pro_seller=True` в параметрах UPDATE (**красный на origin/main по значению** — ключа нет), без блока → NULL (инвариант, COALESCE сохранит известное), форма миграции (идемпотентность, is_homeowner не тронут) - соседи `test_yandex_detail*`: 52 passed Приёмка после деплоя: `SELECT count(*) FROM listings WHERE agency_name IS NOT NULL AND is_pro_seller IS NULL` → ~0 (было 4 469 только по яндексу); дальше признак поддерживается detail-обогащением. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-08-26 07:48:26 +00:00
feat(tradein/yandex): is_pro_seller выводится из агентского блока + бэкфилл 4469 строк (#3063 п.2)
All checks were successful
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m29s
1aaa8b88a1
Detail-обогащение извлекало agency_name, но признак «продаёт профи» не
выводило — 4 469 активных листингов с известным агентством стояли с
is_pro_seller=NULL (признак эрозирован SERP-затиранием до PR #3067, а
заново не появлялся). Признак идёт в оценщик trade-in.

- save_detail_enrichment: is_pro_seller=TRUE при известном agency_name,
  fill-only COALESCE; отсутствие блока НЕ доказывает «частник» (п.3 задачи —
  отдельное решение), в ту сторону ничего не пишем;
- миграция 271: одноразовый бэкфилл уже существующих строк всех источников
  (правило источник-независимо), идемпотентна.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit 01ccf4f133 into main 2026-08-26 07:55:38 +00:00
Owner

Ревью: корректно, мержу. Плюс замер, которого в описании не хватало

Что проверял прицельно и что оказалось

Зеркальный гард IS DISTINCT FROM. Главный риск для этого класса правок: поле добавили в SET, но не в парный unchanged-гард — тогда обновление, меняющее ТОЛЬКО его, молча подавляется (ровно то, что чинилось в #2992/#3067). Проверил: в providers/yandex/detail.py такого гарда нет вовсе — save_detail_enrichment это обычный UPDATE, а не upsert из scraper_kit/base.py. Риск неприменим.

Односторонний вывод. COALESCE(CAST(:is_pro_seller AS boolean), is_pro_seller) + True if e.agency_name else None — отсутствие агентского блока не пишет ничего, а не FALSE. Это правильная граница: «нет блока» и «частник» — разные утверждения, и второе из первого не следует. CAST(... AS boolean), а не ::boolean — по правилу psycopg v3.

Замер миграции 271 на живой БД (Poincare, 26.08)

Предикат is_pro_seller IS DISTINCT FROM TRUE шире, чем описание («у них NULL»): он захватывает и явные FALSE, то есть в принципе способен перевернуть уже проставленное отрицание. Проверил фактический скоуп:

листинги с непустым agency_name:
  NULL  | 8275   ← миграция проставит TRUE
  true  | 5446   ← не трогает (IS DISTINCT FROM TRUE их отсекает)
  false | 0      ← перевернуть нечего

Строк с заполненным агентством и явным FALSE ноль, так что расхождение предиката с комментарием сейчас безвредно — миграция бьёт ровно по NULL. Оставляю как наблюдение, а не как замечание: правило «есть имя агентства ⇒ продаёт профи» источник-независимо, и если FALSE при непустом агентстве когда-нибудь появится, переворот будет скорее верным, чем нет.

В теле PR цифра 4 469 — это активные; всего затронется 8 275 строк. Стоит иметь в виду при проверке после деплоя.

Идемпотентность заявлена верно: после прогона все подходящие строки становятся TRUE, повтор обновит 0.

## Ревью: корректно, мержу. Плюс замер, которого в описании не хватало ### Что проверял прицельно и что оказалось **Зеркальный гард `IS DISTINCT FROM`.** Главный риск для этого класса правок: поле добавили в `SET`, но не в парный unchanged-гард — тогда обновление, меняющее ТОЛЬКО его, молча подавляется (ровно то, что чинилось в #2992/#3067). Проверил: в `providers/yandex/detail.py` такого гарда нет вовсе — `save_detail_enrichment` это обычный UPDATE, а не upsert из `scraper_kit/base.py`. Риск неприменим. **Односторонний вывод.** `COALESCE(CAST(:is_pro_seller AS boolean), is_pro_seller)` + `True if e.agency_name else None` — отсутствие агентского блока не пишет ничего, а не `FALSE`. Это правильная граница: «нет блока» и «частник» — разные утверждения, и второе из первого не следует. `CAST(... AS boolean)`, а не `::boolean` — по правилу psycopg v3. ### Замер миграции 271 на живой БД (Poincare, 26.08) Предикат `is_pro_seller IS DISTINCT FROM TRUE` шире, чем описание («у них NULL»): он захватывает и явные `FALSE`, то есть в принципе способен перевернуть уже проставленное отрицание. Проверил фактический скоуп: ``` листинги с непустым agency_name: NULL | 8275 ← миграция проставит TRUE true | 5446 ← не трогает (IS DISTINCT FROM TRUE их отсекает) false | 0 ← перевернуть нечего ``` Строк с заполненным агентством и явным `FALSE` **ноль**, так что расхождение предиката с комментарием сейчас безвредно — миграция бьёт ровно по NULL. Оставляю как наблюдение, а не как замечание: правило «есть имя агентства ⇒ продаёт профи» источник-независимо, и если `FALSE` при непустом агентстве когда-нибудь появится, переворот будет скорее верным, чем нет. В теле PR цифра 4 469 — это активные; всего затронется 8 275 строк. Стоит иметь в виду при проверке после деплоя. Идемпотентность заявлена верно: после прогона все подходящие строки становятся `TRUE`, повтор обновит 0.
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#3100
No description provided.