Catalog Service — API Contract

Этот документ является единственным источником правды для API Catalog Service.

Бэкенд реализует контракт. Фронтенд потребляет контракт. Отклонения запрещены.

Содержание

Public API (Bearer JWT)

Категории

Модификаторы (справочник) (BR 1.8)

Модификаторы товара (BR 1.8.1)

Прейскуранты (BR 1.10)

Стоп-листы (BR 1.13)

Internal API (Service Token) (BR 1.16)

Вычисляемое меню

Полный снепшот для sync (BR 3.4)

Товары


GET /categories

Получить дерево категорий франшизы.

ПараметрЗначение
AuthBearer JWT (Franchise, Franchisee, Manager; Cashier — 403)

Response 200

{
  "data": [
    {
      "id": "uuid",
      "name": "string",
      "parent_id": "uuid | null",
      "display_order": "integer",
      "is_active": "boolean",
      "color": "string | null",
      "sort_type": "manual | name_asc | name_desc | price_asc",
      "is_available_mobile": "boolean",
      "is_available_website": "boolean",
      "is_available_aggregators": "boolean",
      "children": [],
      "created_at": "datetime",
      "updated_at": "datetime"
    }
  ]
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль Cashier

POST /categories

Создать категорию.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "name": "string, required — название категории",
  "parent_id": "uuid, optional — ID родительской категории (null = корневая)",
  "display_order": "integer, optional — порядок (default 0)",
  "is_active": "boolean, optional — default true",
  "color": "string, optional — HEX-цвет (#RRGGBB)",
  "sort_type": "manual | name_asc | name_desc | price_asc, optional — default manual",
  "is_available_mobile": "boolean, optional — default true",
  "is_available_website": "boolean, optional — default true",
  "is_available_aggregators": "boolean, optional — default false"
}

Response 201

