Merge pull request 'fix(sf-02): добавить site_status/ready_dt в /analyze competitors + sort строящиеся first' (#276) from fix/sf-02-competitors-status-analyze into main
This commit is contained in:
commit
7a27cb857f
2 changed files with 126 additions and 1 deletions
|
|
@ -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},
|
||||
|
|
|
|||
122
backend/tests/api/v1/test_analyze_competitors_status.py
Normal file
122
backend/tests/api/v1/test_analyze_competitors_status.py
Normal 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"] == "Сданные"
|
||||
Loading…
Add table
Reference in a new issue