feat(mera-public): Москва в реестре городов лендинга и кабинета
All checks were successful
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 13s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 10s
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m20s
CI Trade-In / backend-tests (pull_request) Successful in 6m2s
All checks were successful
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 13s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 10s
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m20s
CI Trade-In / backend-tests (pull_request) Successful in 6m2s
Реестр городов один на публичную форму и кабинет, и до сих пор он знал только Свердловскую область. Московский адрес нельзя было выбрать ни там, ни там, хотя бэкенд Москву поддерживает: реестр регионов знает 77, проба покрытия знает московские центроиды, оценка отрабатывает. Москва подана НЕ как ещё один «частично покрытый город области», а отдельной строкой: сбор по ней есть, а замера полноты покрытия нет, и приписывать ей формулировки области было бы неправдой. Для этого у `OblastCity` появилось поле `region`, а `SECONDARY_CITIES` теперь строится из `OBLAST_66_CITIES` — иначе Москва попала бы в перечисление городов области. Бэкенду поле не отправляется: это различение нужно только фронту. В `landing-facts.ts` строки Москвы сознательно нет — там лежат замеры покрытия по городам, а по Москве замера не делали. Придумывать цифру нельзя, поэтому паритет-тест копи сверяет замеры с `OBLAST_66_CITIES`. Тексты про географию переписаны в четырёх местах: плашка покрытия на главной, карточка бесплатной пробы, ответ FAQ про регионы и сообщение «адрес вне покрытия». Везде одна и та же честная формулировка: по области — полное и частичное покрытие, по Москве — считаем, но полноту не мерили. Юридический адрес в подвале не трогали, там «Свердловская область» — это адрес компании, а не география сервиса. Паритет-тест дропдауна и порогов покрытия на бэкенде дополнен Москвой: город, предлагаемый к выбору, обязан быть отвечаемым пробой. Фронт: 218 passed, tsc и eslint чистые. Бэкенд: 34 passed в затронутом файле.
This commit is contained in:
parent
cfcdb9393c
commit
c6711d05c4
8 changed files with 79 additions and 27 deletions
|
|
@ -559,7 +559,12 @@ def test_offered_cities_match_coverage_cities() -> None:
|
|||
import pathlib
|
||||
import re
|
||||
|
||||
from app.api.v1.trade_in import COVERAGE_GREEN_CITIES, COVERAGE_YELLOW_CITIES, _fold_city
|
||||
from app.api.v1.trade_in import (
|
||||
COVERAGE_GREEN_CITIES,
|
||||
COVERAGE_MOSCOW_DISPLAY,
|
||||
COVERAGE_YELLOW_CITIES,
|
||||
_fold_city,
|
||||
)
|
||||
|
||||
registry = (
|
||||
pathlib.Path(__file__).resolve().parents[2]
|
||||
|
|
@ -573,7 +578,10 @@ def test_offered_cities_match_coverage_cities() -> None:
|
|||
assert block, "не нашли OBLAST_CITIES — проверка стала бы пустой"
|
||||
|
||||
offered = {_fold_city(m) for m in re.findall(r'label:\s*"([^"]+)"', block.group(1))}
|
||||
answerable = {_fold_city(c) for c in (*COVERAGE_GREEN_CITIES, *COVERAGE_YELLOW_CITIES)}
|
||||
answerable = {
|
||||
_fold_city(c)
|
||||
for c in (*COVERAGE_GREEN_CITIES, *COVERAGE_YELLOW_CITIES, COVERAGE_MOSCOW_DISPLAY)
|
||||
}
|
||||
|
||||
assert offered == answerable, (
|
||||
"предлагаем выбрать и умеем отвечать — это должно быть одно множество.\n"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { OBLAST_CITIES } from "@/lib/city-registry";
|
||||
import { OBLAST_66_CITIES } from "@/lib/city-registry";
|
||||
|
||||
import { describeCityExpectation, describeCoverage } from "../coverage-copy";
|
||||
import { COVERED_CITIES, REGION_NAME, SECONDARY_CITIES } from "../content";
|
||||
import { CITY_COVERAGE } from "../landing-facts";
|
||||
import type { CoverageProbe } from "../public-api";
|
||||
|
||||
|
|
@ -133,7 +134,7 @@ describe("describeCoverage", () => {
|
|||
*/
|
||||
describe("describeCityExpectation", () => {
|
||||
it("у каждого предлагаемого города есть замер — и лишних замеров нет", () => {
|
||||
const offered = OBLAST_CITIES.map((c) => c.label).sort();
|
||||
const offered = OBLAST_66_CITIES.map((c) => c.label).sort();
|
||||
const measured = CITY_COVERAGE.map((m) => m.city).sort();
|
||||
expect(measured).toEqual(offered);
|
||||
});
|
||||
|
|
@ -202,3 +203,23 @@ describe("describeCityExpectation", () => {
|
|||
expect(describeCityExpectation("Москва")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Москва — отдельный регион (PR-B), не «частичное покрытие области». Три
|
||||
* инварианта реестра, которые должны остаться верными при любом следующем
|
||||
* расширении.
|
||||
*/
|
||||
describe("реестр городов — Москва как отдельный регион", () => {
|
||||
it("Москва предлагается в дропдауне", () => {
|
||||
expect(COVERED_CITIES).toContain("Москва");
|
||||
});
|
||||
|
||||
it("Москва не подаётся как «частичное покрытие области»", () => {
|
||||
expect(SECONDARY_CITIES).not.toContain("Москва");
|
||||
});
|
||||
|
||||
it("REGION_NAME называет оба региона", () => {
|
||||
expect(REGION_NAME).toContain("Свердловская область");
|
||||
expect(REGION_NAME).toContain("Москва");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -149,9 +149,9 @@ export function FreeCheckCard() {
|
|||
if (resolvedCity === OTHER_CITY) {
|
||||
setFeedback({
|
||||
kind: "info",
|
||||
title: `Пока мы считаем только по одному региону — ${REGION_NAME}`,
|
||||
title: `Пока мы считаем только по двум регионам — ${REGION_NAME}`,
|
||||
lines: [
|
||||
`Данные мы собираем сами, город за городом: полностью — ${PRIMARY_CITY}, частично — ${SECONDARY_CITIES.join(", ")}. По остальным адресам оценка была бы догадкой, поэтому мы её не показываем.`,
|
||||
`Данные мы собираем сами, город за городом: полностью — ${PRIMARY_CITY}, частично — ${SECONDARY_CITIES.join(", ")}. По Москве данные тоже собираем и оценку показываем — отдельно от области. По остальным адресам оценка была бы догадкой, поэтому мы её не показываем.`,
|
||||
],
|
||||
});
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -194,10 +194,11 @@ export function HeroV3({ stats }: { stats: LandingStats }) {
|
|||
<div className={styles.heroRegion}>
|
||||
<p className={styles.heroRegionTitle}>{REGION_NAME}</p>
|
||||
<p className={styles.heroRegionText}>
|
||||
Полное покрытие — {PRIMARY_CITY}. По остальным городам области (
|
||||
{SECONDARY_CITIES.join(", ")}) данных меньше, и оценка там может
|
||||
быть ориентировочной. По другим регионам не считаем вовсе — не
|
||||
хотим гадать.
|
||||
Свердловская область: полностью — {PRIMARY_CITY}, частично —{" "}
|
||||
{SECONDARY_CITIES.join(", ")}: там данных меньше, и оценка может
|
||||
быть ориентировочной. Москва — отдельно: данные собираем,
|
||||
оценку показываем, но насколько полно покрыт город, пока не
|
||||
измеряли. По другим регионам не считаем вовсе — не хотим гадать.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
* нельзя.
|
||||
*/
|
||||
|
||||
import { DEFAULT_CITY, OBLAST_CITIES } from "@/lib/city-registry";
|
||||
import { DEFAULT_CITY, OBLAST_66_CITIES, OBLAST_CITIES } from "@/lib/city-registry";
|
||||
import {
|
||||
LIVE_LISTING_SOURCES,
|
||||
SOURCES,
|
||||
|
|
@ -311,7 +311,7 @@ export const PAID_REPORT_RETENTION_MONTHS = 12;
|
|||
// География
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const REGION_NAME = "Свердловская область";
|
||||
export const REGION_NAME = "Свердловская область и Москва";
|
||||
|
||||
/**
|
||||
* Города, которые сервис вообще умеет различать (это же список `city_hint` в
|
||||
|
|
@ -339,7 +339,7 @@ export const COVERED_CITIES: readonly string[] = OBLAST_CITIES.map(
|
|||
export const PRIMARY_CITY: string = DEFAULT_CITY.label;
|
||||
|
||||
/** Остальные города области: сбор заведён, но данных кратно меньше. */
|
||||
export const SECONDARY_CITIES: readonly string[] = OBLAST_CITIES.filter(
|
||||
export const SECONDARY_CITIES: readonly string[] = OBLAST_66_CITIES.filter(
|
||||
(c) => c.id !== DEFAULT_CITY.id,
|
||||
).map((c) => c.label);
|
||||
|
||||
|
|
@ -490,9 +490,10 @@ export const FAQ: readonly FaqItem[] = [
|
|||
},
|
||||
{
|
||||
id: "why-region",
|
||||
q: "Почему только Свердловская область?",
|
||||
q: "Почему только Свердловская область и Москва?",
|
||||
a: [
|
||||
`Мы собираем данные сами, город за городом, и по объёму эти города не равны: полнее всего покрыт ${PRIMARY_CITY}. По остальным городам области данных заметно меньше — там оценка скорее ориентировочная, и мы про это пишем, а не делаем вид, что разницы нет.`,
|
||||
"По Москве данные тоже собираем и оценку показываем, но замеров полноты покрытия по городу пока нет — долю уверенных ответов там мы, в отличие от городов области, пока не публикуем.",
|
||||
"Там, где сбора нет вовсе, оценка была бы догадкой с уверенным видом. Поэтому другие регионы мы не обещаем и добавляем их по мере появления реального покрытия, а не заранее.",
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ export function describeCoverage(probe: CoverageProbe): CoverageVerdict {
|
|||
tone: "none",
|
||||
title: "Этот адрес вне области, по которой мы собираем данные",
|
||||
lines: [
|
||||
"Сейчас это Свердловская область: Екатеринбург целиком и ещё несколько городов вокруг. По другим регионам данных у нас нет — и придумывать цифру мы не станем.",
|
||||
"Сейчас это Свердловская область (Екатеринбург целиком и несколько городов вокруг) и Москва. По другим регионам данных у нас нет — и придумывать цифру мы не станем.",
|
||||
"Проверьте, что в адресе указан нужный город, или попробуйте другой дом.",
|
||||
],
|
||||
tiles: [],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* city-registry — единый источник правды по городам Свердловской области,
|
||||
* поддержанным сбором «Меры» (#2576 этап B: расширение на область).
|
||||
* city-registry — единый источник правды по городам покрытия «Меры»:
|
||||
* Свердловская область (регион 66) и Москва (регион 77, PR-B).
|
||||
*
|
||||
* Раньше геокодер молча подставлял "Екатеринбург, " в любой запрос без явного
|
||||
* города в тексте адреса — житель Нижнего Тагила, вводя «Ленина, 1», получал
|
||||
|
|
@ -48,22 +48,43 @@ export interface OblastCity {
|
|||
* произвольный текст города, не enum/slug).
|
||||
*/
|
||||
readonly label: string;
|
||||
/**
|
||||
* Регион: 66 — Свердловская область, 77 — Москва. НЕ отправляется бэкенду —
|
||||
* нужен только фронту, чтобы отличить «частичное покрытие области» от
|
||||
* отдельного региона. Большинство потребителей читают только id/label.
|
||||
*/
|
||||
readonly region: 66 | 77;
|
||||
}
|
||||
|
||||
// ЕКБ первым — это DEFAULT_CITY (см. ниже) и основной трафик; порядок
|
||||
// остальных — по охвату сбора, не алфавитный.
|
||||
export const OBLAST_CITIES: readonly OblastCity[] = [
|
||||
{ id: "ekaterinburg", label: "Екатеринбург" },
|
||||
{ id: "berezovsky", label: "Берёзовский" },
|
||||
{ id: "verkhnyaya_pyshma", label: "Верхняя Пышма" },
|
||||
{ id: "sredneuralsk", label: "Среднеуральск" },
|
||||
{ id: "nizhny_tagil", label: "Нижний Тагил" },
|
||||
{ id: "kamensk_uralsky", label: "Каменск-Уральский" },
|
||||
{ id: "pervouralsk", label: "Первоуральск" },
|
||||
{ id: "revda", label: "Ревда" },
|
||||
{ id: "serov", label: "Серов" },
|
||||
{ id: "ekaterinburg", label: "Екатеринбург", region: 66 },
|
||||
{ id: "berezovsky", label: "Берёзовский", region: 66 },
|
||||
{ id: "verkhnyaya_pyshma", label: "Верхняя Пышма", region: 66 },
|
||||
{ id: "sredneuralsk", label: "Среднеуральск", region: 66 },
|
||||
{ id: "nizhny_tagil", label: "Нижний Тагил", region: 66 },
|
||||
{ id: "kamensk_uralsky", label: "Каменск-Уральский", region: 66 },
|
||||
{ id: "pervouralsk", label: "Первоуральск", region: 66 },
|
||||
{ id: "revda", label: "Ревда", region: 66 },
|
||||
{ id: "serov", label: "Серов", region: 66 },
|
||||
{ id: "moscow", label: "Москва", region: 77 },
|
||||
];
|
||||
|
||||
/** RU-имена регионов покрытия — та же пара кодов, что `OblastCity.region`. */
|
||||
export const REGION_LABELS: Record<66 | 77, string> = {
|
||||
66: "Свердловская область",
|
||||
77: "Москва",
|
||||
};
|
||||
|
||||
/** Только область (регион 66) — прежнее множество, до добавления Москвы. */
|
||||
export const OBLAST_66_CITIES: readonly OblastCity[] = OBLAST_CITIES.filter(
|
||||
(c) => c.region === 66,
|
||||
);
|
||||
|
||||
/** Москва одним объектом — тем же паттерном, что `DEFAULT_CITY = OBLAST_CITIES[0]` ниже. */
|
||||
export const MOSCOW_CITY: OblastCity = OBLAST_CITIES[OBLAST_CITIES.length - 1];
|
||||
|
||||
/**
|
||||
* Дефолт формы — Екатеринбург (визуальный/best-guess, для текста подсказки и
|
||||
* подстановки значения ПОСЛЕ подтверждения). НЕ означает "отправляется по
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ export function useEstimateHistory(limit = 50) {
|
|||
|
||||
/**
|
||||
* GET /api/v1/geocode/suggest?q=&limit=&city_hint=
|
||||
* Автокомплит адресов в Свердловской области для поля адреса (ParamsPanel).
|
||||
* Автокомплит адресов в регионах покрытия (66/77) для поля адреса (ParamsPanel).
|
||||
* Debounce-friendly: вызывающий компонент дебаунсит строку query, хук
|
||||
* кешируется по queryKey; `enabled` срабатывает только начиная с 3 символов
|
||||
* (бэкенд min 2, берём 3 чтобы не дёргать на 1-2 символа). `select`
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue