feat(tradein/estimator): регион-скоуп ДКП-коридора — фильтр по deals.region_code (#3051 PR-A) #3423
2 changed files with 42 additions and 1 deletions
|
|
@ -50,6 +50,7 @@ from app.schemas.trade_in import (
|
|||
TradeInEstimateInput,
|
||||
)
|
||||
from app.services import account_quota
|
||||
from app.services import regions as regions_mod
|
||||
from app.services.exporters.trade_in_pdf import generate_trade_in_pdf
|
||||
from app.services.image_sanitizer import ImageSanitizationError, sanitize_image
|
||||
from app.services.user_events import schedule_event
|
||||
|
|
@ -763,8 +764,22 @@ def load_estimate(
|
|||
# когда raw payload.address начнёт персиститься — follow-up). Для ЕКБ ок; reorder
|
||||
# ниже безвреден (оба source city-stripped для не-ЕКБ, оба екб для ЕКБ).
|
||||
target_city = _resolve_target_city(row.address) or _resolve_target_city(row.canonical_address)
|
||||
# #3051 PR-A: тот же region_code-скоуп, что и POST /estimate — гарантирует
|
||||
# «регион не резолвится → DEFAULT_REGION_CODE (66)», не NULL (NULL в SQL
|
||||
# обнулил бы фильтр). row.lat/row.lon персистятся с estimate-time.
|
||||
target_region = (
|
||||
regions_mod.region_for_point(row.lat, row.lon)
|
||||
if row.lat is not None and row.lon is not None
|
||||
else None
|
||||
)
|
||||
target_region_code = target_region.code if target_region else regions_mod.DEFAULT_REGION_CODE
|
||||
dkp_raw = _fetch_dkp_corridor(
|
||||
db, address=row.address, rooms=row.rooms, area=area_f, city=target_city
|
||||
db,
|
||||
address=row.address,
|
||||
rooms=row.rooms,
|
||||
area=area_f,
|
||||
city=target_city,
|
||||
region_code=target_region_code,
|
||||
)
|
||||
dkp_corridor = DkpCorridor(**dkp_raw) if dkp_raw else None
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ from app.schemas.trade_in import (
|
|||
PriceTrendPoint,
|
||||
TradeInEstimateInput,
|
||||
)
|
||||
from app.services import regions as regions_mod
|
||||
from app.services.dadata import DadataAddressResult
|
||||
|
||||
# enrich_address = /clean с graceful fallback на /suggest (#dadata-403: на проде услуга
|
||||
|
|
@ -1832,6 +1833,7 @@ def _fetch_dkp_corridor(
|
|||
rooms: int | None,
|
||||
area: float | None,
|
||||
city: str | None = None,
|
||||
region_code: int = regions_mod.DEFAULT_REGION_CODE,
|
||||
period_months: int = DEALS_PERIOD_MONTHS,
|
||||
area_tolerance: float = AREA_TOLERANCE,
|
||||
) -> dict[str, Any] | None:
|
||||
|
|
@ -1851,6 +1853,16 @@ def _fetch_dkp_corridor(
|
|||
Тагиле/Лесном/Краснотурьинске/Серове/ЕКБ/Первоуральске) — коридор превращался
|
||||
в кросс-областной мусор для не-ЕКБ таргетов. None (город не определился) →
|
||||
фильтр не применяется, прежнее (pre-oblast) поведение сохраняется как есть.
|
||||
|
||||
region_code: (#3051 «Москва», PR-A) — регион целевого запроса, ОБЯЗАТЕЛЬНЫЙ
|
||||
фильтр (не advisory, в отличие от city) в обоих ДКП-запросах. `city`
|
||||
распознаётся только для Свердловской области (_resolve_target_city матчит
|
||||
исключительно SVERDLOVSK_OBLAST_CITIES) — для Москвы/любого нового региона
|
||||
city=None и street-ILIKE фильтр остаётся ЕДИНСТВЕННЫМ скоупом сделки; без
|
||||
region_code одноимённая улица чужого региона (или сделка без узнанного
|
||||
города вовсе) утекает в коридор через тот же street ILIKE. Дефолт —
|
||||
regions_mod.DEFAULT_REGION_CODE (66) — байт-в-байт прежнее поведение для
|
||||
всех сегодняшних вызовов (все сделки в БД сейчас region_code=66).
|
||||
"""
|
||||
if not address or rooms is None or not area:
|
||||
return None
|
||||
|
|
@ -1875,6 +1887,7 @@ def _fetch_dkp_corridor(
|
|||
FROM deals d
|
||||
LEFT JOIN deal_city_price_bands b ON b.city = d.city
|
||||
WHERE d.source = 'rosreestr'
|
||||
AND d.region_code = CAST(:region_code AS int)
|
||||
AND d.address ILIKE :street_pattern
|
||||
AND d.address ~* :street_regex
|
||||
AND d.rooms = CAST(:rooms AS integer)
|
||||
|
|
@ -1902,6 +1915,7 @@ def _fetch_dkp_corridor(
|
|||
"ppm_min": DEAL_MIN_PPM2,
|
||||
"ppm_max": DEAL_MAX_PPM2,
|
||||
"target_city": city.lower() if city else None,
|
||||
"region_code": region_code,
|
||||
},
|
||||
)
|
||||
.mappings()
|
||||
|
|
@ -1956,6 +1970,7 @@ def _fetch_dkp_corridor(
|
|||
FROM deals d
|
||||
LEFT JOIN deal_city_price_bands b ON b.city = d.city
|
||||
WHERE d.source = 'rosreestr'
|
||||
AND d.region_code = CAST(:region_code AS int)
|
||||
AND d.city IS NOT NULL
|
||||
AND LOWER(d.city) = CAST(:target_city AS text)
|
||||
AND d.rooms = CAST(:rooms AS integer)
|
||||
|
|
@ -1975,6 +1990,7 @@ def _fetch_dkp_corridor(
|
|||
"period_months": period_months,
|
||||
"ppm_min": DEAL_MIN_PPM2,
|
||||
"ppm_max": DEAL_MAX_PPM2,
|
||||
"region_code": region_code,
|
||||
},
|
||||
)
|
||||
.mappings()
|
||||
|
|
@ -4555,6 +4571,15 @@ async def estimate_quality(
|
|||
target_city = _resolve_target_city(payload.address) or _resolve_target_city(
|
||||
(dadata.canonical_address if dadata else None) or geo.full_address
|
||||
)
|
||||
# #3051 PR-A: региональный скоуп ДКП-коридора — target_city выше распознаёт
|
||||
# ТОЛЬКО города Свердловской области (_resolve_target_city матчит
|
||||
# SVERDLOVSK_OBLAST_CITIES), т.е. для Москвы/любого нового региона city=None
|
||||
# и без region_code street ILIKE ловил бы одноимённую улицу ЧУЖОГО региона.
|
||||
# region_for_point(None) (точка вне охвата ни одного региона) → дефолт 66,
|
||||
# НЕ None — NULL в SQL-параметре обнулил бы фильтр (`= NULL` не матчит
|
||||
# ничего), т.е. держим гарантию «не резолвится → 66», не пусто.
|
||||
target_region = regions_mod.region_for_point(geo.lat, geo.lon)
|
||||
target_region_code = target_region.code if target_region else regions_mod.DEFAULT_REGION_CODE
|
||||
dkp_raw = await asyncio.to_thread(
|
||||
_fetch_dkp_corridor,
|
||||
db,
|
||||
|
|
@ -4562,6 +4587,7 @@ async def estimate_quality(
|
|||
rooms=payload.rooms,
|
||||
area=payload.area_m2,
|
||||
city=target_city,
|
||||
region_code=target_region_code,
|
||||
)
|
||||
|
||||
# ── Stage 3: Avito IMV evaluation as 5-th source (on-demand cached) ──
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue