sequenceDiagram
autonumber
actor User as Пользователь (Экран Cook)
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/manual (С указанием fridge_item_id и веса)
activate API
note over API: Шаг 3: Декодирование JWT (Определение HOME или OFFICE)
Note over API, DB: Старт атомарной SQL-транзакции
loop По каждому ингредиенту из массива
API->>DB: SELECT quantity FROM fridge_inventory WHERE item_id = fridge_item_id
activate DB
DB-->>API: Возвращает текущий остаток выбранной партии
deactivate DB
alt Остатка партии ХВАТАЕТ
API->>DB: UPDATE fridge_inventory SET quantity = quantity - spend
else Остатка НЕ ХВАТАЕТ / Товар отсутствует
alt Тип аккаунта == OFFICE
API-->>APP: HTTP 400 Bad Request (ERR-STOCK-SHORTAGE)
else Тип аккаунта == HOME
note over API: Шаг 9: Логика виртуального ухода в минус
API->>DB: UPDATE fridge_inventory SET quantity = quantity - spend (Значение становится < 0)
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: Коммит атомарной транзакции
API->>KAFKA: send_to_kafka_async("bupar.fridge.events.cook")
API-->>APP: HTTP 201 Created (Блюдо добавлено, остатки списаны)
deactivate API
note over APP: Экран обновляется: списанные партии уменьшились, появилась 1 шт Борща
Метод POST /api/v1/cook/manual
Документация API: Ручная фиксация приготовленного блюда с таргетным списанием ингредиентов и порционной канонизацией
1 Функциональное назначение
Метод предназначен для регистрации факта ручного приготовления сложного составного блюда (например, «Яблочный пирог», «Борщ») внутри экосистемы.
Метод решает три ключевые архитектурные задачи:
- Таргетное списание остатков: Бэкенд не использует автоматические стратегии вроде FIFO. Пользователь в интерфейсе Flutter сам выбирает конкретные записи партий продуктов (из доступных в холодильнике), которые он физически потратил на готовку.
- Унификация порционности готовых блюд: Жидкие, штучные и составные блюда канонизируются к единой базовой единице измерения
упак/штсо значением объема1.0(одна кастрюля, один пирог). Дальнейшее дробление на порции в микросервисе потребления будет происходить через уменьшение этого float-коэффициента (например, 1 тарелка супа =0.2от кастрюли). - Маршрутизация бизнес-профилей: Метод автоматически считывает тип аккаунта из JWT-токена. Для профиля
HOMEдопускается виртуальный уход остатков в минус (если пользователь забыл внести продукты ранее), для профиляOFFICEстрого проверяется физическое наличие веса в выбранных строках.
2 Протокол взаимодействия (HTTP Контракт)
- Метод:
POST - Маршрут:
/api/v1/cook/manual - Формат данных:
application/json
2.1 Спецификация заголовков (HTTP Headers)
| Заголовок | Обязательный | Описание | Пример значения |
|---|---|---|---|
Content-Type |
Да | Указывает на передачу строго типизированного JSON-пакета | application/json |
Authorization |
Да | Токен авторизации (Access Token). Бэкенд извлекает из него user_id, home_group_id и account_type (HOME или OFFICE). |
Bearer eyJhbGciOiJIUzI1Ni... |
X-Request-ID |
Да | Сквозной ID запроса для связывания логов списания и зачисления | req-cook-3c9b-11ef |
2.2 Спецификация тела запроса (Request Body)
В массиве used_ingredients клиент обязан передавать fridge_item_id — уникальный идентификатор конкретной записи товара в холодильнике, выбранной пользователем вручную.
| Поле | Тип | Обязательный | Описание |
|---|---|---|---|
dish_name |
String | Да | Пользовательское название приготовленного блюда |
output_unit |
String | Да | Каноничная единица измерения для кастрюль/пирогов |
used_ingredients |
Array | Да | Список списания конкретных выбранных партий товаров |
2.2.1 Пример сырого JSON-запроса (Payload):
{
"dish_name": "Борщ домашний",
"output_unit": "шт",
"used_ingredients": [
{
"fridge_item_id": 1042,
"product_name": "Мясо говядина (партия 1)",
"quantity_to_spend": 0.600,
"unit": "кг"
},
{
"fridge_item_id": 1089,
"product_name": "Капуста свежая",
"quantity_to_spend": 0.400,
"unit": "кг"
}
]
}3 Схема обработки запроса пользователя (Mermaid)
На диаграмме представлена логика таргетного списания конкретных партий ингредиентов, выбранных пользователем в интерфейсе, и зачисления составного блюда в холодильник как единого масштабируемого объекта со значением объема 1.0.
4 Расшифровка шагов
- Шаг 1 (
User -> APP): Пользователь на экране готовки вводит текстовое наименование блюда (например,"Борщ домашний"). Ниже в списке остатков холодильника он вручную тапает (выбирает чекбоксами) конкретные строки поставок ингредиентов, которые собирается физически израсходовать (например, старую партию мяса от 20 июня, игнорируя свежую). Пользователь указывает вес списания для каждой строки и нажимает кнопку «Приготовить». - Шаг 2 (
APP -> API): Приложение через клиентDioотправляет запросHTTP POST /api/v1/cook/manual. В теле JSON передается массивused_ingredients, где каждая запись строго привязана к системномуfridge_item_id, выбранному пользователем на предыдущем шаге. - Шаг 3 (
API -> API): Бэкенд-шлюзFastAPIпринимает пакет, генерирует сквознойX-Request-IDи декодирует JWT-токен из заголовкаAuthorization. Из токена извлекается флаг типа аккаунта (account_type), определяющий бизнес-логику транзакции (HOMEилиOFFICE). - Шаг 4 (
API -> DB): Бэкенд открывает атомарную SQL-транзакцию вPostgreSQL. Запускается цикл валидации и списания ингредиентов по переданному массиву. Бэкенд делает точечный запросSELECT quantity FROM fridge_inventory WHERE item_id = $1для первой выбранной пользователем партии. - Шаг 5 (
DB -> API): База данных возвращает текущий физический объем или штуки для конкретно этогоfridge_item_id. - Шаг 6 (
API -> DB): Если текущего остатка в выбранной строке достаточно для покрытия указанного веса готовки, бэкенд выполняет операцию вычитания:UPDATE fridge_inventory SET quantity = quantity - $1 WHERE item_id = $2. - Шаг 7 (
API -> API): Если пользователь указал вес списания больше, чем физически есть в этой строке холодильника (или строка вообще отсутствует), бэкенд переходит к вилке условий в зависимости от типа аккаунта. - Шаг 8 (
API -> APP): Ветка OFFICE: Строгий корпоративный контур. Бэкенд мгновенно прерывает транзакцию, делаетROLLBACKвсех изменений и возвращает мобильному приложению ошибкуHTTP 400 Bad Requestс кодомERR-STOCK-SHORTAGE(запрет готовки при дефиците продуктов). - Шаг 9 (
API -> DB): Ветка HOME: Контур ограниченного доверия для живой семьи. Срабатывает логика виртуального ухода в минус. Система предполагает, что продукты физически есть в холодильнике, но пользователь просто забыл вовремя отсканировать чек при покупке. Бэкенд намеренно выполняет запросUPDATE fridge_inventory SET quantity = quantity - $1, уводя полеquantityконкретной записи в отрицательное float-значение (например,-0.250кг). - Шаг 10 (
API -> DB): Процесс шагов 4–9 циклически повторяется для каждого ингредиента из массиваused_ingredients, гарантируя, что спишутся именно те партии, которые выбрал пользователь. - Шаг 11 (
API -> DB): После завершения цикла списания ингредиентов, бэкенд производит канонизацию и зачисление готового сложного блюда. В таблицу инвентаря добавляется новая независимая запись с наименованием блюда. Объём выставляется в базовое значение1.0(одна целая кастрюля или один целый пирог), а единица измерения — в"шт". Это позволит следующему микросервису потребления делить данный объект на дробные порции (например, списать0.2от кастрюли супа как одну тарелку). - Шаг 12 (
API -> KAFKA): Атомарная транзакция фиксируется в PostgreSQL (COMMIT). Бэкенд асинхронно отправляет бизнес-событие в топик Kafkafridge.events.cook, содержащее ID созданного блюда и список потраченныхitem_idдля сквозной продуктовой аналитики. - Шаг 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": "DISH_CREATED",
"data": {
"fridge_item_id": 5012,
"dish_name": "Борщ домашний",
"allocated_quantity": 1.0,
"unit": "шт",
"account_context": "HOME",
"timestamp": "2026-07-01T21:51:30Z"
}
}5.2 2. Спецификация ошибок и вилок исключений (Error Responses)
5.2.1 Ошибка дефицита остатков (HTTP 400 Bad Request — Шаг 8)
Выбрасывается строго в контексте аккаунта OFFICE, если в процессе проверки на Шаге 5 обнаружилось, что физический объем выбранного товара (fridge_item_id) меньше, чем пользователь указал в поле quantity_to_spend. Транзакция полностью откатывается (ROLLBACK).
{
"error_code": "ERR-STOCK-SHORTAGE",
"message": "Операция отклонена: в корпоративном контуре запрещено списывать отсутствующие продукты.",
"details": {
"reason": "Physical volume validation failed for requested ingredient cluster.",
"failed_item_id": 1042,
"requested_quantity": 0.600,
"available_quantity": 0.150,
"unit": "кг"
}
}5.2.2 Ошибка валидации структуры входных данных (HTTP 422 Unprocessable Entity)
Выбрасывается на Шаге 3, если Pydantic-модель на FastAPI обнаружила отрицательные значения в полях списания веса или передана невалидная строка UUID.
{
"error_code": "ERR-VALIDATION-FAILED",
"message": "Переданный JSON-пакет содержит некорректные типы данных или отрицательные веса.",
"details": [
{
"loc": ["body", "used_ingredients", 0, "quantity_to_spend"],
"msg": "Значение веса или количества должно быть строго больше 0.0001",
"type": "value_error.number.not_gt"
}
]
}5.3 3. Техническое примечание для разработчиков (База данных и Стрим Потребления)
Хранение виртуального минуса в PostgreSQL: Поле
quantityв таблицеfridge_inventoryимеет типNUMERIC(12, 4). Для аккаунтов типаHOMEограничениеCHECK (quantity >= 0)отсутствует. Отрицательное значение (например,-0.2500) является валидным техническим состоянием системы, сигнализирующим о задолженности по вводу ресурсов.Масштабируемость для последующего микросервиса Потребления: Зачисленное блюдо
"Борщ домашний"со значением1.0 штготово к порционному делению. В следующем микросервисе метод списания порции будет выполнять обычное математическое вычитание долей:-- Списание одной порции (1 тарелка = 0.2 от объема кастрюли) UPDATE fridge_inventory SET quantity = quantity - 0.2000 WHERE item_id = 5012;Как только значение
quantityдостигает0.0000, запись переводится в статус архива и скрывается из интерфейса Flutter.