Метод GET /api/v1/cook/recipes/{id}/validate

Документация API: Предварительный расчет дефицита ингредиентов с контекстным ветвлением профилей HOME и OFFICE

Published

July 1, 2026

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

Метод предназначен для детальной сверки необходимых объемов ингредиентов конкретного рецепта с текущим фактическим состоянием холодильника (home_group_id) непосредственно перед стартом процесса приготовления.

Метод решает три ключевые задачи: 1. Точный количественный аудит: Сверяет требуемый по рецепту вес/объем (кг, литры, шт) со всеми доступными физическими партиями товаров в холодильнике (суммируя их объемы, если продуктов одного наименования несколько). 2. Формирование структуры дефицита (missing_ingredients): Четко локализует, каких именно позиций не хватает для сборки блюда и в каком объеме. 3. Бизнес-ветвление по типу аккаунта: * Для профиля HOME: Разрешает продолжить процесс. Нехватка маркируется как «Виртуальный дефицит» — система предупреждает пользователя, но не блокирует кнопку старта. * Для профиля OFFICE: Работает в режиме строгого контроля. Метод возвращает признак критической блокировки (is_executable: false), запрещая запуск процесса.

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

  • Метод: GET
  • Маршрут: /api/v1/cook/recipes/{id}/validate
  • Формат данных: application/json (только ответ)

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

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

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

Заголовок Обязательный Описание Пример значения
Authorization Да Токен авторизации (Access Token). Бэкенд извлекает из него home_group_id и account_type. Bearer eyJhbGciOiJIUzI1Ni...
X-Request-ID Да Сквозной ID запроса для связывания логов валидации req-rec-val-11bb

2.3 Спецификация структуры ответа (Response Body)

Поле Тип Описание Пример значения
recipe_id Integer ID проверяемого рецепта 412
is_executable Boolean Флаг-разрешение для UI (блокировать ли кнопку «Начать готовку») true (для HOME) / false (для OFFICE)
account_context String Извлеченный из JWT контекст выполнения "HOME" или "OFFICE"

2.3.1 Пример JSON-ответа для профиля HOME (Разрешено с предупреждением):

{
  "recipe_id": 412,
  "title": "Яблочный пирог классический",
  "account_context": "HOME",
  "is_executable": true, 
  "summary": {
    "total_required_items": 4,
    "available_items_count": 3,
    "missing_items_count": 1
  },
  "missing_ingredients": [
    {
      "product_name": "Яйцо куриное",
      "required_quantity": 4.0,
      "available_quantity": 0.0,
      "deficit_quantity": 4.0,
      "unit": "шт",
      "severity": "WARNING_VIRTUAL_MINUS_ALLOWED"
    }
  ]
}

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

На диаграмме представлена логика расчёта дефицита объёмов. База данных суммирует все доступные партии товара, а бэкенд на уровне бизнес-логики разделяет поведение экрана для домашнего и корпоративного использования.

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

    User->>APP: Переходит на детальный экран выбранного рецепта
    APP->>API: HTTP GET /api/v1/cook/recipes/412/validate (JWT в заголовке)
    activate API
    
    note over API: Шаг 3: Декодирование JWT (Выделение контекста HOME/OFFICE)
    
    API->>DB: Вызов SQL-запроса агрегации остатков по home_group_id
    activate DB
    note over DB: Шаг 5: Суммирование объемов всех живых партий ингредиентов рецепта
    DB-->>API: Возвращает матрицу: Требуется VS Есть в наличии
    deactivate DB
    
    note over API: Шаг 7: Расчет структуры missing_ingredients
    
    alt Обнаружен дефицит продуктов
        alt Контекст аккаунта == OFFICE
            note over API: Шаг 9: Жесткая блокировка контура
            API-->>APP: HTTP 200 OK (is_executable: false, severity: BLOCK)
        else Контекст аккаунта == HOME
            note over API: Шаг 10: Разрешение ухода в виртуальный минус
            API-->>APP: HTTP 200 OK (is_executable: true, severity: WARNING)
        end
    else Продуктов полностью хватает (Идеальный стейт)
        API-->>APP: HTTP 200 OK (is_executable: true, missing_ingredients: [])
    end
    deactivate API
    note over APP: Flutter перерисовывает кнопку старта и красит дефицитные строки

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

  • Шаг 1 (User -> APP): Пользователь тапает по карточке пирога или супа в поисковой выдаче и переходит на детальный экран рецепта. Приложение мгновенно инициирует проверку объёмов для этого конкретного recipe_id.
  • Шаг 2 (APP -> API): Мобильное приложение отправляет запрос HTTP GET /api/v1/cook/recipes/{id}/validate. Параметры сессии и сквозной логирующий заголовок X-Request-ID передаются в стандартном виде.
  • Шаг 3 (API -> API): Бэкенд FastAPI декодирует входящий JWT-токен. Из клеймов (JWT Claims) извлекаются два параметра: home_group_id (идентификатор холодильника) и account_type (HOME или OFFICE).
  • Шаг 4 (API -> DB): Шлюз отправляет в базу данных PostgreSQL агрегационный запрос. Напрямую по item_id здесь искать нельзя, так как продукты одного типа могли закупаться разными партиями и отображаться на складе независимыми строками.
  • Шаг 5 (DB -> DB): СУБД находит все ингредиенты, привязанные к выбранному рецепту, и с помощью функции SUM(quantity) собирает суммарный доступный объём по каждой продуктовой позиции в рамках конкретного home_group_id. Строки с отрицательным весом (старые виртуальные минусы) в расчёт не берутся.
  • Шаг 6 (DB -> API): База данных возвращает бэкенду плоскую матрицу сопоставления: наименование ингредиента, требуемый по рецепту объём (например, 4 шт яиц) и суммарный объём, фактически находящийся в холодильнике (например, 0 шт).
  • Шаг 7 (API -> API): Бэкенд-шлюз запускает цикл сравнения. Если поле фактического наличия меньше требуемого, алгоритм рассчитывает разницу (deficit_quantity = required_quantity - available_quantity) и упаковывает её в массив missing_ingredients.
  • Шаг 8 (API -> API): Если массив дефицита пуст (все продукты есть на складе в нужном объёме), бэкенд выставляет флаг is_executable: true для любого типа аккаунта и отдаёт ответ клиенту.
  • Шаг 9 (API -> APP): Ветка OFFICE (Обнаружен дефицит): Срабатывает правило строгого корпоративного учёта. Бэкенд выставляет флаг is_executable: false. Массив дефицитных ингредиентов маркируется флагом критичности CRITICAL_LOCK_EXECUTION. Метод возвращает статус 200 OK, но интерфейс Flutter блокирует кнопку «Начать готовку» и выводит красный баннер: «Запуск невозможен. На складе офиса не хватает ингредиентов».
  • Шаг 10 (API -> APP): Ветка HOME (Обнаружен дефицит): Срабатывает правило ограниченного доверия для семьи. Бэкенд умышленно выставляет флаг is_executable: true. Строки дефицита маркируются флагом WARNING_VIRTUAL_MINUS_ALLOWED. Метод возвращает статус 200 OK. Во Flutter кнопка старта остаётся активной (зелёной), но дефицитные продукты подсвечиваются жёлтым цветом, предупреждая, что при подтверждении готовки они уйдут в минус.

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

