# План развития сайта KILN: СБИС, меню, заказы и личный кабинет

Статус документа: проект решения до начала реализации.

Приоритет: деньги, целостность заказов, безотказность и понятные действия для клиента и ресторана. Яндекс.Меню временно исключено из реализации до уточнения доступного API; существующий feed сохраняется, но не является источником истины.

## 1. Зафиксированные решения

1. СБИС — первая инстанция для цены, веса/граммовки, наличия и доступного количества.
2. Название, описание и фото редактируются на сайте; обмен этими полями с СБИС выполняется только вручную, с просмотром различий.
3. Сайт может иметь собственные категории. Привязка категории сайта к категории СБИС опциональна; расхождение должно быть явно видно администратору.
4. Сайт не создаёт и не удаляет номенклатуру СБИС.
5. Удаление карточки сайта означает сначала архивирование и отвязку от СБИС. Из архива возможно отдельное окончательное удаление локальной карточки.
6. Исчезновение позиции из актуального прайса СБИС автоматически архивирует карточку сайта и снимает привязку, но только после надёжного подтверждения полного снимка каталога.
7. На первом этапе используется один административный аккаунт, а не общий токен в cookie.
8. SMS-PIN применяется только для регистрации/привязки телефона к личному кабинету. Для гостевого заказа и брони подтверждение телефона не требуется.
9. Заказ и бронь без аккаунта остаются доступны. Бот отправляет штатную одноразовую ссылку на заказ по указанному номеру.
10. Самовывоз, отложенный самовывоз и отложенная доставка являются штатными сценариями и регулируются ботом.
11. Ошибка или устаревание стоп-листа не должны приводить к потере заявки. Неоднозначный заказ переводится на ручную модерацию ресторана.
12. Оплата, возврат и итоговый финансовый статус остаются в СБИС/боте.

## 2. Фактическое состояние production

- Next.js 16, React 19, PostgreSQL 17, Drizzle.
- 9 локальных категорий, 31 опубликованная карточка, 31 вариант.
- Все 31 карточка имеют источник `yandex_initial_import`.
- Ни одна карточка не привязана к СБИС.
- В локальном снимке каталога СБИС находится 89 позиций.
- `orders_enabled=false`, поэтому некорректное оформление сейчас закрыто.
- `/yandex-menu` отвечает, но это только исходящий feed.
- Тест меню не запускается без отсутствующего файла `data/import/yandex-menu-normalized.json`.

## 3. Первая очередь: небольшие исправления фронтенда

Эти изменения выполнить отдельным низкорисковым релизом до перестройки заказов.

### 3.1 Галерея на мобильных устройствах

- При открытии lightbox блокировать прокрутку `body`, сохраняя и восстанавливая предыдущее значение `overflow`.
- Компенсировать исчезновение scrollbar на desktop, чтобы страница не прыгала.
- Разнести заголовок и кнопку закрытия: заголовку задать безопасные боковые отступы, кнопке — fixed/absolute область с учётом `safe-area-inset-top`.
- Запретить прокрутку основной страницы, но оставить управление фотографией и горизонтальную ленту миниатюр.
- Проверить Escape, focus return, swipe/кнопки и закрытие по backdrop.

### 3.2 Главный экран на мобильных устройствах

- Убрать универсальный `-translate-y-[100px]` для мобильного размера.
- Ввести адаптивное позиционирование hero-текста с учётом высоты header/logo и safe area.
- Сдвинуть единым блоком приветствие, `КИЛН` и «Мясо из дровяной печи» ниже.
- Проверить высоты 568, 667, 740, 844 и 932 px, а также ширины 320–430 px.

### 3.3 Favicon/PWA icons

- Фавикон лежит в /kiln/content - favicon.svg - перевести, используя фирменный цвет в нужный формат.
- Проверить читаемость в 16×16, 32×32, 180×180, 192×192 и 512×512.
- Одновременно обновить favicon.ico/svg, apple-touch-icon и PWA icons.

