fix(best-layouts): replace non-existent geom_3857 with on-the-fly geography (#113 PR C)

domrf_kn_objects has no geom_3857 column (only latitude/longitude NUMERIC).
Switch _COMPETITORS_IN_RADIUS_SQL and _SUPPLY_BATCH_SQL to use
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography — consistent
with competitors.py pattern. Rename centroid params x_3857/y_3857 →
center_lon/center_lat. Remove dead _round_to_5 function.

Fixes review-bot 🔴 BLOCK on fdc6456 PR #196.

Tests: 10/10 pass (mock keys updated к center_lon/center_lat), 43/43 regression OK.
This commit is contained in:
lekss361 2026-05-16 11:52:08 +03:00
parent fdc64569d2
commit a88917f0cf
2 changed files with 26 additions and 24 deletions

View file

@ -2,7 +2,7 @@
Источники: Источники:
cad_parcels_geom / cad_quarters_geom центроид участка cad_parcels_geom / cad_quarters_geom центроид участка
domrf_kn_objects ЖК в радиусе (geom_3857) domrf_kn_objects ЖК в радиусе (latitude/longitude geography)
mv_layout_velocity (obj_id, room_bucket) агрегат продаж 24 мес mv_layout_velocity (obj_id, room_bucket) агрегат продаж 24 мес
domrf_kn_flats supply count по (room_bucket, area_bin) domrf_kn_flats supply count по (room_bucket, area_bin)
@ -49,8 +49,8 @@ _VELOCITY_DIVISORS: dict[str, float] = {
# ── SQL: центроид участка ───────────────────────────────────────────────────── # ── SQL: центроид участка ─────────────────────────────────────────────────────
_PARCEL_CENTROID_SQL = text(""" _PARCEL_CENTROID_SQL = text("""
SELECT ST_X(ST_Transform(pt, 3857)) AS x_3857, SELECT ST_X(pt) AS center_lon,
ST_Y(ST_Transform(pt, 3857)) AS y_3857 ST_Y(pt) AS center_lat
FROM ( FROM (
SELECT ST_Centroid(geom) AS pt SELECT ST_Centroid(geom) AS pt
FROM cad_parcels_geom FROM cad_parcels_geom
@ -64,18 +64,22 @@ _PARCEL_CENTROID_SQL = text("""
""") """)
# ── SQL: obj_id конкурентов в радиусе ───────────────────────────────────────── # ── SQL: obj_id конкурентов в радиусе ─────────────────────────────────────────
# Примечание: геометрия domrf_kn_objects.geom_3857 уже в EPSG:3857. # Геометрия domrf_kn_objects вычисляется on-the-fly из (latitude, longitude)
# ST_DWithin на EPSG:3857 — метры (проекция, не geography). # как ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography
# (consistency с competitors.py).
# obj_class_filter: NULL = все классы. # obj_class_filter: NULL = все классы.
# filter_competitor_obj_ids: NULL = не фильтровать по списку. # filter_competitor_obj_ids: NULL = не фильтровать по списку.
_COMPETITORS_IN_RADIUS_SQL = text(""" _COMPETITORS_IN_RADIUS_SQL = text("""
SELECT DISTINCT ON (obj_id) obj_id SELECT DISTINCT ON (obj_id) obj_id
FROM domrf_kn_objects FROM domrf_kn_objects
WHERE geom_3857 IS NOT NULL WHERE latitude IS NOT NULL AND longitude IS NOT NULL
AND ST_DWithin( AND ST_DWithin(
geom_3857, ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint(CAST(:x_3857 AS float), CAST(:y_3857 AS float)), 3857), ST_SetSRID(
ST_MakePoint(CAST(:center_lon AS float), CAST(:center_lat AS float)),
4326
)::geography,
CAST(:radius_m AS float) CAST(:radius_m AS float)
) )
AND ( AND (
@ -126,10 +130,13 @@ _SUPPLY_BATCH_SQL = text("""
COUNT(*) AS units COUNT(*) AS units
FROM domrf_kn_flats f FROM domrf_kn_flats f
JOIN domrf_kn_objects o ON f.obj_id = o.obj_id JOIN domrf_kn_objects o ON f.obj_id = o.obj_id
WHERE o.geom_3857 IS NOT NULL WHERE o.latitude IS NOT NULL AND o.longitude IS NOT NULL
AND ST_DWithin( AND ST_DWithin(
o.geom_3857, ST_SetSRID(ST_MakePoint(o.longitude, o.latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint(CAST(:x_3857 AS float), CAST(:y_3857 AS float)), 3857), ST_SetSRID(
ST_MakePoint(CAST(:center_lon AS float), CAST(:center_lat AS float)),
4326
)::geography,
CAST(:radius_m AS float) CAST(:radius_m AS float)
) )
AND f.snapshot_date = (SELECT MAX(snapshot_date) FROM domrf_kn_flats) AND f.snapshot_date = (SELECT MAX(snapshot_date) FROM domrf_kn_flats)
@ -179,11 +186,6 @@ def _normalize_pct(buckets: dict[str, float]) -> dict[str, int]:
return floors return floors
def _round_to_5(pct: int) -> int:
"""Округлить до ближайших 5%."""
return int(round(pct / 5.0) * 5)
# ── Главная функция ─────────────────────────────────────────────────────────── # ── Главная функция ───────────────────────────────────────────────────────────
@ -217,8 +219,8 @@ def get_best_layouts(
if not coord_row: if not coord_row:
raise ValueError(f"Геометрия для {cad_num} не найдена") raise ValueError(f"Геометрия для {cad_num} не найдена")
x_3857 = float(coord_row["x_3857"]) center_lon = float(coord_row["center_lon"])
y_3857 = float(coord_row["y_3857"]) center_lat = float(coord_row["center_lat"])
# ── Step 2: obj_id конкурентов в радиусе ──────────────────────────────── # ── Step 2: obj_id конкурентов в радиусе ────────────────────────────────
try: try:
@ -226,8 +228,8 @@ def get_best_layouts(
db.execute( db.execute(
_COMPETITORS_IN_RADIUS_SQL, _COMPETITORS_IN_RADIUS_SQL,
{ {
"x_3857": x_3857, "center_lon": center_lon,
"y_3857": y_3857, "center_lat": center_lat,
"radius_m": radius_m, "radius_m": radius_m,
"obj_class_filter": request.obj_class_filter, "obj_class_filter": request.obj_class_filter,
}, },
@ -281,7 +283,7 @@ def get_best_layouts(
supply_rows = ( supply_rows = (
db.execute( db.execute(
_SUPPLY_BATCH_SQL, _SUPPLY_BATCH_SQL,
{"x_3857": x_3857, "y_3857": y_3857, "radius_m": radius_m}, {"center_lon": center_lon, "center_lat": center_lat, "radius_m": radius_m},
) )
.mappings() .mappings()
.all() .all()

View file

@ -26,10 +26,10 @@ CAD_NUM = "66:41:0303161:123"
_TODAY = dt.date.today() _TODAY = dt.date.today()
def _coord_row(x: float = 6747000.0, y: float = 7846000.0) -> MagicMock: def _coord_row(lon: float = 60.6, lat: float = 56.85) -> MagicMock:
"""Центроид участка в EPSG:3857.""" """Центроид участка (EPSG:4326 lon/lat)."""
r = MagicMock() r = MagicMock()
r.__getitem__ = lambda self, k: {"x_3857": x, "y_3857": y}[k] r.__getitem__ = lambda self, k: {"center_lon": lon, "center_lat": lat}[k]
return r return r