Метод POST /api/v1/cook/recipes/{id}/confirm

Документация API: Финальный коммит транзакции готовки по рецепту со списанием ингредиентов и генерацией масштабируемого блюда

Published

July 1, 2026

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

Метод является финальной точкой контура готовки по рецепту. Он вызывается, когда пользователь нажимает кнопку «Блюдо готово» на экране пошагового руководства Flutter-приложения.

Метод выполняет следующие критические операции в рамках единой транзакции: 1. Каскадное списание объемов: Бэкенд списывает требуемый по рецепту вес/количество ингредиентов. В отличие от ручного метода, здесь списание происходит автоматически. Система ищет все живые партии нужного продукта в рамках home_group_id и уменьшает их баланс. 2. Обслуживание вилки аккаунтов (Уход в минус): * В контексте HOME, если суммарного объема партий не хватает, последняя измененная партия уводится в отрицательное float-значение (виртуальный минус). * В контексте OFFICE транзакция падает с ошибкой, так как предварительная валидация (Метод 3) обязана была заблокировать этот шаг на клиенте. 3. Зачисление каноничного блюда: В холодильник пользователя добавляется новая позиция с именем рецепта и базовым порционным объемом 1.0 шт для последующего дробления в контуре потребления.

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

  • Метод: POST
  • Маршрут: /api/v1/cook/recipes/{id}/confirm
  • Формат данных: application/json

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

Параметр Тип Обязательный Описание Пример значения
id Integer Да Уникальный идентификатор выполненного рецепта в мастер-базе 412

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

Заголовок Обязательный Описание Пример значения
Content-Type Да Формат передаваемых данных application/json
Authorization Да Токен авторизации (Access Token). Извлекаются home_group_id и account_type. Bearer eyJhbGciOiJIUzI1Ni...
X-Request-ID Да Сквозной ID запроса для сквозного логирования операции списания req-rec-conf-33cc

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

Тело запроса является опциональным или пустым, так как все необходимые пропорции ингредиентов бэкенд извлекает самостоятельно из мастер-базы рецептов по id, переданному в URL.

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

{}

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

На диаграмме представлена логика автоматического каскадного списания ингредиентов рецепта по существующим записям поставок в холодильнике, а также финальное зачисление готового блюда с коэффициентом объема 1.0.

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/cook/recipes/412/confirm (JWT в заголовке)
    activate API
    
    note over API: Шаг 3: Чтение структуры рецепта из Master Data
    
    Note over API, DB: Старт атомарной SQL-транзакции
    
    loop По каждому ингредиенту из структуры рецепта
        API->>DB: Нахождение всех живых партий продукта (quantity > 0)
        activate DB
        DB-->>API: Массив записей партий товара с их объемами
        deactivate DB
        
        note over API: Шаг 6: Авто-распределение веса списания по записям
        
        alt Суммарного остатка партий ХВАТАЕТ
            API->>DB: UPDATE fridge_inventory (уменьшение баланса записей в ноль или остаток)
        else Суммарного остатка НЕ ХВАТАЕТ
            alt Контекст аккаунта == OFFICE
                API-->>APP: HTTP 400 Bad Request (ERR-STOCK-SHORTAGE-CONFIRM)
            else Контекст аккаунта == HOME
                note over API: Шаг 9: Автоматический уход последней партии в минус
                API->>DB: UPDATE fridge_inventory (последняя партия уходит в технический минус)
            end
        end
    end

    note over API: Шаг 11: Создание готового блюда (Объем = 1.0)
    API->>DB: INSERT INTO fridge_inventory (product_name="Яблочный пирог...", quantity=1.0, unit="шт")
    
    Note over API, DB: Коммит атомарной транзакции (COMMIT)
    
    API->>KAFKA: send_to_kafka_async("bupar.fridge.events.recipe_confirmed")
    API-->>APP: HTTP 201 Created (Остатки обновлены, блюдо зачислено)
    deactivate API
    note over APP: Экран блокируется, юзер перенаправляется на склад, где видит готовое блюдо

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

  • Шаг 1 (User -> APP): Пользователь завершает процесс пошагового приготовления блюда по рецепту (например, испёк пирог) и нажимает на экране смартфона финальную кнопку подтверждения «Блюдо готово! Зафиксировать».
  • Шаг 2 (APP -> API): Мобильное приложение отправляет пустой JSON-запрос методом HTTP POST /api/v1/cook/recipes/{id}/confirm. В заголовках передается JWT-токен сессии и сквозной X-Request-ID.
  • Шаг 3 (API -> API): Бэкенд-шлюз FastAPI обращается к справочнику рецептов и вычитывает точные пропорции ингредиентов, необходимых для выполнения этого recipe_id (например, для рецепта №412 требуется 0.5 кг яблок, 0.3 кг муки).
  • Шаг 4 (API -> DB): FastAPI открывает атомарную SQL-транзакцию в PostgreSQL и запускает цикл автоматического каскадного списания. Бэкенд ищет в холодильнике пользователя (home_group_id) все существующие записи поставок для первого ингредиента рецепта (например, яблок), у которых quantity > 0.
  • Шаг 5 (DB -> API): База данных возвращает массив живых записей партий товара с их текущими физическими объемами.
  • Шаг 6 (API -> API): Бэкенд выполняет внутренний расчет распределения веса. В отличие от ручного ввода, система сама списывает объёмы по цепочке найденных записей: уменьшает в ноль самую старую запись, а остаток требуемого веса переносит на следующую строку поставки товара.
  • Шаг 7 (API -> DB): Бэкенд отправляет серию команд UPDATE fridge_inventory для фиксации уменьшения баланса по конкретным измененным ID партий.
  • Шаг 8 (API -> API): Если в ходе расчёта обнаруживается, что суммарного объема всех живых партий продукта в холодильнике не хватает для покрытия требований рецепта, система переходит к вилке условий бизнес-контекста.
  • Шаг 9 (API -> APP): Ветка OFFICE: Так как в корпоративном контуре уход в минус строго запрещен, бэкенд мгновенно вызывает ROLLBACK, аннулирует все промежуточные списания и возвращает мобильному приложению жесткую ошибку HTTP 400.
  • Шаг 10 (API -> DB): Ветка HOME: Контур семьи. Включается логика автоматического ухода в виртуальный минус. Система полностью списывает в ноль все существующие живые партии продукта, а весь оставшийся дефицит веса (которого не хватило) вычитает из последней измененной записи, уводя её баланс в отрицательное float-значение.
  • Шаг 11 (API -> DB): После завершения цикла автоматического списания по всем ингредиентам, бэкенд производит канонизацию и зачисление готового блюда. В таблицу fridge_inventory добавляется новая независимая запись с названием рецепта (например, "Яблочный пирог классический"). Объём жестко выставляется в базовое значение 1.0, а единица измерения — в "шт". Это подготавливает блюдо к последующему порционному дроблению в контуре потребления.
  • Шаг 12 (API -> KAFKA): Атомарная транзакция фиксируется в базе данных (COMMIT). Бэкенд асинхронно отправляет бизнес-событие в топик Kafka bupar.fridge.events.recipe_confirmed для сквозного контроля изменения остатков и продуктовой аналитики.
  • Шаг 13 (API -> APP): Бэкенд возвращает успешный ответ HTTP 201 Created. Мобильное приложение Flutter закрывает экран готовки и перенаправляет пользователя на главный экран склада, где в списке продуктов появляется готовый пирог объемом 1.0 шт.

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

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

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

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

  • Заголовки ответа (Response Headers):
    • Content-Type: application/json
  • Тело ответа (Response Body):
{
  "status": "RECIPE_COOK_CONFIRMED",
  "data": {
    "fridge_item_id": 7841,
    "dish_name": "Яблочный пирог классический",
    "allocated_quantity": 1.0,
    "unit": "шт",
    "account_context": "HOME",
    "timestamp": "2026-07-01T21:55:00Z"
  }
}

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

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

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

