gendesign/tradein-mvp/backend/tests/test_3253_domclick_house_fields.py
bot-backend d518efbed1
All checks were successful
CI Trade-In / changes (pull_request) Successful in 14s
CI / changes (pull_request) Successful in 16s
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 5m26s
fix(tradein/domclick): дом-поля едут в словаре ДОМ.РФ, а не сырьём Домклика
houses.material_walls уже заполнена словарём ДОМ.РФ (капремонт КР1.2, #2013):
кирпич 2662, железобетонная панель 2107, иное 1850, монолит 754. Ветка писала
туда сырую фразу карточки (Монолитный 2656, Кирпичный 2241, Панельный 1671,
Монолитно-кирпичный 773) — колонка стала бы двухсловарной, и `WHERE
material_walls = 'монолит'` перестал бы видеть весь Домклик. Ровно та болезнь,
которую sale_type уже пережил в #2674.

canon_wall_type / canon_floor_type стоят на границе записи в houses (как
canon_sale_type — на границе записи в listings): Кирпичный→кирпич,
Панельный→железобетонная панель, Монолитный/Монолитно-кирпичный→монолит,
Блочный/Деревянный→иное, Железобетонный→Железобетонные (форма, уже лежащая в
колонке). Незнакомое → None + warning раз на процесс: сырьё в колонку не
попадает никогда, а новое значение словаря видно в логах. В raw_payload сырая
фраза площадки остаётся как была.

Миграция 284 получила тот же CASE lower(...) — иначе backfill залил бы задним
числом ровно то, что код перестал писать. CASE без ELSE: незнакомое → NULL.
2026-09-05 23:17:50 +05:00

174 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Дом-поля карточки Домклика доезжают до houses (#3253).
Парсер читал houseInfo.info (тип стен/перекрытий, число квартир) и складывал всё
в listings.raw_payload, а в houses не переносил: на проде 400 домов-domklik против
3061 объявления, а houses.total_units = 0 у ВСЕХ источников при том, что
quarters_count уже лежал в собранных payload'ах.
(а) Значение: карточка с houseInfo → второй оператор получает total_units /
material_walls / material_floors ЧИСЛАМИ И СТРОКАМИ из фикстуры (не None).
(б) Непустое значение дома не затирается пустым из карточки — COALESCE в обе
стороны: и в SET, и в WHERE-гейте.
(в) Словарь: в material_walls едет КАНОН ДОМ.РФ («монолит»), а не сырая фраза
площадки («Монолитно-кирпичный») — иначе колонка становится двухсловарной.
"""
from __future__ import annotations
import json
import os
from types import SimpleNamespace
from typing import Any
import pytest
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
from scraper_kit.providers.domclick.detail import (
DomClickDetailEnrichment,
canon_floor_type,
canon_wall_type,
parse_detail_html,
save_detail_enrichment,
)
_STATE = {
"productCard": {"id": "t3253-1", "address": {"guid": "bldg-guid-1"}},
"houseInfo": {
"info": {
"wallType": "Монолитно-кирпичный",
"floorType": "Железобетонный",
"quartersCount": 168,
"entranceCount": 4,
"buildingSeries": "II-68",
"energyEfficiency": "B",
}
},
}
_HTML = (
"<html><body><script>window.__SSR_STATE__ = "
f"{json.dumps(_STATE, ensure_ascii=False)};</script></body></html>"
)
class _FakeNested:
def __enter__(self) -> None:
return None
def __exit__(self, *exc: object) -> bool:
return False
class _FakeSession:
"""Пишет все execute(stmt, params); rowcount=1 у каждого оператора."""
def __init__(self) -> None:
self.calls: list[tuple[str, dict[str, Any]]] = []
def execute(self, stmt: Any, params: dict[str, Any] | None = None) -> Any:
self.calls.append((str(stmt), params or {}))
return SimpleNamespace(rowcount=1)
def begin_nested(self) -> _FakeNested:
return _FakeNested()
def commit(self) -> None:
return None
def _house_call(db: _FakeSession) -> dict[str, Any]:
for sql, params in db.calls:
if "UPDATE houses" in sql:
return params
raise AssertionError(f"UPDATE houses не исполнен; операторы: {[s[:40] for s, _ in db.calls]}")
def test_house_fields_reach_houses_upsert() -> None:
"""(а)+(в) Значения из фикстуры доезжают до апсерта дома — канонизированными."""
e = parse_detail_html(_HTML, "https://domclick.ru/card/sale__flat__t3253-1")
assert e.raw_extra["quarters_count"] == 168
# В raw_payload сырая фраза площадки остаётся как есть — канон только в колонках.
assert e.raw_extra["wall_type"] == "Монолитно-кирпичный"
db = _FakeSession()
save_detail_enrichment(db, 555, e) # type: ignore[arg-type]
params = _house_call(db)
assert params["quarters_count"] == 168
assert params["wall_type"] == "монолит"
assert params["floor_type"] == "Железобетонные"
assert params["lid"] == 555
# ── (в) канонизация словаря (#3253) ───────────────────────────────────────────
# houses.material_walls на проде уже заполнена словарём ДОМ.РФ: кирпич 2662,
# железобетонная панель 2107, иное 1850, монолит 754. Домклик пишет свой словарь.
@pytest.mark.parametrize(
("raw", "expected"),
[
("Кирпичный", "кирпич"), # 2241 карточка на проде
("Панельный", "железобетонная панель"), # 1671
("Монолитный", "монолит"), # 2656
("Монолитно-кирпичный", "монолит"), # 773
("Блочный", "иное"),
("Деревянный", "иное"),
(" монолитный ", "монолит"), # регистр и пробелы не мешают
("Саманный", None), # незнакомое — сырьё в колонку НЕ пишем
("", None),
(None, None),
(42, None), # не-строка из JSON
],
)
def test_canon_wall_type_maps_to_domrf_dictionary(raw: object, expected: str | None) -> None:
assert canon_wall_type(raw) == expected
@pytest.mark.parametrize(
("raw", "expected"),
[
("Железобетонный", "Железобетонные"), # форма Домклика
("Железобетонные", "Железобетонные"), # форма, уже лежащая в колонке (avito)
("Деревянные", None),
(None, None),
],
)
def test_canon_floor_type_maps_to_column_form(raw: object, expected: str | None) -> None:
assert canon_floor_type(raw) == expected
def test_unknown_wall_type_is_not_written_to_houses() -> None:
"""Незнакомый тип стен не доезжает до houses — колонка не портится сырьём."""
e = DomClickDetailEnrichment(
item_id="t3253-3",
source_url="u",
raw_extra={"wall_type": "Саманный", "quarters_count": 12},
)
db = _FakeSession()
save_detail_enrichment(db, 558, e) # type: ignore[arg-type]
params = _house_call(db)
assert params["wall_type"] is None
assert params["quarters_count"] == 12 # остальные поля едут как обычно
def test_empty_card_does_not_overwrite_house() -> None:
"""(б) Карточка без дом-полей вообще не трогает houses."""
db = _FakeSession()
e = DomClickDetailEnrichment(item_id="t3253-2", source_url="u", raw_extra={"egrn_area": 42.0})
save_detail_enrichment(db, 556, e) # type: ignore[arg-type]
assert not [s for s, _ in db.calls if "UPDATE houses" in s]
def test_coalesce_protects_filled_columns() -> None:
"""(б) SET и WHERE-гейт оба COALESCE-семантика: пустым непустое не затирается."""
e = parse_detail_html(_HTML, "https://domclick.ru/card/sale__flat__t3253-1")
db = _FakeSession()
save_detail_enrichment(db, 557, e) # type: ignore[arg-type]
sql = next(s for s, _ in db.calls if "UPDATE houses" in s)
for col in ("total_units", "material_walls", "material_floors"):
assert f"COALESCE(h.{col}," in sql
assert f"h.{col} IS NULL" in sql