BR 4.1 — External Menu Builder и монитор кассы

Статус — драфт

Требования сформированы. Не начинать реализацию — сначала проходит /process-br 4.1 через все слои workflow.

Связь с BR 4.2

BR 4.1 — это базовый конструктор + канал «монитор/JSON». BR 4.2 (отдельная) добавит к нему override модификаторов и push в агрегаторы (Yandex.Eda, Коала). Их разделение оправдано тем что 4.1 само-достаточно — даёт видимую ценность (рекламный монитор в зале) без зависимости от внешних API.

Контекст

В ERP существует базовое меню = вычисляемая сущность (каталог × прейскурант × стоп-листы). Это работает для внутренней кухни (POS, KDS, склад), но не покрывает внешние каналы:

  1. Монитор над кассой (рекламный баннер «Digital Menu Board») — вешается в зале, должен показывать товары с ценами/фотками для гостей
  2. Универсальный JSON-экспорт — для интеграций с любыми третьими системами (1C, ERP-партнёра, BI)
  3. (в BR 4.2) Push в Yandex.Eda / Коала — там нужны cвои особенности: завышенная цена под комиссию, переименование, иерархическая структура модификаторов

Внешние каналы требуют:

  • Независимого набора товаров (не все позиции каталога идут в монитор / в Яндекс)
  • Override полей (имя, описание, цена) per-канал
  • Возможности скрывать без удаления
  • Произвольной группировки и порядка
  • Жёсткой связи с оригинальным товаром в каталоге — чтобы при правках цены/имени каталог-первоисточник продолжал работать как для кассы, так и для остальных каналов

Цель

Дать владельцу франшизы конструктор для создания сколько угодно внешних меню разных каналов из товаров каталога, с переопределением полей и автоматической работой stop-list.

В BR 4.1 покрываем 2 канала:

  • tv_screen — рекламный монитор в зале (Live URL для Chromium kiosk + Offline ZIP экспорт)
  • json — универсальный JSON-экспорт (для будущих интеграций)

Другие каналы (yandex_eda, koala) появятся в BR 4.2 как расширение существующей механики.


§1. Скоуп

1.1. Что входит

Сущности:

  • external_menu — заголовок меню (имя, канал, ТТ-привязка, статус)
  • external_menu_item — товар в меню с override-полями (имя, описание, цена, видимость, порядок)
  • external_menu_category — категории внутри меню (можно переименовать, переупорядочить)

Override per-товар (хранится только если значение задано, иначе fallback в каталог):

  • Имя товара (override_name)
  • Описание (override_description)
  • Цена (override_price) — приоритет: override > прейскурант > catalog.price
  • Видимость (visible: bool) — можно скрыть без удаления
  • Категория в этом меню (можно положить в категорию отличную от каталога)
  • Порядок отображения

Stop-list:

  • Товары попавшие в стоп-лист каталога автоматически скрыты при рендере. Возможность раскрытия не делаем — упрощение.

Каналы рендера:

  • tv_screen — HTML-страница на нашем сервере с 2-3 готовыми шаблонами (классическая сетка карточек, слайдер а-ля McDonalds, список с большой ценой)
  • json — экспорт всего меню в универсальный JSON

Кол-во меню: без ограничений. Один товар может быть в N меню одновременно.

1.2. Что НЕ входит (отложено)

  • Override модификаторов (group + option) — отдельная BR 4.2
  • Push в Yandex.Eda через Aggregator Service — BR 4.2
  • Push в Коалу — BR 4.2
  • Сезонность / расписание показа (завтраки 7-11, обеды 11-15) — P1+
  • Override фотографий товара — P1+ (фотки из каталога 1:1)
  • Версионирование меню (история опубликованных версий) — P1+
  • Маркетинговые баннеры / акции на мониторе — P1+

§2. Сценарии использования

