fix(tradein): run same-building anchor without radius analogs (#691) (#704)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 42s
Deploy Trade-In / test (push) Successful in 22s
Deploy Trade-In / deploy (push) Successful in 34s

Co-authored-by: bot-backend <bot-backend@gendsgn.local>
Co-committed-by: bot-backend <bot-backend@gendsgn.local>
This commit is contained in:
bot-backend 2026-05-30 08:34:24 +00:00 committed by bot-reviewer
parent 10626c21ae
commit f04728297b
2 changed files with 42 additions and 4 deletions

View file

@ -1645,11 +1645,18 @@ async def estimate_quality(
# больше не размывается массовой застройкой). За флагом; OFF ⇒ точно старое
# поведение. listing_segment для Tier C берём из самого частого среди аналогов.
anchor_tier: str | None = None
# #691: гейт НЕ требует радиусных аналогов (listings_clean) / median_price>0.
# На проде геокод часто = None → ST_DWithin не находит радиусные комплы →
# median=0, и same-building якорь скипался, ХОТЯ комплы того же дома есть
# (_fetch_anchor_comps резолвит дом по payload.address без гео — Ткачёва 13,
# Сакко 99). Запускаем по resolved-таргету (area + raw-адрес/geo); если комплов
# реально нет — _compute_same_building_anchor вернёт None и мутации не будет
# (поведение идентично старому). Новый гейт ⊇ старого: при непустом radius
# адрес всегда есть, так что ранее-якорившиеся кейсы якорятся по-прежнему.
if (
settings.estimate_same_building_anchor_enabled
and listings_clean
and median_price > 0
and payload.area_m2
and (payload.address or (geo is not None and geo.full_address))
):
seg_counts: dict[str, int] = {}
for lot in listings_clean:

View file

@ -377,13 +377,20 @@ def _run_estimate(
flag_enabled: bool = True,
ratio_tuple: tuple[float | None, str | None] = (0.92, "per_rooms"),
payload=None,
radius_analogs: list[dict[str, Any]] | None = None,
):
"""estimate_quality со всеми I/O застабленными; _fetch_anchor_comps форсирован."""
"""estimate_quality со всеми I/O застабленными; _fetch_anchor_comps форсирован.
radius_analogs=[] моделирует прод-сценарий #691: геокод дал lat/lon, но
ST_DWithin не нашёл радиусных аналогов (median=0) якорь обязан сработать
по same-building комплам независимо.
"""
from app.core.config import settings
from app.services.estimator import estimate_quality
db = MagicMock()
pl = payload or _make_payload()
radius = list(_RADIUS_ANALOGS if radius_analogs is None else radius_analogs)
async def _run():
with (
@ -394,7 +401,7 @@ def _run_estimate(
patch("app.services.estimator.get_house_metadata", new=AsyncMock(return_value=None)),
patch(
"app.services.estimator._fetch_analogs",
return_value=(list(_RADIUS_ANALOGS), False, "W"),
return_value=(radius, False, "W"),
),
patch("app.services.estimator._fetch_deals", return_value=[]),
patch("app.services.estimator._fetch_dkp_corridor", return_value=None),
@ -434,6 +441,30 @@ def test_estimate_premium_lift_real_in_range() -> None:
assert est.range_high_rub >= int(real * 0.9)
def test_estimate_anchor_fires_without_radius_analogs() -> None:
"""#691: радиусных аналогов НЕТ (median=0, прод-кейс провала ST_DWithin), но
same-building комплы есть якорь обязан сработать и дать median>0.
До фикса гейт требовал listings_clean+median>0 якорь скипался."""
est = _run_estimate(
anchor_comps=_SB_COMPS_PREMIUM,
anchor_tier="A",
radius_analogs=[],
)
# Якорь дал ненулевой headline несмотря на пустой радиус.
assert est.median_price_rub > 0
assert est.median_price_per_m2 >= 450_000
# range валиден (low ≤ point ≤ high), не вырожден в 0.
assert 0 < est.range_low_rub <= est.median_price_rub <= est.range_high_rub
def test_estimate_no_anchor_no_radius_stays_insufficient() -> None:
"""#691 контроль: ни радиуса, ни same-building комплов → median остаётся 0
(insufficient), фикс НЕ фабрикует число из воздуха."""
est = _run_estimate(anchor_comps=[], anchor_tier=None, radius_analogs=[])
assert est.median_price_rub == 0
assert est.median_price_per_m2 == 0
def test_estimate_economy_no_regression() -> None:
"""(b) Эконом-комплы ~112k → guardrail не раздувает, headline ≈ комплов."""
eco_comps = [