fix(tradein/ingest): раз-хардкод rosreestr DKP импорта на всю обл. 66
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 7s
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 51s

- scheduler.py import_rosreestr_dkp: снят фильтр city ILIKE '%катеринбург%', address из реального city источника, deals.region_code + новая deals.city заполняются (было: хардкод 'Екатеринбург,' + region_code NULL)
- migration 177: deals.city + индекс + бэкфилл существующих EKB-строк region_code=66/city
- guard city IS NOT NULL → address не NULL для ~15 null-city строк источника
- sync deploy/import-rosreestr.sh (ops-fallback) под тот же oblast-scope
- split dedup-теста: 077 (историческая) хранит EKB-фильтр, живой импорт — нет

Открывает +47183 не-ЕКБ сделок region 66, уже сидящих в источнике, ранее резавшихся на импорте.
This commit is contained in:
bot-backend 2026-07-12 18:11:47 +03:00
parent 4f205bd7da
commit 752cb1044b
4 changed files with 103 additions and 14 deletions

View file

@ -180,9 +180,16 @@ def import_rosreestr_dkp(
SERVER gendesign_remote настроен в 060_postgres_fdw_extension.sql.
USER MAPPING создаётся при startup в core/fdw.py (tradein_fdw_reader).
Область покрытия: вся Свердловская область (region_code=66), не только Екатеринбург
прежний ILIKE-фильтр по подстроке города (ограничивавший импорт одним Екатеринбургом)
снят (Mera trade-in расширяется на весь регион, unlocks +47183 сделок вне ЕКБ уже
сидящих в source foreign table). address и deals.city строятся из реального city
источника (не хардкод "Екатеринбург"), deals.region_code заполняется из строки
источника (= 66 при текущем фильтре).
Фильтры (совпадают с import-rosreestr.sh + Fix_Rosreestr_Dkp_Filter_May24):
- region_code = 66 (Свердловская область)
- city ILIKE '%катеринбург%'
- region_code = 66 (вся Свердловская область, все города)
- city IS NOT NULL AND trim(city) != '' (непустой город корректный address)
- realestate_type_code = '002001003000' (квартира)
- area BETWEEN 18 AND 200
- deal_price BETWEEN 1000000 AND 100000000
@ -273,7 +280,9 @@ def import_rosreestr_dkp(
id,
id AS source_id_src,
'ros:dkp:' || CAST(id AS text) AS dedup_hash,
'Екатеринбург, ' || trim(street) AS address,
trim(city) || ', ' || trim(street) AS address,
region_code,
trim(city) AS city,
CASE
WHEN area < 30 THEN 0
WHEN area < 44 THEN 1
@ -296,7 +305,7 @@ def import_rosreestr_dkp(
period_start_date AS deal_date
FROM gendesign_rosreestr_deals
WHERE region_code = 66
AND city ILIKE '%катеринбург%'
AND city IS NOT NULL AND trim(city) <> ''
AND realestate_type_code = '002001003000'
AND area BETWEEN 18 AND 200
AND deal_price BETWEEN 1000000 AND 100000000
@ -331,14 +340,17 @@ def import_rosreestr_dkp(
inserted = db.execute(
text("""
INSERT INTO deals (
source, dedup_hash, source_id, address, rooms, area_m2,
floor, year_built, price_rub, price_per_m2, deal_date
source, dedup_hash, source_id, address, region_code, city,
rooms, area_m2, floor, year_built, price_rub, price_per_m2,
deal_date
)
VALUES (
'rosreestr',
CAST(:dedup_hash AS text),
CAST(:source_id AS text),
CAST(:address AS text),
CAST(:region_code AS int),
CAST(:city AS text),
CAST(:rooms AS int),
CAST(:area_m2 AS numeric),
CAST(:floor_num AS int),
@ -354,6 +366,8 @@ def import_rosreestr_dkp(
"dedup_hash": row["dedup_hash"],
"source_id": str(row["source_id_src"]),
"address": row["address"],
"region_code": row["region_code"],
"city": row["city"],
"rooms": row["rooms"],
"area_m2": row["area_m2"],
"floor_num": row["floor_num"],

View file

@ -0,0 +1,51 @@
-- 177_deals_city_region.sql
-- deals.city + region_code backfill — расширение Mera trade-in estimator с
-- Екатеринбурга на ВСЮ Свердловскую область (region_code=66, все города).
--
-- WHY:
-- Ночной import_rosreestr_dkp (scheduler.py, source='rosreestr_dkp_import')
-- до сих пор фильтровал `city ILIKE '%катеринбург%'` и хардкодил
-- 'Екатеринбург, ' в address, а deals.region_code никогда не заполнялся
-- (NULL на всех 49791 текущих строках). Foreign table
-- gendesign_rosreestr_deals уже отдаёт чистые city + region_code из
-- source-таблицы (migration 072) — снятие фильтра открывает +47183
-- не-ЕКБ сделок, уже сидящих в источнике.
--
-- WHAT:
-- - deals.region_code уже существует (002_core_tables.sql), но deals.city
-- отсутствовал (было только текстовое поле address с хардкод-префиксом
-- 'Екатеринбург, '). Добавляем city text — импортёр (scheduler.py,
-- тот же PR) теперь заполняет его из реального source.city.
-- - Индекс (city, deal_date) — под будущие per-city аналитические запросы
-- (аналог deals_source_idx для (source, deal_date)).
-- - Backfill существующих 49791 строк: все текущие deals с source='rosreestr'
-- импортированы ДО этой миграции старым EKB-only кодом, т.е. фактически
-- все они — Екатеринбург (то же допущение, что зашито в address
-- 'Екатеринбург, ' + street). Простановка region_code=66, city='Екатеринбург'
-- задним числом делает старые строки self-describing наравне с новыми.
--
-- SCOPE: только schema (city column + index) + backfill старых rosreestr-строк.
-- Новые импорты (после деплоя scheduler.py) заполняют оба поля сами.
--
-- IDEMPOTENCY / SAFETY:
-- - ADD COLUMN IF NOT EXISTS / CREATE INDEX IF NOT EXISTS — безопасный re-run.
-- - Backfill UPDATE ограничен `region_code IS NULL OR city IS NULL` —
-- повторный прогон no-op (после первого раза условие уже false).
--
-- Dependencies: 002_core_tables.sql (deals, deals.region_code)
BEGIN;
ALTER TABLE deals ADD COLUMN IF NOT EXISTS city text;
CREATE INDEX IF NOT EXISTS deals_city_deal_date_idx ON deals (city, deal_date);
-- Backfill: все существующие rosreestr-сделки импортированы старым EKB-only
-- кодом (до этой миграции) => гарантированно Екатеринбург, region_code=66.
UPDATE deals
SET region_code = 66,
city = 'Екатеринбург'
WHERE source = 'rosreestr'
AND (region_code IS NULL OR city IS NULL);
COMMIT;

View file

@ -7,6 +7,7 @@
Чисто-юнит: ассертим SQL-текст, эмитируемый import_rosreestr_dkp (через inspect.getsource),
плюс содержимое backfill-миграции 077 без живого FDW / DB-фикстуры.
"""
import inspect
import os
from pathlib import Path
@ -89,12 +90,18 @@ def test_migration_077_converts_md5_to_plain_key() -> None:
assert "BEGIN;" in sql and "COMMIT;" in sql
def test_migration_077_filter_matches_live_import() -> None:
"""Фильтр src CTE байт-в-байт совпадает с живым импортом → все строки сконвертируются."""
def test_migration_077_shared_filter_matches_live_import() -> None:
"""Дедуп-релевантные фильтры src CTE миграции 077 совпадают с живым импортом.
Исключение city ILIKE (см. test_live_import_dropped_ekb_city_filter ниже):
077 backfill'ил ЕКБ-строки под EKB-only scope того времени; живой импорт расширен
на всю Свердловскую область (region_code=66, все города), поэтому city-фильтр из
живого импорта СНЯТ намеренно. Остальные клозы обязаны совпадать байт-в-байт,
иначе 077 конвертировал бы не тот набор строк.
"""
sql = _MIGRATION_077.read_text("utf-8")
for clause in (
"region_code = 66",
"city ILIKE '%катеринбург%'",
"realestate_type_code = '002001003000'",
"area BETWEEN 18 AND 200",
"deal_price BETWEEN 1000000 AND 100000000",
@ -103,3 +110,17 @@ def test_migration_077_filter_matches_live_import() -> None:
):
assert clause in sql, f"missing filter clause in migration: {clause!r}"
assert clause in _IMPORT_SRC, f"missing filter clause in import: {clause!r}"
def test_live_import_dropped_ekb_city_filter() -> None:
"""Живой импорт БОЛЬШЕ не фильтрует по городу — Mera расширена на всю обл. 66.
Историческая миграция 077 (уже применена на прод, трогать нельзя) сохраняет свой
EKB-only city-фильтр; живой импорт нет. Guard против случайного возврата
ЕКБ-скоупа, который снова отрезал бы +47k не-ЕКБ сделок.
"""
sql = _MIGRATION_077.read_text("utf-8")
# 077 — исторический EKB-scope, city-фильтр там остаётся неизменным.
assert "city ILIKE '%катеринбург%'" in sql
# Живой импорт city-фильтр снял намеренно (region_code=66 = вся область).
assert "ILIKE '%катеринбург%'" not in _IMPORT_SRC

View file

@ -27,7 +27,8 @@ docker exec "$DST_PG" psql -U tradein -d tradein -v ON_ERROR_STOP=on -c "
-- запуск упал между CREATE и финальным DROP под старым кодом (без source_id).
DROP TABLE IF EXISTS deals_ros_staging;
CREATE TABLE deals_ros_staging (
dedup_hash text PRIMARY KEY, source_id text, address text, rooms int, area_m2 numeric,
dedup_hash text PRIMARY KEY, source_id text, address text, region_code int, city text,
rooms int, area_m2 numeric,
floor int, year_built int, price_rub bigint, price_per_m2 int, deal_date date
);
"
@ -38,7 +39,9 @@ docker exec "$SRC_PG" psql -U "$SRC_USER" -d "$SRC_DB" -v ON_ERROR_STOP=on -c "
SELECT
'ros:dkp:' || id::text AS dedup_hash,
id::text AS source_id,
'Екатеринбург, ' || trim(street) AS address,
trim(city) || ', ' || trim(street) AS address,
region_code AS region_code,
trim(city) AS city,
CASE
WHEN area < 30 THEN 0 WHEN area < 44 THEN 1
WHEN area < 62 THEN 2 WHEN area < 85 THEN 3
@ -51,7 +54,7 @@ docker exec "$SRC_PG" psql -U "$SRC_USER" -d "$SRC_DB" -v ON_ERROR_STOP=on -c "
period_start_date AS deal_date
FROM rosreestr_deals
WHERE region_code = 66
AND city ILIKE '%катеринбург%'
AND city IS NOT NULL AND trim(city) <> ''
AND realestate_type_code = '002001003000'
AND area BETWEEN 18 AND 200
AND deal_price BETWEEN 1000000 AND 100000000
@ -71,10 +74,10 @@ docker exec "$DST_PG" psql -U tradein -d tradein -v ON_ERROR_STOP=on -c "
DELETE FROM deals WHERE address = 'Екатеринбург, реальная сделка';
INSERT INTO deals (
source, dedup_hash, source_id, address, rooms, area_m2, floor,
source, dedup_hash, source_id, address, region_code, city, rooms, area_m2, floor,
year_built, price_rub, price_per_m2, deal_date
)
SELECT 'rosreestr', dedup_hash, source_id, address, rooms, area_m2, floor,
SELECT 'rosreestr', dedup_hash, source_id, address, region_code, city, rooms, area_m2, floor,
year_built, price_rub, price_per_m2, deal_date
FROM deals_ros_staging
ON CONFLICT (dedup_hash) DO NOTHING;