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).

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
franchise_iduuidNOT NULLID франшизы
namevarchar(255)NOT NULLНазвание категории
parent_iduuidNULLFK → categories.id (null = корневая)
display_orderintegerNOT NULL0Порядок отображения внутри уровня
is_activebooleanNOT NULLtrueАктивна / скрыта от клиентов
colorvarchar(7)NULLHEX-цвет для POS (напр. #FF5733) (BR 2.2)
sort_typevarchar(20)NOT NULLmanualТип сортировки товаров: manual / name_asc / name_desc / price_asc (BR 2.2)
is_available_mobilebooleanNOT NULLtrueДоступна в мобильном приложении (BR 2.2)
is_available_websitebooleanNOT NULLtrueДоступна на сайте (BR 2.2)
is_available_aggregatorsbooleanNOT NULLfalseДоступна в агрегаторах (Яндекс.Еда и др.) (BR 2.2)
created_attimestampNOT NULLnow()Дата создания
updated_attimestampNOT NULLnow()Дата обновления

Индексы:

  • idx_categories_franchise_idfranchise_id
  • idx_categories_parent_idparent_id

Ограничения:

  • FK parent_id REFERENCES categories(id) ON DELETE RESTRICT

products

Товары каталога. Изменения применяются мгновенно (без версионирования).

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
franchise_iduuidNOT NULLID франшизы
namevarchar(255)NOT NULLНазвание товара
descriptiontextNULLОписание
typevarchar(20)NOT NULLdish (блюдо) / good (продукт) (BR 1.11: type=ingredient removed — ingredients moved to Warehouse Service)
category_iduuidNULLFK → categories.id. NULL = “Без категории”
base_pricedecimal(10,2)NOT NULL(Убрано в BR 1.10. Цена определяется прейскурантом.)
unit_of_measurevarchar(20)NOT NULLшт, кг, г, л, мл, порция
statusvarchar(20)NOT NULL'active'active / inactive
skuvarchar(100)NULLАртикул (BR 2.1)
barcodevarchar(100)NULLШтрихкод (UPC) (BR 2.1)
sort_orderintegerNOT NULL0Порядок внутри категории (BR 2.1)
gross_weightdecimal(8,3)NULLВес брутто (кг) (BR 2.1)
net_weightdecimal(8,3)NULLВес нетто (кг) (BR 2.1)
kcaldecimal(7,2)NULLКалории на 100г (BR 2.1)
proteindecimal(7,2)NULLБелки на 100г (BR 2.1)
fatdecimal(7,2)NULLЖиры на 100г (BR 2.1)
carbsdecimal(7,2)NULLУглеводы на 100г (BR 2.1)
assembly_timeintegerNULLВремя приготовления (мин) (BR 2.1)
colorvarchar(7)NULLHex-цвет (#RRGGBB) для POS (BR 2.1)
image_urlvarchar(500)NULLПубличный URL изображения в S3 (BR 2.1)
is_open_pricebooleanNOT NULLfalseСвободная цена (задаётся на кассе) (BR 2.1)
is_by_weightbooleanNOT NULLfalseПродажа на развес (BR 2.1)
is_exclude_from_promobooleanNOT NULLfalseИсключить из акций (BR 2.1)
is_manual_discount_bannedbooleanNOT NULLfalseЗапрет ручных скидок (BR 2.1)
is_admin_onlybooleanNOT NULLfalseТолько администратор (BR 2.1)
is_alcoholbooleanNOT NULLfalseАлкоголь (BR 2.1)
is_tobaccobooleanNOT NULLfalseТабак (BR 2.1)
is_sugary_drinkbooleanNOT NULLfalseСахаросодержащий напиток (BR 2.1)
is_markedbooleanNOT NULLfalseМаркированный товар — требует сканирования DataMatrix на кассе (ЧЗ, миграция 024)
available_in_all_storesbooleanNOT NULLtrueДоступен во всех ТТ; если false — см. product_stores (BR 2.1)
requires_kitchenbooleanNOT NULLfalseТовар требует приготовления (шаурма, бургер). Определяет flow заказа. (Добавлено в BR 2.5)
kitchen_station_iduuidNULLFK → kitchen_stations.id. Обязательно при requires_kitchen=true, иначе NULL. (Добавлено в BR 2.5)
vat_ratevarchar(10)NOT NULLvat20Ставка НДС для 54-ФЗ: none / vat0 / vat10 / vat20 / vat110 / vat120. Обязательно для инвойса PayKeeper. (Добавлено в BR 3.3)
payment_subjectvarchar(20)NOT NULLgoodsПредмет расчёта (тег 1212): goods / service / work / excise / job / payment / agency / composite / another. (Добавлено в BR 3.3)
payment_typevarchar(20)NOT NULLfullСпособ расчёта (тег 1214): full / prepay / advance / partial_prepay / credit / credit_pay / partial. (Добавлено в BR 3.3)
deleted_attimestampNULLSoft delete
created_attimestampNOT NULLnow()Дата создания
updated_attimestampNOT NULLnow()Дата обновления

Индексы:

  • idx_products_franchise_idfranchise_id
  • idx_products_category_idcategory_id
  • uq_products_franchise_name — UNIQUE (franchise_id, name) WHERE deleted_at IS NULL (название уникально в рамках франшизы среди неудалённых)
  • idx_products_kitchen_stationkitchen_station_id WHERE kitchen_station_id IS NOT NULL (BR 2.5)

Ограничения:

  • FK category_id REFERENCES categories(id) ON DELETE SET NULL
  • FK 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.

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
franchise_iduuidNOT NULLМультитенантность
namevarchar(50)NOT NULL«Горячая кухня», «Бар», «Мангал»
descriptiontextNULLСвободное описание
yellow_threshold_minutesintegerNOT NULL5Порог жёлтой зоны на KDS-карточке: карточка желтеет за N минут до expected_ready_at. (Добавлено в BR 5.1)
red_threshold_minutesintegerNOT NULL0Просрочка: карточка краснеет когда now > expected_ready_at + N минут (0 = просрочка с момента дедлайна). (BR 5.1)
deleted_attimestampNULLSoft delete
created_attimestampNOT NULLnow()
updated_attimestampNOT NULLnow()

Ограничения:

  • uq_kitchen_stations_franchise_name — UNIQUE (franchise_id, LOWER(name)) WHERE deleted_at IS NULL
  • CHECK (yellow_threshold_minutes >= 0) (BR 5.1)
  • CHECK (red_threshold_minutes >= 0) (BR 5.1)

Индексы:

  • idx_kitchen_stations_franchisefranchise_id WHERE deleted_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.

КолонкаТипNullableDefaultОписание
franchise_iduuidNOT NULLPK; FK → franchises (cross-service)
new_order_soundvarchar(20)NOT NULL'bell'Имя встроенной мелодии (bell / chime / buzzer / marimba / digital)
new_order_repeat_secondsintegerNOT NULL30Интервал повтора звука пока повар не открыл карточку. Диапазон 5–120
overdue_soundvarchar(20)NOT NULL'alarm'Звук для просроченных заказов
sound_volumesmallintNOT NULL80Громкость 0–100
auto_logout_minutesintegerNOT NULL30Авто-логаут после N мин неактивности. Диапазон 5–240
created_attimestampNOT NULLnow()
updated_attimestampNOT NULLnow()

Ограничения:

  • 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-значениями
  • При PATCHupdated_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).

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
product_iduuidNOT NULLFK → products.id, CASCADE
store_iduuidNOT NULLOpaque ref на Store Service
created_attimestampNOT NULLnow()

Ограничения:

  • UNIQUE (product_id, store_id)
  • FK product_id REFERENCES products(id) ON DELETE CASCADE

Индексы:

  • idx_product_stores_product_idproduct_id

Тип ingredient убран (BR 1.11)

Ингредиенты перенесены в Warehouse Service как отдельная сущность ingredients. В каталоге остаются только dish и good. См. ADR-012.

modifier_groups

(Добавлено в BR 1.8)

Группы модификаторов. Soft delete. Изменения применяются мгновенно (без версионирования).

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
franchise_iduuidNOT NULLID франшизы
namevarchar(255)NOT NULLНазвание группы
typevarchar(20)NOT NULLgroup (групповой) / single (простой)
min_amountintegerNOT NULL0Мин. кол-во опций для выбора
max_amountintegerNOT NULL1Макс. кол-во опций
statusvarchar(20)NOT NULL'active'active / inactive
deleted_attimestampNULLSoft delete
created_attimestampNOT NULLnow()
updated_attimestampNOT NULLnow()

Индексы:

  • idx_modifier_groups_franchise_idfranchise_id
  • uq_modifier_groups_franchise_name — UNIQUE (franchise_id, name) WHERE deleted_at IS NULL

modifier_options

Опции внутри группы модификаторов.

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
modifier_group_iduuidNOT NULLFK → modifier_groups.id
namevarchar(255)NOT NULLНазвание опции
base_pricedecimal(10,2)NOT NULL(Убрано в BR 1.10. Цена определяется прейскурантом.)
min_amountintegerNOT NULL0Мин. кол-во этой опции
max_amountintegerNOT NULL1Макс. кол-во
default_amountintegerNOT NULL0Кол-во по умолчанию
free_quantityintegerNOT NULL0Бесплатное кол-во (MOD-06)
descriptiontextNULLОписание опции (MOD-06)
is_activebooleanNOT NULLtrueАктивна/неактивна (MOD-06)
display_orderintegerNOT NULL0Порядок отображения
sku_1cvarchar(50)NULLКод номенклатуры 1С. Обязателен если опция используется в structural-моде (валидируется на write). См. 1С Общепит. (Добавлено в BR 1.17)
deleted_attimestampNULLSoft-delete для сохранения исторических заказов (BR 1.17)
created_attimestampNOT NULLnow()
updated_attimestampNOT NULLnow()

Ограничения:

  • FK modifier_group_id REFERENCES modifier_groups(id) ON DELETE CASCADE
  • idx_modifier_options_sku_1cWHERE sku_1c IS NOT NULL (поиск по 1С-коду) (BR 1.17)
  • uq_modifier_options_group_sku_1c — UNIQUE (modifier_group_id, sku_1c) WHERE sku_1c IS NOT NULL AND deleted_at IS NULL (в одной группе не может быть двух опций с одним 1С-кодом) (BR 1.17)

Миграция: 031-br-1-17-modifier-options-sku-1c.xmlADD COLUMN sku_1c, ADD COLUMN deleted_at, индекс + unique constraint.

product_modifiers

(Добавлено в BR 1.8.1) (Обновлено в BR 1.9.2)

Привязка групп модификаторов к товарам. Изменения применяются мгновенно (без версионирования).

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
product_iduuidNOT NULLFK → products.id
modifier_group_iduuidNOT NULLFK → modifier_groups.id
binding_typevarchar(10)NOT NULL’free’Тип привязки: structural (закреплённый) / free (свободный) (BR 1.9.2)
override_min_amountintegerNULLPer-product override мин. кол-во (null = использовать из группы)
override_max_amountintegerNULLPer-product override макс. кол-во
created_attimestampNOT NULLnow()Дата привязки

Ограничения:

  • UNIQUE (product_id, modifier_group_id) — группа привязана к товару один раз
  • FK product_id REFERENCES products(id) ON DELETE CASCADE
  • FK modifier_group_id REFERENCES modifier_groups(id) ON DELETE RESTRICT

Индексы:

  • idx_pm_product_idproduct_id — быстрый поиск модификаторов товара

price_lists

(Добавлено в BR 1.10)

Справочник прейскурантов.

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
franchise_iduuidNOT NULLФраншиза-владелец
namevarchar(255)NOT NULLУникальное название
is_defaultbooleanNOT NULLfalseДефолтный прейскурант (ровно один на франшизу)
statusvarchar(20)NOT NULL'active'active / inactive
created_attimestampNOT NULLnow()
updated_attimestampNOT NULLnow()
deleted_attimestampNULLSoft delete

Ограничения:

  • UNIQUE (franchise_id, name) WHERE deleted_at IS NULL

Индексы:

  • idx_price_lists_franchise_idfranchise_id

price_list_items

(Добавлено в BR 1.10)

Цены товаров. Изменения применяются мгновенно (без версионирования).

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
price_list_iduuidNOT NULLFK → price_lists.id, CASCADE
product_iduuidNOT NULLFK → products.id
pricedecimal(10,2)NOT NULL>= 0

Ограничения:

  • UNIQUE (price_list_id, product_id)

price_list_modifier_items

(Добавлено в BR 1.10)

Цены опций модификаторов. Изменения применяются мгновенно (без версионирования).

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
price_list_iduuidNOT NULLFK → price_lists.id, CASCADE
modifier_option_iduuidNOT NULLFK → modifier_options.id
pricedecimal(10,2)NOT NULL>= 0

Ограничения:

  • UNIQUE (price_list_id, modifier_option_id)

product_stop_list

(Добавлено в BR 1.13)

Per-store блокировка отдельных товаров от продажи.

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
franchise_iduuidNOT NULLID франшизы
store_iduuidNOT NULLCross-service ref на Store Service
product_iduuidNOT NULLFK → products.id, CASCADE
reasontextNULLПричина остановки
stopped_byuuidNOT NULLID пользователя, остановившего товар
created_attimestampNOT NULLnow()Дата остановки

Ограничения:

  • UNIQUE (store_id, product_id)
  • FK product_id REFERENCES products(id) ON DELETE CASCADE

Индексы:

  • idx_psl_store_idstore_id

category_stop_list

(Добавлено в BR 1.13)

Per-store блокировка целых категорий от продажи. Блокирует все товары категории.

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
franchise_iduuidNOT NULLID франшизы
store_iduuidNOT NULLCross-service ref на Store Service
category_iduuidNOT NULLFK → categories.id, CASCADE
reasontextNULLПричина остановки
stopped_byuuidNOT NULLID пользователя, остановившего категорию
created_attimestampNOT NULLnow()Дата остановки

Ограничения:

  • UNIQUE (store_id, category_id)
  • FK category_id REFERENCES categories(id) ON DELETE CASCADE

Индексы:

  • idx_csl_store_idstore_id

external_menus (BR 4.1)

Заголовок внешнего меню — конструктор для рекламного монитора, JSON-экспорта и (в BR 4.2) push в агрегаторы.

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
franchise_iduuidNOT NULLID франшизы (tenant)
namevarchar(200)NOT NULLИмя меню для админки
channelvarchar(20)NOT NULLtv_screen / json (в BR 4.2 + yandex_eda, koala)
store_iduuidNULLCross-service ref на Store Service. NULL — меню на всю сеть
templatevarchar(20)NULLДля tv_screen: grid / slider / list. Для json — NULL
slugvarchar(40)NOT NULLURL-friendly slug для live URL /r/{slug}. UNIQUE глобально
statusvarchar(20)NOT NULL'draft'draft / published / archived
archived_attimestampNULLЗаполняется при soft-delete. Cron физически удаляет через 30 дней
created_byuuidNOT NULLКто создал меню
created_attimestampNOT NULLnow()
updated_attimestampNOT NULLnow()

Ограничения:

  • CHECK channel IN ('tv_screen', 'json', 'yandex_eda', 'koala')
  • CHECK status IN ('draft', 'published', 'archived')
  • CHECK template IN ('grid', 'slider', 'list') OR template IS NULL
  • UNIQUE (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 NULL
  • idx_external_menus_archivedarchived_at WHERE status = 'archived' (для cron-чистки)

external_menu_categories (BR 4.1)

Категории внутри одного external_menu — можно переименовать или создать кастомные (не связанные с каталог-категорией).

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
external_menu_iduuidNOT NULLFK → external_menus.id, CASCADE
namevarchar(200)NOT NULLИмя категории как показывается на мониторе
original_category_iduuidNULLFK → categories.id, SET NULL. Если задан — наследуется имя при пустом override
display_orderintNOT NULL0Порядок отображения
icon_urlvarchar(500)NULLИконка для шаблонов где поддерживается
created_attimestampNOT NULLnow()
updated_attimestampNOT NULLnow()

Ограничения:

  • FK external_menu_id REFERENCES external_menus(id) ON DELETE CASCADE
  • FK original_category_id REFERENCES categories(id) ON DELETE SET NULL
  • UNIQUE (external_menu_id, display_order) — порядок не дублируется в рамках одного меню

Индексы:

  • idx_emc_menu(external_menu_id, display_order)
  • idx_emc_originaloriginal_category_id WHERE original_category_id IS NOT NULL

external_menu_items (BR 4.1)

Товар каталога в external_menu с возможным override-ом полей. Ключевая черта: product_id — обязательная якорная связь, никаких «свободных» товаров.

КолонкаТипNullableDefaultОписание
iduuidNOT NULLgen_random_uuid()PK
external_menu_iduuidNOT NULLFK → external_menus.id, CASCADE
category_iduuidNOT NULLFK → external_menu_categories.id, CASCADE
product_iduuidNOT NULL★ Якорь — FK → products.id. ON DELETE — переводит item в status='orphan' через trigger / app-логику (НЕ CASCADE)
override_namevarchar(200)NULLЕсли NULL — берём product.name
override_descriptiontextNULLЕсли NULL — берём product.description
override_pricedecimal(12,2)NULLЕсли NULL — fallback по иерархии: price_list_items.price > product.price
visiblebooleanNOT NULLtrueСкрыть без удаления
display_orderintNOT NULL0Порядок в категории
statusvarchar(20)NOT NULL'ok'ok (нормально) / orphan (оригинал удалён в каталоге)
created_attimestampNOT NULLnow()
updated_attimestampNOT NULLnow()

Ограничения:

  • CHECK status IN ('ok', 'orphan')
  • FK external_menu_id REFERENCES external_menus(id) ON DELETE CASCADE
  • FK category_id REFERENCES external_menu_categories(id) ON DELETE CASCADE
  • FK 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_productproduct_id — для каскада orphan при удалении товара
  • idx_emi_orphanstatus WHERE status = 'orphan' — для индикации в админке

Trigger / app-logic при удалении товара в каталоге:

При DELETE FROM products WHERE id=X (или soft-delete deleted_at SET) — все external_menu_items WHERE product_id=XUPDATE status='orphan'. Реализуется в ProductService.deleteProduct() + Kafka-event catalog.product.deleted consumer (на тот случай если удаление приходит через event’ы).