From 92371e7d9e94bc609f00fa5e620fff9fa042fb7b Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sun, 12 Jul 2026 22:11:04 +0000 Subject: [PATCH] fix(tradein/pdf): scale offer-page rent by room count (LOW audit R2 #5) (#2506) --- .../app/services/exporters/trade_in_pdf.py | 27 +++++++++--- .../services/test_trade_in_pdf_dual_price.py | 41 +++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/tradein-mvp/backend/app/services/exporters/trade_in_pdf.py b/tradein-mvp/backend/app/services/exporters/trade_in_pdf.py index ead22a51..6e46660d 100644 --- a/tradein-mvp/backend/app/services/exporters/trade_in_pdf.py +++ b/tradein-mvp/backend/app/services/exporters/trade_in_pdf.py @@ -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
Аренда после сделки
-
однокомнатной квартиры на 3 месяца
+
+ {f"{rooms}-комн. квартиры на 3 месяца" if rooms > 0 else "квартиры на 3 месяца"}
бесплатно @@ -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 = ( diff --git a/tradein-mvp/backend/tests/services/test_trade_in_pdf_dual_price.py b/tradein-mvp/backend/tests/services/test_trade_in_pdf_dual_price.py index 2f405445..4d5cf26e 100644 --- a/tradein-mvp/backend/tests/services/test_trade_in_pdf_dual_price.py +++ b/tradein-mvp/backend/tests/services/test_trade_in_pdf_dual_price.py @@ -349,3 +349,44 @@ def test_deals_banner_neutral_when_no_discount() -> None: _assert_well_formed(html) assert "10–18%" 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