Метод POST /api/v1/menu/custom

Документация API: Создание нового кастомного шаблона меню компании (Create) с регистрацией структуры в СУБД

Published

July 2, 2026

1 Функциональное назначение

Метод предназначен для проектирования и первичного сохранения нового кастомного шаблона комплексного меню менеджером предприятия (account_type == OFFICE).

Метод решает следующие задачи: 1. Регистрация новой технологической карты: Принимает текстовое название меню и массив ингредиентов (наименования, веса, штуки), которые сотрудники офиса будут списывать за один клик при обеде. 2. Автоматическая изоляция (Multi-Tenancy): Намертво привязывает создаваемый шаблон к home_group_id текущего офиса, извлеченному из JWT-токена. Никакая другая компания в системе не сможет увидеть или использовать этот шаблон. 3. Первичный аудит лимитов: Ограничивает максимальное количество блюд внутри одного меню (не более 15 позиций), чтобы защитить СУБД от раздувания и гарантировать быструю gRPC-передачу данных в будущем.

2 Протокол взаимодействия (HTTP Контракт)

  • Метод: POST
  • Маршрут: /api/v1/menu/custom
  • Формат данных: application/json

2.1 Спецификация заголовков (HTTP Headers)

Заголовок Обязательный Описание Пример значения
Content-Type Да Указывает на передачу строго типизированного JSON-пакета application/json
Authorization Да Токен авторизации (Access Token) для проверки прав администратора офиса Bearer eyJhbGciOiJIUzI1Ni...
X-Request-ID Да Сквозной ID запроса для связывания логов создания сущности req-menu-create-11aa

2.2 Спецификация тела запроса (Request Body)

В массиве items_matrix менеджер передает канонические имена продуктов из мастер-словарей, которые необходимы для сборки этого меню.

Поле Тип Обязательный Описание Пример значения
menu_title String Да Понятное название комплексного обеда для сотрудников "Обед Среда: Плов и Компот"
items_matrix Array Да Список канонических продуктов, весов/штук и единиц измерения [..._name: "Рис", qty: 0.200]

2.2.1 Пример сырого JSON-запроса (Payload):

{
  "menu_title": "Обед Среда: Плов и Компот",
  "items_matrix": [
    {
      "product_name": "Рис лазер каноничный",
      "required_quantity": 0.2000,
      "unit": "кг"
    },
    {
      "product_name": "Мясо говядина",
      "required_quantity": 0.1500,
      "unit": "кг"
    },
    {
      "product_name": "Компот ягодный готовый",
      "required_quantity": 1.0000,
      "unit": "шт"
    }
  ]
}

3 Схема обработки запроса пользователя (Mermaid)

На диаграмме представлена логика создания нового кастомного меню компании. Происходит атомарная запись в родительскую и дочернюю таблицы Базы Данных с жесткой привязкой к контексту корпоративного home_group_id из JWT.

sequenceDiagram
    autonumber
    actor Manager as Менеджер (Офисный аккаунт)
    participant APP as Мобильное приложение (Flutter)
    participant MS as Сервис Меню (FastAPI)
    participant DB as База Данных (PostgreSQL)
    participant KAFKA as Брокер событий (Kafka)

    Manager->>APP: Заполняет форму меню и нажимает "Создать шаблон"
    APP->>MS: HTTP POST /api/v1/menu/custom (JSON структуры меню)
    activate MS
    
    note over MS: Шаг 3: Валидация JWT (account_type == OFFICE)
    note over MS: Шаг 4: Валидация лимита элементов (макс. 15 позиций)
    
    Note over MS, DB: Старт атомарной SQL-транзакции
    
    MS->>DB: INSERT INTO company_custom_menus (home_group_id, menu_title)
    activate DB
    DB-->>MS: Возвращает сгенерированный menu_template_id
    deactivate DB
    
    loop По каждому продукту из массива items_matrix
        MS->>DB: INSERT INTO company_menu_items (menu_template_id, product_name, ...)
    end
    
    Note over MS, DB: Коммит транзакции (COMMIT)
    
    MS->>KAFKA: send_to_kafka_async("masterdata.menu.mutated.v1")
    MS-->>APP: HTTP 201 Created (JSON с ID нового меню)
    deactivate MS
    
    note over APP: Интерфейс очищается, новое меню добавляется в список доступных обедов