2.1. Создание меню для монитора над кассой

  1. Владелец заходит в админку → раздел «Внешние меню» → кнопка «Создать»
  2. Выбирает канал «Монитор кассы», задаёт имя «Бар — основной экран», привязывает к ТТ
  3. На странице редактора видит каталог товаров → drag-drop в правую панель «Товары меню»
  4. Группирует по категориям (можно переименовать категории, перетянуть в нужный порядок)
  5. По желанию — открывает товар → переопределяет цену (например, в зале дороже на 10%) или название
  6. Нажимает «Опубликовать»
  7. Получает уникальный URL: https://menu.nirbi.ru/r/abc123 — это URL для Chromium kiosk-mode на мини-ПК монитора
  8. (Опционально) Скачивает ZIP-архив для оффлайн-копии на флешке

2.2. Универсальный JSON-экспорт

Аналогично, но канал = json. По публикации владелец получает endpoint GET /api/v1/external-menus/{id}/export.json (с авторизацией) который отдаёт меню в JSON. Это для будущих интеграций (партнёров, BI-аналитики).

2.3. Stop-list работает прозрачно

  • Менеджер ТТ в стоп-листе ставит «Капучино» как недоступный
  • Через секунды этот товар исчезает с монитора в зале (live-обновление через WebSocket / SSE)
  • Когда возвращают доступность — товар появляется обратно
  • Владелец ничего не делает в External Menu — всё автоматом

2.4. Удалили товар в каталоге

  • Товар из каталога удалён
  • В external_menu_item проставляется status=orphan
  • В админке банер «1 товар удалён в каталоге, удалить из этого меню?»
  • При рендере на монитор orphan-товары не показываются

§3. Сущности (бизнес-уровень)

External Menu

ПолеОбязательностьОписание
ИмяОбязательноДля админки. «Монитор кассы — Бар», «Универсальный JSON для 1С»
КаналОбязательноtv_screen / json (в BR 4.2 + yandex_eda, koala)
Привязка к ТТОпциональноNULL — на всю сеть; иначе конкретная ТТ
СтатусОбязательноdraft / published
Slug (для tv_screen)АвтоURL-frienly идентификатор для render URL
Шаблон рендера (только tv_screen)Обязательно для tv_screengrid / slider / list
Дата создания / обновленияАвто

External Menu Category

ПолеОбязательностьОписание
ИмяОбязательноМожно отличаться от каталог-категории
Привязка к каталог-категорииОпциональноЕсли задана — берётся имя из каталога если override пуст. Можно создать кастомную категорию специально для этого меню
Порядок отображенияОбязательно
ИконкаОпциональноДля шаблонов с иконками

External Menu Item

ПолеОбязательностьОписание
Связь с товаром каталогаОбязательноFK на products.id — якорь, не nullable. Если товар удалён → status=orphan
Категория в этом менюОбязательноFK на external_menu_category
Override имениОпциональноЕсли NULL — берём product.name
Override описанияОпциональноЕсли NULL — берём product.description
Override ценыОпциональноЕсли NULL — берём прейскурант ?? product.price
ВидимостьОбязательноtrue/false. Можно скрыть без удаления
ПорядокОбязательноdisplay_order в категории
СтатусОбязательноok / orphan (если оригинал удалён)

§4. Бизнес-правила

  1. Связь с каталогом обязательна — нельзя создать external_menu_item «с нуля». Только по существующему product_id.
  2. Один товар может быть в N меню — никаких UNIQUE на product_id.
  3. Iерархия цены: override_price > price_list_price > product.price. Где price_list_price — цена из прейскуранта для ТТ к которой привязано меню (если ТТ задана).
  4. Stop-list скрывает автоматически — при рендере фильтруем по текущему стопу ТТ. Если у меню store_id=NULL (на всю сеть) — стоп-листом не управляется (показываем всё что доступно где-либо).
  5. Удаление товара в каталоге — каскадно меняет status=orphan во всех external_menu_item. Не удаляем физически — для аудита и возможности восстановления.
  6. Восстановление товара — если в каталоге удалённый товар восстановили, владелец должен явно нажать «Восстановить» в external_menu_item. Авто-восстановления нет.
  7. Категории — могут совпадать с каталог-категорией (тогда наследуется имя), либо быть кастомными (например «Хиты продаж» — собранные руками для монитора).
  8. Slug для tv_screen — генерируется автоматом (например menu-{external_menu_id_short}), уникален глобально. Можно изменить вручную.
  9. Live-обновление монитора — при изменении меню (override, видимость, добавление/удаление товара, попадание в стоп-лист) монитор должен подхватить изменение в течение 30 сек.
  10. Offline ZIP — содержит index.html + assets (картинки, шрифты). Открывается локально без интернета (двойной клик на index.html). На момент скачивания — снимок состояния.

