From c137af88f53134eb3669e307affac5b7146c7435 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Mon, 24 Aug 2026 18:57:10 +0300 Subject: [PATCH] =?UTF-8?q?feat(tradein/domklik):=20=D0=96=D0=9A=20=D0=B2?= =?UTF-8?q?=20=D1=81=D0=B2=D0=BE=D1=8E=20=D0=BA=D0=BE=D0=BB=D0=BE=D0=BD?= =?UTF-8?q?=D0=BA=D1=83=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20=D1=82?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20raw=5Fpayload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Безрисковая половина пункта 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 --- .../backend/tests/test_domclick_sweep.py | 36 +++++++++++++++++++ .../scraper_kit/providers/domclick/serp.py | 18 ++++++++++ 2 files changed, 54 insertions(+) diff --git a/tradein-mvp/backend/tests/test_domclick_sweep.py b/tradein-mvp/backend/tests/test_domclick_sweep.py index f8733e4a..aeb6080d 100644 --- a/tradein-mvp/backend/tests/test_domclick_sweep.py +++ b/tradein-mvp/backend/tests/test_domclick_sweep.py @@ -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). diff --git a/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/domclick/serp.py b/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/domclick/serp.py index 7f08d272..e7f58bc2 100644 --- a/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/domclick/serp.py +++ b/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/domclick/serp.py @@ -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: -- 2.45.3