Метод POST /api/v1/consume/session

Документация API: Пакетное и одиночное списание продуктов (перекус / комплексный обед) с поддержкой динамической порционности и хука Меню

Published

July 1, 2026

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

Метод является главной точкой фиксации потребления ресурсов в системе. Он обрабатывает как легкие одиночные перекусы (одно съеденное яблоко или lovely cup of tea), так и комплексные корпоративные приемы пищи («1-е, 2-е и компот»).

Метод решает четыре ключевые задачи: 1. Атомарный пакетный инпут: Принимает массив выбранных позиций, выполняя списание всех продуктов в рамках одной транзакции СУБД. 2. Динамическое порционирование (Дробление): Поддерживает списание готовых сложных блюд долями (float-коэффициентами). Если пользователь делит пирог на 8 частей и съедает одну, Flutter рассчитывает шаг на клиенте (1 / 8 = 0.125) и шлет команду уменьшить баланс блюда строго на 0.125. 3. Изоляция фабричных партий (Magnum / Small): Запрещает автоматическое схлопывание готовых продуктов из супермаркетов для сохранения контроля сроков годности. Списание происходит строго по целевому item_id конкретной упаковки. 4. Интеграционный хук Меню: Архитектура метода подготовлена к подключению внешнего микросервиса Menu (для предприятий). При передаче параметра menu_template_id система автоматически разворачивает шаблон комплексного обеда (пирог, салат, котлеты) в массив списания.

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

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

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

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

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

Поле Тип Обязательный Описание Пример значения
menu_template_id Integer Нет Идентификатор шаблона меню для интеграции с сервисом предприятий 102 (или null)
consume_items Array Да Список списываемых товаров (может содержать от 1 до N элементов) [..._id: 5012, portion: 0.125]

2.2.1 Пример JSON-запроса (Сценарий: перекус 1/8 частью яблочного пирога и фабричным йогуртом):

{
  "menu_template_id": null,
  "consume_items": [
    {
      "fridge_item_id": 5012,
      "product_name": "Яблочный пирог классический",
      "quantity_to_consume": 0.1250, 
      "unit": "шт"
    },
    {
      "fridge_item_id": 8841,
      "product_name": "Йогурт Danone Клубника",
      "quantity_to_consume": 1.0000,
      "unit": "шт"
    }
  ]
}

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

На диаграмме представлена логика пакетного списания сессии потребления. СУБД обрабатывает как фабричные штучные товары, так и составные блюда, уменьшая их float-баланс на указанную порцию (например, 0.125 для 1/8 пирога).