§5. Каналы рендера

5.1. tv_screen — Монитор над кассой

Целевое железо: мини-ПК или встроенный SoC телевизора в Chromium kiosk-mode. Подключение chrome --kiosk https://menu.nirbi.ru/r/{slug} через автостарт Windows.

Шаблоны (минимум 2):

  • Grid — классическая сетка карточек (фото + имя + цена). Подходит для кофеен, бургерных
  • Slider — крупный фокус на одном товаре, авто-перелистывание. Подходит для рекламного режима
  • (опционально) List — длинный список с большой ценой справа. Для табло «бизнес-ланч»

Адаптивность:

  • 16:9 1080p (основной)
  • 16:9 4K
  • 9:16 вертикальный (на стене у входа)

CSS-clamp + media queries автоматически масштабируют.

Live-обновление:

  • WebSocket / SSE подключение от страницы к серверу
  • Когда меню изменилось — сервер шлёт menu_updated → страница плавно перерисовывает (без F5)
  • Polling fallback каждые 30 сек если WebSocket упал

Offline ZIP:

  • Кнопка «Скачать офлайн-копию» в админке
  • Архив: index.html + style.css + assets/{images,fonts}
  • Без auto-refresh (snapshot на момент скачивания)
  • Для редких оффлайн-точек

5.2. json — Универсальный экспорт

  • Endpoint: GET /api/v1/external-menus/{id}/export.json (Bearer JWT)
  • Структура:
    {
      "menu_id": "...",
      "name": "...",
      "channel": "json",
      "store_id": "...",
      "generated_at": "...",
      "categories": [
        {
          "id": "...",
          "name": "Кофе",
          "items": [
            {
              "product_id": "...",
              "name": "...",
              "description": "...",
              "price": 250.00,
              "image_url": "...",
              "in_stop_list": false
            }
          ]
        }
      ]
    }
  • Stop-list применён: товары в стопе либо отсутствуют, либо in_stop_list: true (на выбор)

§6. Ролевая модель

ДействиеВладелец франшизыВладелец партнёраМенеджер ТТКассир
Видеть раздел «Внешние меню»✅ (только свои ЮЛ)
Создать меню✅ (только свои ТТ)
Редактировать меню✅ (свои)
Опубликовать / снять с публикации✅ (свои)
Удалить меню✅ (свои)
Скачать ZIP✅ (свои)
Открыть live URLпублично (URL знаешь — открыл)

Permissions: новый external_menus.read + external_menus.edit. Аналогично существующим menu.read/edit но отдельные потому что управление меню для внешних каналов — другая операция (затрагивает имидж, цены для гостей).

§7. Открытые вопросы

  1. Монитор kiosk-mode конфигурация — кто настраивает Chromium на старте Windows? Прилагаем ли инструкцию владельцу или это его IT-ответственность?
  2. Несколько мониторов в одной ТТ (бар + зал) — это разные external_menu или один с разными «зонами»? Дефолт: разные external_menu (так гибче).
  3. Обновление цены при изменении прейскуранта — если override не задан, при пересчёте прейскуранта меню должно обновиться live на мониторе. Подтвердить что Catalog Service публикует событие при изменении прейскуранта.
  4. Индикация в админке «Этот товар в N меню» — чтобы владелец понимал последствия удаления/правки в каталоге. Полезно, но не критично.
  5. JSON export — публичный endpoint или с auth? Голосую за JWT.
  6. Slug для tv_screen — short URL — нужен ли красивый поддомен menu.nirbi.ru или путь https://erp-test.nirbi.ru/r/{slug} ок? Поддомен потребует DNS + nginx + SSL. Дефолт MVP — путь.

