Compare commits

..

No commits in common. "c3840019c55ee90ff413a50e0e9fb7d707747bae" and "1eee4b955dcf51fb8d1c58cb66cd4cfa525a9f85" have entirely different histories.

8 changed files with 27 additions and 79 deletions

View file

@ -559,12 +559,7 @@ def test_offered_cities_match_coverage_cities() -> None:
import pathlib import pathlib
import re import re
from app.api.v1.trade_in import ( from app.api.v1.trade_in import COVERAGE_GREEN_CITIES, COVERAGE_YELLOW_CITIES, _fold_city
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]
@ -578,10 +573,7 @@ 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 = { answerable = {_fold_city(c) for c in (*COVERAGE_GREEN_CITIES, *COVERAGE_YELLOW_CITIES)}
_fold_city(c)
for c in (*COVERAGE_GREEN_CITIES, *COVERAGE_YELLOW_CITIES, COVERAGE_MOSCOW_DISPLAY)
}
assert offered == answerable, ( assert offered == answerable, (
"предлагаем выбрать и умеем отвечать — это должно быть одно множество.\n" "предлагаем выбрать и умеем отвечать — это должно быть одно множество.\n"

View file

@ -1,9 +1,8 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { OBLAST_66_CITIES } from "@/lib/city-registry"; import { OBLAST_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";
@ -134,7 +133,7 @@ describe("describeCoverage", () => {
*/ */
describe("describeCityExpectation", () => { describe("describeCityExpectation", () => {
it("у каждого предлагаемого города есть замер — и лишних замеров нет", () => { it("у каждого предлагаемого города есть замер — и лишних замеров нет", () => {
const offered = OBLAST_66_CITIES.map((c) => c.label).sort(); const offered = OBLAST_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);
}); });
@ -203,23 +202,3 @@ 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("Москва");
});
});

View file

@ -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;

View file

@ -194,11 +194,10 @@ 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>

View file

@ -22,7 +22,7 @@
* нельзя. * нельзя.
*/ */
import { DEFAULT_CITY, OBLAST_66_CITIES, OBLAST_CITIES } from "@/lib/city-registry"; import { DEFAULT_CITY, 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_66_CITIES.filter( export const SECONDARY_CITIES: readonly string[] = OBLAST_CITIES.filter(
(c) => c.id !== DEFAULT_CITY.id, (c) => c.id !== DEFAULT_CITY.id,
).map((c) => c.label); ).map((c) => c.label);
@ -490,10 +490,9 @@ export const FAQ: readonly FaqItem[] = [
}, },
{ {
id: "why-region", id: "why-region",
q: "Почему только Свердловская область и Москва?", q: "Почему только Свердловская область?",
a: [ a: [
`Мы собираем данные сами, город за городом, и по объёму эти города не равны: полнее всего покрыт ${PRIMARY_CITY}. По остальным городам области данных заметно меньше — там оценка скорее ориентировочная, и мы про это пишем, а не делаем вид, что разницы нет.`, `Мы собираем данные сами, город за городом, и по объёму эти города не равны: полнее всего покрыт ${PRIMARY_CITY}. По остальным городам области данных заметно меньше — там оценка скорее ориентировочная, и мы про это пишем, а не делаем вид, что разницы нет.`,
"По Москве данные тоже собираем и оценку показываем, но замеров полноты покрытия по городу пока нет — долю уверенных ответов там мы, в отличие от городов области, пока не публикуем.",
"Там, где сбора нет вовсе, оценка была бы догадкой с уверенным видом. Поэтому другие регионы мы не обещаем и добавляем их по мере появления реального покрытия, а не заранее.", "Там, где сбора нет вовсе, оценка была бы догадкой с уверенным видом. Поэтому другие регионы мы не обещаем и добавляем их по мере появления реального покрытия, а не заранее.",
], ],
}, },

View file

@ -149,7 +149,7 @@ export function describeCoverage(probe: CoverageProbe): CoverageVerdict {
tone: "none", tone: "none",
title: "Этот адрес вне области, по которой мы собираем данные", title: "Этот адрес вне области, по которой мы собираем данные",
lines: [ lines: [
"Сейчас это Свердловская область (Екатеринбург целиком и несколько городов вокруг) и Москва. По другим регионам данных у нас нет — и придумывать цифру мы не станем.", "Сейчас это Свердловская область: Екатеринбург целиком и ещё несколько городов вокруг. По другим регионам данных у нас нет — и придумывать цифру мы не станем.",
"Проверьте, что в адресе указан нужный город, или попробуйте другой дом.", "Проверьте, что в адресе указан нужный город, или попробуйте другой дом.",
], ],
tiles: [], tiles: [],

View file

@ -1,6 +1,6 @@
/** /**
* city-registry единый источник правды по городам покрытия «Меры»: * city-registry единый источник правды по городам Свердловской области,
* Свердловская область (регион 66) и Москва (регион 77, PR-B). * поддержанным сбором «Меры» (#2576 этап B: расширение на область).
* *
* Раньше геокодер молча подставлял "Екатеринбург, " в любой запрос без явного * Раньше геокодер молча подставлял "Екатеринбург, " в любой запрос без явного
* города в тексте адреса житель Нижнего Тагила, вводя «Ленина, 1», получал * города в тексте адреса житель Нижнего Тагила, вводя «Ленина, 1», получал
@ -48,43 +48,22 @@ 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: "Екатеринбург", region: 66 }, { id: "ekaterinburg", label: "Екатеринбург" },
{ id: "berezovsky", label: "Берёзовский", region: 66 }, { id: "berezovsky", label: "Берёзовский" },
{ id: "verkhnyaya_pyshma", label: "Верхняя Пышма", region: 66 }, { id: "verkhnyaya_pyshma", label: "Верхняя Пышма" },
{ id: "sredneuralsk", label: "Среднеуральск", region: 66 }, { id: "sredneuralsk", label: "Среднеуральск" },
{ id: "nizhny_tagil", label: "Нижний Тагил", region: 66 }, { id: "nizhny_tagil", label: "Нижний Тагил" },
{ id: "kamensk_uralsky", label: "Каменск-Уральский", region: 66 }, { id: "kamensk_uralsky", label: "Каменск-Уральский" },
{ id: "pervouralsk", label: "Первоуральск", region: 66 }, { id: "pervouralsk", label: "Первоуральск" },
{ id: "revda", label: "Ревда", region: 66 }, { id: "revda", label: "Ревда" },
{ id: "serov", label: "Серов", region: 66 }, { id: "serov", label: "Серов" },
{ 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, для текста подсказки и
* подстановки значения ПОСЛЕ подтверждения). НЕ означает "отправляется по * подстановки значения ПОСЛЕ подтверждения). НЕ означает "отправляется по

View file

@ -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=
* Автокомплит адресов в регионах покрытия (66/77) для поля адреса (ParamsPanel). * Автокомплит адресов в Свердловской области для поля адреса (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`