5.1 1. Контекстные ответы сервера (Success Responses)

Метод всегда возвращает статус 200 OK (если структура запроса и ID рецепта верны), но внутреннее наполнение JSON кардинально меняется в зависимости от бизнес-профиля пользователя при обнаружении дефицита продуктов.

5.1.1 Пример JSON-ответа для профиля OFFICE (Жесткая блокировка выполнения)

Бэкенд выставляет флаг is_executable: false. Интерфейс Flutter обязан заблокировать кнопку «Начать готовку» и вывести список отсутствующих позиций в красной цветовой гамме.

{
  "recipe_id": 412,
  "title": "Яблочный пирог классический",
  "account_context": "OFFICE",
  "is_executable": false,
  "summary": {
    "total_required_items": 4,
    "available_items_count": 3,
    "missing_items_count": 1
  },
  "missing_ingredients": [
    {
      "product_name": "Яйцо куриное",
      "required_quantity": 4.0,
      "available_quantity": 0.0,
      "deficit_quantity": 4.0,
      "unit": "шт",
      "severity": "CRITICAL_LOCK_EXECUTION"
    }
  ]
}

5.2 2. Спецификация серверной логики агрегации (SQL)

Для того чтобы узнать реальное наличие ингредиента, СУБД обязана просуммировать объемы всех независимых партий товаров, купленных пользователем в разное время (разные строки в БД), отсекая старые виртуальные минусы.

SELECT 
    ri.ingredient_name AS product_name,
    ri.amount AS required_quantity,
    ri.unit AS unit,
    -- Суммируем только живые партии товара, игнорируя долги (минусы)
    COALESCE(SUM(CASE WHEN fi.quantity > 0 THEN fi.quantity ELSE 0 END), 0) AS available_quantity
FROM recipe_ingredients ri
LEFT JOIN fridge_inventory fi 
    ON LOWER(fi.product_name) = LOWER(ri.ingredient_name) 
    AND fi.home_group_id = \$1
WHERE ri.recipe_id = \$2
GROUP BY ri.ingredient_name, ri.amount, ri.unit;

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

5.3.1 Ошибка: Рецепт не найден в Мастер-базе (HTTP 404 Not Found)

Выбрасывается на Шаге 4, если переданный в URL идентификатор recipe_id отсутствует в глобальном справочнике рецептов экосистемы.

{
  "error_code": "ERR-RECIPE-NOT-FOUND",
  "message": "Запрошенный рецепт отсутствует в мастер-базе данных.",
  "details": {
    "requested_recipe_id": 9999,
    "reason": "Entity not found in recipes dictionary table."
  }
}

5.3.2 Ошибка: Коллизия единиц измерения (HTTP 422 Unprocessable Entity)

Выбрасывается на Шаге 7, если в справочнике рецептов ингредиент указан в одних единицах (например, Мука — 500 грамм), а в холодильнике пользователя этот же товар числится в других (например, Мука — 1.0 упаковка), и бэкенд не имеет коэффициента для автоматической конвертации.

{
  "error_code": "ERR-UNIT-MISMATCH",
  "message": "Ошибка сопоставления: невозможно пересчитать единицы измерения рецепта и склада.",
  "details": {
    "product_name": "Мука",
    "recipe_unit": "г",
    "fridge_unit": "упак",
    "action": "Требуется ручная калибровка мастер-данных словаря мер и весов."
  }
}