4 Расшифровка шагов

  • Шаг 1 (Manager -> APP): Менеджер предприятия открывает конструктор корпоративного меню во Flutter-приложении, вводит название («Обед Среда: Плов и Компот»), добавляет строки ингредиентов с указанием их канонических имен, точных весов или штук и нажимает кнопку «Создать шаблон».
  • Шаг 2 (APP -> MS): Приложение через клиент Dio отправляет запрос HTTP POST /api/v1/menu/custom. Все структурированные данные упаковываются в JSON-тело. В заголовках прокидывается Access Token авторизации и сквозной X-Request-ID.
  • Шаг 3 (MS -> MS): Микросервис Меню (FastAPI) перехватывает запрос и проводит декодирование JWT-токена. Если параметр account_type равен HOME (обычная семья), метод аварийно прерывает выполнение с ошибкой HTTP 403 Forbidden, так как контур планирования меню изолирован для нужд предприятий.
  • Шаг 4 (MS -> MS): Валидация бизнес-лимитов: Срабатывает проверка длины массива items_matrix. Если менеджер попытался внести более 15 позиций в один шаблон, бэкенд отсекает запрос с ошибкой HTTP 400 Bad Request, предотвращая избыточную нагрузку на СУБД и будущие gRPC-вызовы.
  • Шаг 5 (MS -> DB): Сервис Меню открывает атомарную SQL-транзакцию в PostgreSQL. Первым действием выполняется вставка в родительскую таблицу: INSERT INTO company_custom_menus (home_group_id, menu_title) VALUES ($1, $2) RETURNING id. Параметр home_group_id берется строго из токена, что исключает фальсификацию данных.
  • Шаг 6 (DB -> MS): База данных генерирует и возвращает уникальный последовательный идентификатор menu_template_id для созданной шапки меню.
  • Шаг 7 (MS -> DB): Запускается цикл формирования технологической карты обеда (связь 1:M). Бэкенд итерирует массив items_matrix и для каждого продукта выполняет DML-команду множественной вставки: INSERT INTO company_menu_items (menu_template_id, product_name, required_quantity, unit) VALUES ($1, $2, $3, $4).
  • Шаг 8 (MS -> API): После успешной записи всех дочерних строк транзакция фиксируется в базе данных командой COMMIT. Новый шаблон становится доступен для использования.
  • Шаг 9 (MS -> KAFKA): Сервис Меню публикует асинхронное бизнес-событие в топик Kafka masterdata.menu.mutated.v1 с типом "MENU_CREATED". Это уведомляет ИИ-сервер аналитики о появлении нового паттерна данных, который необходимо проиндексировать в кэше.
  • Шаг 10 (MS -> APP): Сервер возвращает мобильному приложению статус HTTP 201 Created с телом ответа, содержащим ID созданного меню. Во Flutter-приложении форма конструктора очищается, экран обновляется, и новый шаблон появляется в общем списке корпоративных обедов компании.

5 Спецификация ответов сервера (Response Body) и ошибок

5.1 1. Успешный ответ (Success Response)

5.1.1 HTTP 201 Created (Ответ на Шаге 10)

Возвращается мобильному приложению после успешного коммита транзакции создания. В теле ответа возвращаются системный ID нового шаблона меню и полные метаданные.

  • Заголовки ответа (Response Headers):
    • Content-Type: application/json
  • Тело ответа (Response Body):
{
  "status": "MENU_TEMPLATE_CREATED",
  "data": {
    "menu_template_id": 105,
    "home_group_id": "company-office-center",
    "menu_title": "Обед Среда: Плов и Компот",
    "items_count": 3,
    "timestamp": "2026-07-02T00:46:00Z"
  }
}

5.2 2. Спецификация структуры таблиц PostgreSQL (Связь 1:M)

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

-- Таблица 1: Шапки кастомных меню компаний (Родительская структура)
CREATE TABLE company_custom_menus (
    id SERIAL PRIMARY KEY,
    home_group_id VARCHAR(50) NOT NULL, -- UUID или строковый ID офиса/компании
    menu_title VARCHAR(255) NOT NULL,   -- Название («Обед Среда»)
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Таблица 2: Технологические карты меню (Дочерняя структура)
CREATE TABLE company_menu_items (
    id SERIAL PRIMARY KEY,
    menu_template_id INT NOT NULL REFERENCES company_custom_menus(id) ON DELETE CASCADE,
    product_name VARCHAR(255) NOT NULL,       -- Каноническое имя продукта
    required_quantity NUMERIC(10, 4) NOT NULL, -- Вес или штуки
    unit VARCHAR(20) NOT NULL                 -- "кг", "шт", "л"
);

-- Индекс для мгновенной выборки меню конкретного предприятия
CREATE INDEX idx_company_menus_group ON company_custom_menus(home_group_id);
-- Индекс для каскадных gRPC-вызовов по ID шаблона
CREATE INDEX idx_menu_items_template ON company_menu_items(menu_template_id);

5.3 3. Вилки исключений и обработка ошибок

5.3.1 Ошибка: Превышение максимального лимита позиций в меню (HTTP 400 Bad Request — Шаг 4)

Выбрасывается на Шаге 4, если массив items_matrix содержит более 15 элементов. Это архитектурное ограничение защищает систему от раздувания payload и снижает задержки gRPC.

{
  "error_code": "ERR-MENU-ITEMS-LIMIT-EXCEEDED",
  "message": "Превышен лимит позиций. Один шаблон меню не может содержать более 15 ингредиентов.",
  "details": {
    "current_items_count": 18,
    "max_allowed_limit": 15
  }
}

5.3.2 Ошибка валидации структуры (HTTP 422 Unprocessable Entity)

Выбрасывается на Шаге 3, если Pydantic-модель на FastAPI обнаружила отрицательные веса продуктов или пустое название меню.

{
  "error_code": "ERR-VALIDATION-FAILED",
  "message": "Переданный JSON-пакет не прошел валидацию схемы данных.",
  "details": [
    {
      "loc": ["body", "items_matrix", 0, "required_quantity"],
      "msg": "Значение необходимого объема должно быть строго больше 0.0000",
      "type": "value_error.number.not_gt"
    }
  ]
}