Appearance
DEV-402 — Геолокация и адресный флоу (frontend design)
Дата: 2026-06-01 YouTrack: DEV-402 (umbrella initiative) Зависит от: DEV-379 (импорт ГАР, расширен), DEV-380 (suggest API + GeoProvider, расширен) Premortem: dev-402-geo-address-flow-frontend.mdСтатус: Дизайн готов, ждёт расширения бэк-эпиков
1. Контекст
DEV-402 — umbrella-инициатива расширения геолокации Partizap на всю РФ. Текущая система: ручной справочник (СПб 18 районов + 69 станций метро; Москва 12 районов + 41 станция). Цель эпика: 85 регионов, ~1100 городов, дома из ГАР (~30M записей), автокомплит адреса, IP-определение города.
Под DEV-402 уже декомпозировано два backend-эпика:
- DEV-379 — инфраструктура и импорт данных (ГАР XML-дамп → Postgres)
- DEV-380 — API и runtime-интеграция (GeoProvider abstraction, suggest endpoint)
Этот документ описывает фронтовую часть эпика — она в исходный план 2026-04-21-geo-expansion-russia.md явно не входила («Frontend-изменения — отдельные таски»).
1.1 Принятые решения (по результатам brainstorming + premortem)
| Решение | Значение | Источник |
|---|---|---|
| Backend стратегия | Полный ГАР с домами (50ГБ), свой suggest (без DaData runtime) | Brainstorm Q1: A |
| Frontend scope | 4 сценария + админка-просмотр | Brainstorm Q2: a+b+c+d+e |
| Метро в форме продавца | Гибрид — селект предзаполняется автоматически, продавец может перевыбрать | Brainstorm Q3: гибрид |
| Определение города посетителя | IP по умолчанию + опциональная точная геолокация | Brainstorm Q4: гибрид |
| Порядок поставки | Сначала бэк (расширенный), затем фронт | Brainstorm Q5: сначала бэк |
Legacy-продукты без address_id | Принудительный перевыбор (база пуста, юзеров нет) + триггер на bulk-миграцию при > 50 продавцов | Brainstorm Q6: перевыбор; Premortem H-001 |
| Шапка / фильтр каталога | Иерархический picker (как Avito/WB) с поиском | Brainstorm Q7: 7.4 |
| Ключ адреса в публичном контракте | Internal address_id (UUID), не fias_id | Premortem H-006 |
| Координаты домов | Обязательны в resolve-ответе (для гибрида метро) | Premortem H-003 |
| SLO suggest | P95 ≤ 200ms, P99 ≤ 500ms при 100 RPS | Premortem H-004 |
| Качество suggest | Acceptance gold-set 50 запросов + словарь синонимов на бэке | Premortem H-002 |
| Защита suggest | Rate-limit + anon-session cookie | Premortem H-005 |
1.2 Аудитория и успех
Аудитория: продавцы автозапчастей (вендоры размещают карточки) + покупатели на partizap.ru.
Критерий успеха: все 4 сценария работают + 0 регрессий в существующих фичах + полное прохождение manual regression checklist.
Горизонт оценки: 12 месяцев после релиза.
2. Архитектура
2.1 Высокоуровневая карта
┌─────────────────────────────────────────────────────────┐
│ ВИДЖЕТЫ / СЦЕНАРИИ │
│ ├─ vendor-address-form (форма продавца) │
│ ├─ geo-city-picker (иерархич. модал шапки) │
│ └─ geo-banner (IP-баннер главной) │
└─────────────────────────────────────────────────────────┘
↓ собраны из атомов
┌─────────────────────────────────────────────────────────┐
│ АТОМЫ (composables в features/geo-*) │
│ ├─ useGeoSuggest — автокомплит по своему API │
│ ├─ useGeoCascade — регион→город (existing, расшир.) │
│ ├─ useGeoIpDetect — SSR resolve по IP │
│ └─ useGeoLocate — браузерная геолокация │
└─────────────────────────────────────────────────────────┘
↓ обращаются к
┌─────────────────────────────────────────────────────────┐
│ ENTITIES (схемы + эндпоинты) │
│ └─ entities/geo │
│ ├─ model/geo.schema.ts │
│ │ + AddressSuggestion (address_id, source meta) │
│ │ + ResolvedAddress (+ coords дома, metro) │
│ │ + GeoIpResult │
│ └─ api/endpoints.ts │
│ + suggest, resolve, ip, locate │
└─────────────────────────────────────────────────────────┘
↓ читают/пишут
┌─────────────────────────────────────────────────────────┐
│ STORES (pinia) │
│ ├─ geo — выбранный регион/город (cookies) │
│ └─ geoLookup — id↔name обратный резолв │
└─────────────────────────────────────────────────────────┘2.2 FSD-границы
- features/geo-suggest, features/geo-cascade (existing), features/geo-ip-detect, features/geo-locate — атомы.
- features/vendor-address-form, features/geo-city-picker, features/geo-banner — сценарии, импортируют атомы.
- Cross-feature импорты запрещены (enforced ESLint). Атомы общаются только через
entities/geo.
3. Контракт фронт ↔ бэк
3.1 Расширения существующих эндпоинтов
| Endpoint | Изменения |
|---|---|
GET /store/geo/regions | Возвращает 85 записей вместо 2. Без изменений схемы. |
GET /store/geo/regions/:id/cities | Поддерживает ?q=<query> для серверного поиска. |
POST/PUT /vendor/products | Принимает address_id (UUID, обязательно для новых продуктов; null допустим для legacy). Старые поля region_id, city_id, district_id, metro_station_id, address остаются для read-path и обратной совместимости — на сохранении бэк их перевыставляет через GeoResolver из address_id. |
3.2 Новые эндпоинты (за бэком DEV-380)
| Endpoint | Назначение | Метод |
|---|---|---|
GET /store/geo/suggest | Автокомплит адреса | ?q=&city_id=&level=&limit= |
GET /store/geo/resolve | Полный адрес + координаты + ближайшее метро | ?address_id= |
GET /store/geo/ip | Город по IP | (SSR-only, читает X-Forwarded-For) |
POST /store/geo/locate | Город по координатам браузера | { lat, lon } |
Все 4 эндпоинта обязаны:
- Требовать cookie
PARTIZAP_SESSION(HTTP-only, гостевая выдаётся при первом GET страницы). - Соблюдать rate-limit: 60 req/min/session + 600 req/min/IP (см. §7.5).
- Иметь записанный SLO в openapi.yaml (см. §7.4).
3.3 Схема ответов
ts
// AddressSuggestion — /store/geo/suggest
export const addressSuggestionSchema = z.object({
address_id: z.string().uuid(),
value: z.string(), // "г Санкт-Петербург, Невский пр-кт, д 1"
unrestricted: z.string(), // полный с индексом
level: z.enum(['region', 'city', 'district', 'street', 'house']),
region_id: z.number().nullable(),
city_id: z.number().nullable(),
district_id: z.number().nullable(),
street: z.string().nullable(),
house: z.string().nullable(),
})
// ResolvedAddress — /store/geo/resolve
export const resolvedAddressSchema = addressSuggestionSchema.extend({
coords: z.object({ lat: z.number(), lon: z.number() }).nullable(),
metro_station_id: z.number().nullable(), // ближайшая если < 1500m
metro_distance_m: z.number().nullable(),
})
// GeoIpResult — /store/geo/ip
export const geoIpResultSchema = z.object({
region_id: z.number().nullable(),
city_id: z.number().nullable(),
region_name: z.string().nullable(),
city_name: z.string().nullable(),
confidence: z.enum(['city', 'region', 'unknown']),
})Важно (H-006): address_id — UUID, генерируемый бэком при resolve. fias_id остаётся на бэке как метаданные источника в таблице addresses (address_id, source, source_value). Фронт fias_id не использует.
3.4 Schema продукта
В product.schema.ts добавляется:
ts
// ProductDetail
address_id: z.string().uuid().nullable(), // null для legacy
// ProductForm
address_id: z.string().uuid().optional(),Существующие поля (region_id, city_id, district_id, metro_station_id, address) остаются для отображения карточек в каталоге и обратной совместимости.
4. Компоненты
4.1 Атомы (composables)
features/geo-suggest/composables/useGeoSuggest.ts (новое)
ts
function useGeoSuggest(opts: {
cityId?: Ref<number | undefined>,
level?: ('city' | 'street' | 'house')[],
debounceMs?: number,
}) → {
query: Ref<string>,
suggestions: Readonly<Ref<AddressSuggestion[]>>,
loading: Readonly<Ref<boolean>>,
error: Readonly<Ref<Error | null>>,
pick: (s: AddressSuggestion) => Promise<ResolvedAddress>,
clear: () => void,
}Внутри: debounce 250ms, abort предыдущего запроса (AbortController), LRU кэш 20 ключей query+cityId+level.
features/geo-cascade/composables/useGeoCascade.ts (расширить существующий)
- Добавляется
search: Ref<string>+ клиентская фильтрация регионов по name (85 шт — приемлемо). - Для городов — серверный поиск через
?q=на/regions/:id/cities. - Existing
initialize()остаётся как есть.
features/geo-ip-detect/composables/useGeoIpDetect.ts (новое)
ts
async function useGeoIpDetect() → {
result: Ref<GeoIpResult | null>,
loading: Ref<boolean>,
}Через useFetch('/store/geo/ip') (SSR-friendly). Кэшируется в payload, повторно на клиенте не дёргается. Timeout 3s — лучше без баннера, чем медленная главная.
features/geo-locate/composables/useGeoLocate.ts (новое)
ts
function useGeoLocate() → {
request: () => Promise<ResolvedAddress | null>,
denied: Ref<boolean>,
}Запрашивает navigator.geolocation.getCurrentPosition() → POST /store/geo/locate.
4.2 Сценарные компоненты
features/vendor-address-form/ui/VendorAddressForm.vue (новое)
- Каскад регион→город через
useGeoCascade(с поиском). <AddressAutocomplete>(на базеUInputMenuилиCombobox) →useGeoSuggest(cityId)→pick()заполняет внутреннее состояние{ address_id, address_text, district_id, metro_station_id }.- Метро-селект остаётся: предзаполняется из
resolve(полеmetro_station_id), продавец может перевыбрать. - Эмитит
update:modelValueс объектом{ region_id, city_id, district_id, metro_station_id, address_id, address }→ встраивается вProductFormPageвместо текущегоGeoSelect + UInput address. - Legacy-режим: если product приходит с
address_id == null→ форма открывается, каскад восстанавливается изcity_id, autocomplete пуст с badgeУточните адрес заново, submit-guard блокирует сохранение до новой выборки.
features/geo-city-picker/ui/GeoCityPickerModal.vue (новое)
- Иерархическая модалка как у Avito/WB: регионы слева, города справа.
- Поиск сверху — запрос
/store/geo/suggest?q=&level=city,region(объединённый список с подсветкой совпадений). - Multi-city: чипы под input.
- Кнопка «Определить автоматически» →
useGeoLocate()→ подсвечивает найденный город (без закрытия модалки). - Используется в
widgets/headerвместо текущегоCitySelector.vue.
features/geo-banner/ui/GeoBanner.vue (новое)
Показывается на pages/index.vue если:
- В cookie нет
geo_region_id(новый посетитель) - И
useGeoIpDetect()вернулconfidence in ['city', 'region'] - И в localStorage нет
geo_banner_dismissed
Содержание: Ваш город — Москва? [Да, верно] [Сменить] + кнопка Уточнить точнее.
- «Да» →
geoStore.setRegion + setCities→ cookies →localStorage.setItem('geo_banner_dismissed', '1')→ баннер исчезает. - «Сменить» → открывает
GeoCityPickerModal. - «Уточнить точнее» →
useGeoLocate.request()→ подсвечивает найденный город в баннере, кнопкаДаобновляется.
4.3 Stores
stores/geo.ts — расширить:
selectedRegionиselectedCitiesхранят такжеaddress_id(если нужно для будущих фильтров).- Action
applyIpDetection(GeoIpResult)— устанавливает регион/город из IP без записи в cookie до подтверждения пользователем.
stores/geoLookup.ts — без изменений (обратный резолв id↔name остаётся).
4.4 Admin (apps/admin)
features/admin-references расширить geo-таб:
- Регионы — серверная пагинация + поиск (85 шт, но единый интерфейс).
- Города — серверная пагинация + поиск + фильтр по региону (~1100).
- Просмотр без CRUD (CRUD — отдельный таск; справочник заполняется ГАР-импортом).
entities/admin/api/endpoints.tsдополнить?q=параметром.
5. Потоки данных
5.1 Создание продукта (новый продавец)
ProductFormPage (mounted, продукта нет)
│
├─ VendorAddressForm
│ │
│ ├─ useGeoCascade.fetchRegions() ───► GET /store/geo/regions
│ │
│ ├─ Продавец выбирает регион → useGeoCascade.fetchCities()
│ │ ───► GET /store/geo/regions/:id/cities
│ │
│ ├─ Продавец выбирает город (id=78) → useGeoSuggest(cityId=78) активен
│ │
│ ├─ Продавец вводит "невский 1"
│ │ ↓ debounce 250ms, AbortController отменяет предыдущий
│ │ ───► GET /store/geo/suggest?q=невский+1&city_id=78&limit=10
│ │ ◄─── [{ address_id, value, district_id, … }]
│ │
│ ├─ Продавец выбирает строку → useGeoSuggest.pick()
│ │ ───► GET /store/geo/resolve?address_id=…
│ │ ◄─── ResolvedAddress { …, coords, metro_station_id, metro_distance_m }
│ │
│ ├─ Внутреннее состояние:
│ │ { region_id=47, city_id=78, district_id=12,
│ │ metro_station_id=33 (предзаполнен),
│ │ address_id="a1b2…", address="Невский пр-кт, д 1" }
│ │
│ └─ Метро-селект показывает "Адмиралтейская" с бейджем "auto"
│
└─ Submit → POST /vendor/products
body: { …product, address_id, region_id, city_id, district_id,
metro_station_id (если перевыбрал), address }Бэк (GeoResolver) на сохранении сам перевалидирует address_id и проставит правильные region/city/district. Фронт отправляет полную выкладку — бэк её перезапишет.
5.2 Редактирование legacy-продукта (без address_id)
GET /vendor/products/:id → продукт с city_id=2, district_id=5, address="…",
address_id=null
VendorAddressForm.initialize(product):
├─ useGeoCascade.initialize({ region_id, city_id }) — восстанавливает каскад
├─ Метро-селект: предзаполняется product.metro_station_id (если был)
└─ AddressAutocomplete: пустой ввод, visual state="empty-legacy",
бейдж «Уточните адрес заново», input подсвечен warning-цветом
Submit-guard: canSubmit = computed(() => !!address_id)
Кнопка «Сохранить» disabled до выборки через suggest.5.3 Фильтр каталога (шапка)
SSR (page load на partizap.ru/catalog)
│
├─ Header читает cookie geo_region_id/geo_city_ids
│ └─ есть → geoStore.setRegion + setCities → CatalogPage передаёт
│ geoQueryParams (region_id или city_ids)
│ └─ нет → "Вся Россия" в шапке; GeoBanner может показаться на /
│
├─ Клик "Москва +2 города" в шапке → открыть GeoCityPickerModal
│
GeoCityPickerModal
├─ Левая колонка: регионы (кэш сессии)
├─ Поиск сверху: debounce 200ms
│ ↓ GET /store/geo/suggest?q=казан&level=city,region
│ ◄── мердж регионов и городов с подсветкой совпадений
│
├─ Клик "Казань" → выбран регион (Татарстан) + город (Казань) → чип
│
├─ "Определить автоматически" → useGeoLocate
│ ↓ navigator.geolocation.getCurrentPosition()
│ ↓ POST /store/geo/locate { lat, lon }
│ ◄── ResolvedAddress
│ ↓ подсветить регион/город в модалке (без закрытия)
│
└─ "Применить" → geoStore.setRegion(region) + setCities([city, …])
→ cookies persist → emit close
→ страница перерендерит каталог через geoQueryParams5.4 IP-баннер на главной
SSR pages/index.vue
├─ const { data: ip } = await useFetch('/store/geo/ip', { timeout: 3000 })
│ (бэк читает X-Forwarded-For, отдаёт {region_id, city_id, confidence, …})
│
└─ const showBanner = computed(() =>
!cookie.geo_region_id &&
!localStorage.geo_banner_dismissed &&
ip.confidence !== 'unknown'
)
GeoBanner.vue (client mount, если showBanner)
├─ "Ваш город — Москва?"
├─ "Да, верно" → geoStore.setRegion+setCities → cookie+localStorage persist
├─ "Сменить" → открыть GeoCityPickerModal
└─ "Уточнить точнее" → useGeoLocate.request → /store/geo/locate
→ подсветить найденный city, кнопка "Да" обновляется5.5 Отображение карточки продукта
Карточки в каталоге не меняются на уровне UI. geoLookup.resolve([products]) подгружает cityMap/districtMap (как сейчас, но теперь до ~1100 ключей по требованию). Renders: Москва, Тверская, м. Тверская.
6. Edge-кейсы и обработка ошибок
6.1 Suggest лежит (5xx / timeout)
| Сценарий | Поведение |
|---|---|
| 500 / timeout | useGeoSuggest.error выставлен, dropdown показывает «Не удалось загрузить — попробуйте ещё раз» + retry. Поле редактируемое, но address_id не выставляется → submit-guard блокирует. |
| 422 (q < 3) | Console-warn, dropdown «Введите минимум 3 символа». |
| Сеть пропала между suggest и pick | AbortController срабатывает; suggestions сбрасываются. |
Не делаем fallback на free-text — выбран вариант A (свой suggest). Лучше пятиминутный простой формы, чем кривые адреса в БД.
6.2 Resolve вернул 404 (address_id не найден)
useGeoSuggest.pick()ловит 404 → toast «Адрес временно недоступен, выберите другой вариант».suggestionsсбрасываются.- Sentry-лог с
address_idдля расследования (рассинхрон индекса).
6.3 IP-определение не сработало
| Случай | Поведение |
|---|---|
confidence: 'unknown' | Баннер не показывается. Шапка светит акцентом «Выбрать город». |
| Эндпоинт 5xx / timeout > 3s | SSR useFetch возвращает error; баннер не рендерится. |
confidence: 'region' (без города) | Баннер: «Вы из Татарстана? Уточнить город?» — только «Сменить». |
6.4 Геолокация браузера
| Случай | Поведение |
|---|---|
| Permission denied | useGeoLocate.denied = true. Кнопка «Уточнить точнее» исчезает на сессию. |
getCurrentPosition timeout 15s | Toast «Не удалось определить местоположение». Retry возможен. |
| Координаты вне РФ | /store/geo/locate 422 → toast «Адрес вне зоны обслуживания». |
6.5 Legacy-продукт со сломанным city_id
После ГАР-импорта city_id сохраняются (по плану — без breaking changes). Защита:
useGeoCascade.initializeловит 404 наregions/:id/citiesили отсутствие city_id в списке → каскад сбрасывается, баннер «Город из карточки больше не доступен, выберите заново».- Submit-guard блокирует.
6.6 Пустые состояния
| Где | Что показываем |
|---|---|
| Suggest, q < 3 символов | «Введите минимум 3 символа» |
| Suggest, q ≥ 3, 0 результатов | «Адрес не найден — попробуйте уточнить запрос» |
| GeoCityPicker, поиск пустой | «Ничего не найдено» + кнопка «Очистить поиск» |
| Каскад городов: 0 в регионе | Не должно случиться, но защита: «Города не загружены» + retry |
6.7 Race conditions
| Случай | Защита |
|---|---|
| Быстрый ввод | AbortController отменяет предыдущий запрос |
| Каскад: пользователь выбрал город до загрузки districts | Suggest disabled пока promise pending |
| GeoIpDetect ещё не вернулся, CityPicker уже открыт | Баннер ждёт promise; picker работает независимо |
6.8 Кэширование
- Регионы (85 шт) — кэш на сессию в
geoStore.regions. - Города по региону — кэш в
useGeoCascade.cities, инвалидируется при смене региона. - Suggest — LRU 20 ключей в
useGeoSuggest. geoLookup.cityMap/districtMap— кэш на сессию (может пухнуть до ~1100 ключей — приемлемо).geo_ip— кэш в Nuxt payload, на клиенте не повторяется.
6.9 Accessibility
- AddressAutocomplete —
aria-autocomplete="list", навигация ↑↓, Enter, Esc. - GeoCityPickerModal —
role="dialog", фокус-трап, Esc. - «Уточнить точнее» —
aria-busy="true"пока promise pending, screen-reader announce «Определяем местоположение».
7. Backend Dependencies (БЛОКЕРЫ старта фронт-работ)
Фронт-эпик не стартует до письменного подтверждения от бэк-команды по всем пунктам ниже. Все требования — следствие premortem-решений (см. premortem-файл, дыры H-001…H-006).
7.1 ADR Address Identifier Architecture (H-006)
Owner: — Дедлайн: — Что: написать ADR partizap-docs/docs/dev/adr/ADR-XXX-address-identifier.md. Опции: internal address_id (UUID) с addresses (address_id, source, source_value) таблицей vs оставление fias_id в публичном контракте. Решение фронта: внутренний address_id. ADR фиксирует. Acceptance: ADR согласован с бэк-командой и архитектором; openapi.yaml обновлён под address_id.
7.2 Backend-таск на batch-геокодинг домов (H-003)
Owner: — Дедлайн: — Что: новый YouTrack-таск под DEV-402 (subtask), DEV-380 → blockedBy на него. Реализует batch-геокодинг адресов уровня дома, источник — OSM Nominatim self-host или DaData геокод-API (выбор по budget + квоте + покрытию малых городов). Таблица houses.coords. Acceptance: /store/geo/resolve для топ-10 случайных домов в МСК+СПб возвращает coords и предзаполненный metro_station_id если станция < 1500m. Manual проверка по карте.
7.3 GeoResolver: address_id ↔ region/city/district (H-006)
Owner: — Дедлайн: — Что: на сохранении продукта POST /vendor/products бэк извлекает из address_id правильные region_id, city_id, district_id через GeoResolver. Read-path продукта возвращает эти поля для рендера в каталоге. Acceptance: unit-тест бэка покрывает 5 случаев (московский дом, питерский, региональный город, посёлок, спорный fias_id).
7.4 SLO suggest (H-004)
Owner: — Дедлайн: — Что: записать в openapi.yaml SLO для /store/geo/suggest: P95 ≤ 200ms, P99 ≤ 500ms при 100 RPS на полном ГАР. Нагрузочный тест бэка через k6 (или wrk) с gold-set queries × 100 RPS × 5 мин на dev-стенде. Acceptance: evidence отчёт k6 с pass на dev-стенде до мержа фронт-эпика.
7.5 Rate-limit + anon-session (H-005)
Owner: — Дедлайн: — Что: бэк выдаёт гостям cookie PARTIZAP_SESSION (HTTP-only, при первом GET страницы). Rate-limit на /store/geo/suggest: 60 req/min/session + 600 req/min/IP. При превышении — 429 с Retry-After заголовком. Acceptance: load test 1000 RPS не валит сервис; нормальный юзер не получает 429 за разумный сценарий (форма + picker одновременно).
7.6 Acceptance suggest QA: gold-set (H-002)
Owner: — Дедлайн: — Что: бэк держит словарь синонимов и алиасов на стороне индекса: Питер/СПб/Спб → Санкт-Петербург; Мск → Москва; ул./пр-кт/мкр/б-р/р-н — раскрытие сокращений. Fuzzy ≥ 0.7 (pg_trgm) или typo-tolerance (Meilisearch). Gold-set 50 типичных запросов в partizap-docs/specs/geo-suggest-gold-set.md (создаётся в этом эпике). Acceptance: 50/50 запросов gold-set возвращают релевантный address_id в топ-3 на dev-стенде.
7.7 Bulk-миграция legacy-продуктов (H-001, условно)
Owner: — Дедлайн: — Что: если на момент готовности DEV-380 в проде > 50 продавцов с продуктами без address_id — создаётся отдельный backend-таск на CLI-миграцию через тот же suggest API (idempotent, dry-run, threshold confidence). Иначе шаг пропускается, форма продавца с принудительным перевыбором. Acceptance: проверка метрики «count(distinct vendor_id) where address_id is null» перед стартом фронт-работ.
8. Тестирование
8.1 Unit (Vitest) — атомы
| Файл | Покрытие |
|---|---|
useGeoSuggest.test.ts | debounce 250ms, AbortController, LRU кэш, 5xx → error, pick() с 404, empty query |
useGeoCascade.test.ts (расширить) | поиск по регионам клиентский, поиск по городам серверный, initialize() как есть |
useGeoIpDetect.test.ts | SSR useFetch, unknown confidence, 5xx → null без throw |
useGeoLocate.test.ts | getCurrentPosition success → POST locate, denied, timeout |
8.2 Unit — сценарные компоненты
| Файл | Покрытие |
|---|---|
VendorAddressForm.test.ts | Новый продукт через autocomplete; legacy режим с blocked submit; метро-гибрид (auto + manual override); suggest 5xx → error UI |
GeoCityPickerModal.test.ts | Загрузка регионов; выбор региона → города; поиск merged; multi-city чипы; «Определить автоматически» → useGeoLocate; «Применить» → geoStore + cookie |
GeoBanner.test.ts | Cookie есть → не рендерится; cookie нет + unknown → не рендерится; cookie нет + city → рендерится; «Да» → geoStore + dismissed; «Сменить» → emit open-picker; «Уточнить точнее» → useGeoLocate |
8.3 Schema-тесты
addressSuggestionSchema, resolvedAddressSchema, geoIpResultSchema — валидные / невалидные payloads (паттерн как у существующих geo.schema.test.ts).
8.4 Endpoint-тесты
endpoints.test.ts (расширить) — проверка URL-сборки для suggest, resolve, ip, locate. pnpm lint:endpoints enforces отсутствие хардкод-URL.
8.5 E2E (Playwright) — apps/main/tests/e2e/
| Файл | Сценарий |
|---|---|
geo-banner.spec.ts | X-Forwarded-For → баннер → «Да» → cookie → перезагрузка → баннер исчез |
city-picker.spec.ts | Открыть picker → найти «Казань» поиском → выбрать → шапка обновилась |
vendor-address-flow.spec.ts | Создать продукт → регион/город → «ленин» → выбрать → метро auto → сохранить → продукт с правильным адресом |
vendor-legacy-address.spec.ts | Открыть legacy-продукт → autocomplete пуст с badge → submit заблокирован → выбрать новый адрес → submit ок |
8.6 Manual regression checklist
Дополнить partizap-docs/docs/dev/frontend/manual-regression-checklist.md:
- Главная: гео-баннер при первом заходе, после «Да» не возвращается
- Шапка: смена города через picker, persist между сессиями
- Каталог: фильтр multi-city на новом справочнике
- Форма продавца: новый продукт через автокомплит
- Форма продавца: legacy требует перевыбора
- Карточка: имена города/района/метро отображаются
9. Вне scope
- PostGIS / гео-радиус поиск (отложено backend ADR-004, Phase 3)
- Карта (Leaflet / Yandex Maps) в карточке продукта — отдельный таск
- Адрес до квартиры — только до дома (как в backend плане)
- Admin CRUD геосправочника — отдельный таск; в этом эпике только просмотр + поиск
- Перенос multi-city из cookies в auth-профиль — фича персонализации
- A/B тест IP-баннера vs модалки — отложено
- Перевод на Meilisearch — backend-решение, фронт-контракт остаётся
- EN-локаль — только ru
- Bulk-миграция legacy — условно (см. §7.7)
10. Открытые вопросы (от premortem)
| ID | Вопрос | Адресат |
|---|---|---|
| H-006-1 | UUID генерируется на бэке при resolve или на этапе миграции схемы? | Backend |
| H-006-2 | Нужна ли версионность addresses (history) для аудита смены источников? | Backend / архитектор |
| H-003-1 | Достаточно ли покрытия OSM в малых городах РФ? | Backend |
| H-003-2 | Стоимость и time-to-bootstrap self-host Nominatim для всей РФ? | DevOps |
| H-004-1 | Работает ли pg_trgm на полном ГАР (~30M строк) или сразу Meilisearch? | Backend |
| H-004-2 | Где хранится Meilisearch контейнер на инфраструктуре? | DevOps |
| H-005-1 | Rate-limit state хранится в Redis или DB? | Backend |
| H-005-2 | Анонимная сессия создаётся на SSR (Nuxt) или на первом API-запросе? | Backend / Frontend |
| H-002-1 | Словарь синонимов хранится в БД или в коде бэка? | Backend |
| H-002-2 | Кто отвечает за поддержку gold-set после релиза? | Product / QA |