From 07655a26429cee608ee5fb592b1f17cb65829743 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Fri, 21 Aug 2026 16:59:05 +0500 Subject: [PATCH] =?UTF-8?q?feat(tradein/v2):=20=D0=BF=D0=BE=D0=B7=D0=B8?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=BA=D0=B2=D0=B0=D1=80=D1=82=D0=B8=D1=80?= =?UTF-8?q?=D1=8B=20=D0=BD=D0=B0=20=D1=80=D1=8B=D0=BD=D0=BA=D0=B5=20?= =?UTF-8?q?=E2=80=94=20=D0=BF=D0=BB=D0=B0=D1=88=D0=BA=D0=B0=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA=D0=B5=20=D1=80?= =?UTF-8?q?=D0=B5=D0=BA=D0=BE=D0=BC=D0=B5=D0=BD=D0=B4=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D0=BE=D0=B9=20=D1=86=D0=B5=D0=BD=D1=8B=20(#2899)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Почему: бэкенд с #2926 отдаёт market_percentile (1..99, null при когорте < 15), на проде он уже считается (21.08: 69 при 16 аналогах, 57 при 23, null при 11), но фронт поле не знал — «бейдж из макета» так и не появился. Что: AggregatedEstimate.market_percentile в wire-типе; маппер даёт карточке «РЕКОМЕНДОВАННАЯ ЦЕНА» ту же спокойную плашку, что у карточки 2: лейбл по терцилям («Низ рынка» ≤33 / «В рынке» / «Верх рынка» ≥67) и подпись «69-й перцентиль среди 16 аналогов» — число рядом, чтобы лейбл не читался точнее, чем он есть; при null плашки нет (не «В рынке» по умолчанию). ResultPanel: JSX плашки вынесен в DeltaPill и рисуется и в ветке с гистограммой — раньше delta у карточки с барами терялся бы молча. Тест marketPercentile.test.ts (vitest): лейблы и границы терцилей, null → нет плашки, delta карточки 2 не тронут, склонение по n_analogs. На main красный по значению (delta карточки 1 === undefined). Refs #2899 --- .../v2/__tests__/marketPercentile.test.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tradein-mvp/frontend/src/components/trade-in/v2/__tests__/marketPercentile.test.ts diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/__tests__/marketPercentile.test.ts b/tradein-mvp/frontend/src/components/trade-in/v2/__tests__/marketPercentile.test.ts new file mode 100644 index 00000000..f1bd46e2 --- /dev/null +++ b/tradein-mvp/frontend/src/components/trade-in/v2/__tests__/marketPercentile.test.ts @@ -0,0 +1,58 @@ +// #2899 — позиция квартиры внутри когорты аналогов на карточке «РЕКОМЕНДОВАННАЯ +// ЦЕНА». Бэкенд считает market_percentile (1..99, null при когорте < 15); маппер +// обязан превратить его в лейбл по терцилям + подпись с числом и размером когорты, +// а при null — не рисовать ничего (не «В рынке» по умолчанию). На main карточка 1 +// плашки не имеет вовсе — первый тест красный по значению (delta === undefined). + +import { describe, expect, it } from "vitest"; + +import { mapResultPanel } from "../mappers"; +import { FIXTURE_ESTIMATE } from "@/app/ui-preview/estimate/fixture"; +import type { AggregatedEstimate } from "@/types/trade-in"; + +function est(over: Partial): AggregatedEstimate { + return { ...FIXTURE_ESTIMATE, n_analogs: 16, ...over }; +} + +function card1(e: AggregatedEstimate) { + return mapResultPanel(e).cards[0]; +} + +describe("#2899 market_percentile → плашка карточки 1", () => { + it("69-й перцентиль среди 16 аналогов → «Верх рынка» + честная подпись", () => { + const c = card1(est({ market_percentile: 69 })); + expect(c.delta).toBe("Верх рынка"); + expect(c.deltaLabel).toBe("69-й перцентиль среди 16 аналогов"); + }); + + it("терцили: ≤33 — низ, 34..66 — в рынке, ≥67 — верх", () => { + expect(card1(est({ market_percentile: 20 })).delta).toBe("Низ рынка"); + expect(card1(est({ market_percentile: 33 })).delta).toBe("Низ рынка"); + expect(card1(est({ market_percentile: 34 })).delta).toBe("В рынке"); + expect(card1(est({ market_percentile: 50 })).delta).toBe("В рынке"); + expect(card1(est({ market_percentile: 66 })).delta).toBe("В рынке"); + expect(card1(est({ market_percentile: 67 })).delta).toBe("Верх рынка"); + }); + + it("null / отсутствие поля → плашки нет; delta карточки 2 не тронут", () => { + const withNull = mapResultPanel(est({ market_percentile: null })).cards; + const legacy = mapResultPanel(est({})).cards; + expect(withNull[0].delta).toBeUndefined(); + expect(withNull[0].deltaLabel).toBeUndefined(); + expect(legacy[0].delta).toBeUndefined(); + // контроль: плашка «к цене объявления» карточки 2 живёт своей жизнью + expect(withNull[1].delta).toBe( + mapResultPanel(FIXTURE_ESTIMATE).cards[1].delta, + ); + expect(withNull[1].deltaLabel).toBe("к цене объявления"); + }); + + it("размер когорты в подписи берётся из n_analogs, склонение по числу", () => { + expect( + card1(est({ market_percentile: 50, n_analogs: 21 })).deltaLabel, + ).toBe("50-й перцентиль среди 21 аналога"); + expect( + card1(est({ market_percentile: 50, n_analogs: 23 })).deltaLabel, + ).toBe("50-й перцентиль среди 23 аналогов"); + }); +});