Метод POST /api/v1/consume/session/{session_id}/cancel

Документация API: Атомарный реверс (откат) сессии потребления с восстановлением float-коэффициентов остатков

Published

July 1, 2026

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

Метод предназначен для восстановления складских запасов в случае ошибочного или случайного фиксации приема пищи пользователем во Flutter-интерфейсе (например, пользователь случайно «съел» в приложении чужой пирог или йогурт). Метод выполняет каскадный откат всей сессии потребления (session_id), содержащей как одну позицию (перекус), так и комплексный набор блюд («1-е, 2-е и компот»).

Метод решает следующие задачи: 1. Реверсивный пересчет float-остатков: Извлекает из исторического лога consumption_log точные объемы и порции, которые были списаны в рамках данной сессии, и математически возвращает их обратно на баланс соответствующих fridge_item_id (например, возвращает 0.1250 к остатку пирога). 2. Атомарность операции: Весь комплексный обед восстанавливается строго внутри одной SQL-транзакции. Невозможно восстановить только суп, оставив пирог списанным. 3. Очистка исторического следа: Удаляет или маркирует сессию в consumption_log как аннулированную, предотвращая искажение метрик КБЖУ для команды MLOps.

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

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

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

Параметр Тип Обязательный Описание Пример значения
session_id String Да Уникальный UUID сессии приема пищи, которую необходимо отменить sess-meal-99aa-11ef

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

Заголовок Обязательный Описание Пример значения
Authorization Да Токен авторизации (Access Token). Используется для валидации прав на управление холодильником. Bearer eyJhbGciOiJIUzI1Ni...
X-Request-ID Да Сквозной ID запроса для распределенного логирования req-consume-cancel-22cc

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

Тело запроса является пустым, так как всю структуру списанных продуктов бэкенд вычитывает самостоятельно из исторической таблицы логов по session_id.

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

{}

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