sequenceDiagram
    autonumber
    actor User as Пользователь (Экран Обеда / Перекуса)
    participant APP as Мобильное приложение (Flutter)
    participant API as Бэкенд-шлюз (FastAPI)
    participant DB as Реляционная СУБД (PostgreSQL)
    participant KAFKA as Брокер событий (Kafka)

    User->>APP: Выбирает 1/8 пирога и йогурт, нажимает "Подтвердить прием пищи"
    APP->>API: HTTP POST /api/v1/consume/session (JSON-массив с fridge_item_id и portion)
    activate API
    
    note over API: Шаг 3: Декодирование JWT (Выделение контекста HOME или OFFICE)
    note over API: Шаг 4: Проверка интеграции с сервисом Menu (menu_template_id is null)
    
    Note over API, DB: Старт атомарной SQL-транзакции
    
    loop По каждому элементу из массива consume_items
        API->>DB: SELECT quantity FROM fridge_inventory WHERE item_id = fridge_item_id FOR UPDATE
        activate DB
        DB-->>API: Возвращает текущий float-баланс (например, пирог: 1.0, йогурт: 1.0)
        deactivate DB
        
        note over API: Шаг 7: Математическое вычитание порции (quantity - quantity_to_consume)
        
        alt Остатка ХВАТАЕТ
            API->>DB: UPDATE fridge_inventory SET quantity = quantity - quantity_to_consume
        else Остатка НЕ ХВАТАЕТ
            alt Контекст аккаунта == OFFICE
                API-->>APP: HTTP 400 Bad Request (ERR-CONSUME-SHORTAGE)
            else Контекст аккаунта == HOME
                note over API: Шаг 10: Скрытый уход в виртуальный минус (Запись скрывается в общем списке)
                API->>DB: UPDATE fridge_inventory SET quantity = quantity - quantity_to_consume (quantity < 0)
            end
        end
    end

    Note over API, DB: Коммит атомарной транзакции (COMMIT)
    
    API->>KAFKA: send_to_kafka_async("bupar.fridge.events.consumed")
    API-->>APP: HTTP 200 OK (status: "CONSUME_SESSION_RECORDED")
    deactivate API
    note over APP: Экран обновляется: йогурт исчез, у пирога остаток стал 0.875

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

  • Шаг 1 (User -> APP): Пользователь на экране потребления формирует свой прием пищи. Это может быть как одиночный перекус (ввод одного яблока), так и комплексный обед из нескольких позиций. При выборе пирога пользователь указывает, что берет 1 кусок из 8 — интерфейс Flutter мгновенно переводит это в дробное значение 0.1250. Пользователь нажимает кнопку подтверждения.
  • Шаг 2 (APP -> API): Приложение через клиент Dio отправляет запрос HTTP POST /api/v1/consume/session. В теле запроса передается структурированный JSON-массив с точными идентификаторами записей поставок (fridge_item_id) и вычисленными float-коэффициентами порций.
  • Шаг 3 (API -> API): Бэкенд-шлюз FastAPI извлекает метаданные из JWT-токена авторизации. Параметр account_type определяет, по какому сценарию пойдет обработка в случае нехватки продуктов на складе: жесткая блокировка для OFFICE или скрытый уход в минус для HOME.
  • Шаг 4 (API -> API): Системный шаг проверки интеграционного хука: бэкенд проверяет поле menu_template_id. Если бы запрос пришел от корпоративного модуля Menu (предприятия), шлюз предварительно развернул бы шаблон комплексного обеда (пирог, салат, котлеты) в массив списания по технологическим картам. В данном кейсе перекус ручной, поле равно null, шаг пропускается.
  • Шаг 5 (API -> DB): Бэкенд открывает атомарную SQL-транзакцию в PostgreSQL. Запускается цикл обработки корзины потребления. Бэкенд выполняет точечный запрос SELECT quantity FROM fridge_inventory WHERE item_id = $1 FOR UPDATE для первой позиции (яблочного пирога), блокируя строку от параллельных изменений.
  • Шаг 6 (DB -> API): База данных возвращает текущий float-баланс записи. Для свежеприготовленного пирога он равен базовому значению 1.0000 шт.
  • Шаг 7 (API -> API): Алгоритм бэкенда производит математическое вычитание порции: 1.0000 - 0.1250 = 0.8750.
  • Шаг 8 (API -> DB): Так как остатка пирога хватает, бэкенд выполняет обновление строки в СУБД: UPDATE fridge_inventory SET quantity = 0.8750 WHERE item_id = 5012.
  • Шаг 9 (API -> API): Цикл переходит к следующему элементу — фабричному йогурту с item_id = 8841. Если обнаруживается, что продукт полностью отсутствует на балансе или его количества не хватает для списания, система активирует вилку бизнес-контекстов.
  • Шаг 10 (API -> DB):
    • Ветка OFFICE: Бэкенд немедленно вызывает ROLLBACK, полностью отменяя списание пирога, и возвращает мобильному приложению ошибку HTTP 400 (запрет дефицита в офисе).
    • Ветка HOME: Срабатывает логика скрытого ухода в виртуальный минус. Бэкенд вычитает объем, уводя баланс конкретной фабричной записи в минус. В общем списке продуктов на главном экране холодильника эта запись больше не показывается (её видимость блокируется фильтром quantity > 0), но сама строка долга перемещается в скрытый реестр задолженностей (Метод 5) для последующего слияния или закрытия.
  • Шаг 11 (API -> KAFKA): Атомарная транзакция фиксируется в PostgreSQL (COMMIT). Бэкенд асинхронно отправляет бизнес-событие в топик Kafka bupar.fridge.events.consumed. Сообщение содержит массив съеденных item_id и их точные float-порции, что необходимо для работы команды MLOps при расчете калорийности и КБЖУ.
  • Шаг 12 (API -> APP): FastAPI возвращает успешный ответ HTTP 200 OK. Во Flutter-приложении мгновенно обновляется состояние склада: карточка йогурта исчезает из видимости, а у карточки пирога остаток пересчитывается в 0.875 шт.

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

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

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

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

  • Заголовки ответа (Response Headers):
    • Content-Type: application/json
  • Тело ответа (Response Body):
{
  "status": "CONSUME_SESSION_RECORDED",
  "data": {
    "session_id": "sess-meal-99aa-11ef",
    "account_context": "HOME",
    "total_items_mutated": 2,
    "mutated_items": [
      {
        "fridge_item_id": 5012,
        "product_name": "Яблочный пирог классический",
        "remaining_quantity": 0.8750,
        "unit": "шт"
      },
      {
        "fridge_item_id": 8841,
        "product_name": "Йогурт Danone Клубника",
        "remaining_quantity": 0.0000,
        "unit": "шт"
      }
    ],
    "timestamp": "2026-07-01T23:45:00Z"
  }
}

5.2 2. Спецификация ошибок и вилок исключений (Error Responses)

5.2.1 Ошибка дефицита при списании (HTTP 400 Bad Request — Шаг 10)

Выбрасывается строго в контексте аккаунта OFFICE, если в процессе циклической проверки на Шаге 5 обнаружилось, что физический остаток товара или порции на складе меньше, чем указано в запросе в поле quantity_to_consume. Транзакция полностью откатывается (ROLLBACK).

{
  "error_code": "ERR-CONSUME-SHORTAGE",
  "message": "Списание отклонено: в корпоративном контуре запрещено потребление отсутствующих позиций.",
  "details": {
    "reason": "Physical partition volume underflow during transactional batch consumption.",
    "failed_item_id": 8841,
    "product_name": "Йогурт Danone Клубника",
    "requested_quantity": 1.0000,
    "available_quantity": 0.0000,
    "unit": "шт"
  }
}

5.2.2 Ошибка валидации структуры float-коэффициентов (HTTP 422 Unprocessable Entity)

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

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