{
  "error_code": "ERR-STOCK-SHORTAGE-CONFIRM",
  "message": "Финиширование рецепта отклонено: обнаружен критический дефицит ингредиентов на складе офиса.",
  "details": {
    "reason": "Race condition or parallel consumption cleared required stock before commit.",
    "missing_product": "Мука",
    "required_total": 0.300,
    "available_total": 0.050,
    "unit": "кг"
  }
}

5.2.2 Ошибка коллизии состояний рецепта (HTTP 409 Conflict)

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

{
  "error_code": "ERR-RECIPE-SESSION-CONFLICT",
  "message": "Данная сессия приготовления уже была зафиксирована или аннулирована.",
  "details": {
    "recipe_id": 412,
    "action": "Refresh fridge stock state and check actual inventory grid."
  }
}

6 Архитектурное резюме микросервиса Cook (Готовка)

Мы полностью спроектировали и задокументировали в формате QMD ключевое ядро микросервиса Cook:

  1. Метод 1 (POST /manual): Ручная фиксация. Пользователь сам тапает по конкретным строкам партий товаров на экране (как на скриншоте интерфейса) и списывает их. Для HOME доступен виртуальный уход в минус. Готовое блюдо (суп, пирог) канонизируется к базовому объему 1.0 шт.
  2. Метод 2 (GET /recipes/search): Умный подбор рецептов. Работает на уровне СУБД через агрегации и CTE. Ищет пересечения ингредиентов с живым холодильником (совпадение минимум по 1 продукту) и отдает максимум 500 записей, отсортированных по проценту готовности.
  3. Метод 3 (GET /recipes/{id}/validate): Предварительный расчет дефицита. Суммирует объемы раздельных закупленных партий. Выдает структуру missing_ingredients и делит интерфейс Flutter: подсвечивает желтым WARNING для семьи или жестко блокирует кнопку старта через красный BLOCK для офиса.
  4. Метод 4 (POST /recipes/{id}/confirm): Финальный коммит. Автоматически распределяет вес списания по цепочке живых записей холодильника, уводит в минус последнюю партию (в контексте HOME) и зачисляет готовое блюдо с коэффициентом 1.0 шт для последующего порционного деления.