feat(tradein/domklik): ЖК в свою колонку вместо только raw_payload
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
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 / backend-tests (pull_request) Successful in 4m39s

Безрисковая половина пункта 3 из #3064. flatComplex.slug доезжает до
ScrapedLot.newbuilding_id — колонки, которая для идентификатора ЖК и
предназначена (докстринг задаёт формат «federatsiya-ekaterinburg», и
avito, единственный заполняющий её источник, кладёт туда слаг).
Поэтому slug, а НЕ id.

ЧЕГО ЗДЕСЬ НЕТ И ПОЧЕМУ. Привязка к дому (house_source/house_ext_id)
намеренно не трогается. Замер на проде: все 1351 активных
domklik-листингов — ВТОРИЧКА, новостроек ноль, а flatComplex.id даёт 116
значений на 737 листингов (~6.4 на ключ). Использовать его как
house_ext_id значило бы схлопнуть разные здания одного ЖК в один «дом» —
и заставить якорь того же дома в оценщике сравнивать квартиры из разных
корпусов. Это порча ценового механизма, а не улучшение дедупа.

Правильный ключ здания существует — domclick_building_guid, 467 значений
на 619 листингов, по одному адресу на ключ. Но он приходит detail-путём
(providers/domclick/detail.py), где match_or_create_house не вызывается,
а сам матчинг живёт в save_listings на SERP-пути. Это архитектурная
развилка с тремя вариантами, вынесена в issue — решение за владельцем,
потому что затрагивает цену.

Миграция не нужна: колонка newbuilding_id существует давно.

Риск нулевой: поле бэкендом НЕ ЧИТАЕТСЯ нигде (проверено grep'ом по
всему tradein-mvp — только запись, COALESCE-защита в upsert и тесты),
на проде заполнено лишь у avito (1564 из 10287), у cian/yandex/domklik
ноль.

Проверено:
  - фальсификация: до правки тест падает «assert None == malysheva»
  - тест стережёт и границу: house_source/house_ext_id остаются None
  - отсутствующий flatComplex даёт None, а не пустую строку (пустая
    читалась бы как «ЖК есть, но безымянный»)
  - регрессия -k domclick: 115 passed, 1 skipped
  - ruff чист

Refs #3064
This commit is contained in:
bot-backend 2026-08-24 18:57:10 +03:00
parent 9810ae350f
commit c137af88f5
2 changed files with 54 additions and 0 deletions

View file

@ -90,6 +90,42 @@ def test_map_item_basic_mapping() -> None:
assert lot.lon == pytest.approx(60.612)
def test_map_item_puts_complex_slug_into_newbuilding_id() -> None:
"""flatComplex.slug доезжает до ScrapedLot.newbuilding_id (#3064).
Слаг, а не id: докстринг ScrapedLot задаёт формат «federatsiya-ekaterinburg»,
и avito единственный источник, который это поле заполняет, кладёт туда слаг.
ЭТО НЕ привязка к дому. house_source/house_ext_id намеренно не трогаются:
все активные domklik-листинги вторичка, а flatComplex идентификатор ЖК,
а не здания (116 значений на 737 листингов по проду). Ключ здания
(domclick_building_guid) приходит detail-путём отдельная развилка в #3064.
"""
import json
from pathlib import Path
fixture_path = Path(__file__).parent / "fixtures" / "domclick_bff_offers_sample.json"
items = json.loads(fixture_path.read_text(encoding="utf-8"))["result"]["items"]
config = SimpleNamespace(browser_http_endpoint="http://tradein-browser:9000")
scraper = DomClickScraper(config, delay_provider=lambda _name: 8.0)
# item 1001: flatComplex.slug='malysheva' -> слаг доезжает до колонки.
lot_with_complex = scraper._map_item(items[0])
assert lot_with_complex is not None
assert lot_with_complex.newbuilding_id == "malysheva"
# Привязка к дому НЕ появилась — это стережёт границу правки.
assert lot_with_complex.house_source is None
assert lot_with_complex.house_ext_id is None
# item 1002: flatComplex отсутствует -> None, а не пустая строка: пустая
# строка попала бы в колонку как значение и читалась бы как «ЖК есть, но без
# имени», что неотличимо от настоящего ЖК с пустым слагом.
lot_no_complex = scraper._map_item(items[1])
assert lot_no_complex is not None
assert lot_no_complex.newbuilding_id is None
def test_map_item_promotes_rosreestr_from_bff_fixture() -> None:
"""isRosreestrApproved из BFF-фикстуры доезжает до
ScrapedLot.is_rosreestr_checked (#3064 п.1).

View file

@ -668,6 +668,23 @@ class DomClickScraper(BaseScraper):
is_rosreestr_checked: bool | None = item.get("isRosreestrApproved")
flat_complex = item.get("flatComplex") or {}
# ── ЖК в свою колонку, а не только в raw_payload (#3064) ──────────
# newbuilding_id — слаг ЖК (докстринг ScrapedLot: e.g.
# "federatsiya-ekaterinburg"), поэтому берём slug, а НЕ id.
#
# ЭТО НЕ ПРИВЯЗКА К ДОМУ. house_source/house_ext_id намеренно НЕ
# трогаются: замер на проде (#3064) — все 1351 активных domklik-
# листингов ВТОРИЧКА, а flatComplex.id даёт 116 значений на 737
# листингов (~6.4 на ключ). Использовать его как house_ext_id значило
# бы схлопнуть разные здания одного ЖК в один «дом» и заставить якорь
# того же дома в оценщике сравнивать квартиры из разных корпусов.
# Правильный ключ здания — domclick_building_guid (467 значений на
# 619 листингов), но он приходит detail-путём, где match_or_create_house
# не вызывается. Развилка вынесена в issue, здесь — только захват ЖК.
#
# Риска нет: newbuilding_id бэкендом не читается нигде (проверено),
# в upsert защищён COALESCE, на проде заполнен только у avito.
newbuilding_id: str | None = (flat_complex.get("slug") or None) if flat_complex else None
raw_payload: dict[str, Any] = {
"isRosreestrApproved": item.get("isRosreestrApproved"),
"squarePrice": square_price_raw,
@ -720,6 +737,7 @@ class DomClickScraper(BaseScraper):
description_minhash=description_minhash,
agency_name=agency_name,
is_rosreestr_checked=is_rosreestr_checked,
newbuilding_id=newbuilding_id,
raw_payload=raw_payload,
)
except Exception: