Метод PATCH /api/v1/menu/custom/{id}

Документация API: Точечное обновление шаблона корпоративного меню (PATCH) с инвалидацией кэша через Apache Kafka

Published

July 2, 2026

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

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

В отличие от тяжелого метода PUT, который требует полной перезаписи объекта, PATCH решает следующие задачи: 1. Экономия сетевого трафика (Low Overheads): Позволяет изменить только отдельные свойства меню (например, обновить название сета или точечно заменить один ингредиент в технологической карте), не гоняя по сети весь массив связанных блюд. 2. Асинхронный патч-нотификатор: Автоматически уведомляет ИИ-контур аналитики и сервис push-нотификаций об изменении шаблона меню через шину данных Apache Kafka, разгружая синхронный HTTP-поток. 3. Сохранение целостности внутренних связей: Подготавливает обновленную структуру, которая при следующем gRPC-вызове со стороны контура Потребления (Consume) мгновенно развернется в актуальную корзину списания.

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

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

2.1 Спецификация параметров пути (Path Parameters)

Параметр Тип Обязательный Описание Пример значения
id Integer Да Уникальный ID изменяемого кастомного меню в базе компании 102

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

Заголовок Обязательный Описание Пример значения
Content-Type Да Формат передаваемых данных application/json
Authorization Да Токен авторизации (Access Token). Метод строго проверяет, что account_type == OFFICE. Bearer eyJhbGciOiJIUzI1Ni...
X-Request-ID Да Сквозной ID запроса для трассировки изменений req-menu-patch-44aa

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

Все поля являются опциональными. Валидатор Pydantic на FastAPI обновляет только те ключи, которые физически переданы в JSON-пакете.

Поле Тип Обязательный Описание Пример значения
menu_title String Нет Новое текстовое название комплексного меню "Пятничный Бургер-День v2"
items_matrix Array Нет Модифицированный список канонических продуктов и порций [..._name: "Котлета", qty: 1.0]

2.3.1 Пример сырого JSON-запроса (Payload — Сценарий: менеджер меняет только название меню, не трогая блюда):

{
  "menu_title": "Пятничный Бургер-День v2"
}

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

На диаграмме представлена логика частичного обновления (PATCH) корпоративного меню. Сервис меню изолированно фиксирует изменения в PostgreSQL, после чего асинхронно публикует событие в брокер Apache Kafka. Смежные сервисы (ИИ-аналитика и Уведомления) подхватывают этот сигнал, а основной контур Потребления (Consume) при следующем вызове заберет обновленный эталон через gRPC.

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

    Manager->>APP: Корректирует название меню в интерфейсе
    APP->>MS: HTTP PATCH /api/v1/menu/custom/102 (JSON с новым заголовком)
    activate MS
    
    note over MS: Шаг 3: Валидация JWT (account_type == OFFICE)
    
    Note over MS, DB: Старт атомарной SQL-транзакции
    MS->>DB: SELECT * FROM company_custom_menus WHERE id = 102 FOR UPDATE
    activate DB
    DB-->>MS: Возвращает текущее состояние записи
    deactivate DB
    
    note over MS: Шаг 6: Проверка принадлежности к home_group_id
    note over MS: Шаг 7: Pydantic-слияние (Экстраполяция только переданных полей)
    
    MS->>DB: UPDATE company_custom_menus SET menu_title = \$1 WHERE id = 102
    Note over MS, DB: Коммит транзакции (COMMIT)
    
    MS->>KAFKA: send_to_kafka_async("masterdata.menu.mutated.v1")
    MS-->>APP: HTTP 200 OK (status: "UPDATED")
    deactivate MS
    
    note over KAFKA: Шаг 11: Асинхронная инвалидация кэша ИИ-аналитики
    
    %% ДЕМОНСТРАЦИЯ ИНТЕГРАЦИОННОЙ СВЯЗИ
    Note over CS, MS: Будущий вызов эталона при обеде сотрудников
    CS->>MS: gRPC GetMenuTemplate(menu_template_id=102)
    activate MS
    MS-->>CS: gRPC MenuTemplateResponse (Массив свежих данных)
    deactivate MS

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

  • Шаг 1 (Manager -> APP): Менеджер предприятия на экране администратора корпоративного меню изменяет одно или несколько полей шаблона (например, переименовывает сет «Пятничный Бургер-День» в «Пятничный Бургер-День v2», оставляя состав блюд неизменным) и нажимает «Сохранить изменения».
  • Шаг 2 (APP -> MS): Мобильное приложение Flutter через клиент Dio отправляет запрос HTTP PATCH /api/v1/menu/custom/{id}. В теле JSON передается исключительно измененный ключ "menu_title". В заголовках прокидывается токен авторизации и сквозной X-Request-ID.
  • Шаг 3 (MS -> MS): Микросервис Меню (FastAPI) перехватывает запрос и валидирует JWT. Из токена извлекается account_type. Если флаг равен HOME, сервис мгновенно отсекает операцию со статусом HTTP 403 Forbidden, так как контур корпоративного меню недоступен для обычных семейных аккаунтов.
  • Шаг 4 (MS -> DB): Сервис открывает атомарную SQL-транзакцию в PostgreSQL и блокирует целевую строку шаблона меню компании: SELECT id, home_group_id, menu_title FROM company_custom_menus WHERE id = $1 FOR UPDATE.
  • Шаг 5 (DB -> MS): База данных возвращает текущую физическую структуру записи из таблицы кастомных меню.
  • Шаг 6 (MS -> MS): Проверка контекста безопасности (Security Scope): Сервис сверяет поле home_group_id из базы данных с home_group_id, зашитым в токене менеджера. Если идентификаторы не совпадают (попытка изменить меню чужого офиса), транзакция откатывается (ROLLBACK) с ошибкой HTTP 403.
  • Шаг 7 (MS -> MS): Pydantic-экстраполяция: FastAPI берет структуру из базы данных и накладывает на неё входящий JSON-патч с помощью встроенного механизма частичного обновления response_model.model_validate(existing_db_model, update=patch_data). Изменяются только переданные поля, остальные сохраняют свои исходные значения.
  • Шаг 8 (MS -> DB): Обновленный объект записывается обратно в базу данных: UPDATE company_custom_menus SET menu_title = $1, updated_at = NOW() WHERE id = $2. Транзакция успешно закрывается командой COMMIT.
  • Шаг 9 (MS -> KAFKA): Сервис Меню выступает в роли продюсера (Producer) и отправляет асинхронное системное уведомление в топик Kafka masterdata.menu.mutated.v1. Сообщение содержит тип события "MENU_UPDATED", menu_template_id и home_group_id.
  • Шаг 10 (MS -> APP): Сервер возвращает мобильному приложению статус HTTP 200 OK с подтверждением частичной модификации. Экран администратора во Flutter разблокируется.
  • Шаг 11 (KAFKA -> KAFKA): Асинхронное следствие: шина сообщений Kafka доставляет событие до ИИ-сервиса аналитики и модуля push-нотификаций. ИИ сбрасывает (инвалидирует) старый кэш структуры меню для этой компании, а сервис уведомлений отправляет пуш сотрудникам офиса: «Менеджер обновил меню! Проверьте состав на завтра».
  • Шаг 12 (CS -> MS): Интеграционное взаимодействие (gRPC): Когда сотрудник этого предприятия садится обедать и запускает метод пакетного списания (из микросервиса Consume, Метод 1), транзакционный шлюз Потребления обращается к сервису Меню через внутренний gRPC-канал: gRPC GetMenuTemplate(menu_template_id=102). Сервис Меню мгновенно возвращает уже измененный, актуальный эталон технологической карты блюд по протоколу HTTP/2, изолируя основную базу холодильника от структуры хранения шаблонов меню.

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

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

5.1.1 HTTP 200 OK (Ответ на Шаге 10)

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

  • Заголовки ответа (Response Headers):
    • Content-Type: application/json
  • Тело ответа (Response Body):
{
  "status": "MENU_PARTIALLY_UPDATED",
  "data": {
    "menu_template_id": 102,
    "home_group_id": "company-office-center",
    "updated_fields": {
      "menu_title": "Пятничный Бургер-День v2"
    },
    "timestamp": "2026-07-02T00:35:02Z"
  }
}

5.2 2. Спецификация интеграционного gRPC-контракта (Шаг 12)

Описание структуры Protocol Buffers (menu_integration.proto) для внутреннего взаимодействия между транзакционным шлюзом Потребления (Consume) и сервисом Меню.

syntax = "proto3";

package masterdata.menu.v1;

service MenuIntegrationService {
  // Вызывается контуром Consume для развертывания шаблона обеда в позиции списания
  rpc GetMenuTemplate (MenuTemplateRequest) returns (MenuTemplateResponse);
}

message MenuTemplateRequest {
  int32 menu_template_id = 1;
  string home_group_id = 2;
}

message MenuItem {
  string product_name = 1;     // Каноническое имя (например, "Котлета говяжья")
  double required_quantity = 2;// Требуемый объем/вес (например, 1.0)
  string unit = 3;             // Единица измерения ("шт", "кг")
}

message MenuTemplateResponse {
  int32 menu_template_id = 1;
  string menu_title = 2;
  repeated MenuItem items = 3; // Массив позиций для каскадного списания из холодильника
}

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

5.3.1 Ошибка: Попытка изменения глобального неизменяемого шаблона (HTTP 403 Forbidden)

Выбрасывается на Шаге 6, если менеджер передал в URL идентификатор, принадлежащий таблице системных мастер-данных (master_menu_templates), которые запрещено изменять пользователям.

{
  "error_code": "ERR-MASTERDATA-IMMUTABLE",
  "message": "Операция отклонена: системные шаблоны меню защищены от записи и доступны только для чтения.",
  "details": {
    "requested_id": 102,
    "table_scope": "master_menu_templates"
  }
}

5.3.2 Ошибка: Нарушение бизнес-профиля аккаунта (HTTP 403 Forbidden — Шаг 3)

Выбрасывается, если метод вызван под учетной записью с типом HOME. Контур управления меню полностью изолирован от семейных аккаунтов.

{
  "error_code": "ERR-OFFICE-CONTEXT-REQUIRED",
  "message": "Доступ заблокирован: данный эндпоинт предназначен исключительно для корпоративных учетных записей предприятий.",
  "details": {
    "current_account_type": "HOME",
    "action": "Block administrative UI elements on Flutter side."
  }
}