gendesign/tradein-mvp/backend/tests/test_3051_houses_region_code.py
bot-backend 27fdd55aae
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
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 4m32s
feat(tradein/matching): region_code у houses — вывод, а не выдумка (#3051 п.4)
Миграция 272: колонка + бэкфилл ТОЛЬКО по bbox региона 66 (значения
байт-в-байт из реестра, синхронизацию держит тест). Карантин NULL:
620 домов без geom, 23 порченых (ЕКБ-адреса с чужими координатами —
«Вильгельма де Геннина» на Байкале, «Крауля» под Москвой, «Учителей» в
Таллине, с живыми ссылками листингов) — им регион не присваивается,
включая 3 дома с координатами в bbox Москвы (порча, не переезд). NOT NULL
из постановки — отдельной миграцией, когда карантин опустеет.

Запись: новый дом наследует регион РАЗВЁРТКИ (base.py → контракт →
адаптер → matching), только если координаты не противоречат; координаты
другого региона → NULL-карантин + warning. Вне-bbox координаты при живой
развёртке наследуют её регион (адрес и развёртка согласны, координатам
веры нет) — семантика закреплена тестом, чтобы смена была осознанной.

Матчинг-запросы НЕ тронуты — гард по региону это #3052.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 13:51:48 +05:00

137 lines
6.7 KiB
Python
Raw Permalink 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.

"""#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