fix(tradein/pdf): scale offer-page rent by room count (LOW audit R2 #5) (#2506)
Some checks failed
Deploy Trade-In / test (push) Blocked by required conditions
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / build-frontend (push) Blocked by required conditions
Deploy Trade-In / build-browser (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Has been cancelled

This commit is contained in:
bot-backend 2026-07-12 22:11:04 +00:00
parent 7fa52bb3ad
commit 92371e7d9e
2 changed files with 63 additions and 5 deletions

View file

@ -1600,7 +1600,7 @@ def _build_deals_page(estimate: AggregatedEstimate, input_snapshot: dict, brand)
# ── Page 4: Offer (Trade-In vs Самопродажа) ──────────────────────────────────
def _build_offer_page(estimate: AggregatedEstimate, brand) -> str: # type: ignore[no-untyped-def]
def _build_offer_page(estimate: AggregatedEstimate, input_snapshot: dict, brand) -> str: # type: ignore[no-untyped-def,type-arg]
median = estimate.median_price_rub
# Расчёт расходов: часть — на основе медианы (торг «от цены в объявлении»),
@ -1627,8 +1627,24 @@ def _build_offer_page(estimate: AggregatedEstimate, brand) -> str: # type: igno
rieltor_low = int(sold_price * rieltor_pct_low / 100)
rieltor_high = int(sold_price * rieltor_pct_high / 100)
# 3 месяца аренды 1-комнатной квартиры, ЕКБ 2026: ~28-45 тыс. руб./мес.
rent_low, rent_high = 84_000, 135_000
# 3 месяца аренды квартиры, ЕКБ 2026 (базовая ставка — студия/1-комн., ~28-45
# тыс. руб./мес.), масштабируется по числу комнат объекта (#pdf-honesty: раньше
# был плоский 1-комнатный тариф для ЛЮБОГО объекта, включая 3-4-комнатные).
# Ориентировочные месячные ставки аренды по комнатности, ЕКБ 2026:
# студия/1к ~28-45k, 2к ~40-60k, 3к ~55-80k, 4к+ ~75-110k.
# Множители — грубое отношение верхних границ этих диапазонов к базовой (1к).
rooms = int(input_snapshot.get("rooms", 0) or 0)
if rooms <= 1:
rent_multiplier = 1.0
elif rooms == 2:
rent_multiplier = 1.35
elif rooms == 3:
rent_multiplier = 1.8
else:
rent_multiplier = 2.4
rent_base_low, rent_base_high = 84_000, 135_000
rent_low = round(rent_base_low * rent_multiplier / 1000) * 1000
rent_high = round(rent_base_high * rent_multiplier / 1000) * 1000
# Юридическое сопровождение сделки — диапазон (не фикс), варьируется по объёму работ.
juridical_low, juridical_high = 15_000, 50_000
# Реклама/продвижение за 3 месяца — низкая уверенность в оценке (нет единого
@ -1709,7 +1725,8 @@ def _build_offer_page(estimate: AggregatedEstimate, brand) -> str: # type: igno
<tr style="border-bottom:1px solid {_BORDER};">
<td style="padding:6pt 4pt;">
<div class="bold">Аренда после сделки</div>
<div style="font-size:{_FS_XS};color:{_MUTED};">однокомнатной квартиры на 3 месяца</div>
<div style="font-size:{_FS_XS};color:{_MUTED};">
{f"{rooms}-комн. квартиры на 3 месяца" if rooms > 0 else "квартиры на 3 месяца"}</div>
</td>
<td style="padding:6pt 4pt;text-align:right;color:{_SUCCESS};font-weight:700;">
бесплатно</td>
@ -2190,7 +2207,7 @@ def generate_trade_in_pdf(
_build_cover(estimate, input_snapshot, brand)
+ _build_listings_page(estimate, input_snapshot, brand)
+ _build_deals_page(estimate, input_snapshot, brand)
+ _build_offer_page(estimate, brand)
+ _build_offer_page(estimate, input_snapshot, brand)
)
html_str = (

View file

@ -349,3 +349,44 @@ def test_deals_banner_neutral_when_no_discount() -> None:
_assert_well_formed(html)
assert "1018%" not in html
assert "продаются дешевле, чем заявлено" in html
# ── _build_offer_page — rent scales with room count, not a flat 1-room figure ──
# (#pdf-honesty LOW audit R2 #5: «Общие финансовые потери» hardcoded a 1-room
# 3-month rent for EVERY object, including 3-4-room ones.)
def test_offer_page_rent_scales_up_for_larger_objects() -> None:
est = _estimate()
snapshot_1room = {**_SNAPSHOT, "rooms": 1}
snapshot_3room = {**_SNAPSHOT, "rooms": 3}
html_1room = mod._build_offer_page(est, snapshot_1room, _BRAND)
html_3room = mod._build_offer_page(est, snapshot_3room, _BRAND)
_assert_well_formed(html_1room)
_assert_well_formed(html_3room)
# Base (1-room) rent range unchanged: 84 000 135 000.
assert mod._fmt_rub(84_000) in html_1room
assert mod._fmt_rub(135_000) in html_1room
# 3-room rent scaled up (×1.8 heuristic): 151 000 243 000.
assert mod._fmt_rub(151_000) in html_3room
assert mod._fmt_rub(243_000) in html_3room
# The flat 1-room figure must NOT leak into the 3-room object's report.
assert mod._fmt_rub(135_000) not in html_3room
def test_offer_page_rent_subtitle_reflects_room_count() -> None:
est = _estimate()
html_1room = mod._build_offer_page(est, {**_SNAPSHOT, "rooms": 1}, _BRAND)
assert "1-комн. квартиры на 3 месяца" in html_1room
assert "однокомнатной квартиры на 3 месяца" not in html_1room
html_3room = mod._build_offer_page(est, {**_SNAPSHOT, "rooms": 3}, _BRAND)
assert "3-комн. квартиры на 3 месяца" in html_3room
# Unknown room count (0/missing) → neutral subtitle, no fabricated room number.
html_unknown = mod._build_offer_page(est, {**_SNAPSHOT, "rooms": 0}, _BRAND)
assert "квартиры на 3 месяца" in html_unknown
assert "-комн. квартиры на 3 месяца" not in html_unknown