{
  "data": {
    "id": "uuid",
    "name": "string",
    "parent_id": "uuid | null",
    "display_order": "integer",
    "is_active": true,
    "color": "string | null",
    "sort_type": "manual",
    "is_available_mobile": true,
    "is_available_website": true,
    "is_available_aggregators": false,
    "children": [],
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль не Franchise
VALIDATION_ERROR400Невалидные данные
NAME_DUPLICATE409Категория с таким названием уже существует
PARENT_NOT_FOUND404Родительская категория не найдена

PATCH /categories/{id}

Обновить категорию.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Path Parameters

ParamTypeRequiredDescription
iduuidyesID категории

Request Body

{
  "name": "string, optional",
  "parent_id": "uuid | null, optional",
  "display_order": "integer, optional",
  "is_active": "boolean, optional",
  "cascade": "boolean, optional — каскадная деактивация/активация дочерних",
  "color": "string, optional — HEX-цвет (#RRGGBB)",
  "sort_type": "manual | name_asc | name_desc | price_asc, optional",
  "is_available_mobile": "boolean, optional",
  "is_available_website": "boolean, optional",
  "is_available_aggregators": "boolean, optional"
}

Каскадная деактивация и активация (ADR-013)

  • is_active: false — автоматически деактивирует все дочерние категории
  • is_active: true, cascade: false — активируется только эта категория
  • is_active: true, cascade: true — активируется эта категория + все дочерние рекурсивно

Response 200

{
  "data": {
    "id": "uuid",
    "name": "string",
    "parent_id": "uuid | null",
    "display_order": "integer",
    "is_active": "boolean",
    "color": "string | null",
    "sort_type": "manual | name_asc | name_desc | price_asc",
    "is_available_mobile": "boolean",
    "is_available_website": "boolean",
    "is_available_aggregators": "boolean",
    "children": [],
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль не Franchise
CATEGORY_NOT_FOUND404Категория не найдена
NAME_DUPLICATE409Категория с таким названием уже существует
VALIDATION_ERROR400Невалидные данные

DELETE /categories/{id}

Удалить категорию.

ПараметрЗначение
AuthBearer JWT (только Franchise)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID категории

Условия удаления

  1. Категория не должна содержать дочерних категорий (CATEGORY_HAS_CHILDREN)
  2. Категория не должна содержать привязанных товаров (CATEGORY_HAS_PRODUCTS)

Response 204

Нет тела.

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль не Franchise
CATEGORY_NOT_FOUND404Категория не найдена
CATEGORY_HAS_CHILDREN422Есть дочерние категории
CATEGORY_HAS_PRODUCTS422Есть привязанные товары

GET /modifier-groups

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

Получить список групп модификаторов с пагинацией и поиском.

ПараметрЗначение
AuthBearer JWT (Franchise — все; Franchisee/Manager — активные; Cashier — 403)

Query Parameters

ParamTypeRequiredDescription
pageintegernoНомер страницы (default: 1)
per_pageintegernoЗаписей на страницу (default: 20, max: 100)
searchstringnoПоиск по названию
typestringnoФильтр: group / single
statusstringnoФильтр: active / inactive
deletedbooleannotrue — показать удалённые
exclude_structuralbooleannotrue — исключить группы, привязанные как structural к товарам (BUG-036)

Response 200

{
  "data": [
    {
      "id": "uuid",
      "name": "string",
      "type": "group | single",
      "min_amount": "integer",
      "max_amount": "integer",
      "status": "active | inactive",
      "option_count": "integer"
    }
  ],
  "meta": { "page": "integer", "per_page": "integer", "total": "integer" }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Cashier

GET /modifier-groups/{id}

(BR 1.8)

Получить детали группы модификаторов с опциями.

ПараметрЗначение
AuthBearer JWT (Franchise — любой; Franchisee/Manager — только активные; Cashier — 403)

Response 200

{
  "data": {
    "id": "uuid",
    "name": "string",
    "type": "group | single",
    "min_amount": "integer",
    "max_amount": "integer",
    "status": "active | inactive",
    "options": [
      {
        "id": "uuid",
        "name": "string",
        "min_amount": "integer",
        "max_amount": "integer",
        "default_amount": "integer",
        "free_quantity": "integer",
        "description": "string | null",
        "is_active": "boolean",
        "display_order": "integer",
        "sku_1c": "string | null"
      }
    ],
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

(sku_1c добавлено в BR 1.17) — код номенклатуры 1С для опции. Заполняется только для опций structural-мода. См. 1С Общепит.

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
MODIFIER_GROUP_NOT_FOUND404

POST /modifier-groups

(BR 1.8)

Создать группу модификаторов с опциями.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "name": "string, required",
  "type": "string, required — group | single",
  "min_amount": "integer, optional — default 0",
  "max_amount": "integer, optional — default 1",
  "options": [
    {
      "name": "string, required",
      "min_amount": "integer, optional — default 0",
      "max_amount": "integer, optional — default 1",
      "default_amount": "integer, optional — default 0",
      "free_quantity": "integer, optional — default 0",
      "description": "string, optional",
      "is_active": "boolean, optional — default true",
      "display_order": "integer, optional — auto",
      "sku_1c": "string, optional — код 1С (BR 1.17)"
    }
  ]
}

Response 201

Аналогично GET /modifier-groups/{id} response.

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Не Franchise
VALIDATION_ERROR400Невалидные данные
NAME_DUPLICATE409Группа с таким названием уже существует
DUPLICATE_SKU_1C_IN_GROUP422Две опции в группе имеют одинаковый sku_1c (BR 1.17)

PATCH /modifier-groups/{id}

(BR 1.8)

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

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

Partial update. Опции заменяются целиком (если переданы).

{
  "name": "string, optional",
  "type": "string, optional",
  "min_amount": "integer, optional",
  "max_amount": "integer, optional",
  "status": "string, optional",
  "options": "array, optional — полная замена списка опций"
}

Response 200

Аналогично GET /modifier-groups/{id} response.

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
MODIFIER_GROUP_NOT_FOUND404
VALIDATION_ERROR400
NAME_DUPLICATE409
STRUCTURAL_OPTION_MISSING_SKU_1C422Опция в группе, которая привязана как structural к товару, не имеет sku_1c (BR 1.17)
DUPLICATE_SKU_1C_IN_GROUP422Две опции в группе имеют одинаковый sku_1c (BR 1.17)

DELETE /modifier-groups/{id}

(BR 1.8)

Soft delete группы модификаторов.

ПараметрЗначение
AuthBearer JWT (только Franchise)

Response 204

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
MODIFIER_GROUP_NOT_FOUND404

POST /modifier-groups/{id}/restore

(BR 1.8)

Восстановить удалённую группу.

ПараметрЗначение
AuthBearer JWT (только Franchise)

Response 200

Аналогично GET /modifier-groups/{id} response.

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
MODIFIER_GROUP_NOT_FOUND404
MODIFIER_GROUP_NOT_DELETED422Группа не удалена
NAME_DUPLICATE409Имя занято

GET /modifier-groups/{id}/assignments

(MOD-05)

Получить список продуктов, к которым привязана группа модификаторов.

ПараметрЗначение
AuthBearer JWT (Franchise — все; Franchisee/Manager — 403)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID группы модификаторов

Response 200

{
  "data": [
    {
      "product_id": "uuid",
      "product_name": "string",
      "category_name": "string | null",
      "binding_type": "structural | free"
    }
  ]
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Не Franchise
MODIFIER_GROUP_NOT_FOUND404

POST /modifier-groups/{id}/assignments

(MOD-04)

Массово назначить группу модификаторов на несколько продуктов. Уже привязанные продукты пропускаются.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "product_ids": ["uuid", "uuid"],
  "binding_type": "structural | free, optional, default: free"
}

Response 200

{
  "data": [
    {
      "product_id": "uuid",
      "product_name": "string",
      "category_name": "string | null",
      "binding_type": "structural | free"
    }
  ]
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Не Franchise
MODIFIER_GROUP_NOT_FOUND404
VALIDATION_ERROR400Пустой product_ids

POST /products/{id}/dependent-modifiers

(MOD-02)

Создать зависимый модификатор прямо на странице продукта. Создаёт группу типа single и автоматически привязывает к товару как structural.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Path Parameters

ParamTypeRequiredDescription
iduuidyesID товара

Request Body

{
  "name": "string, required — название группы",
  "min_amount": "integer, optional — default 0",
  "max_amount": "integer, optional — default 1",
  "options": [
    {
      "name": "string, required",
      "min_amount": "integer, optional — default 0",
      "max_amount": "integer, optional — default 1",
      "default_amount": "integer, optional — default 0",
      "free_quantity": "integer, optional — default 0",
      "description": "string, optional",
      "is_active": "boolean, optional — default true",
      "display_order": "integer, optional — auto"
    }
  ]
}

Response 201

Аналогично GET /products/{id}/modifiers response (один элемент).

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Не Franchise
PRODUCT_NOT_FOUND404
VALIDATION_ERROR400
NAME_DUPLICATE409Группа с таким названием уже существует

GET /products/{id}/modifiers

(BR 1.8.1, обновлено в BR 1.9.2)

Получить модификаторы товара.

ПараметрЗначение
AuthBearer JWT (Franchise — любой; Franchisee/Manager — только active товары)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID товара

Response 200

{
  "data": [
    {
      "modifier_group_id": "uuid",
      "binding_type": "structural | free",
      "name": "string",
      "type": "group | single",
      "min_amount": "integer",
      "max_amount": "integer",
      "override_min_amount": "integer | null",
      "override_max_amount": "integer | null",
      "options": [
        {
          "id": "uuid",
          "name": "string",
          "min_amount": "integer",
          "max_amount": "integer",
          "default_amount": "integer",
          "free_quantity": "integer",
          "description": "string | null",
          "is_active": "boolean",
          "display_order": "integer"
        }
      ]
    }
  ]
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
PRODUCT_NOT_FOUND404

POST /products/{id}/modifiers

(BR 1.8.1, обновлено в BR 1.9.2)

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

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "modifier_group_id": "uuid, required",
  "binding_type": "structural | free, optional, default: free — тип привязки (BR 1.9.2)",
  "override_min_amount": "integer, optional — per-product override",
  "override_max_amount": "integer, optional — per-product override"
}

Response 201

{
  "data": {
    "modifier_group_id": "uuid",
    "binding_type": "structural | free",
    "name": "string",
    "type": "group | single",
    "min_amount": "integer",
    "max_amount": "integer",
    "override_min_amount": "integer | null",
    "override_max_amount": "integer | null"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403Роль не Franchise
PRODUCT_NOT_FOUND404
MODIFIER_GROUP_NOT_FOUND404
MODIFIER_ALREADY_ATTACHED409Группа уже привязана к этому товару
STRUCTURAL_MODIFIER_MIN_REQUIRED422binding_type=structural, но override_min_amount < 1 (BR 1.9.2)
STRUCTURAL_OPTION_MISSING_SKU_1C422binding_type=structural, но хотя бы одна опция группы не имеет sku_1c (BR 1.17)

DELETE /products/{id}/modifiers/{groupId}

(BR 1.8.1)

Отвязать группу модификаторов от товара.

ПараметрЗначение
AuthBearer JWT (только Franchise)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID товара
groupIduuidyesID группы модификаторов

Response 204

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
PRODUCT_NOT_FOUND404
MODIFIER_NOT_ATTACHED404Группа не привязана к этому товару

PATCH /products/{id}/modifiers/{groupId}

(BR 1.8.1, обновлено в BR 1.9.2)

Обновить привязку: override min/max или тип привязки.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "binding_type": "structural | free, optional — тип привязки (BR 1.9.2)",
  "override_min_amount": "integer | null, optional",
  "override_max_amount": "integer | null, optional"
}

Response 200

Аналогично POST .../modifiers response.

Errors

CodeHTTPКогда
UNAUTHORIZED401
FORBIDDEN403
PRODUCT_NOT_FOUND404
MODIFIER_NOT_ATTACHED404
STRUCTURAL_MODIFIER_MIN_REQUIRED422Смена на binding_type=structural, но override_min_amount < 1
STRUCTURAL_OPTION_MISSING_SKU_1C422Смена на binding_type=structural, но опции группы не имеют sku_1c (BR 1.17)

GET /price-lists

(BR 1.10)

Список прейскурантов франшизы.

ПараметрЗначение
AuthBearer JWT (Franchise — все; Franchisee/Manager — только свой assigned; Cashier — 403)

Query Parameters

ParamTypeRequiredDescription
pageintegernoНомер страницы (default: 1)
per_pageintegernoЗаписей на страницу (default: 20, max: 100)
searchstringnoПоиск по названию

Response 200

{
  "data": [
    {
      "id": "uuid",
      "name": "string",
      "is_default": "boolean",
      "status": "active | inactive",
      "store_count": "integer — сколько ТТ назначено",
      "created_at": "datetime"
    }
  ],
  "meta": {
    "page": "integer",
    "per_page": "integer",
    "total": "integer"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Cashier

GET /price-lists/{id}

(BR 1.10)

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

ПараметрЗначение
AuthBearer JWT (Franchise — любой; Franchisee/Manager — только если назначен на их ТТ; Cashier — 403)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID прейскуранта

Response 200

{
  "data": {
    "id": "uuid",
    "name": "string",
    "is_default": "boolean",
    "status": "active | inactive",
    "stores": [
      {
        "id": "uuid",
        "name": "string"
      }
    ],
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Cashier или прейскурант не назначен на ТТ пользователя
PRICE_LIST_NOT_FOUND404Прейскурант не найден

POST /price-lists

(BR 1.10)

Создать прейскурант. Автоматически заполняется всеми активными товарами и опциями модификаторов — цены копируются из дефолтного прейскуранта (или 0₽ если дефолтного нет). (Исправлено в BUG-001)

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "name": "string, required — название прейскуранта",
  "is_default": "boolean, optional — default false"
}

Response 201

{
  "data": {
    "id": "uuid",
    "name": "string",
    "is_default": "boolean",
    "status": "active",
    "stores": [],
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль не Franchise
VALIDATION_ERROR400Невалидные данные
NAME_DUPLICATE409Прейскурант с таким названием уже существует

PATCH /price-lists/{id}

(BR 1.10)

Обновить прейскурант (название, статус, is_default).

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Path Parameters

ParamTypeRequiredDescription
iduuidyesID прейскуранта

Request Body

{
  "name": "string, optional",
  "is_default": "boolean, optional",
  "status": "string, optional — active | inactive"
}

Response 200

{
  "data": {
    "id": "uuid",
    "name": "string",
    "is_default": "boolean",
    "status": "active | inactive",
    "stores": [
      {
        "id": "uuid",
        "name": "string"
      }
    ],
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль не Franchise
PRICE_LIST_NOT_FOUND404Прейскурант не найден
NAME_DUPLICATE409Прейскурант с таким названием уже существует

DELETE /price-lists/{id}

(BR 1.10)

Удалить прейскурант. Нельзя удалить дефолтный прейскурант или назначенный на ТТ.

ПараметрЗначение
AuthBearer JWT (только Franchise)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID прейскуранта

Response 204

Нет тела.

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль не Franchise
PRICE_LIST_NOT_FOUND404Прейскурант не найден
CANNOT_DELETE_DEFAULT422Нельзя удалить дефолтный прейскурант
PRICE_LIST_ASSIGNED_TO_STORES422Прейскурант назначен на ТТ (details: список названий ТТ)

GET /price-lists/{id}/items

(BR 1.10)

Получить цены прейскуранта (товары + модификаторы). Цены редактируются напрямую, без версионирования.

ПараметрЗначение
AuthBearer JWT (только Franchise)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID прейскуранта

Query Parameters

ParamTypeRequiredDescription
searchstringnoПоиск по названию товара / опции

Response 200

{
  "data": {
    "price_list_id": "uuid",
    "items": [
      {
        "product_id": "uuid",
        "product_name": "string",
        "price": "decimal"
      }
    ],
    "modifier_items": [
      {
        "modifier_option_id": "uuid",
        "modifier_option_name": "string",
        "modifier_group_name": "string",
        "price": "decimal"
      }
    ]
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль не Franchise
PRICE_LIST_NOT_FOUND404Прейскурант не найден

PATCH /price-lists/{id}/items

(BR 1.10)

Пакетное обновление цен товаров в прейскуранте. Изменения применяются мгновенно.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Path Parameters

ParamTypeRequiredDescription
iduuidyesID прейскуранта

Request Body

{
  "items": [
    {
      "product_id": "uuid, required",
      "price": "decimal, required — >= 0"
    }
  ]
}

Response 200

{
  "data": {
    "updated_count": "integer"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль не Franchise
PRICE_LIST_NOT_FOUND404Прейскурант не найден
VALIDATION_ERROR400Невалидные данные (price < 0)

PATCH /price-lists/{id}/modifier-items

(BR 1.10)

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

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Path Parameters

ParamTypeRequiredDescription
iduuidyesID прейскуранта

Request Body

{
  "items": [
    {
      "modifier_option_id": "uuid, required",
      "price": "decimal, required — >= 0"
    }
  ]
}

Response 200

{
  "data": {
    "updated_count": "integer"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль не Franchise
PRICE_LIST_NOT_FOUND404Прейскурант не найден
VALIDATION_ERROR400Невалидные данные (price < 0)


GET /products

Получить список товаров с пагинацией, поиском и фильтрами.

ПараметрЗначение
AuthBearer JWT (Franchise, Franchisee, Manager; Cashier — 403)

Query Parameters

ParamTypeRequiredDescription
pageintegernoНомер страницы (default: 1)
per_pageintegernoЗаписей на страницу (default: 20, max: 100)
searchstringnoПоиск по названию товара
typestringnoФильтр: dish / good (BR 1.11: type=ingredient removed — ingredients moved to Warehouse Service)
statusstringnoФильтр: active / inactive
deletedbooleannotrue — показать только удалённые товары (для вкладки “Удалённые”, только Franchise)
category_iduuidnoФильтр по категории
sortstringnoСортировка: name_asc (default) / name_desc / created_at_desc / sort_order_asc (BR 2.1)

Response 200

{
  "data": [
    {
      "id": "uuid",
      "name": "string",
      "type": "dish | good",
      "category_id": "uuid | null",
      "category_name": "string | null",
      "unit_of_measure": "string",
      "status": "active | inactive",
      "sku": "string | null",
      "sort_order": "integer",
      "is_alcohol": "boolean",
      "image_url": "string | null"
    }
  ],
  "meta": {
    "page": "integer",
    "per_page": "integer",
    "total": "integer"
  }
}

Ролевой доступ

  • Franchise: все товары франшизы — это справочник
  • Franchisee: активные товары
  • Manager: активные товары
  • Cashier: 403 FORBIDDEN

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль Cashier

GET /products/{id}

Получить детали товара.

ПараметрЗначение
AuthBearer JWT (Franchise — любой товар; Franchisee/Manager — активные товары; Cashier — 403)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID товара

Response 200

{
  "data": {
    "id": "uuid",
    "name": "string",
    "description": "string | null",
    "type": "dish | good",
    "category_id": "uuid | null",
    "category_name": "string | null",
    "unit_of_measure": "string",
    "status": "active | inactive",
    "sku": "string | null",
    "barcode": "string | null",
    "sort_order": "integer",
    "gross_weight": "decimal | null",
    "net_weight": "decimal | null",
    "kcal": "decimal | null",
    "protein": "decimal | null",
    "fat": "decimal | null",
    "carbs": "decimal | null",
    "assembly_time": "integer | null",
    "color": "string | null",
    "image_url": "string | null",
    "is_open_price": "boolean",
    "is_by_weight": "boolean",
    "is_exclude_from_promo": "boolean",
    "is_manual_discount_banned": "boolean",
    "is_admin_only": "boolean",
    "is_alcohol": "boolean",
    "is_tobacco": "boolean",
    "is_sugary_drink": "boolean",
    "is_marked": "boolean",
    "available_in_all_stores": "boolean",
    "store_ids": ["uuid"] ,
    "modifiers": [],
    "deleted_at": "datetime | null",
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

(BR 1.10) Цена определяется прейскурантом. (BR 2.1) store_ids — null когда available_in_all_stores = true, иначе список UUID ТТ.

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Cashier
PRODUCT_NOT_FOUND404Товар не найден или удалён

POST /products

Создать товар в справочнике франшизы.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Request Body

{
  "name": "string, required — название товара (уникальное в рамках франшизы)",
  "description": "string, optional — описание",
  "type": "string, required — dish | good",
  "category_id": "uuid, optional — FK → categories.id",
  "unit_of_measure": "string, required — piece | kg | g | liter | ml | portion",
 
  // BR 2.1: optional extended fields
  "sku": "string, optional — артикул",
  "barcode": "string, optional — штрихкод",
  "sort_order": "integer, optional — порядок сортировки (default: 0)",
  "gross_weight": "decimal, optional — вес брутто, кг",
  "net_weight": "decimal, optional — вес нетто, кг",
  "kcal": "decimal, optional — калорийность на 100г",
  "protein": "decimal, optional — белки на 100г",
  "fat": "decimal, optional — жиры на 100г",
  "carbs": "decimal, optional — углеводы на 100г",
  "assembly_time": "integer, optional — время приготовления, мин",
  "color": "string, optional — цвет POS (hex, e.g. '#FF5733')",
  "is_open_price": "boolean, optional — свободная цена (default: false)",
  "is_by_weight": "boolean, optional — продажа на развес (default: false)",
  "is_exclude_from_promo": "boolean, optional — исключить из акций (default: false)",
  "is_manual_discount_banned": "boolean, optional — запрет ручных скидок (default: false)",
  "is_admin_only": "boolean, optional — только администратор (default: false)",
  "is_alcohol": "boolean, optional — алкоголь (default: false)",
  "is_tobacco": "boolean, optional — табак (default: false)",
  "is_sugary_drink": "boolean, optional — сахаросодержащий напиток (default: false)",
  "is_marked": "boolean, optional — маркированный товар ЧЗ (default: false)",
  "available_in_all_stores": "boolean, optional — доступен во всех ТТ (default: true)",
  "vat_rate": "string, optional — ставка НДС для 54-ФЗ (default: vat20) — BR 3.3",
  "payment_subject": "string, optional — предмет расчёта (default: goods) — BR 3.3",
  "payment_type": "string, optional — способ расчёта (default: full) — BR 3.3"
}

unit_of_measure

Допустимые значения: piece, kg, g, liter, ml, portion. Маппинг на UI: piece=шт, kg=кг, g=г, liter=л, ml=мл, portion=порция.

Фискальные поля (BR 3.3)

vat_rate enum: none / vat0 / vat10 / vat20 / vat110 / vat120.
payment_subject enum: goods / service / work / excise / job / payment / agency / composite / another.
payment_type enum: full / prepay / advance / partial_prepay / credit / credit_pay / partial.
Передаются Paykeeper Adapter при формировании инвойса (fiscal_cart по 54-ФЗ). См. бизнес-спека PayKeeper.

Response 201

Полный ProductResponse (аналогично GET /products/{id}).

{
  "data": {
    "id": "uuid",
    "name": "string",
    "description": "string | null",
    "type": "dish | good",
    "category_id": "uuid | null",
    "category_name": "string | null",
    "unit_of_measure": "string",
    "status": "active",
    "sku": "string | null",
    "barcode": "string | null",
    "sort_order": "integer",
    "gross_weight": "decimal | null",
    "net_weight": "decimal | null",
    "kcal": "decimal | null",
    "protein": "decimal | null",
    "fat": "decimal | null",
    "carbs": "decimal | null",
    "assembly_time": "integer | null",
    "color": "string | null",
    "image_url": "string | null",
    "is_open_price": "boolean",
    "is_by_weight": "boolean",
    "is_exclude_from_promo": "boolean",
    "is_manual_discount_banned": "boolean",
    "is_admin_only": "boolean",
    "is_alcohol": "boolean",
    "is_tobacco": "boolean",
    "is_sugary_drink": "boolean",
    "is_marked": "boolean",
    "available_in_all_stores": "boolean",
    "vat_rate": "string",
    "payment_subject": "string",
    "payment_type": "string",
    "store_ids": ["uuid"],
    "modifiers": [],
    "deleted_at": "null",
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль не Franchise
VALIDATION_ERROR400Невалидные данные (пустое название, невалидный тип)
NAME_DUPLICATE409Товар с таким названием уже существует в каталоге франшизы

PATCH /products/{id}

Обновить товар. Изменения применяются мгновенно.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Path Parameters

ParamTypeRequiredDescription
iduuidyesID товара

Request Body

Partial update — отправляются только изменяемые поля.

{
  "name": "string, optional",
  "description": "string | null, optional",
  "type": "string, optional — dish | good",
  "category_id": "uuid | null, optional — FK → categories.id",
  "unit_of_measure": "string, optional — piece | kg | g | liter | ml | portion",
  "status": "string, optional — active | inactive",
 
  // BR 2.1: optional extended fields
  "sku": "string | null, optional",
  "barcode": "string | null, optional",
  "sort_order": "integer, optional",
  "gross_weight": "decimal | null, optional",
  "net_weight": "decimal | null, optional",
  "kcal": "decimal | null, optional",
  "protein": "decimal | null, optional",
  "fat": "decimal | null, optional",
  "carbs": "decimal | null, optional",
  "assembly_time": "integer | null, optional",
  "color": "string | null, optional",
  "is_open_price": "boolean, optional",
  "is_by_weight": "boolean, optional",
  "is_exclude_from_promo": "boolean, optional",
  "is_manual_discount_banned": "boolean, optional",
  "is_admin_only": "boolean, optional",
  "is_alcohol": "boolean, optional",
  "is_tobacco": "boolean, optional",
  "is_sugary_drink": "boolean, optional",
  "is_marked": "boolean, optional",
  "available_in_all_stores": "boolean, optional"
}

Response 200

Полный ProductResponse (аналогично GET /products/{id}).

{
  "data": {
    "id": "uuid",
    "name": "string",
    "description": "string | null",
    "type": "dish | good",
    "category_id": "uuid | null",
    "category_name": "string | null",
    "unit_of_measure": "string",
    "status": "active | inactive",
    "sku": "string | null",
    "barcode": "string | null",
    "sort_order": "integer",
    "gross_weight": "decimal | null",
    "net_weight": "decimal | null",
    "kcal": "decimal | null",
    "protein": "decimal | null",
    "fat": "decimal | null",
    "carbs": "decimal | null",
    "assembly_time": "integer | null",
    "color": "string | null",
    "image_url": "string | null",
    "is_open_price": "boolean",
    "is_by_weight": "boolean",
    "is_exclude_from_promo": "boolean",
    "is_manual_discount_banned": "boolean",
    "is_admin_only": "boolean",
    "is_alcohol": "boolean",
    "is_tobacco": "boolean",
    "is_sugary_drink": "boolean",
    "is_marked": "boolean",
    "available_in_all_stores": "boolean",
    "vat_rate": "string",
    "payment_subject": "string",
    "payment_type": "string",
    "store_ids": ["uuid"],
    "modifiers": [],
    "deleted_at": "datetime | null",
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль не Franchise
PRODUCT_NOT_FOUND404Товар не найден или удалён
VALIDATION_ERROR400Невалидные данные
NAME_DUPLICATE409Товар с таким названием уже существует в каталоге франшизы

DELETE /products/{id}

Удалить товар (soft delete). Устанавливает deleted_at, товар скрывается из общего списка.

ПараметрЗначение
AuthBearer JWT (только Franchise)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID товара

Response 204

Нет тела.

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль не Franchise
PRODUCT_NOT_FOUND404Товар не найден или уже удалён

POST /products/{id}/restore

Восстановить удалённый товар. Убирает deleted_at.

ПараметрЗначение
AuthBearer JWT (только Franchise)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID товара

Response 200

Полный ProductResponse (аналогично GET /products/{id}).

{
  "data": {
    "id": "uuid",
    "name": "string",
    "description": "string | null",
    "type": "dish | good",
    "category_id": "uuid | null",
    "category_name": "string | null",
    "unit_of_measure": "string",
    "status": "active | inactive",
    "sku": "string | null",
    "barcode": "string | null",
    "sort_order": "integer",
    "gross_weight": "decimal | null",
    "net_weight": "decimal | null",
    "kcal": "decimal | null",
    "protein": "decimal | null",
    "fat": "decimal | null",
    "carbs": "decimal | null",
    "assembly_time": "integer | null",
    "color": "string | null",
    "image_url": "string | null",
    "is_open_price": "boolean",
    "is_by_weight": "boolean",
    "is_exclude_from_promo": "boolean",
    "is_manual_discount_banned": "boolean",
    "is_admin_only": "boolean",
    "is_alcohol": "boolean",
    "is_tobacco": "boolean",
    "is_sugary_drink": "boolean",
    "is_marked": "boolean",
    "available_in_all_stores": "boolean",
    "vat_rate": "string",
    "payment_subject": "string",
    "payment_type": "string",
    "store_ids": ["uuid"],
    "modifiers": [],
    "deleted_at": "null",
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль не Franchise
PRODUCT_NOT_FOUND404Товар не найден
PRODUCT_NOT_DELETED422Товар не удалён (нельзя восстановить активный товар)
NAME_DUPLICATE409Товар с таким названием уже существует (другой товар занял имя после удаления)

PATCH /products/{id}/stores

(BR 2.1) Задать список ТТ, в которых доступен товар. Если передать пустой массив — товар доступен во всех ТТ (available_in_all_stores = true).

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typeapplication/json

Path Parameters

ParamTypeRequiredDescription
iduuidyesID товара

Request Body

{
  "store_ids": ["uuid", "uuid"]
}
ПолеТипRequiredОписание
store_idsuuid[]yesСписок store_id. Пустой массив = доступен везде

Response 200

Возвращает полный объект товара (см. GET /products/{id}).

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль не Franchise
PRODUCT_NOT_FOUND404Товар не найден

POST /products/{id}/image

(BR 2.1) Загрузить или заменить изображение товара в S3. Возвращает публичный URL.

ПараметрЗначение
AuthBearer JWT (только Franchise)
Content-Typemultipart/form-data

Path Parameters

ParamTypeRequiredDescription
iduuidyesID товара

Request Body (form-data)

ПолеТипRequiredОписание
fileFileyesФайл изображения (jpg, png, webp)

Response 200

{
  "data": {
    "image_url": "https://s3.example.com/erp-catalog/products/{id}/image.jpg"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль не Franchise
PRODUCT_NOT_FOUND404Товар не найден

DELETE /products/{id}/image

(BR 2.1) Удалить изображение товара из S3 и очистить поле image_url.

ПараметрЗначение
AuthBearer JWT (только Franchise)

Path Parameters

ParamTypeRequiredDescription
iduuidyesID товара

Response 204

Нет тела.

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
FORBIDDEN403Роль не Franchise
PRODUCT_NOT_FOUND404Товар не найден

GET /stop-lists/stores/{storeId}

(BR 1.13)

Получить стоп-лист торговой точки — все остановленные товары и категории.

ПараметрЗначение
AuthBearer JWT (Franchise — любая ТТ; Franchisee — свои ТТ; Manager — своя ТТ; Cashier — read-only)

Path Parameters

ParamTypeRequiredDescription
storeIduuidyesID торговой точки

Response 200

{
  "data": {
    "products": [
      {
        "product_id": "uuid",
        "product_name": "string",
        "reason": "string | null",
        "stopped_by": "uuid",
        "created_at": "datetime"
      }
    ],
    "categories": [
      {
        "category_id": "uuid",
        "category_name": "string",
        "reason": "string | null",
        "stopped_by": "uuid",
        "created_at": "datetime"
      }
    ]
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
STORE_ACCESS_DENIED403Нет доступа к этой ТТ

POST /stop-lists/stores/{storeId}/products

(BR 1.13)

Остановить товар на ТТ (добавить в стоп-лист).

ПараметрЗначение
AuthBearer JWT (Franchise — любая ТТ; Franchisee — свои ТТ; Manager — своя ТТ; Cashier — 403)
Content-Typeapplication/json

Path Parameters

ParamTypeRequiredDescription
storeIduuidyesID торговой точки

Request Body

{
  "product_id": "uuid, required — ID товара",
  "reason": "string, optional — причина остановки"
}

Response 201

{
  "data": {
    "product_id": "uuid",
    "product_name": "string",
    "reason": "string | null",
    "stopped_by": "uuid",
    "created_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
STORE_ACCESS_DENIED403Нет доступа к этой ТТ
PRODUCT_NOT_FOUND404Товар не найден
PRODUCT_ALREADY_STOPPED409Товар уже в стоп-листе на этой ТТ

DELETE /stop-lists/stores/{storeId}/products/{productId}

(BR 1.13)

Снять стоп с товара (убрать из стоп-листа).

ПараметрЗначение
AuthBearer JWT (Franchise — любая ТТ; Franchisee — свои ТТ; Manager — своя ТТ; Cashier — 403)

Path Parameters

ParamTypeRequiredDescription
storeIduuidyesID торговой точки
productIduuidyesID товара

Response 204

Нет тела.

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
STORE_ACCESS_DENIED403Нет доступа к этой ТТ
PRODUCT_NOT_STOPPED404Товар не в стоп-листе

POST /stop-lists/stores/{storeId}/categories

(BR 1.13)

Остановить категорию на ТТ (добавить в стоп-лист). Блокирует все товары этой категории.

ПараметрЗначение
AuthBearer JWT (Franchise — любая ТТ; Franchisee — свои ТТ; Manager — своя ТТ; Cashier — 403)
Content-Typeapplication/json

Path Parameters

ParamTypeRequiredDescription
storeIduuidyesID торговой точки

Request Body

{
  "category_id": "uuid, required — ID категории",
  "reason": "string, optional — причина остановки"
}

Response 201

{
  "data": {
    "category_id": "uuid",
    "category_name": "string",
    "reason": "string | null",
    "stopped_by": "uuid",
    "created_at": "datetime"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
STORE_ACCESS_DENIED403Нет доступа к этой ТТ
CATEGORY_NOT_FOUND404Категория не найдена
CATEGORY_ALREADY_STOPPED409Категория уже в стоп-листе на этой ТТ

DELETE /stop-lists/stores/{storeId}/categories/{categoryId}

(BR 1.13)

Снять стоп с категории (убрать из стоп-листа).

ПараметрЗначение
AuthBearer JWT (Franchise — любая ТТ; Franchisee — свои ТТ; Manager — своя ТТ; Cashier — 403)

Path Parameters

ParamTypeRequiredDescription
storeIduuidyesID торговой точки
categoryIduuidyesID категории

Response 204

Нет тела.

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
STORE_ACCESS_DENIED403Нет доступа к этой ТТ
CATEGORY_NOT_STOPPED404Категория не в стоп-листе

GET /stop-lists/stores/{storeId}/check/{productId}

(BR 1.13)

Проверить доступность товара на ТТ (учитывает стоп-листы по товару и по категории). Используется для POS и вычисляемого меню.

ПараметрЗначение
AuthBearer JWT (все роли)

Path Parameters

ParamTypeRequiredDescription
storeIduuidyesID торговой точки
productIduuidyesID товара

Response 200

{
  "data": {
    "available": "boolean",
    "reason": "string | null — причина блокировки (если есть)",
    "stopped_by": "product | category | null — источник блокировки"
  }
}

Errors

CodeHTTPКогда
UNAUTHORIZED401Невалидный JWT
PRODUCT_NOT_FOUND404Товар не найден

GET /internal/catalog/menu

Вычисляемое меню для ТТ (BR 1.16). Возвращает категории, товары с ценами и модификаторами в одном запросе. При передаче store_id — фильтрует по стоп-листам.

ПараметрЗначение
AuthService token (X-Service-Token)

Query Parameters

ПараметрТипОбязательныйОписание
franchise_iduuidДаTenant scope
price_list_iduuidНетПрейскурант; если не передан — используется дефолтный франшизы
store_iduuidНетТТ для фильтрации по стоп-листам

Response 200

{
  "data": {
    "price_list_id": "uuid | null",
    "categories": [
      {
        "id": "uuid",
        "name": "string",
        "parent_id": "uuid | null",
        "sort_order": "integer"
      }
    ],
    "products": [
      {
        "id": "uuid",
        "name": "string",
        "category_id": "uuid",
        "image_url": "string | null",
        "is_open_price": "boolean",
        "price": "decimal | null",
        "modifiers": [
          {
            "group_id": "uuid",
            "group_name": "string",
            "binding_type": "string — free | structural",
            "min": "integer — effective min (override или group default)",
            "max": "integer — effective max (override или group default)",
            "options": [
              {
                "id": "uuid",
                "name": "string",
                "price": "decimal | null — цена из прейскуранта (только для free)"
              }
            ]
          }
        ]
      }
    ]
  }
}

Фильтрация по стоп-листам

Когда передан store_id:

  • Категории из стоп-листа ТТ исключаются
  • Товары из стоп-листа ТТ исключаются
  • Товары, чья категория в стоп-листе, тоже исключаются

Без store_id — возвращается полный каталог без фильтрации.

Модификаторы

  • binding_type: "structural" — опции без цены (вариации товара, цена в product price)
  • binding_type: "free" — опции с ценой из прейскуранта (price_list_modifier_items)
  • min/max — effective значения: если в product_modifiers есть override — он приоритетнее

Кухонные станции (BR 2.5)

Секция добавлена в BR 2.5. Бизнес-спека: Кухонные станции.

GET /kitchen-stations

Список станций франшизы.

ПараметрЗначение
AuthBearer JWT с permission catalog.read

Query Parameters

ParamTypeRequiredDescription
include_deletedboolnoПо умолчанию false. True — вернуть и soft-deleted

Response 200

{
  "data": [
    {
      "id": "uuid",
      "name": "Горячая кухня",
      "description": "string | null",
      "yellow_threshold_minutes": 5,
      "red_threshold_minutes": 0,
      "product_count": 42,
      "created_at": "datetime",
      "updated_at": "datetime"
    }
  ]
}

product_count — количество активных товаров, ссылающихся на станцию.

yellow_threshold_minutes / red_threshold_minutes — пороги цвета на KDS-карточке (см. Кухонные станции). (Добавлено в BR 5.1)


POST /kitchen-stations

Создать станцию.

ПараметрЗначение
AuthBearer JWT с catalog.edit
Content-Typeapplication/json

Request Body

{
  "name": "Горячая кухня",
  "description": "Плита, вок, гриль — станция для горячих блюд",
  "yellow_threshold_minutes": 5,
  "red_threshold_minutes": 0
}
FieldTypeRequiredDescription
namestringyesУникально per franchise (регистронезависимо), max 50 символов
descriptionstringno
yellow_threshold_minutesintnoDefault 5. Порог жёлтой зоны на KDS. (BR 5.1)
red_threshold_minutesintnoDefault 0. Порог просрочки. (BR 5.1)

Response 201

{
  "data": {
    "id": "uuid",
    "name": "Горячая кухня",
    "description": "...",
    "yellow_threshold_minutes": 5,
    "red_threshold_minutes": 0,
    "product_count": 0,
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPDescription
VALIDATION_ERROR400Пустое name, name >50 символов
DUPLICATE_STATION409Станция с таким именем (case-insensitive) уже есть во франшизе

PATCH /kitchen-stations/{id}

Обновить станцию.

ПараметрЗначение
AuthBearer JWT с catalog.edit

Request Body

{
  "name": "string",                  // optional
  "description": "string",           // optional
  "yellow_threshold_minutes": 5,     // optional (BR 5.1)
  "red_threshold_minutes": 0         // optional (BR 5.1)
}

При изменении порогов публикуется событие catalog.kds_settings.updated (kind=station_thresholds) — см. Events.

Response 200

Полная запись станции.

Errors

CodeHTTPDescription
STATION_NOT_FOUND404
DUPLICATE_STATION409
VALIDATION_ERROR400Пороги < 0

DELETE /kitchen-stations/{id}

Soft-delete. Проставляет deleted_at.

ПараметрЗначение
AuthBearer JWT с catalog.edit

Response 204 — успех

Errors

CodeHTTPDescription
STATION_NOT_FOUND404
STATION_IN_USE422На станцию ссылается минимум один активный товар. В details — массив { product_id, product_name } первых 10 связанных товаров + total_count

GET /admin/kds/settings

(BR 5.1 — KDS)

Per-франшиза настройки KDS-приложения (звуки, интервалы, авто-логаут). Если запись не существует — auto-create со значениями default и вернуть.

ПараметрЗначение
AuthBearer JWT с permission catalog.read или kds.settings.edit

Response 200

{
  "data": {
    "franchise_id": "uuid",
    "new_order_sound": "bell",
    "new_order_repeat_seconds": 30,
    "overdue_sound": "alarm",
    "sound_volume": 80,
    "auto_logout_minutes": 30,
    "created_at": "datetime",
    "updated_at": "datetime"
  }
}

Errors

CodeHTTPDescription
UNAUTHORIZED401
FORBIDDEN403Нет permission

PATCH /admin/kds/settings

(BR 5.1 — KDS)

Обновление настроек KDS франшизы. Partial update — указываются только изменяемые поля.

ПараметрЗначение
AuthBearer JWT с kds.settings.edit
Content-Typeapplication/json

Request Body

{
  "new_order_sound": "chime",                // optional
  "new_order_repeat_seconds": 45,            // optional, 5–120
  "overdue_sound": "alarm",                  // optional
  "sound_volume": 70,                        // optional, 0–100
  "auto_logout_minutes": 60                  // optional, 5–240
}

Response 200

Обновлённая запись (формат как GET). Публикуется событие catalog.kds_settings.updated с kind=settings — см. Events.

Errors

CodeHTTPDescription
UNAUTHORIZED401
FORBIDDEN403Нет permission kds.settings.edit
VALIDATION_ERROR400Значение вне допустимого диапазона (детали в details[])
INVALID_SOUND400Неизвестное имя мелодии (не из встроенного набора bell/chime/buzzer/marimba/digital для new_order и alarm/siren/bell-loud для overdue)

Recipe / техкарты для KDS

Endpoint для получения техкарты блюда (используется в KDS при тапе «?» рядом с позицией) живёт в Warehouse Service: GET /tech-cards?product_id={uuid} (см. Warehouse Service API). pos-bff проксирует это для KDS как GET /api/v1/pos/products/{id}/recipe.


GET /internal/catalog/full-snapshot

Полный срез каталога франшизы: все товары + группы модификаторов + резолв-мапа категорий. Используется Paykeeper Adapter для ночного cron full re-sync и manual «Пересинхронизировать каталог» (BR 3.4).

ПараметрЗначение
AuthX-Service-Token

Query Parameters

ПараметрТипОбяз.Описание
franchise_iduuidДаФраншиза, чей каталог возвращаем

Response 200

{
  "data": {
    "franchise_id": "uuid",
    "generated_at": "ISO-8601 UTC",
    "products": [
      {
        "id": "uuid",
        "name": "string",
        "category_path": "string | null",
        "base_price": "decimal",
        "vat_rate": "string",
        "unit_of_measure": "string",
        "is_marked": "boolean",
        "is_open_price": "boolean",
        "is_by_weight": "boolean",
        "is_alcohol": "boolean",
        "modifier_group_ids": ["uuid"],
        "deleted_at": "datetime | null"
      }
    ],
    "modifier_groups": [
      {
        "id": "uuid",
        "group_name": "string",
        "binding_type": "structural | free",
        "min": "integer",
        "max": "integer",
        "options": [
          {
            "id": "uuid",
            "name": "string",
            "price": "decimal | null"
          }
        ]
      }
    ]
  }
}

category_path — готовый префикс имени, сформированный из дерева категорий (например "Кофе / Холодное"). Консьюмеру не нужно резолвить иерархию самостоятельно.

Возвращаются все записи включая soft-deleted (поле deleted_at заполнено для фильтрации на стороне консьюмера). Это позволяет adapter’у корректно пометить удалённые товары в своём mapping’е при first-time онбординге, когда событий catalog.*.deleted он пропустил.

Нет секции categories

Отдельная сущность категорий в PayKeeper ims-api не синхронизируется — категории используются только как часть имени товара через category_path. В snapshot’е отдельного массива категорий нет.

Rate limiting

1 запрос / 10 секунд на один franchise_id. При превышении — 429 RATE_LIMITED с заголовком Retry-After.

Errors

CodeHTTPDescription
FRANCHISE_NOT_FOUND404franchise_id не существует
VALIDATION_ERROR400Отсутствует или невалиден franchise_id
UNAUTHORIZED401Нет / невалидный X-Service-Token
RATE_LIMITED429Превышен лимит

GET /internal/catalog/products/{id}/expand

Возвращает раскладку одного товара ERP на виртуальные PK-продукты по правилу развёртывания (спека §Catalog Sync). Используется Paykeeper Adapter при event-driven delta-sync, чтобы не дёргать full-snapshot ради одного товара (BR 3.4).

ПараметрЗначение
AuthX-Service-Token

Path Parameters

ПараметрТипОписание
iduuidproducts.id

Response 200

{
  "data": {
    "product_id": "uuid",
    "franchise_id": "uuid",
    "variants": [
      {
        "erp_structural_option_id": "uuid | null",
        "erp_free_option_id": "uuid | null",
        "variant_kind": "base | structural_variant | free_addon",
        "name": "string",
        "sku": "string",
        "price": "decimal",
        "tax": "string"
      }
    ]
  }
}

Каждый элемент variants — готовый payload для POST /products в ims-api.

  • name уже содержит category_path как префикс и развёрнутые имена модификаторов.
  • sku в формате "{product_id}[:{structural_option_id}][:+{free_option_id}]".
  • price:
    • base — базовая цена товара
    • structural_variant — базовая цена + доплата опции
    • free_addon — только цена опции (доплата)

Errors

CodeHTTPDescription
PRODUCT_NOT_FOUND404Товар не существует (или soft-deleted — используй .deleted_at из snapshot’а)
UNAUTHORIZED401

External Menus (BR 4.1)

Конструктор внешних меню — рекламный монитор, JSON-экспорт. Якорная связь с каталогом обязательна. Stop-list применяется автоматически при рендере. См. спека.


GET /external-menus

Список внешних меню франшизы.

ПараметрЗначение
AuthBearer JWT (external_menus.read)

Query Parameters

ПараметрТипОписание
channelstringФильтр: tv_screen / json / опц.
statusstringФильтр: draft / published / all (default: draft,published без archived)
store_iduuidФильтр: показать только меню привязанные к этой ТТ (или включая null-bound)
searchstringПоиск по name (debounce клиента)
pageintdefault 1
per_pageintdefault 20, max 100

Response 200

{
  "data": [
    {
      "id": "uuid",
      "name": "Бар — основной экран",
      "channel": "tv_screen",
      "store_id": "uuid | null",
      "store_name": "Название ТТ | null",
      "template": "grid",
      "slug": "bar-mainscreen",
      "status": "published",
      "live_url": "https://erp-test.nirbi.ru/r/bar-mainscreen",
      "created_at": "2026-04-28T10:00:00Z",
      "updated_at": "2026-04-28T11:00:00Z"
    }
  ],
  "meta": { "page": 1, "per_page": 20, "total": 5 }
}

live_url рассчитывается на сервере: WEBHOOK_BASE_URL + /r/ + slug. Возвращается только для status = published.


GET /external-menus/archived

Список меню в корзине (архивных) — для вкладки «Корзина». Только за последние 30 дней (более старые удаляются cron’ом физически).

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Response 200

Тот же формат что GET /external-menus, но возвращает только status = archived. Дополнительно поле archived_at.


GET /external-menus/{id}

Детали меню с категориями и items (для редактора).

ПараметрЗначение
AuthBearer JWT (external_menus.read)

Response 200

{
  "data": {
    "id": "uuid",
    "name": "Бар — основной экран",
    "channel": "tv_screen",
    "store_id": "uuid | null",
    "template": "grid",
    "slug": "bar-mainscreen",
    "status": "published",
    "live_url": "https://erp-test.nirbi.ru/r/bar-mainscreen",
    "created_at": "...",
    "updated_at": "...",
    "categories": [
      {
        "id": "uuid",
        "name": "Кофе",
        "original_category_id": "uuid | null",
        "display_order": 0,
        "icon_url": "string | null",
        "items": [
          {
            "id": "uuid",
            "product_id": "uuid",
            "product_name": "Капучино",
            "product_image_url": "string | null",
            "catalog_price": 250.00,
            "price_list_price": 270.00,
            "override_name": "string | null",
            "override_description": "string | null",
            "override_price": 290.00,
            "effective_price": 290.00,
            "visible": true,
            "display_order": 0,
            "status": "ok",
            "in_stop_list": false
          }
        ]
      }
    ]
  }
}

effective_price рассчитывается на сервере по иерархии override > price_list > catalog. in_stop_list — для индикации владельцу (но при рендере на монитор всё равно скрыто).

Errors

CodeHTTPОписание
NOT_FOUND404Меню не найдено или вне scope пользователя

POST /external-menus

Создать меню (статус draft). Категорий и items нет — добавляются отдельно.

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Request Body

{
  "name": "Бар — основной экран",
  "channel": "tv_screen",
  "store_id": "uuid | null",
  "template": "grid | slider | list",
  "slug": "string | null"
}
ПолеОбяз.Описание
nameДаУникально среди активных в франшизе
channelДаtv_screen / json
store_idНетNULL — на всю сеть
templateУсловноОбязателен для tv_screen, NULL для json
slugНетЕсли NULL — авто-генерируется как menu-{8-char-id}. Regex ^[a-z0-9-]{3,40}$, UNIQUE глобально

Response 201

Идентично GET /external-menus/{id} (categories + items пустые).

Errors

CodeHTTPОписание
VALIDATION_ERROR400Невалидные поля, отсутствует template для tv_screen, слишком короткий slug
NAME_EXISTS409Имя уже занято в активных меню
SLUG_TAKEN409slug уже используется
STORE_NOT_IN_SCOPE403ТТ не принадлежит scope пользователя

PATCH /external-menus/{id}

Обновить заголовок меню (имя, шаблон, slug, ТТ-привязку). channel менять нельзя.

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Request Body (partial)

{
  "name": "string",
  "template": "grid | slider | list",
  "slug": "string",
  "store_id": "uuid | null"
}

Response 200

Идентично GET /external-menus/{id}.

Errors

CodeHTTPОписание
NAME_EXISTS409
SLUG_TAKEN409
CHANNEL_LOCKED422Попытка сменить channel

POST /external-menus/{id}/publish

Перевести меню в published. Для tv_screen после этого работает live URL.

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Response 200

{ "data": { "id": "uuid", "status": "published", "live_url": "..." } }

Errors

CodeHTTPОписание
BUSINESS_RULE_VIOLATION422Меню пустое (нет items) — публикация отклонена
ALREADY_PUBLISHED422Уже опубликовано

POST /external-menus/{id}/unpublish

Вернуть меню в draft. Live URL начинает возвращать 404, WebSocket-клиенты получают menu_unpublished.

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Response 200

{ "data": { "id": "uuid", "status": "draft" } }

POST /external-menus/{id}/duplicate

Создать копию меню с тем же содержимым (категории + items с overrides). Новое меню в draft, имя {Original} — копия, новый slug menu-{new-8-char-id}.

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Response 201

Возвращает новый объект (как POST /external-menus).


DELETE /external-menus/{id}

Soft delete. Меню переходит в status = archived, заполняется archived_at. Через 30 дней — физическое удаление cron’ом.

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Response 204

Live URL сразу 404. WebSocket-клиенты получают menu_archived.


POST /external-menus/{id}/restore

Восстановить из архива. Меню возвращается в status = draft, archived_at = NULL.

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Response 200

Идентично GET /external-menus/{id}.

Errors

CodeHTTPОписание
NOT_FOUND404Меню не в архиве (или физически удалено cron’ом по истечении 30 дней)
NAME_EXISTS409За время архива создано другое меню с таким же именем — нужно переименовать перед восстановлением
SLUG_TAKEN409Аналогично

POST /external-menus/{id}/categories

Создать категорию внутри меню.

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Request Body

{
  "name": "Кофе",
  "original_category_id": "uuid | null",
  "icon_url": "string | null"
}

Response 201

{
  "data": {
    "id": "uuid",
    "name": "Кофе",
    "original_category_id": "uuid | null",
    "display_order": 0,
    "icon_url": null
  }
}

display_order назначается автоматически (MAX + 1 в этом меню).


PATCH /external-menus/{id}/categories/{categoryId}

Обновить категорию (имя, иконка).

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Request Body (partial)

{ "name": "string", "icon_url": "string | null" }

DELETE /external-menus/{id}/categories/{categoryId}

Удалить категорию вместе со всеми items в ней (CASCADE).

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Response 204


POST /external-menus/{id}/categories/reorder

Изменить порядок категорий в меню массивом.

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Request Body

{
  "order": [
    { "category_id": "uuid", "display_order": 0 },
    { "category_id": "uuid", "display_order": 1 }
  ]
}

Response 200

{ "data": { "ok": true } }

POST /external-menus/{id}/items

Добавить товар в меню (drag-drop из каталога). Override-поля по умолчанию NULL — наследует значения каталога.

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Request Body

{
  "product_id": "uuid",
  "category_id": "uuid"
}

Response 201

{
  "data": {
    "id": "uuid",
    "product_id": "uuid",
    "product_name": "Капучино",
    "product_image_url": "string | null",
    "catalog_price": 250.00,
    "price_list_price": 270.00,
    "override_name": null,
    "override_description": null,
    "override_price": null,
    "effective_price": 270.00,
    "visible": true,
    "display_order": 0,
    "status": "ok",
    "in_stop_list": false
  }
}

Errors

CodeHTTPОписание
PRODUCT_NOT_FOUND404product_id не существует или soft-deleted
PRODUCT_NOT_IN_FRANCHISE422Товар не принадлежит этой франшизе
ITEM_DUPLICATE409Товар уже есть в этом меню (UNIQUE external_menu_id+product_id)
CATEGORY_NOT_FOUND404category_id не принадлежит этому меню

PATCH /external-menus/{id}/items/{itemId}

Обновить override-поля item. null для override_* очищает override (вернёт значение из каталога).

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Request Body (partial)

{
  "override_name": "string | null",
  "override_description": "string | null",
  "override_price": "number | null",
  "visible": true,
  "category_id": "uuid"
}

Изменение category_id перемещает item в другую категорию этого же меню.

Response 200

Идентично POST /external-menus/{id}/items.


DELETE /external-menus/{id}/items/{itemId}

Удалить item из меню (физически — это override-запись, не товар каталога).

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Response 204


POST /external-menus/{id}/items/reorder

Изменить порядок items внутри категории.

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Request Body

{
  "category_id": "uuid",
  "order": [
    { "item_id": "uuid", "display_order": 0 },
    { "item_id": "uuid", "display_order": 1 }
  ]
}

Response 200

{ "data": { "ok": true } }


POST /external-menus/{id}/items/{itemId}/restore

Снять статус orphan с item (если в каталоге товар восстановили из soft-delete).

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Response 200

{ "data": { "id": "uuid", "status": "ok" } }

Errors

CodeHTTPОписание
BUSINESS_RULE_VIOLATION422Товар каталога всё ещё удалён — нельзя восстановить item пока товар не восстановлен

GET /external-menus/{id}/export.json

Универсальный JSON-экспорт меню. Stop-list применён, orphan-items скрыты, override применены.

ПараметрЗначение
AuthBearer JWT (external_menus.read)

Response 200

{
  "menu_id": "uuid",
  "name": "Бар — основной экран",
  "channel": "json",
  "store_id": "uuid | null",
  "generated_at": "2026-04-28T15:00:00Z",
  "categories": [
    {
      "id": "uuid",
      "name": "Кофе",
      "items": [
        {
          "product_id": "uuid",
          "name": "Капучино",
          "description": "string | null",
          "price": 290.00,
          "image_url": "string | null"
        }
      ]
    }
  ]
}

Errors

CodeHTTPОписание
MENU_NOT_PUBLISHED422Меню в draft — экспорт недоступен

GET /external-menus/{id}/export.zip

Скачать offline ZIP-архив для канала tv_screen. Содержит index.html + style.css + assets/ со снапшотом меню на момент скачивания.

ПараметрЗначение
AuthBearer JWT (external_menus.edit)

Response 200

Content-Type: application/zip. Файл ERP-POS-menu-{slug}-{date}.zip.

Errors

CodeHTTPОписание
INVALID_CHANNEL422Меню не tv_screen — ZIP не предусмотрен

GET /r/{slug}

Public render endpoint для рекламного монитора. Открывается в Chromium kiosk-mode на железе монитора. Не требует авторизации — защита через знание slug.

ПараметрЗначение
AuthPublic (slug — secret enough)

Response 200

HTML-страница с шаблоном (grid / slider / list), embedded data, JS для WebSocket подключения.

Response 404

  • Меню не найдено
  • Меню в status != published
  • Меню в архиве

Query Parameters

ПараметрТипОписание
previewbool1 — preview-режим, рендерит draft (требует preview-токен в cookie или header)

GET /r/{slug}/data

JSON для AJAX-обновления (используется когда WebSocket недоступен — fallback к polling).

ПараметрЗначение
AuthPublic

Response 200

{
  "menu_id": "uuid",
  "version_hash": "string — SHA-256 от состояния меню для оптимизации",
  "categories": [
    {
      "id": "uuid",
      "name": "Кофе",
      "icon_url": "string | null",
      "items": [
        {
          "id": "uuid",
          "product_id": "uuid",
          "name": "Капучино",
          "description": "string | null",
          "price": 290.00,
          "image_url": "string | null",
          "display_order": 0
        }
      ]
    }
  ]
}

version_hash — клиент сравнивает с прошлой версией; если не изменилось — не перерисовывает DOM.


WebSocket /r/{slug}/stream

WebSocket для live-обновлений. Сервер шлёт menu_updated при изменении меню. Клиент при получении делает GET /r/{slug}/data для свежих данных.

ПараметрЗначение
AuthPublic
ProtocolWebSocket

Server messages

{ "type": "menu_updated", "version_hash": "string" }
{ "type": "menu_unpublished" }
{ "type": "menu_archived" }
{ "type": "ping" }

Client messages

{ "type": "pong" }

Расширение формы товара (BR 2.5)

Добавлены поля в existing endpoint-ах POST /products, PATCH /products/{id}, GET /products/{id}:

  • requires_kitchen (boolean, default false)
  • kitchen_station_id (uuid, nullable)

Валидация при сохранении (POST/PATCH):

  • Если requires_kitchen = true и kitchen_station_id IS NULL422 MISSING_KITCHEN_STATION
  • Если requires_kitchen = false и kitchen_station_id передан → сервис автоматически сбрасывает kitchen_station_id = NULL (не отклоняет запрос)
  • Если kitchen_station_id указывает на несуществующую / soft-deleted станцию → 422 STATION_NOT_FOUND

Общий формат ошибок

{
  "error": {
    "code": "string — машиночитаемый код (UPPER_SNAKE_CASE)",
    "message": "string — человекочитаемое описание",
    "details": [
      {
        "field": "string — путь к полю",
        "message": "string — описание ошибки"
      }
    ]
  }
}