gendesign/backend/tests/sql/test_2982_land_reservation_reparse.py
bot-backend 7501fef5c3
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 14s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m31s
CI / backend-tests (pull_request) Successful in 6m44s
fix(ptica): номер акта в land_reservation извлекается у постановлений Администрации ЕКБ (#2982)
Регекс номера требовал суффикс областных актов (-ПП/-ПА/-РП/-ПГ/-ГП/-МО),
а у постановлений Администрации Екатеринбурга его нет: на проде act_number
пуст у всех 27 строк (17.09.2026).

- izyatie_ocr: номер берётся у того же акта, чью дату выбирает
  _extract_act_date (вплотную после «от DD.MM.YYYY», иначе перед ней).
  Первое «№» в теле — «Решение Думы № 60/1» или «Приказ № 746-П».
- page_reservation_parser: суффикс необязателен; номер с суффиксом не из
  списка («218-ФЗ», «746-П», «60/1») отбрасывается целиком, а не обрезается.
- izyatie_ocr_ingest: перед записью удаляется прежний разбор того же
  участка из того же документа с другим номером. act_number в ключе
  конфликта, без этого прогон положил бы 27 строк с номером рядом с 27
  строками без номера.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 13:24:06 +05:00

134 lines
5.1 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.

"""Починка разбора номера не удваивает land_reservation (#2982).
act_number входит в ключ `UNIQUE NULLS NOT DISTINCT (cad_num, act_number)`. На
проде 27 строк записаны с `act_number IS NULL`. Когда номер начинает извлекаться,
строка (участок, «1413») с (участок, NULL) не конфликтует — без очистки прогон
кладёт вторую строку рядом, и reservation_lookup показывает изъятие дважды.
Проверяется `_upsert_records` загрузчика целиком на временной таблице той же
формы: боевые данные не читаются и не меняются.
"""
from __future__ import annotations
import os
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
import pytest
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from app.workers.tasks.izyatie_ocr_ingest import _upsert_records
def _dsn() -> str:
raw = os.environ.get("TEST_DATABASE_URL") or os.environ.get(
"DATABASE_URL", "postgresql+psycopg://gendesign@localhost:15432/gendesign"
)
return (
raw
if raw.startswith("postgresql+")
else raw.replace("postgresql://", "postgresql+psycopg://")
)
def _db_reachable() -> tuple[bool, str]:
try:
eng = create_engine(_dsn(), connect_args={"connect_timeout": 3})
with eng.connect() as c:
c.execute(text("SELECT 1"))
return True, ""
except Exception as exc:
return False, str(exc)
_DB_OK, _DB_ERR = _db_reachable()
pytestmark = pytest.mark.skipif(not _DB_OK, reason=f"Postgres недоступен: {_DB_ERR}")
_TABLE = """
CREATE TEMP TABLE land_reservation (
id bigserial PRIMARY KEY,
cad_num text NOT NULL,
reservation_kind text NOT NULL,
basis_act text NOT NULL,
act_number text,
act_date date,
purpose text,
doc_url text,
source text NOT NULL DEFAULT 'page_pdf',
is_active boolean NOT NULL DEFAULT true,
raw_excerpt text,
fetched_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT uq_t UNIQUE NULLS NOT DISTINCT (cad_num, act_number)
) ON COMMIT DROP;
"""
КАД = "66:41:0303004:22"
ДОК = "https://xn--80acgfbsl1azdqr.xn--p1ai/file/89adb28a3677e7df933e2d9ce0f205c8"
def _row(cad: str = КАД, act: str | None = "1413", url: str = ДОК) -> dict:
return {
"cad_num": cad,
"reservation_kind": "изъятие",
"basis_act": "Сообщение о планируемом изъятии",
"act_number": act,
"act_date": "2022-05-27",
"purpose": None,
"doc_url": url,
"source": "izyatie_ekb_ocr",
"raw_excerpt": None,
}
@pytest.fixture
def db():
engine = create_engine(_dsn())
session = sessionmaker(bind=engine)()
try:
session.execute(text(_TABLE))
n = session.execute(text("SELECT count(*) FROM land_reservation")).scalar()
assert n == 0, f"запрос попал НЕ во временную таблицу ({n} строк)"
yield session
finally:
session.rollback()
session.close()
engine.dispose()
def _rows(db) -> list[tuple[str, str | None, str]]:
return [
(r[0], r[1], r[2])
for r in db.execute(
text("SELECT cad_num, act_number, doc_url FROM land_reservation ORDER BY 1, 2, 3")
).all()
]
def test_numbered_parse_replaces_numberless_row_of_same_document(db) -> None:
"""Головной: прод-состояние (номер NULL) + прогон с номером → одна строка с номером."""
_upsert_records(db, [_row(act=None)])
assert _rows(db) == [(КАД, None, ДОК)]
assert _upsert_records(db, [_row(act="1413")]) == 1
assert _rows(db) == [(КАД, "1413", ДОК)], "строка без номера осталась рядом с новой"
_upsert_records(db, [_row(act="1413")])
assert _rows(db) == [(КАД, "1413", ДОК)], "повторный прогон не идемпотентен"
def test_same_parcel_in_another_document_is_kept(db) -> None:
"""Контроль ширины: участок в ДРУГОМ документе — другое основание, его не трогаем."""
другой = "https://xn--80acgfbsl1azdqr.xn--p1ai/file/9b9d9a998f578db56315bb816fc2ebf5"
_upsert_records(db, [_row(act="259", url=другой)])
_upsert_records(db, [_row(act=None)])
_upsert_records(db, [_row(act="1413")])
assert _rows(db) == [(КАД, "1413", ДОК), (КАД, "259", другой)]
def test_other_parcels_of_same_document_are_kept(db) -> None:
"""Контроль: чистка идёт по участку, а не по всему документу."""
_upsert_records(db, [_row(cad="66:41:0303004:23", act=None)])
_upsert_records(db, [_row(act="1413")])
assert _rows(db) == [("66:41:0303004:22", "1413", ДОК), ("66:41:0303004:23", None, ДОК)]