### 3.4 Названия категорий

- Переименовать «Мясо из Коптильни» в «МЯСО ИЗ ПЕЧИ».
- Переименовать «Уличная еда» в «УЛИЧНАЯ ЕДА».
- Выполнить миграцией/административной операцией в PostgreSQL, а не только в статическом `src/data/menu.ts`.
- Не менять slug без отдельной необходимости, чтобы не ломать ссылки и сохранённые состояния.

### 3.5 Очистка медиа

- После загрузки новой локальной версии построить список фактических ссылок из
  кода, CSS, metadata, feed и manifest.
- Удалить неиспользуемые PDF и старые варианты логотипа только после этой
  проверки; рабочие favicon, OpenGraph, печатные и email-ресурсы не считать
  мусором по одному лишь отсутствию импорта в TypeScript.
- Перед удалением сохранить перечень и проверить production build на битые URL.
- Очистку выполнить отдельным коммитом, чтобы при необходимости восстановить
  конкретный ресурс без отката функциональных изменений.

## 4. Целевая модель меню

### 4.1 Разделение сущностей

`menu_items` — редакционная карточка сайта:

- название, описание, короткое описание;
- фото, SEO, сортировка;
- публикация на сайте;
- разрешение заказа/доставки;
- архивный статус;
- ссылка на СБИС через отдельную связь.

`sbis_catalog_items` — последний снимок СБИС:

- `sbis_id`, position id, SBIS category id/name;
- название/описание/фото из СБИС;
- цена;
- вес, единица измерения и порционность;
- остаток и доступность;
- признак нахождения в актуальном прайсе;
- время получения и идентификатор снимка.

`menu_item_sbis_links` — явная связь:

- `menu_item_id` unique;
- `sbis_id` unique;
- `linked_at`, `linked_by`;
- снимки/хеши полей в момент последнего pull/push;
- `last_pull_at`, `last_push_at`, результат и ошибка;
- `unlinked_at`, `unlink_reason` для аудита.

Не использовать `source_external_id` как одновременно источник импорта и активную связь: это мешает аудиту и безопасной перепривязке.

### 4.2 Категории

В `menu_categories` добавить:

- nullable `sbis_category_id`;
- `sbis_category_name_snapshot`;
- `category_sync_state`: `unlinked | matched | name_mismatch | missing_in_sbis`;
- `archived_at` вместо одного `is_active` для прозрачной истории.

Собственная категория сайта может содержать товары из нескольких категорий СБИС. UI должен показывать это как расхождение, а не запрещать.

### 4.3 Архив

- Первое удаление: транзакционно снять связь СБИС, выключить публикацию/заказ и установить `archived_at`.
- В архиве доступны восстановление, повторная привязка и окончательное локальное удаление.
- Окончательное удаление выполняется только вручную оператором и допускается
  независимо от участия карточки в заказах.
- Строки завершённого заказа хранят собственный неизменяемый снимок названия,
  количества, цены и суммы и не имеют внешнего ключа, способного удалить или
  изменить их вслед за карточкой меню.
- Ни одна операция архива не вызывает удаление/создание номенклатуры СБИС.

## 5. Административный доступ

### 5.1 Один защищённый аккаунт

Заменить raw `ADMIN_MENU_TOKEN` в cookie:

- логин + пароль администратора из env/секрет-хранилища;
- пароль хранить только как Argon2id/scrypt hash;
- случайная серверная сессия, в БД только hash токена;
- cookie: `HttpOnly`, `Secure`, `SameSite=Strict`, ограниченный срок;
- ротация сессии после входа;
- logout и принудительный отзыв всех сессий;
- CSRF-токен для POST/PATCH/DELETE;
- rate limit входа с задержкой и временной блокировкой;
- журнал административных операций без секретов и содержимого изображений.

### 5.2 Экран карточки

Администратор должен видеть:

