feat(tradein): surface Avito IMV anchor + ДКП corridor in HeroSummary (#651, #652)

Frontend surfacing in the DEPLOYED trade-in UI (tradein-mvp/frontend):
- types/trade-in.ts: add AvitoImvSummary + DkpCorridor, optional avito_imv /
  dkp_corridor on AggregatedEstimate (mirror backend snake_case, no any)
- HeroSummary.tsx: 'Avito IMV: X млн ₽' caption near price + 'Коридор реальных
  сделок: X–Y млн · N ДКП (Росреестр)' line. No new fetch, no-op when null.
tsc --noEmit clean. Root legacy frontend/ untouched.
This commit is contained in:
lekss361 2026-05-29 19:04:32 +03:00
parent 70ee7b282c
commit 6a81e91a83
2 changed files with 62 additions and 0 deletions

View file

@ -92,6 +92,18 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
const showMockDisclaimer = isMockAddress(estimate);
// ── #651: Avito IMV anchor — реальная рыночная оценка дома. Маркер у цены. ──
const imv = estimate.avito_imv;
const imvPrice =
imv && typeof imv.recommended_price === "number" && imv.recommended_price > 0
? imv.recommended_price
: null;
// ── #652: коридор реальных ДКП-сделок (advisory). ₽/м² → млн через площадь. ──
const dkp = estimate.dkp_corridor;
const dkpArea = estimate.area_m2 ?? input.area_m2 ?? 0;
const showDkp =
!!dkp && dkp.count > 0 && dkpArea > 0 && dkp.low_ppm2 > 0 && dkp.high_ppm2 > 0;
// Progressive enrichment state
const needsHouseType = estimate.house_type === null;
const needsRepairState = estimate.repair_state === null;
@ -264,6 +276,15 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
<div className="price-figure-perm2 mono">
{estimate.median_price_per_m2.toLocaleString("ru-RU")} /м²
</div>
{imvPrice !== null && (
<div
className="price-figure-imv mono"
title="Индекс Market Value Avito по этому дому — реальный рыночный якорь"
style={{ marginTop: 6, fontSize: 12, color: "var(--muted)" }}
>
Avito IMV: {formatMln(imvPrice)}
</div>
)}
</div>
{hasSold && (
@ -299,6 +320,20 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
</p>
)}
{showDkp && dkp && (
<p
className="hero-prices-note"
title="Коридор реальных ₽/м² по ДКП-сделкам Росреестра (справочно)"
>
<b>Коридор реальных сделок:</b>{" "}
<span className="mono">
{formatMln(dkp.low_ppm2 * dkpArea)} {formatMln(dkp.high_ppm2 * dkpArea)}
</span>{" "}
· <span className="mono">{dkp.median_ppm2.toLocaleString("ru-RU")} /м²</span> ·{" "}
{dkp.count} ДКП (Росреестр)
</p>
)}
<div className="hero-bars">
<div className="bar-block">
<div className="bar-head">

View file

@ -72,6 +72,27 @@ export interface CianValuationSummary {
// Базис коэффициента asking→sold (S3): по группе комнат либо городской fallback.
export type RatioBasis = "per_rooms" | "global_fallback";
// ── #651: Avito IMV (Индекс Market Value) якорь для target-дома ──
// Реальная рыночная оценка Avito по дому (house_imv_evaluations) — anchor для
// blend'а медианы. Сурфейсится как референсный маркер. null при отсутствии записи.
export interface AvitoImvSummary {
recommended_price: number | null; // рекомендованная цена Avito, ₽
lower_price: number | null; // нижняя граница IMV-коридора, ₽
higher_price: number | null; // верхняя граница IMV-коридора, ₽
market_count: number | null; // объём рынка, на котором построена оценка
}
// ── #652: коридор реальных ДКП-сделок Росреестра (advisory, не клампит) ──
// Агрегаты ₽/м² по сопоставимым сделкам за период. null / count=0 если по улице
// нет сделок. count..period_months — required (бэкенд не отдаёт частичный объект).
export interface DkpCorridor {
count: number; // число ДКП-сделок в выборке
low_ppm2: number; // min ₽/м² по сделкам
median_ppm2: number; // медиана ₽/м²
high_ppm2: number; // max ₽/м²
period_months: number; // окно поиска сделок
}
export interface AggregatedEstimate {
estimate_id: string; // UUID
median_price_rub: number; // ASKING-ориентир: по чему выставлены аналоги
@ -117,6 +138,12 @@ export interface AggregatedEstimate {
has_balcony: boolean | null;
// ── Cian valuation surface ──
cian_valuation?: CianValuationSummary | null;
// ── #651/#652: внешние якоря ──
// avito_imv — реальная Avito IMV-оценка дома, anchor для blend'а медианы.
// dkp_corridor — коридор ₽/м² по ДКП-сделкам Росреестра (advisory). Оба nullable:
// null при отсутствии данных (graceful, common case без регрессий).
avito_imv?: AvitoImvSummary | null;
dkp_corridor?: DkpCorridor | null;
}
// ── Stage 4a/4b response types ──