Catalog Service — Data Model
База данных: catalog_db
erDiagram categories { uuid id PK uuid franchise_id "NOT NULL" varchar name "NOT NULL, (255)" uuid parent_id FK "NULL → categories.id" integer display_order "NOT NULL, default 0" boolean is_active "NOT NULL, default true" varchar color "NULL, (7)" varchar sort_type "NOT NULL, default manual, (20)" boolean is_available_mobile "NOT NULL, default true" boolean is_available_website "NOT NULL, default true" boolean is_available_aggregators "NOT NULL, default false" timestamp created_at "NOT NULL" timestamp updated_at "NOT NULL" } products { uuid id PK uuid franchise_id "NOT NULL" varchar name "NOT NULL, (255)" text description "NULL" varchar type "NOT NULL (dish/good), (20)" uuid category_id FK "NULL → categories.id" varchar unit_of_measure "NOT NULL, (20)" varchar status "NOT NULL, default active, (20)" varchar sku "NULL, (100)" varchar barcode "NULL, (100)" integer sort_order "NOT NULL, default 0" decimal gross_weight "NULL, (8,3)" decimal net_weight "NULL, (8,3)" decimal kcal "NULL, (7,2)" decimal protein "NULL, (7,2)" decimal fat "NULL, (7,2)" decimal carbs "NULL, (7,2)" integer assembly_time "NULL" varchar color "NULL, (7)" varchar image_url "NULL, (500)" boolean is_open_price "NOT NULL, default false" boolean is_by_weight "NOT NULL, default false" boolean is_exclude_from_promo "NOT NULL, default false" boolean is_manual_discount_banned "NOT NULL, default false" boolean is_admin_only "NOT NULL, default false" boolean is_alcohol "NOT NULL, default false" boolean is_tobacco "NOT NULL, default false" boolean is_sugary_drink "NOT NULL, default false" boolean is_marked "NOT NULL, default false" boolean available_in_all_stores "NOT NULL, default true" boolean requires_kitchen "NOT NULL, default false (BR 2.5)" uuid kitchen_station_id FK "NULL → kitchen_stations.id (BR 2.5)" varchar vat_rate "NOT NULL, default vat20 (BR 3.3)" varchar payment_subject "NOT NULL, default goods (BR 3.3)" varchar payment_type "NOT NULL, default full (BR 3.3)" timestamp deleted_at "NULL" timestamp created_at "NOT NULL" timestamp updated_at "NOT NULL" } kitchen_stations { uuid id PK uuid franchise_id "NOT NULL" varchar name "NOT NULL, (50)" text description "NULL" int yellow_threshold_minutes "NOT NULL, default 5 (BR 5.1)" int red_threshold_minutes "NOT NULL, default 0 (BR 5.1)" timestamp deleted_at "NULL" timestamp created_at "NOT NULL" timestamp updated_at "NOT NULL" } kds_franchise_settings { uuid franchise_id PK "PK, FK → franchises (cross-service)" varchar new_order_sound "NOT NULL, default bell, (20)" int new_order_repeat_seconds "NOT NULL, default 30" varchar overdue_sound "NOT NULL, default alarm, (20)" smallint sound_volume "NOT NULL, default 80, 0-100" int auto_logout_minutes "NOT NULL, default 30" timestamp created_at "NOT NULL" timestamp updated_at "NOT NULL" } kitchen_stations ||--o{ products : "routed to" product_stores { uuid id PK uuid product_id FK "NOT NULL → products.id" uuid store_id "NOT NULL (opaque)" timestamp created_at "NOT NULL" } categories ||--o{ products : "has products" categories ||--o{ categories : "parent → children" products ||--o{ product_stores : "available in" modifier_groups ||--o{ modifier_options : "has options" products ||--o{ product_modifiers : "has modifiers" modifier_groups ||--o{ product_modifiers : "attached via" modifier_groups { uuid id PK uuid franchise_id "NOT NULL" varchar name "NOT NULL, (255)" varchar type "NOT NULL (group/single), (20)" integer min_amount "NOT NULL, default 0" integer max_amount "NOT NULL, default 1" varchar status "NOT NULL, default active, (20)" timestamp deleted_at "NULL" timestamp created_at "NOT NULL" timestamp updated_at "NOT NULL" } modifier_options { uuid id PK uuid modifier_group_id FK "NOT NULL" varchar name "NOT NULL, (255)" integer min_amount "NOT NULL, default 0" integer max_amount "NOT NULL, default 1" integer default_amount "NOT NULL, default 0" integer free_quantity "NOT NULL, default 0" text description "NULL" boolean is_active "NOT NULL, default true" integer display_order "NOT NULL, default 0" varchar sku_1c "NULL, (50)" timestamp deleted_at "NULL" timestamp created_at "NOT NULL" timestamp updated_at "NOT NULL" } product_modifiers { uuid id PK uuid product_id FK "NOT NULL" uuid modifier_group_id FK "NOT NULL" varchar binding_type "NOT NULL, default free, (10)" integer override_min_amount "NULL" integer override_max_amount "NULL" timestamp created_at "NOT NULL" } price_lists { uuid id PK uuid franchise_id "NOT NULL" varchar name "NOT NULL, (255)" boolean is_default "NOT NULL, default false" varchar status "NOT NULL, default active, (20)" timestamp created_at "NOT NULL" timestamp updated_at "NOT NULL" timestamp deleted_at "NULL" } price_list_items { uuid id PK uuid price_list_id FK "NOT NULL" uuid product_id FK "NOT NULL" decimal price "NOT NULL, (10,2)" } price_list_modifier_items { uuid id PK uuid price_list_id FK "NOT NULL" uuid modifier_option_id FK "NOT NULL" decimal price "NOT NULL, (10,2)" } product_stop_list { uuid id PK uuid franchise_id "NOT NULL" uuid store_id "NOT NULL (opaque)" uuid product_id FK "NOT NULL → products.id" text reason "NULL" uuid stopped_by "NOT NULL" timestamp created_at "NOT NULL" } category_stop_list { uuid id PK uuid franchise_id "NOT NULL" uuid store_id "NOT NULL (opaque)" uuid category_id FK "NOT NULL → categories.id" text reason "NULL" uuid stopped_by "NOT NULL" timestamp created_at "NOT NULL" } price_lists ||--o{ price_list_items : "product prices" price_lists ||--o{ price_list_modifier_items : "modifier prices" products ||--o{ price_list_items : "priced in" modifier_options ||--o{ price_list_modifier_items : "priced in" products ||--o{ product_stop_list : "stopped in" categories ||--o{ category_stop_list : "stopped in"
Таблицы
categories
Категории товаров. Иерархическая структура (adjacency list).
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
franchise_id | uuid | NOT NULL | — | ID франшизы |
name | varchar(255) | NOT NULL | — | Название категории |
parent_id | uuid | NULL | — | FK → categories.id (null = корневая) |
display_order | integer | NOT NULL | 0 | Порядок отображения внутри уровня |
is_active | boolean | NOT NULL | true | Активна / скрыта от клиентов |
color | varchar(7) | NULL | — | HEX-цвет для POS (напр. #FF5733) (BR 2.2) |
sort_type | varchar(20) | NOT NULL | manual | Тип сортировки товаров: manual / name_asc / name_desc / price_asc (BR 2.2) |
is_available_mobile | boolean | NOT NULL | true | Доступна в мобильном приложении (BR 2.2) |
is_available_website | boolean | NOT NULL | true | Доступна на сайте (BR 2.2) |
is_available_aggregators | boolean | NOT NULL | false | Доступна в агрегаторах (Яндекс.Еда и др.) (BR 2.2) |
created_at | timestamp | NOT NULL | now() | Дата создания |
updated_at | timestamp | NOT NULL | now() | Дата обновления |
Индексы:
idx_categories_franchise_id—franchise_ididx_categories_parent_id—parent_id
Ограничения:
FK parent_id REFERENCES categories(id) ON DELETE RESTRICT
products
Товары каталога. Изменения применяются мгновенно (без версионирования).
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
franchise_id | uuid | NOT NULL | — | ID франшизы |
name | varchar(255) | NOT NULL | — | Название товара |
description | text | NULL | — | Описание |
type | varchar(20) | NOT NULL | — | dish (блюдо) / good (продукт) (BR 1.11: type=ingredient removed — ingredients moved to Warehouse Service) |
category_id | uuid | NULL | — | FK → categories.id. NULL = “Без категории” |
base_price | — | (Убрано в BR 1.10. Цена определяется прейскурантом.) | ||
unit_of_measure | varchar(20) | NOT NULL | — | шт, кг, г, л, мл, порция |
status | varchar(20) | NOT NULL | 'active' | active / inactive |
sku | varchar(100) | NULL | — | Артикул (BR 2.1) |
barcode | varchar(100) | NULL | — | Штрихкод (UPC) (BR 2.1) |
sort_order | integer | NOT NULL | 0 | Порядок внутри категории (BR 2.1) |
gross_weight | decimal(8,3) | NULL | — | Вес брутто (кг) (BR 2.1) |
net_weight | decimal(8,3) | NULL | — | Вес нетто (кг) (BR 2.1) |
kcal | decimal(7,2) | NULL | — | Калории на 100г (BR 2.1) |
protein | decimal(7,2) | NULL | — | Белки на 100г (BR 2.1) |
fat | decimal(7,2) | NULL | — | Жиры на 100г (BR 2.1) |
carbs | decimal(7,2) | NULL | — | Углеводы на 100г (BR 2.1) |
assembly_time | integer | NULL | — | Время приготовления (мин) (BR 2.1) |
color | varchar(7) | NULL | — | Hex-цвет (#RRGGBB) для POS (BR 2.1) |
image_url | varchar(500) | NULL | — | Публичный URL изображения в S3 (BR 2.1) |
is_open_price | boolean | NOT NULL | false | Свободная цена (задаётся на кассе) (BR 2.1) |
is_by_weight | boolean | NOT NULL | false | Продажа на развес (BR 2.1) |
is_exclude_from_promo | boolean | NOT NULL | false | Исключить из акций (BR 2.1) |
is_manual_discount_banned | boolean | NOT NULL | false | Запрет ручных скидок (BR 2.1) |
is_admin_only | boolean | NOT NULL | false | Только администратор (BR 2.1) |
is_alcohol | boolean | NOT NULL | false | Алкоголь (BR 2.1) |
is_tobacco | boolean | NOT NULL | false | Табак (BR 2.1) |
is_sugary_drink | boolean | NOT NULL | false | Сахаросодержащий напиток (BR 2.1) |
is_marked | boolean | NOT NULL | false | Маркированный товар — требует сканирования DataMatrix на кассе (ЧЗ, миграция 024) |
available_in_all_stores | boolean | NOT NULL | true | Доступен во всех ТТ; если false — см. product_stores (BR 2.1) |
requires_kitchen | boolean | NOT NULL | false | Товар требует приготовления (шаурма, бургер). Определяет flow заказа. (Добавлено в BR 2.5) |
kitchen_station_id | uuid | NULL | — | FK → kitchen_stations.id. Обязательно при requires_kitchen=true, иначе NULL. (Добавлено в BR 2.5) |
vat_rate | varchar(10) | NOT NULL | vat20 | Ставка НДС для 54-ФЗ: none / vat0 / vat10 / vat20 / vat110 / vat120. Обязательно для инвойса PayKeeper. (Добавлено в BR 3.3) |
payment_subject | varchar(20) | NOT NULL | goods | Предмет расчёта (тег 1212): goods / service / work / excise / job / payment / agency / composite / another. (Добавлено в BR 3.3) |
payment_type | varchar(20) | NOT NULL | full | Способ расчёта (тег 1214): full / prepay / advance / partial_prepay / credit / credit_pay / partial. (Добавлено в BR 3.3) |
deleted_at | timestamp | NULL | — | Soft delete |
created_at | timestamp | NOT NULL | now() | Дата создания |
updated_at | timestamp | NOT NULL | now() | Дата обновления |
Индексы:
idx_products_franchise_id—franchise_ididx_products_category_id—category_iduq_products_franchise_name— UNIQUE(franchise_id, name)WHEREdeleted_at IS NULL(название уникально в рамках франшизы среди неудалённых)idx_products_kitchen_station—kitchen_station_idWHEREkitchen_station_id IS NOT NULL(BR 2.5)
Ограничения:
FK category_id REFERENCES categories(id) ON DELETE SET NULLFK kitchen_station_id REFERENCES kitchen_stations(id) ON DELETE RESTRICT(BR 2.5) — станцию нельзя удалить если на неё ссылается хотя бы один товарCHECK (requires_kitchen = false OR kitchen_station_id IS NOT NULL)(BR 2.5) — если товар требует кухни, станция обязательна
kitchen_stations
(Добавлено в BR 2.5)
Справочник кухонных/барных станций — производственных зон франшизы. Товар ссылается на станцию через products.kitchen_station_id.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
franchise_id | uuid | NOT NULL | — | Мультитенантность |
name | varchar(50) | NOT NULL | — | «Горячая кухня», «Бар», «Мангал» |
description | text | NULL | — | Свободное описание |
yellow_threshold_minutes | integer | NOT NULL | 5 | Порог жёлтой зоны на KDS-карточке: карточка желтеет за N минут до expected_ready_at. (Добавлено в BR 5.1) |
red_threshold_minutes | integer | NOT NULL | 0 | Просрочка: карточка краснеет когда now > expected_ready_at + N минут (0 = просрочка с момента дедлайна). (BR 5.1) |
deleted_at | timestamp | NULL | — | Soft delete |
created_at | timestamp | NOT NULL | now() | |
updated_at | timestamp | NOT NULL | now() |
Ограничения:
uq_kitchen_stations_franchise_name— UNIQUE(franchise_id, LOWER(name))WHEREdeleted_at IS NULLCHECK (yellow_threshold_minutes >= 0)(BR 5.1)CHECK (red_threshold_minutes >= 0)(BR 5.1)
Индексы:
idx_kitchen_stations_franchise—franchise_idWHEREdeleted_at IS NULL
Правило защиты от удаления: При попытке soft-delete станции, на которую ссылается хотя бы один товар (products.kitchen_station_id = X AND products.deleted_at IS NULL), сервис возвращает 422 STATION_IN_USE с указанием количества/списка связанных товаров.
kds_franchise_settings
(Добавлено в BR 5.1)
Per-франшиза настройки KDS-приложения (звуки, интервалы повтора, авто-логаут). Одна строка на франшизу. При первом запросе франшизы — auto-create со значениями default.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
franchise_id | uuid | NOT NULL | — | PK; FK → franchises (cross-service) |
new_order_sound | varchar(20) | NOT NULL | 'bell' | Имя встроенной мелодии (bell / chime / buzzer / marimba / digital) |
new_order_repeat_seconds | integer | NOT NULL | 30 | Интервал повтора звука пока повар не открыл карточку. Диапазон 5–120 |
overdue_sound | varchar(20) | NOT NULL | 'alarm' | Звук для просроченных заказов |
sound_volume | smallint | NOT NULL | 80 | Громкость 0–100 |
auto_logout_minutes | integer | NOT NULL | 30 | Авто-логаут после N мин неактивности. Диапазон 5–240 |
created_at | timestamp | NOT NULL | now() | |
updated_at | timestamp | NOT NULL | now() |
Ограничения:
- PK
(franchise_id)— одна запись на франшизу CHECK (new_order_repeat_seconds BETWEEN 5 AND 120)CHECK (sound_volume BETWEEN 0 AND 100)CHECK (auto_logout_minutes BETWEEN 5 AND 240)
Бизнес-правила:
- При первом
GET /admin/kds/settingsдля франшизы — auto-insert с default-значениями - При
PATCH—updated_at = NOW(), публикуется событиеcatalog.kds_settings.updated
Миграция (Liquibase changeset YY-add-kds-settings.xml):
- Добавить таблицу
kds_franchise_settings - Добавить колонки
yellow_threshold_minutes,red_threshold_minutesвkitchen_stations(с DEFAULT для backfill старых записей)
product_stores
(Добавлено в BR 2.1)
Список ТТ, в которых доступен товар (используется когда available_in_all_stores = false).
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
product_id | uuid | NOT NULL | — | FK → products.id, CASCADE |
store_id | uuid | NOT NULL | — | Opaque ref на Store Service |
created_at | timestamp | NOT NULL | now() |
Ограничения:
UNIQUE (product_id, store_id)FK product_id REFERENCES products(id) ON DELETE CASCADE
Индексы:
idx_product_stores_product_id—product_id
Тип
ingredientубран (BR 1.11)Ингредиенты перенесены в Warehouse Service как отдельная сущность
ingredients. В каталоге остаются толькоdishиgood. См. ADR-012.
modifier_groups
(Добавлено в BR 1.8)
Группы модификаторов. Soft delete. Изменения применяются мгновенно (без версионирования).
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
franchise_id | uuid | NOT NULL | — | ID франшизы |
name | varchar(255) | NOT NULL | — | Название группы |
type | varchar(20) | NOT NULL | — | group (групповой) / single (простой) |
min_amount | integer | NOT NULL | 0 | Мин. кол-во опций для выбора |
max_amount | integer | NOT NULL | 1 | Макс. кол-во опций |
status | varchar(20) | NOT NULL | 'active' | active / inactive |
deleted_at | timestamp | NULL | — | Soft delete |
created_at | timestamp | NOT NULL | now() | |
updated_at | timestamp | NOT NULL | now() |
Индексы:
idx_modifier_groups_franchise_id—franchise_iduq_modifier_groups_franchise_name— UNIQUE(franchise_id, name)WHEREdeleted_at IS NULL
modifier_options
Опции внутри группы модификаторов.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
modifier_group_id | uuid | NOT NULL | — | FK → modifier_groups.id |
name | varchar(255) | NOT NULL | — | Название опции |
base_price | — | (Убрано в BR 1.10. Цена определяется прейскурантом.) | ||
min_amount | integer | NOT NULL | 0 | Мин. кол-во этой опции |
max_amount | integer | NOT NULL | 1 | Макс. кол-во |
default_amount | integer | NOT NULL | 0 | Кол-во по умолчанию |
free_quantity | integer | NOT NULL | 0 | Бесплатное кол-во (MOD-06) |
description | text | NULL | — | Описание опции (MOD-06) |
is_active | boolean | NOT NULL | true | Активна/неактивна (MOD-06) |
display_order | integer | NOT NULL | 0 | Порядок отображения |
sku_1c | varchar(50) | NULL | — | Код номенклатуры 1С. Обязателен если опция используется в structural-моде (валидируется на write). См. 1С Общепит. (Добавлено в BR 1.17) |
deleted_at | timestamp | NULL | — | Soft-delete для сохранения исторических заказов (BR 1.17) |
created_at | timestamp | NOT NULL | now() | |
updated_at | timestamp | NOT NULL | now() |
Ограничения:
FK modifier_group_id REFERENCES modifier_groups(id) ON DELETE CASCADEidx_modifier_options_sku_1c—WHERE sku_1c IS NOT NULL(поиск по 1С-коду) (BR 1.17)uq_modifier_options_group_sku_1c— UNIQUE(modifier_group_id, sku_1c)WHEREsku_1c IS NOT NULL AND deleted_at IS NULL(в одной группе не может быть двух опций с одним 1С-кодом) (BR 1.17)
Миграция: 031-br-1-17-modifier-options-sku-1c.xml — ADD COLUMN sku_1c, ADD COLUMN deleted_at, индекс + unique constraint.
product_modifiers
Привязка групп модификаторов к товарам. Изменения применяются мгновенно (без версионирования).
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
product_id | uuid | NOT NULL | — | FK → products.id |
modifier_group_id | uuid | NOT NULL | — | FK → modifier_groups.id |
binding_type | varchar(10) | NOT NULL | ’free’ | Тип привязки: structural (закреплённый) / free (свободный) (BR 1.9.2) |
override_min_amount | integer | NULL | — | Per-product override мин. кол-во (null = использовать из группы) |
override_max_amount | integer | NULL | — | Per-product override макс. кол-во |
created_at | timestamp | NOT NULL | now() | Дата привязки |
Ограничения:
UNIQUE (product_id, modifier_group_id)— группа привязана к товару один разFK product_id REFERENCES products(id) ON DELETE CASCADEFK modifier_group_id REFERENCES modifier_groups(id) ON DELETE RESTRICT
Индексы:
idx_pm_product_id—product_id— быстрый поиск модификаторов товара
price_lists
(Добавлено в BR 1.10)
Справочник прейскурантов.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
franchise_id | uuid | NOT NULL | — | Франшиза-владелец |
name | varchar(255) | NOT NULL | — | Уникальное название |
is_default | boolean | NOT NULL | false | Дефолтный прейскурант (ровно один на франшизу) |
status | varchar(20) | NOT NULL | 'active' | active / inactive |
created_at | timestamp | NOT NULL | now() | |
updated_at | timestamp | NOT NULL | now() | |
deleted_at | timestamp | NULL | — | Soft delete |
Ограничения:
UNIQUE (franchise_id, name)WHEREdeleted_at IS NULL
Индексы:
idx_price_lists_franchise_id—franchise_id
price_list_items
(Добавлено в BR 1.10)
Цены товаров. Изменения применяются мгновенно (без версионирования).
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
price_list_id | uuid | NOT NULL | — | FK → price_lists.id, CASCADE |
product_id | uuid | NOT NULL | — | FK → products.id |
price | decimal(10,2) | NOT NULL | — | >= 0 |
Ограничения:
UNIQUE (price_list_id, product_id)
price_list_modifier_items
(Добавлено в BR 1.10)
Цены опций модификаторов. Изменения применяются мгновенно (без версионирования).
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
price_list_id | uuid | NOT NULL | — | FK → price_lists.id, CASCADE |
modifier_option_id | uuid | NOT NULL | — | FK → modifier_options.id |
price | decimal(10,2) | NOT NULL | — | >= 0 |
Ограничения:
UNIQUE (price_list_id, modifier_option_id)
product_stop_list
(Добавлено в BR 1.13)
Per-store блокировка отдельных товаров от продажи.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
franchise_id | uuid | NOT NULL | — | ID франшизы |
store_id | uuid | NOT NULL | — | Cross-service ref на Store Service |
product_id | uuid | NOT NULL | — | FK → products.id, CASCADE |
reason | text | NULL | — | Причина остановки |
stopped_by | uuid | NOT NULL | — | ID пользователя, остановившего товар |
created_at | timestamp | NOT NULL | now() | Дата остановки |
Ограничения:
UNIQUE (store_id, product_id)FK product_id REFERENCES products(id) ON DELETE CASCADE
Индексы:
idx_psl_store_id—store_id
category_stop_list
(Добавлено в BR 1.13)
Per-store блокировка целых категорий от продажи. Блокирует все товары категории.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
franchise_id | uuid | NOT NULL | — | ID франшизы |
store_id | uuid | NOT NULL | — | Cross-service ref на Store Service |
category_id | uuid | NOT NULL | — | FK → categories.id, CASCADE |
reason | text | NULL | — | Причина остановки |
stopped_by | uuid | NOT NULL | — | ID пользователя, остановившего категорию |
created_at | timestamp | NOT NULL | now() | Дата остановки |
Ограничения:
UNIQUE (store_id, category_id)FK category_id REFERENCES categories(id) ON DELETE CASCADE
Индексы:
idx_csl_store_id—store_id
external_menus (BR 4.1)
Заголовок внешнего меню — конструктор для рекламного монитора, JSON-экспорта и (в BR 4.2) push в агрегаторы.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
franchise_id | uuid | NOT NULL | — | ID франшизы (tenant) |
name | varchar(200) | NOT NULL | — | Имя меню для админки |
channel | varchar(20) | NOT NULL | — | tv_screen / json (в BR 4.2 + yandex_eda, koala) |
store_id | uuid | NULL | — | Cross-service ref на Store Service. NULL — меню на всю сеть |
template | varchar(20) | NULL | — | Для tv_screen: grid / slider / list. Для json — NULL |
slug | varchar(40) | NOT NULL | — | URL-friendly slug для live URL /r/{slug}. UNIQUE глобально |
status | varchar(20) | NOT NULL | 'draft' | draft / published / archived |
archived_at | timestamp | NULL | — | Заполняется при soft-delete. Cron физически удаляет через 30 дней |
created_by | uuid | NOT NULL | — | Кто создал меню |
created_at | timestamp | NOT NULL | now() | |
updated_at | timestamp | NOT NULL | now() |
Ограничения:
CHECK channel IN ('tv_screen', 'json', 'yandex_eda', 'koala')CHECK status IN ('draft', 'published', 'archived')CHECK template IN ('grid', 'slider', 'list') OR template IS NULLUNIQUE (slug)— глобально, чтобы live URL не пересекалисьUNIQUE (franchise_id, name) WHERE status != 'archived'— имя уникально среди активных в франшизе
Индексы:
idx_external_menus_franchise—(franchise_id, status)idx_external_menus_store—(store_id, status) WHERE store_id IS NOT NULLidx_external_menus_archived—archived_at WHERE status = 'archived'(для cron-чистки)
external_menu_categories (BR 4.1)
Категории внутри одного external_menu — можно переименовать или создать кастомные (не связанные с каталог-категорией).
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
external_menu_id | uuid | NOT NULL | — | FK → external_menus.id, CASCADE |
name | varchar(200) | NOT NULL | — | Имя категории как показывается на мониторе |
original_category_id | uuid | NULL | — | FK → categories.id, SET NULL. Если задан — наследуется имя при пустом override |
display_order | int | NOT NULL | 0 | Порядок отображения |
icon_url | varchar(500) | NULL | — | Иконка для шаблонов где поддерживается |
created_at | timestamp | NOT NULL | now() | |
updated_at | timestamp | NOT NULL | now() |
Ограничения:
FK external_menu_id REFERENCES external_menus(id) ON DELETE CASCADEFK original_category_id REFERENCES categories(id) ON DELETE SET NULLUNIQUE (external_menu_id, display_order)— порядок не дублируется в рамках одного меню
Индексы:
idx_emc_menu—(external_menu_id, display_order)idx_emc_original—original_category_id WHERE original_category_id IS NOT NULL
external_menu_items (BR 4.1)
Товар каталога в external_menu с возможным override-ом полей. Ключевая черта: product_id — обязательная якорная связь, никаких «свободных» товаров.
| Колонка | Тип | Nullable | Default | Описание |
|---|---|---|---|---|
id | uuid | NOT NULL | gen_random_uuid() | PK |
external_menu_id | uuid | NOT NULL | — | FK → external_menus.id, CASCADE |
category_id | uuid | NOT NULL | — | FK → external_menu_categories.id, CASCADE |
product_id | uuid | NOT NULL | — | ★ Якорь — FK → products.id. ON DELETE — переводит item в status='orphan' через trigger / app-логику (НЕ CASCADE) |
override_name | varchar(200) | NULL | — | Если NULL — берём product.name |
override_description | text | NULL | — | Если NULL — берём product.description |
override_price | decimal(12,2) | NULL | — | Если NULL — fallback по иерархии: price_list_items.price > product.price |
visible | boolean | NOT NULL | true | Скрыть без удаления |
display_order | int | NOT NULL | 0 | Порядок в категории |
status | varchar(20) | NOT NULL | 'ok' | ok (нормально) / orphan (оригинал удалён в каталоге) |
created_at | timestamp | NOT NULL | now() | |
updated_at | timestamp | NOT NULL | now() |
Ограничения:
CHECK status IN ('ok', 'orphan')FK external_menu_id REFERENCES external_menus(id) ON DELETE CASCADEFK category_id REFERENCES external_menu_categories(id) ON DELETE CASCADEFK product_id REFERENCES products(id)— БЕЗ CASCADE (логика app-уровня меняет наstatus=orphan)UNIQUE (external_menu_id, product_id)— один товар не может быть дважды в одном менюUNIQUE (category_id, display_order)— порядок не дублируется в категории
Индексы:
idx_emi_menu—(external_menu_id, status, visible)— основной запрос рендераidx_emi_category—(category_id, display_order)— для упорядочиванияidx_emi_product—product_id— для каскада orphan при удалении товараidx_emi_orphan—status WHERE status = 'orphan'— для индикации в админке
Trigger / app-logic при удалении товара в каталоге:
При DELETE FROM products WHERE id=X (или soft-delete deleted_at SET) — все external_menu_items WHERE product_id=X → UPDATE status='orphan'. Реализуется в ProductService.deleteProduct() + Kafka-event catalog.product.deleted consumer (на тот случай если удаление приходит через event’ы).