На диаграмме представлена логика транзакционного реверса сессии: СУБД вычитывает исторический лог, возвращает float-коэффициенты порций и фабричных товаров на баланс холодильника, а затем аннулирует запись сессии.

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

    User->>APP: Нажимает кнопку "Отменить прием пищи" (Реверс сессии)
    APP->>API: HTTP POST /api/v1/consume/session/sess-meal-99aa-11ef/cancel (с JWT)
    activate API
    
    note over API: Шаг 3: Верификация прав доступа к session_id по home_group_id
    
    Note over API, DB: Старт атомарной SQL-транзакции
    
    API->>DB: SELECT * FROM consumption_log WHERE session_id = target_session_id FOR UPDATE
    activate DB
    DB-->>API: Массив списанных продуктов (item_id, consumed_quantity, unit)
    deactivate DB
    
    loop По каждому продукту из лога сессии
        API->>DB: UPDATE fridge_inventory SET quantity = quantity + consumed_quantity WHERE item_id
    end
    
    API->>DB: DELETE FROM consumption_log WHERE session_id = target_session_id
    
    Note over API, DB: Коммит атомарной транзакции (COMMIT)
    
    API->>KAFKA: send_to_kafka_async("bupar.fridge.events.consume_cancelled")
    API-->>APP: HTTP 200 OK (status: "CONSUME_SESSION_CANCELLED")
    deactivate API
    note over APP: Экран обновляется: отмененный обед исчезает, остатки в холодильнике восстановлены

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

  • Шаг 1 (User -> APP): Пользователь заходит в журнал питания, видит ошибочно зафиксированный прием пищи (например, комплексный обед с session_id = sess-meal-99aa-11ef) и нажимает кнопку «Отменить прием пищи».
  • Шаг 2 (APP -> API): Приложение через клиент Dio отправляет запрос HTTP POST /api/v1/consume/session/{session_id}/cancel. Идентификатор сессии передается в URL в качестве Path-параметра.
  • Шаг 3 (API -> API): Бэкенд-шлюз FastAPI декодирует JWT-токен и извлекает home_group_id. Система выполняет валидацию безопасности: проверяет, принадлежит ли данная сессия потребления к холодильнику этой семьи или предприятия, исключая подмену ID.
  • Шаг 4 (API -> DB): Шлюз открывает атомарную SQL-транзакцию в PostgreSQL и блокирует исторические строки запросом SELECT * FROM consumption_log WHERE session_id = $1 FOR UPDATE.
  • Шаг 5 (DB -> API): База данных возвращает полный массив продуктов и точных дробных порций, которые были списаны в рамках этой сессии (например, 0.1250 от яблочного пирога и 1.0000 от йогурта).
  • Шаг 6 (API -> DB): Запускается цикл реверсивного восстановления остатков. Для каждого элемента массива бэкенд отправляет DML-команду инкремента: UPDATE fridge_inventory SET quantity = quantity + $1 WHERE item_id = $2. Математическое сложение гарантирует, что к остатку пирога вернутся ровно его 0.1250 долей.
    • Валидация скрытых минусов: Если фабричный товар ранее ушел в скрытый минус и строка имела отрицательный баланс, этот инкремент вернет её в нулевое или положительное состояние, автоматически восстанавливая корректный баланс склада.
  • Шаг 7 (API -> DB): После успешного возврата всех весов и штук на склад, бэкенд выполняет команду очистки исторического следа: DELETE FROM consumption_log WHERE session_id = $1. Это защищает аналитический контур MLOps от ложных КБЖУ-данных.
  • Шаг 8 (API -> API): Атомарная транзакция фиксируется в PostgreSQL (COMMIT). Данные склада и истории синхронизируются.
  • Шаг 9 (API -> KAFKA): Бэкенд асинхронно отправляет системное событие в топик Kafka bupar.fridge.events.consume_cancelled для уведомления смежных BI-модулей об отмене приема пищи.
  • Шаг 10 (API -> APP): FastAPI возвращает успешный ответ HTTP 200 OK. Мобильное приложение Flutter скрывает карточку сессии из таймлайна журнала, а общий экран холодильника перерисовывается, отображая восстановленный объем продуктов.

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

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

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

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

  • Заголовки ответа (Response Headers):
    • Content-Type: application/json
  • Тело ответа (Response Body):
{
  "status": "CONSUME_SESSION_CANCELLED",
  "data": {
    "session_id": "sess-meal-99aa-11ef",
    "home_group_id": "group-772-alpha",
    "total_items_restored": 2,
    "restored_items": [
      {
        "product_name": "Яблочный пирог классический",
        "restored_quantity_increment": 0.1250,
        "unit": "шт"
      },
      {
        "product_name": "Йогурт Danone Клубника",
        "restored_quantity_increment": 1.0000,
        "unit": "шт"
      }
    ],
    "timestamp": "2026-07-01T23:56:00Z"
  }
}

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

5.2.1 Ошибка: Сессия не найдена или уже отменена (HTTP 404 Not Found — Шаг 5)

Выбрасывается на Шаге 5, если переданный session_id отсутствует в таблице consumption_log (например, сессия была отменена ранее или передан неверный UUID).

{
  "error_code": "ERR-CONSUME-SESSION-NOT-FOUND",
  "message": "Указанная сессия приема пищи не найдена в журнале питания.",
  "details": {
    "requested_session_id": "sess-meal-99aa-11ef",
    "reason": "Entity not found or already deleted from consumption_log table."
  }
}

5.2.2 Ошибка нарушения прав доступа к истории (HTTP 403 Forbidden — Шаг 3)

Выбрасывается на Шаге 3 бэкенд-шлюзом FastAPI, если запрашиваемый session_id принадлежит другой семье или предприятию (home_group_id из JWT-токена не совпадает со скоупом сессии).

{
  "error_code": "ERR-CONSUME-HISTORY-ACCESS-DENIED",
  "message": "Операция отклонена: у вас нет прав на отмену приемов пищи в чужом контуре.",
  "details": {
    "reason": "Security token home_group_id mismatch with target consumption session context."
  }
}