From db314736117d58bd1199fcca6732ffddd779e627 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Mon, 24 Aug 2026 00:32:54 +0300 Subject: [PATCH 1/2] =?UTF-8?q?fix(tradein/domklik):=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=BE=D1=81=D0=B8=D1=82=D1=8C=20isRosreestrApprove?= =?UTF-8?q?d=20=D0=B8=20is=5Fpro=5Fseller=20=D0=B2=20ScrapedLot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BFF-ответ Домклика уже парсит isRosreestrApproved и seller.company/agent (agency_name), но клал их только в raw_payload jsonb — на проде обе колонки is_rosreestr_checked и is_pro_seller у всех 1338 активных domklik-листингов были 100% NULL, хотя данные для их заполнения были в наличии всегда. - is_rosreestr_checked: прямой passthrough isRosreestrApproved (mirror cian serp.py: offer.get("isRosreestrChecked") -> is_rosreestr_checked). НЕ то же самое, что listings.registry_match (Avito-специфичная сверка "площадь/адрес/этаж совпадают" из detail-страницы, заполняется только avito/detail.py) — поэтому registry_match не трогаем. - is_pro_seller выводится из agency_name: agency_name is None -> None (нет данных о продавце), иначе bool(agency_name). agency_name="" на практике не возникает (_extract_agency_name уже схлопывает whitespace-only в None), но проверка сделана через `is not None`, а не `!= ""`, чтобы случайно не превратить None в True. Refs #3064 --- .../backend/tests/test_domclick_sweep.py | 34 +++++++++++++++++++ .../scraper_kit/providers/domclick/serp.py | 15 ++++++++ 2 files changed, 49 insertions(+) diff --git a/tradein-mvp/backend/tests/test_domclick_sweep.py b/tradein-mvp/backend/tests/test_domclick_sweep.py index c0a9bc40..7bdea470 100644 --- a/tradein-mvp/backend/tests/test_domclick_sweep.py +++ b/tradein-mvp/backend/tests/test_domclick_sweep.py @@ -90,6 +90,40 @@ def test_map_item_basic_mapping() -> None: assert lot.lon == pytest.approx(60.612) +def test_map_item_promotes_rosreestr_and_pro_seller_from_bff_fixture() -> None: + """isRosreestrApproved / seller.company из BFF-фикстуры пробрасываются в + ScrapedLot.is_rosreestr_checked / is_pro_seller (#3064 п.1-2). + + На проде 1338 активных domklik-листингов имели ОБЕ колонки 100% NULL, хотя + isRosreestrApproved и seller уже парсились и оседали только в raw_payload. + """ + 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: isRosreestrApproved=true, seller.company="АН Этажи" (непустая + # строка) -> ЕГРН-сверка пройдена и продавец агентство/агент. + lot_checked = scraper._map_item(items[0]) + assert lot_checked is not None + assert lot_checked.is_rosreestr_checked is True + assert lot_checked.agency_name == "АН Этажи" + assert lot_checked.is_pro_seller is True + + # item 1003: isRosreestrApproved отсутствует в payload, seller=None -> + # оба поля неизвестны (None), а НЕ False/True — отсутствие данных не + # то же самое, что "проверено — не сходится" / "не агентство". + lot_unknown = scraper._map_item(items[2]) + assert lot_unknown is not None + assert lot_unknown.is_rosreestr_checked is None + assert lot_unknown.agency_name is None + assert lot_unknown.is_pro_seller is None + + # ── fetch_city: report_ban на QRATOR-блок (#2600 п.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 9e93cbe1..1ab8643d 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 @@ -661,6 +661,19 @@ class DomClickScraper(BaseScraper): if not isinstance(seller, dict): seller = {} agency_name = _extract_agency_name(seller) + # is_pro_seller выводим из agency_name (тот же паттерн промоута, что + # у cian/yandex #2008): непустое имя агентства/агента -> pro-продавец. + # None остаётся None (нет данных о продавце) — не путать с "точно + # частник" (False). agency_name="" не бывает (_extract_agency_name + # уже схлопывает whitespace-only в None), но проверяем `is not None` + # явно, а не `!= ""`, чтобы не превратить None в True по ошибке. + is_pro_seller: bool | None = None if agency_name is None else bool(agency_name) + + # ── ЕГРН-проверка (Layer A promote, #3064) ───────────────────────── + # isRosreestrApproved — прямое 1:1 соответствие ScrapedLot.is_rosreestr_checked + # (mirror cian serp.py: offer.get("isRosreestrChecked")). Оставляем ключ и + # в raw_payload (как cian) — совместимость с существующими consumers. + is_rosreestr_checked: bool | None = item.get("isRosreestrApproved") flat_complex = item.get("flatComplex") or {} raw_payload: dict[str, Any] = { @@ -714,6 +727,8 @@ class DomClickScraper(BaseScraper): repair_state=repair_state, description_minhash=description_minhash, agency_name=agency_name, + is_pro_seller=is_pro_seller, + is_rosreestr_checked=is_rosreestr_checked, raw_payload=raw_payload, ) except Exception: -- 2.45.3 From 7f829ccc61084d93c6fa629d568086eb0b434bd3 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Mon, 24 Aug 2026 00:35:44 +0300 Subject: [PATCH 2/2] =?UTF-8?q?fix(tradein/domklik):=20=D1=83=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=20is=5Fpro?= =?UTF-8?q?=5Fseller=20=E2=80=94=20=D0=BA=D0=BE=D0=BB=D0=BE=D0=BD=D0=BA?= =?UTF-8?q?=D0=B0=20=D1=83=D0=B6=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B3?= =?UTF-8?q?=D1=80=D1=83=D0=B6=D0=B5=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проверка после написания: у is_pro_seller НЕТ единого смысла, из-за которого её можно было бы выводить из agency_name. cian — offer["isPro"], флаг ПЛАТНОЙ PRO-подписки (019_listings_alter_cian.sql:43: «PRO подписка у продавца») yandex — author.category in {AGENCY, AGENT}, категория продавца (yandex/serp.py:203-220) Это уже два разных смысла в одной колонке. «Есть непустой agency_name» стал бы третьим — и поехал бы в оценщик trade-in наравне с первыми двумя, причём у domklik agency_name заполнен у 100% записей, то есть колонка разом получила бы 1338 значений с новой семантикой. Правильный ход — сначала решить, что эта колонка вообще означает, и привести к одному смыслу всех троих. Это отдельная задача, не проброс поля. Здесь остаётся только isRosreestrApproved, у которого соответствие однозначное и подтверждается cian (offer["isRosreestrChecked"] -> is_rosreestr_checked, cian/serp.py:1014) — прямой passthrough. registry_match не трогается: судя по 011_listings_alter.sql:54 («Совпадают площадь, адрес и этаж») и по тому, что его пишет только avito/detail.py, это сверка ЕГРН по трём атрибутам с детальной страницы, а не SERP-флаг «проверено». Проверено: 114 passed, 1 skipped (-k domclick). Refs #3064 --- .../backend/tests/test_domclick_sweep.py | 29 ++++++++++--------- .../scraper_kit/providers/domclick/serp.py | 9 ------ 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/tradein-mvp/backend/tests/test_domclick_sweep.py b/tradein-mvp/backend/tests/test_domclick_sweep.py index 7bdea470..f8733e4a 100644 --- a/tradein-mvp/backend/tests/test_domclick_sweep.py +++ b/tradein-mvp/backend/tests/test_domclick_sweep.py @@ -90,12 +90,18 @@ def test_map_item_basic_mapping() -> None: assert lot.lon == pytest.approx(60.612) -def test_map_item_promotes_rosreestr_and_pro_seller_from_bff_fixture() -> None: - """isRosreestrApproved / seller.company из BFF-фикстуры пробрасываются в - ScrapedLot.is_rosreestr_checked / is_pro_seller (#3064 п.1-2). +def test_map_item_promotes_rosreestr_from_bff_fixture() -> None: + """isRosreestrApproved из BFF-фикстуры доезжает до + ScrapedLot.is_rosreestr_checked (#3064 п.1). - На проде 1338 активных domklik-листингов имели ОБЕ колонки 100% NULL, хотя - isRosreestrApproved и seller уже парсились и оседали только в raw_payload. + На проде у всех 1338 активных domklik-листингов колонка была NULL, хотя поле + уже парсилось и оседало в raw_payload. Соответствие 1:1 с cian + (`offer.get("isRosreestrChecked")`, cian/serp.py:1014) — без трансформаций. + + is_pro_seller здесь НЕ проверяется намеренно: колонка уже несёт два разных + смысла (cian — платная PRO-подписка, `019_listings_alter_cian.sql:43`; + yandex — категория продавца AGENCY/AGENT, `yandex/serp.py:203-220`), и + выводить её из наличия agency_name значило бы завести третий. См. #3064. """ import json from pathlib import Path @@ -106,22 +112,17 @@ def test_map_item_promotes_rosreestr_and_pro_seller_from_bff_fixture() -> None: config = SimpleNamespace(browser_http_endpoint="http://tradein-browser:9000") scraper = DomClickScraper(config, delay_provider=lambda _name: 8.0) - # item 1001: isRosreestrApproved=true, seller.company="АН Этажи" (непустая - # строка) -> ЕГРН-сверка пройдена и продавец агентство/агент. + # item 1001: isRosreestrApproved=true -> ЕГРН-сверка пройдена. lot_checked = scraper._map_item(items[0]) assert lot_checked is not None assert lot_checked.is_rosreestr_checked is True - assert lot_checked.agency_name == "АН Этажи" - assert lot_checked.is_pro_seller is True - # item 1003: isRosreestrApproved отсутствует в payload, seller=None -> - # оба поля неизвестны (None), а НЕ False/True — отсутствие данных не - # то же самое, что "проверено — не сходится" / "не агентство". + # item 1003: ключа isRosreestrApproved в payload нет вовсе -> None, а НЕ + # False. Отсутствие данных не то же самое, что "проверено — не сходится": + # False поехал бы в оценщик как утверждение о квартире. lot_unknown = scraper._map_item(items[2]) assert lot_unknown is not None assert lot_unknown.is_rosreestr_checked is None - assert lot_unknown.agency_name is None - assert lot_unknown.is_pro_seller is None # ── fetch_city: report_ban на QRATOR-блок (#2600 п.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 1ab8643d..7f08d272 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 @@ -661,14 +661,6 @@ class DomClickScraper(BaseScraper): if not isinstance(seller, dict): seller = {} agency_name = _extract_agency_name(seller) - # is_pro_seller выводим из agency_name (тот же паттерн промоута, что - # у cian/yandex #2008): непустое имя агентства/агента -> pro-продавец. - # None остаётся None (нет данных о продавце) — не путать с "точно - # частник" (False). agency_name="" не бывает (_extract_agency_name - # уже схлопывает whitespace-only в None), но проверяем `is not None` - # явно, а не `!= ""`, чтобы не превратить None в True по ошибке. - is_pro_seller: bool | None = None if agency_name is None else bool(agency_name) - # ── ЕГРН-проверка (Layer A promote, #3064) ───────────────────────── # isRosreestrApproved — прямое 1:1 соответствие ScrapedLot.is_rosreestr_checked # (mirror cian serp.py: offer.get("isRosreestrChecked")). Оставляем ключ и @@ -727,7 +719,6 @@ class DomClickScraper(BaseScraper): repair_state=repair_state, description_minhash=description_minhash, agency_name=agency_name, - is_pro_seller=is_pro_seller, is_rosreestr_checked=is_rosreestr_checked, raw_payload=raw_payload, ) -- 2.45.3