"""#3051 п.4: region_code у houses — вывод, а не выдумка. Прод-факты (27.08): 10 362 дома, 9 719 выводятся геометрией в регион 66; 23 порченых (ЕКБ-адреса с чужими координатами: «Вильгельма де Геннина» на Байкале, «Крауля» под Москвой, «Учителей» в Таллине) с живыми ссылками листингов; 620 без geom. Порченым и безгеомным регион НЕ присваивается — NULL-карантин (присвоить по координатам = узаконить порчу). Три слоя: миграция 272 — колонка + бэкфилл ТОЛЬКО по bbox региона 66 (значения байт-в-байт из реестра — синхронизацию держит тест ниже); запись — новый дом наследует регион РАЗВЁРТКИ, только если координаты не противоречат; противоречие → NULL + warning; проводка — base.py передаёт region_code развёртки через контракт и адаптер. """ from __future__ import annotations import os os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") import re from pathlib import Path from typing import Any from unittest.mock import MagicMock import pytest from app.services import regions from app.services.matching import houses as houses_mod _MIGRATION = Path(__file__).resolve().parents[1] / "data" / "sql" / "272_houses_region_code.sql" # ── 1. Миграция синхронизирована с реестром ────────────────────────────────── def test_migration_bbox_matches_registry() -> None: """bbox в SQL бэкфилла — байт-в-байт bbox_region региона 66 из реестра. Разъезд значений = бэкфилл и рантайм выводят регион по разным границам.""" sql = _MIGRATION.read_text() m = re.search( r"ST_Y\(geom\) BETWEEN ([\d.]+) AND ([\d.]+)\s*" r"AND ST_X\(geom\) BETWEEN ([\d.]+) AND ([\d.]+)", sql, ) assert m, "в миграции 272 не найден bbox-предикат бэкфилла" lat_min, lat_max, lon_min, lon_max = (float(g) for g in m.groups()) assert (lat_min, lat_max, lon_min, lon_max) == regions.REGIONS[66].bbox_region # 77 на бэкфилле не присваивается вообще (3 «московских» дома — порченые ЕКБ). assert "= 77" not in sql # NOT NULL не ставится, пока карантин не пуст: ни в определении колонки, # ни отдельным ALTER (слова в комментариях — не DDL). assert "SET NOT NULL" not in sql assert re.search(r"region_code smallint\s*;", sql), "колонка обязана быть nullable" # ── 2. Правило записи: унаследуй или карантинь ─────────────────────────────── class _CreateDb: """Двойник: все матч-тиры промахиваются, INSERT возвращает id и пишет параметры.""" def __init__(self) -> None: self.insert_params: dict[str, Any] | None = None def execute(self, stmt: Any, params: dict[str, Any] | None = None) -> Any: text = str(stmt) res = MagicMock() if "INSERT INTO houses" in text: self.insert_params = dict(params or {}) res.mappings.return_value.one.return_value = {"id": 4242} return res # advisory lock / все тиры матчинга — промах res.fetchone.return_value = None res.mappings.return_value.first.return_value = None res.mappings.return_value.all.return_value = [] res.scalar.return_value = None return res def commit(self) -> None: pass def rollback(self) -> None: pass def _create(lat: float | None, lon: float | None, region_code: int | None) -> dict[str, Any]: db = _CreateDb() house_id, _conf, method = houses_mod.match_or_create_house( db, # type: ignore[arg-type] "avito", "ext-1", address="ул. Крауля, 44", lat=lat, lon=lon, region_code=region_code, ) assert house_id == 4242, f"дом не создан (method={method})" assert db.insert_params is not None return db.insert_params def test_coords_confirm_region_inherited() -> None: """Координаты в bbox 66 + развёртка 66 → дом рождается с region_code=66.""" assert _create(56.83, 60.60, 66).get("region_code") == 66 def test_no_coords_no_contradiction_inherited() -> None: """Без координат противоречия нет — регион развёртки наследуется.""" assert _create(None, None, 66).get("region_code") == 66 def test_contradicting_coords_quarantine(caplog: pytest.LogCaptureFixture) -> None: """ЕКБ-развёртка с координатами в bbox Москвы (порченый геокод, прод-кейс «Крауля, гп 1») → NULL-карантин + warning, а НЕ выдуманный 77.""" import logging with caplog.at_level(logging.WARNING): params = _create(55.716, 37.268, 66) assert params.get("region_code") is None, "порченому геокоду выдумали регион вместо карантина" assert "карантин" in caplog.text def test_legacy_call_without_region_stays_null() -> None: """Вызов без региона развёртки (легаси) — NULL, не дефолт-66.""" assert _create(56.83, 60.60, None).get("region_code") is None def test_alien_coords_outside_all_regions_quarantine() -> None: """Координаты вне всех регионов (Таллин, прод-кейс «Учителей») при развёртке 66: region_for_point → None → противоречия ФОРМАЛЬНО нет — наследуем 66? НЕТ: правило намеренно наследует (см. док-стринг matching/houses.py — вне-bbox координаты не выводят ДРУГОЙ регион). Закрепляем текущую семантику, чтобы смена была осознанной, а не случайной.""" assert _create(59.42, 24.75, 66).get("region_code") == 66