§8. Фазинг

ФазаЧтоСрок
A. Backend — сущности и API3 миграции (external_menu, external_menu_category, external_menu_item) + CRUD endpoints в Catalog Service + JSON exportСпринт 1
B. Frontend — конструкторРаздел /external-menus в админке + список + редактор с drag-drop + override-формыСпринт 1-2 (параллельно)
C. Render для монитораEndpoint /r/{slug} с шаблонами + WebSocket live-update + offline ZIP экспортСпринт 2
D. Smoke test и pilotУстановить на реальный монитор в demo-coffee → 1 неделя реального использованияСпринт 3

§9. План работ — файлы под создание/правку

CREATE

  • 08-Specs/Админка Франшизы/Внешние меню.md — бизнес-спека
  • 09-Frontend Specs/Админка Франшизы/Внешние меню — Список.md
  • 09-Frontend Specs/Админка Франшизы/Внешние меню — Конструктор.md
  • 09-Frontend Specs/Админка Франшизы/Внешние меню — Шаблоны монитора.md
  • 07-Tasks/Decomposition/4.1 External Menu/Overview.md
  • 07-Tasks/Decomposition/4.1 External Menu/Catalog Service.md
  • 07-Tasks/Decomposition/4.1 External Menu/Admin BFF.md
  • 07-Tasks/Decomposition/4.1 External Menu/Admin Franchise.md
  • 07-Tasks/Decomposition/4.1 External Menu/Menu Renderer.md (может быть отдельным мини-сервисом или endpoint в Catalog BFF — решим в декомпозиции)

EDIT

  • 03-Services/Catalog Service/API.md — новые endpoints (/external-menus, /external-menus/{id}/items, /external-menus/{id}/export.json, /r/{slug})
  • 03-Services/Catalog Service/Data Model.md — 3 новые таблицы + ER
  • 03-Services/Catalog Service/Events.md — топик external_menu.updated
  • 03-Services/Catalog Service/Overview.md — функция «External Menu Builder» в зоне ответственности
  • 08-Specs/Админка Франшизы/Overview.md — добавить модуль «Внешние меню» в навигацию
  • 08-Specs/Админка Франшизы/Роли.md — добавить permissions external_menus.read / external_menus.edit

CODE (после verify)

  • erp-catalog-service — миграция, entities, repositories, service, controllers, JSON export, WebSocket gateway, render endpoint
  • erp-admin/bff — proxy-routes
  • erp-admin/web — раздел «Внешние меню» (список, редактор, превью)
  • (опционально) отдельный menu-renderer-service — если решим вынести render в свой контейнер

§10. Verification

После реализации (на erp-test.nirbi.ru + demo-coffee tenant):

  1. Создать меню «Бар основной» канал tv_screen → опубликовать → получить URL
  2. Открыть URL в Chrome kiosk-mode на тестовой машине → должен отрисоваться шаблон с товарами
  3. Изменить цену в каталоге товара → монитор обновляется через ≤30 сек (live)
  4. Поставить товар в стоп-лист → исчезает с монитора
  5. Снять стоп → возвращается
  6. Скачать offline ZIP → распаковать на другой машине → открыть index.html без интернета → отображается snapshot
  7. JSON export → структура соответствует §5.2
  8. Удалить товар в каталоге → в админке external_menu_item появляется badge «orphan» → на мониторе товар скрыт

§11. Что входит в BR 4.2 (на будущее)

Чтобы зафиксировать границу скоупа:

  • external_menu_modifier_group + external_menu_modifier_option — override модификаторов
  • Канал yandex_eda — push в Yandex через расширение Aggregator.MenuSnapshotService
  • Канал koala — push в Коалу
  • Listener external_menu.updated в Aggregator → инвалидация menu_snapshots
  • Поле binding.external_menu_id в Aggregator
  • Расширение YandexEdaConnector с поддержкой override модификаторов

BR 4.2 запускается после того как BR 4.1 в проде и работает.

§12. Ссылки