"""#3063 п.2: is_pro_seller выводится из наличия агентского блока на детальной. Прод-факт (23-24.08): 4 469 активных yandex-листингов с заполненным agency_name (из detail-бэкфилла) имели is_pro_seller = NULL — признак эрозирован SERP-затиранием до PR #3067, а detail-обогащение его и не выводило, хотя данных достаточно. Признак идёт в оценщик trade-in. Два конца: - save_detail_enrichment пишет is_pro_seller=TRUE при известном agency_name (fill-only COALESCE; отсутствие блока НЕ доказывает «частник» — п.3 задачи); - миграция 271 бэкфиллит уже существующие строки («починка разбора ≠ починка строк»: UPDATE прошлого задним числом никто не перезапишет). Красный на origin/main по ЗНАЧЕНИЮ: в параметрах UPDATE нет ключа is_pro_seller → .get() даёт None, не True. """ from __future__ import annotations import os os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") from pathlib import Path from typing import Any from unittest.mock import MagicMock from scraper_kit.providers.yandex.detail import DetailEnrichment, save_detail_enrichment _MIGRATION = ( Path(__file__).resolve().parents[1] / "data" / "sql" / "271_listings_pro_seller_from_agency.sql" ) class _CaptureDb: def __init__(self) -> None: self.params: list[dict[str, Any]] = [] def execute(self, _stmt: Any, params: dict[str, Any] | None = None) -> Any: if params: self.params.append(params) return MagicMock(rowcount=1) def commit(self) -> None: pass def _update_params(db: _CaptureDb) -> dict[str, Any]: upd = [p for p in db.params if "listing_id" in p] assert upd, "save_detail_enrichment не выполнил UPDATE" return upd[-1] def test_agency_block_derives_pro_seller_true() -> None: db = _CaptureDb() save_detail_enrichment( db, 101, DetailEnrichment(offer_id="o1", source_url="https://x/1", agency_name="Этажи") ) # type: ignore[arg-type] assert _update_params(db).get("is_pro_seller") is True, ( "агентский блок известен, а is_pro_seller не выводится — " "4 469 строк на проде копились именно так" ) def test_no_agency_block_leaves_pro_seller_untouched() -> None: """Отсутствие блока — NULL в параметре: COALESCE сохранит ранее известное значение, и «частник» из этого НЕ выводится (п.3 задачи — отдельно).""" db = _CaptureDb() save_detail_enrichment( db, 102, DetailEnrichment(offer_id="o2", source_url="https://x/2", agency_name=None) ) # type: ignore[arg-type] assert _update_params(db).get("is_pro_seller") is None def test_backfill_migration_shape() -> None: """Миграция существует, идемпотентна (IS DISTINCT FROM TRUE), не трогает is_homeowner и пустые имена агентств.""" sql = _MIGRATION.read_text() assert "SET is_pro_seller = TRUE" in sql assert "agency_name IS NOT NULL" in sql assert "IS DISTINCT FROM TRUE" in sql assert "is_homeowner" not in sql.split("--")[0] or "SET is_homeowner" not in sql