- локальные значения;
- выбранную позицию СБИС и возможность ввести ID вручную;
- поиск/выбор из актуального каталога СБИС;
- цену, вес, остаток и время обновления как read-only;
- список доступных прайсов получать через `GET /api/sbis/prices`, выбранный
  прайс отображать администратору; переключение выполняется оператором через
  настройку `SBIS_PRICE_ID` бота с последующим обновлением каталога;
- состав позиции и явный `Output` СБИС показывать как источник данных, ручной
  вес сайта хранить отдельно и явно помечать расхождение;
- категорию сайта и категорию СБИС рядом;
- статусы `не привязано`, `совпадает`, `есть расхождения`, `нет в прайсе`, `данные устарели`;
- diff названия/описания/фото;
- отдельные pull/push для каждого разрешённого поля;
- историю связи и обмена.

### 5.3 Правила привязки

- Один `sbis_id` может быть связан только с одной активной карточкой сайта.
- Ручной ID сначала проверяется свежим `GET /api/catalog` или отдельным read API.
- Привязка невозможна к отсутствующей позиции без явного аварийного режима; аварийный режим не разрешает публикацию/заказ.
- При привязке администратор выбирает, какие редакционные поля первоначально забрать из СБИС.
- Цена и вес всегда заменяются значениями СБИС без выбора.

## 6. Синхронизация СБИС

### 6.1 Каталог и стоп-лист

- Фоновая задача сайта получает каталог бота каждые 30–60 секунд.
- Нужна блокировка задания в PostgreSQL (`pg_try_advisory_lock`), чтобы несколько экземпляров Next.js не запускали sync одновременно.
- Каждый полный успешный ответ получает `snapshot_id`.
- Только полный валидный снимок может помечать отсутствующие позиции как исключённые из прайса.
- Автоархив выполнять после двух последовательных полных снимков без позиции либо после конфигурируемого grace period (рекомендация: 5 минут), чтобы кратковременный сбой не архивировал меню.
- Автоархив: снять связь, выключить сайт/заказ, записать причину `removed_from_sbis_price`, уведомить администратора.
- Ошибка запроса не считается пустым каталогом.

### 6.2 Свежесть

Хранить:

- время последней попытки;
- время последнего успеха;
- источник/версию снимка;
- длительность запроса;
- число полученных категорий/позиций;
- sanitised error;
- возраст стоп-листа.

Порог 3 минуты применяется не как безусловный отказ, а как триггер модерации заказа.

### 6.3 Ручной обмен контентом

`Pull from SBIS`:

1. Получить свежую карточку СБИС.
2. Показать diff.
3. Администратор выбирает название/описание/фото.
4. Применить одной DB-транзакцией.
5. Записать audit event.

`Push to SBIS`:

1. Проверить активную связь и существование позиции.
2. Показать payload/diff.
3. Потребовать повторное подтверждение.
4. Отправить только название/описание/фото.
5. Перечитать СБИС и подтвердить фактический результат.
6. Частичный успех показывать по полям; локальные данные не откатывать молча.

## 7. Граница с API бота

Сайт не реализует протокол СБИС, очередь ресторана, отправку SMS/email или
логику Достависты. Это отдельный проект `kiln-bot` и отдельное ТЗ. Сайт работает
только с опубликованным `api_site_contract.txt` и должен:

- вызывать API исключительно со своего backend;
- сохранять `client_ref` и продолжать polling после timeout;
- корректно показывать `pending_confirmation`, `processing`,
  `await_payment`, `rejected` и `needs_operator`;
- не показывать оплату до получения `pay_url`;
- обновлять интерфейс при изменении контракта только после совместимой версии
  API бота;
- никогда не вызывать legacy-методы создания/удаления номенклатуры СБИС.

## 8. Надёжный контур создания заказа

### 8.1 Типы заказа

Поддержать четыре явных сценария:

1. доставка сейчас;
2. самовывоз сейчас;
3. доставка к дате/времени;
4. самовывоз к дате/времени.

