From 39094ba644883b85fbae63c5f982ca18ee1a9ca7 Mon Sep 17 00:00:00 2001 From: Light1YT Date: Sat, 13 Jun 2026 10:38:31 +0500 Subject: [PATCH] =?UTF-8?q?fix(supply=5Flayers):=20L3=20future-supply=20?= =?UTF-8?q?=D0=B1=D0=B5=D1=80=D1=91=D1=82=20=D0=9F=D0=9E=D0=A1=D0=9B=D0=95?= =?UTF-8?q?=D0=94=D0=9D=D0=98=D0=99=20=D1=81=D0=BD=D0=B0=D0=BF=D1=88=D0=BE?= =?UTF-8?q?=D1=82=20=E2=86=92=20=D0=BF=D0=BE=D1=82=D0=BE=D0=BC=20=D1=84?= =?UTF-8?q?=D0=B8=D0=BB=D1=8C=D1=82=D1=80=20(#1212)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _L3_FUTURE_SQL применял volatile-фильтры (ready_dt > horizon, free_flats <= threshold) в WHERE ВНУТРИ CTE с DISTINCT ON (obj_id) ORDER BY snapshot_date DESC. Это значит фильтр применялся ДО DISTINCT ON → бралась «последний снапшот, ПРОШЕДШИЙ фильтр», а не последний в принципе. Эффект на проде: объект, когда-то бывший «объявлен, не продаётся» (free_flats=0/NULL), остаётся в L3 future-supply даже после открытия продаж — свежий снапшот с free_flats=180 отфильтрован, взят старый с free_flats=0. Двойной счёт с L1/L2 (open + future одного объекта), stale flat_count/ready_dt в supply_layers и форсайте. Patch: разделил фильтры на стабильные (в CTE: region_cd, district_name) и volatile (во внешнем WHERE после DISTINCT ON: ready_dt, free_flats). Зеркало паттерна L2-CTE (там volatile уже снаружи). Семантика теперь матчит docstring: «свежий снапшот, затем фильтр». 62/62 supply_layers тестов зелёные. ruff clean. SQL psycopg v3 (CAST(:x AS interval)) уже корректен. Closes #1212 --- backend/app/services/site_finder/supply_layers.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/app/services/site_finder/supply_layers.py b/backend/app/services/site_finder/supply_layers.py index ab54dee5..f045dd58 100644 --- a/backend/app/services/site_finder/supply_layers.py +++ b/backend/app/services/site_finder/supply_layers.py @@ -252,6 +252,15 @@ _L2_HIDDEN_SQL = text( _L3_FUTURE_SQL = text( """ WITH latest AS ( + -- #1212: сначала ВСЕГДА берём последний снапшот на obj_id + -- (DISTINCT ON + ORDER BY snapshot_date DESC). Внутри CTE — только + -- стабильные фильтры (region_cd / админ-район). Volatile-фильтры + -- (ready_dt, free_flats) применяются ПОСЛЕ DISTINCT ON во внешнем + -- WHERE. Иначе WHERE применяется ДО DISTINCT ON и берётся «последний + -- снапшот, прошедший фильтр»: проект, когда-то бывший «объявлен, + -- не продаётся», остаётся в L3 и после открытия продаж (свежий + -- снапшот с большим free_flats отфильтрован, взят старый) → + -- двойной счёт с L1/L2, stale flat_count/ready_dt. SELECT DISTINCT ON (o.obj_id) o.obj_id, o.district_name, @@ -264,9 +273,6 @@ _L3_FUTURE_SQL = text( FROM domrf_kn_objects o WHERE o.region_cd = :region_cd AND o.district_name IS NOT NULL - AND o.ready_dt IS NOT NULL - AND o.ready_dt > CURRENT_DATE + CAST(:horizon AS interval) - AND COALESCE(o.free_flats, 0) <= :max_free_flats AND ( CAST(:district AS text) IS NULL OR o.district_name = CAST(:district AS text) @@ -281,6 +287,9 @@ _L3_FUTURE_SQL = text( COALESCE(SUM(square_living), 0) AS area_future, MIN(ready_dt) AS expected_online_date FROM latest + WHERE ready_dt IS NOT NULL + AND ready_dt > CURRENT_DATE + CAST(:horizon AS interval) + AND COALESCE(free_flats, 0) <= :max_free_flats GROUP BY district_name, dev_group_name """ )