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 перерисовывает кнопку старта и красит дефицитные строки
Метод GET /api/v1/cook/recipes/{id}/validate
Документация API: Предварительный расчет дефицита ингредиентов с контекстным ветвлением профилей HOME и OFFICE
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)
На диаграмме представлена логика расчёта дефицита объёмов. База данных суммирует все доступные партии товара, а бэкенд на уровне бизнес-логики разделяет поведение экрана для домашнего и корпоративного использования.
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": "Требуется ручная калибровка мастер-данных словаря мер и весов."
}
}