UI выбирает способ и режим времени отдельно. `due` формируется только для отложенного варианта.

### 8.2 Checkout

1. Получить `/api/info` и показать реальные часы/зоны/условия.
2. Проверить каждую строку на активную связь СБИС.
3. Выполнить свежую серверную проверку цены и стоп-листа.
4. Для доставки проверить адрес/зону через бот; убрать mock Яндекс Доставки.
5. Для самовывоза не требовать адрес и не начислять доставку.
6. Передавать email, `comment` и отдельный `courier_comment` по актуальному контракту.
7. Не требовать SMS-PIN у гостя.
8. Создать `client_ref` один раз и сохранить до однозначного исхода.
9. При timeout сначала запросить статус по тому же `client_ref`, а не создавать новый заказ.

### 8.3 Идемпотентность сайта

Добавить локальную таблицу `order_attempts`:

- `client_ref` unique;
- hash нормализованного payload;
- user/session id nullable;
- состояние запроса и ответ бота без секретов;
- timestamps;
- `bot_order_id` nullable;
- moderation id nullable.

Повтор с тем же ref и другим payload отклонять. Незавершённую попытку восстанавливать после перезагрузки страницы.

### 8.4 Обработка ошибок

- `pending_confirmation` после расхождения: сохранить корзину и ждать решения
  ресторана; если бот позднее вернёт обновлённую цену, запросить новое согласие
  клиента до оплаты.
- `offhours`: предложить допустимое время или самовывоз, используя текст API.
- `min_sum`: показать недостающую сумму и актуальную зону.
- `zone_unknown`: не обещать доставку; дать исправить адрес или выбрать самовывоз.
- `datetime_past`: вернуть к выбору времени.
- `conflict`/timeout: polling по существующему ref.
- Недоступный СБИС, stale stop-list и неоднозначный остаток: бот сам сохраняет
  заявку на модерацию; сайт не отправляет второй заказ.

## 9. Отображение модерации на сайте

Сайт не управляет решением ресторана. Он сохраняет корзину и показывает
понятное состояние по ответу бота:

- `pending_confirmation`: заявка принята, ресторан скоро подтвердит её или
  свяжется с клиентом;
- `processing`: не отправлять повторный заказ;
- `await_payment`: показать сумму и единственную кнопку оплаты;
- `rejected`/`needs_operator`: не показывать техническую причину, сохранить
  контакты и сообщить, что ресторан свяжется с клиентом.

Все отложенные заказы проходят это состояние до оплаты. Сайт продолжает
polling по тому же `client_ref` и восстанавливает состояние после перезагрузки.

## 10. Личный кабинет и минимизация SMS

### 10.1 Идентичность

Пользователь может войти через Яндекс/VK либо создать локальный аккаунт. Для показа истории обязательна подтверждённая связь хотя бы с телефоном; email хранится отдельно с признаком подтверждения.

Таблицы:

- `users`;
- `user_identities` для VK/Yandex;
- `user_phones` с `verified_at`;
- `user_emails` с `verified_at`;
- `phone_verification_challenges`;
- `user_order_links`/audit доступа при необходимости.

### 10.2 SMS-PIN

- Сайт генерирует криптографический 6-значный PIN.
- В БД хранится только HMAC/hash PIN, срок жизни и число попыток.
- Отправка через `/api/sms/pin` бота.
- Cooldown 60 секунд, не более 3 отправок за час на номер/IP и дневной лимит.
- Не раскрывать, существует ли номер в системе.
- После успешной проверки пометить телефон подтверждённым и удалить/закрыть challenge.
- Повторно не отправлять SMS для уже подтверждённого номера при обычном входе: использовать сессию, OAuth или email link.

### 10.3 История

После подтверждения телефона backend сайта вызывает `/api/history/{phone}`. Историю нельзя получать непосредственно из браузера по произвольному номеру. Показать активные и архивные заказы/брони, ссылки отслеживания, оплату и статусы без внутренних операторских деталей.

