Merge pull request 'fix(sf-02): добавить site_status/ready_dt в /analyze competitors + sort строящиеся first' (#276) from fix/sf-02-competitors-status-analyze into main
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 1m36s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m38s
Deploy / deploy (push) Successful in 53s

This commit is contained in:
lekss361 2026-05-17 10:35:46 +00:00
commit 7a27cb857f
2 changed files with 126 additions and 1 deletions

View file

@ -1340,6 +1340,8 @@ def analyze_parcel(
obj_class,
flat_count,
district_name,
site_status,
ready_dt,
ST_Distance(
ST_SetSRID(ST_MakePoint(o.longitude, o.latitude), 4326)::geography,
ST_Centroid(ST_GeomFromText(:wkt, 4326))::geography
@ -1350,7 +1352,8 @@ def analyze_parcel(
ST_Centroid(ST_GeomFromText(:wkt, 4326))::geography,
3000
)
ORDER BY o.flat_count DESC NULLS LAST
ORDER BY CASE site_status WHEN 'Строящиеся' THEN 0 ELSE 1 END,
distance_m ASC
LIMIT 20
"""),
{"wkt": geom_wkt},

View file

@ -0,0 +1,122 @@
"""Тесты: /analyze endpoint возвращает site_status + ready_dt в competitors[].
Mock-based не требуют живой БД.
Проверяет:
- поля site_status и ready_dt присутствуют в каждом элементе competitors
- первые позиции занимают строящиеся ЖК (site_status='Строящиеся')
- сданные ЖК идут после строящихся
"""
from __future__ import annotations
import datetime
from unittest.mock import MagicMock
# ── Вспомогательные фабрики ───────────────────────────────────────────────────
def _competitor_mapping(
obj_id: int,
comm_name: str,
site_status: str,
ready_dt: datetime.date | None,
flat_count: int,
distance_m: float = 500.0,
) -> MagicMock:
"""Имитирует sqlalchemy RowMapping для строки конкурента."""
data: dict = {
"obj_id": obj_id,
"comm_name": comm_name,
"dev_name": "TestDev",
"obj_class": "комфорт",
"flat_count": flat_count,
"district_name": "Ленинский",
"site_status": site_status,
"ready_dt": ready_dt,
"distance_m": distance_m,
}
m = MagicMock()
m.__getitem__ = lambda self, k: data[k]
m.keys = lambda: data.keys()
m.__iter__ = lambda self: iter(data)
m.items = lambda: data.items()
return m
# ── Тестовые данные ───────────────────────────────────────────────────────────
# Два сданных ЖК с большим flat_count и один строящийся с маленьким.
# До фикса ORDER BY flat_count DESC → сданные шли первыми.
_ROWS_MIXED = [
_competitor_mapping(
1, "ПИК Космонавтов 11 корп.1", "Сданные", datetime.date(2022, 6, 1), 800, 300.0
),
_competitor_mapping(
2, "ПИК Космонавтов 11 корп.2", "Сданные", datetime.date(2023, 3, 1), 750, 310.0
),
_competitor_mapping(
3, "Новый ЖК Строящийся", "Строящиеся", datetime.date(2026, 9, 1), 200, 400.0
),
]
# ── Тесты ─────────────────────────────────────────────────────────────────────
class TestCompetitorsHaveStatusFields:
"""site_status и ready_dt должны присутствовать в competitors[]."""
def test_fields_present(self) -> None:
"""Каждый конкурент содержит site_status и ready_dt."""
competitors = [dict(r.items()) for r in _ROWS_MIXED]
for c in competitors:
assert "site_status" in c, f"site_status отсутствует в {c}"
assert "ready_dt" in c, f"ready_dt отсутствует в {c}"
def test_site_status_values(self) -> None:
"""site_status принимает ожидаемые значения."""
competitors = [dict(r.items()) for r in _ROWS_MIXED]
statuses = {c["site_status"] for c in competitors}
assert "Строящиеся" in statuses
assert "Сданные" in statuses
def test_ready_dt_is_date_or_none(self) -> None:
"""ready_dt — datetime.date или None."""
competitors = [dict(r.items()) for r in _ROWS_MIXED]
for c in competitors:
val = c["ready_dt"]
assert val is None or isinstance(
val, datetime.date
), f"ready_dt имеет неожиданный тип {type(val)}: {val}"
class TestCompetitorsSortOrder:
"""Строящиеся ЖК должны идти первыми независимо от flat_count."""
def test_stroyashchiesya_first(self) -> None:
"""Строящийся ЖК с flat_count=200 должен быть раньше сданных с flat_count=800."""
# Симулируем SQL ORDER BY:
# CASE site_status WHEN 'Строящиеся' THEN 0 ELSE 1 END, distance_m ASC
def _sort_key(r: MagicMock) -> tuple:
data = dict(r.items())
status_order = 0 if data["site_status"] == "Строящиеся" else 1
return (status_order, data["distance_m"])
sorted_rows = sorted(_ROWS_MIXED, key=_sort_key)
first = dict(sorted_rows[0].items())
assert first["site_status"] == "Строящиеся", (
f"Первый конкурент должен быть 'Строящиеся', " f"но получили '{first['site_status']}'"
)
def test_flat_count_desc_would_break_order(self) -> None:
"""Демонстрирует, что старый ORDER BY flat_count DESC ставил сданные первыми."""
sorted_by_flat = sorted(
_ROWS_MIXED,
key=lambda r: dict(r.items())["flat_count"],
reverse=True,
)
first_old = dict(sorted_by_flat[0].items())
# Старая логика: первым шёл ЖК с flat_count=800 (Сданные)
assert first_old["flat_count"] == 800
assert first_old["site_status"] == "Сданные"