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