## 11. Наблюдаемость и защита денег

- Структурные события для каждого перехода состояния заказа.
- Correlation id: `client_ref`, moderation id, SBIS id, DV id.
- Метрики: ошибки СБИС, возраст каталога/стоп-листа, очередь модерации, время ответа ресторана, неоплаченные/оплаченные конфликты.
- Alert при необработанной модерации, ошибке создания СБИС, оплаченной проблемной позиции и рассинхронизации суммы.
- Никаких телефонов, email, PIN, адресов, API-ключей и платёжных ссылок в обычных логах.
- Резервное копирование PostgreSQL и MariaDB; проверяемое восстановление.
- Feature flags отдельно для гостевых заказов, отложенных заказов, самовывоза, moderation fallback и личного кабинета.
- Kill switch, который запрещает новые заказы, но не ломает статус существующих.

## 12. Тестовая стратегия

### 12.1 Обязательные автоматические сценарии

- привязка/перепривязка/отвязка СБИС;
- запрет двух карточек на один `sbis_id`;
- diff и частичный push/pull;
- автоархив только после подтверждённых полных снимков;
- stale catalog не архивирует позиции;
- цена/вес не редактируются локально;
- ограничения stock_left, включая qty > остатка;
- все четыре типа заказа;
- повтор POST с тем же `client_ref`;
- timeout после фактического создания заказа;
- price mismatch;
- moderation approve/reject/retry;
- ранняя оплата отложенного заказа и поздний конфликт;
- самовывоз без адреса и delivery fee;
- SMS rate limits и одноразовость challenge;
- невозможность получить чужую историю;
- мобильная галерея и блокировка body scroll.

### 12.2 Перед production

- staging на тестовых СБИС/Достависта либо контролируемых тестовых заказах;
- таблица ожидаемых сумм и статусов;
- тест возврата через СБИС;
- тест падения сети в каждой точке checkout;
- тест перезапуска сайта/бота во время создания заказа;
- нагрузочный тест каталога и polling;
- ручной чек-лист персонала ресторана;
- документированный rollback без потери заявок.

## 13. Этапы реализации

### Этап A — фронтенд-корректировки и тестовая база

Галерея, hero, favicon, названия категорий, восстановление fixture теста, visual regression для мобильных размеров.

### Этап B — безопасный административный аккаунт

Сессии, CSRF, rate limit, аудит и миграция с raw token без блокировки текущего доступа.

### Этап C — новая модель связей СБИС

Миграции, backfill, экран поиска/ручного ID, статусы расхождений, архив. Затем вручную сопоставить текущие 31 карточку с 89 позициями СБИС.

### Этап D — фоновая синхронизация сайта

Advisory lock, полные снимки, свежесть, автоархив с grace period, dashboard состояния.

### Этап E — подключение актуального контракта бота

Подключить `/api/info`, `/api/delivery/quote`, каталог и карточку только после
публикации совместимой версии API бота. На сайте не дублировать правила СБИС.

### Этап F — checkout v2

Самовывоз/доставка сейчас/ко времени, отдельные комментарии, email, реальная калькуляция, устойчивый client_ref, точные ошибки и polling.

### Этап G — состояния модерации

Подключить polling и клиентские экраны к уже реализованной очереди бота.
Сайт не содержит операторских действий подтверждения/отклонения.

### Этап H — личный кабинет

Телефонная регистрация с единичным SMS, OAuth linking, email, защищённая история заказов и броней.

### Этап I — ограниченный запуск

Сначала самовывоз, затем доставка сейчас, потом отложенный самовывоз и последней — отложенная доставка. Каждый этап включать после наблюдения полного рабочего цикла и сверки финансов.

## 14. Условия начала работ

План составлен по версии `4b5ee25`. Перед реализацией обязательно загрузить
более новую локальную версию сайта, повторно сравнить схему, маршруты и UI и
актуализировать этот документ. Яндекс-интеграцию до получения официальной
спецификации не расширять.
