fix(supply_layers): L3 future-supply берёт ПОСЛЕДНИЙ снапшот → потом фильтр (#1212)
Some checks failed
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 10s
Deploy / build-worker (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled

_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
This commit is contained in:
Light1YT 2026-06-13 10:38:31 +05:00 committed by bot-backend
parent c75e6a4cfa
commit d587b9e199

View file

@ -252,6 +252,15 @@ _L2_HIDDEN_SQL = text(
_L3_FUTURE_SQL = text( _L3_FUTURE_SQL = text(
""" """
WITH latest AS ( 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) SELECT DISTINCT ON (o.obj_id)
o.obj_id, o.obj_id,
o.district_name, o.district_name,
@ -264,9 +273,6 @@ _L3_FUTURE_SQL = text(
FROM domrf_kn_objects o FROM domrf_kn_objects o
WHERE o.region_cd = :region_cd WHERE o.region_cd = :region_cd
AND o.district_name IS NOT NULL 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 ( AND (
CAST(:district AS text) IS NULL CAST(:district AS text) IS NULL
OR o.district_name = CAST(:district AS text) OR o.district_name = CAST(:district AS text)
@ -281,6 +287,9 @@ _L3_FUTURE_SQL = text(
COALESCE(SUM(square_living), 0) AS area_future, COALESCE(SUM(square_living), 0) AS area_future,
MIN(ready_dt) AS expected_online_date MIN(ready_dt) AS expected_online_date
FROM latest 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 GROUP BY district_name, dev_group_name
""" """
) )