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/fridge/items/merge (JSON с source_id и target_id)
activate API
note over API: Шаг 3: Декодирование JWT (Выделение home_group_id и account_type)
Note over API, DB: Старт атомарной SQL-транзакции
API->>DB: SELECT * FROM fridge_inventory WHERE item_id IN (source_id, target_id)
activate DB
DB-->>API: Возвращает метаданные обеих партий товара
deactivate DB
note over API: Шаг 6: Валидация безопасности (home_group_id совпадает?)
note over API: Шаг 7: Валидация однородности (Имена и Unit совпадают?)
alt Имена, Единицы или Группы НЕ СОВПАДАЮТ
API-->>APP: HTTP 422 Unprocessable Entity (ERR-MERGE-VIOLATION)
else Валидация успешна
alt Контекст аккаунта == OFFICE
note over API: Шаг 9: Ветка OFFICE (Оба значения гарантированно > 0)
API->>DB: UPDATE target SET quantity = target.qty + source.qty
API->>DB: DELETE FROM fridge_inventory WHERE item_id = source_id
else Контекст аккаунта == HOME
note over API: Шаг 10: Ветка HOME (Схлопывание с учетом скрытого минуса)
API->>DB: UPDATE target SET quantity = target.qty + source.qty
API->>DB: DELETE FROM fridge_inventory WHERE item_id = source_id
end
Note over API, DB: Коммит атомарной транзакции (COMMIT)
API->>KAFKA: send_to_kafka_async("bupar.fridge.events.merged")
API-->>APP: HTTP 200 OK (status: "SUCCESS_MERGED")
end
deactivate API
note over APP: Экран обновляется: исходная карточка исчезает, целевая увеличивает вес
Метод POST /api/v1/fridge/items/merge
Документация API: Ручное объединение (слияние) раздельных партий однородных продуктов питания
1 Функциональное назначение
Метод предназначен для оптимизации складского пространства и наведения порядка в интерфейсе холодильника. Если в процессе нескольких независимых покупок (через фото чека или интеграцию с магазинами и продуктовыми сетями) в системе образовалось несколько раздельных записей одного и того же товара (например, две строки «Яблоки» с весом 1.0 кг и 0.5 кг), пользователь может вручную “схлопнуть” их в одну общую позицию.
Метод решает следующие задачи:
- Валидация однородности (Защита от смешивания): Гарантирует, что слияние возможно только для товаров с идентичным наименованием и одинаковой единицей измерения (кг с кг, шт со шт). Слияние «яблок» с «апельсинами» блокируется на уровне бизнес-логики.
- Математическая калибровка (Погашение долгов): Корректно суммирует объемы float/double, включая случаи, когда одна из партий находится в техническом отрицательном состоянии (виртуальном минусе).
- Консолидация данных: Суммирует объемы, переносит вес на целевую запись (Target), а исходную запись (Source) безвозвратно удаляет из таблицы
fridge_inventory.
2 Протокол взаимодействия (HTTP Контракт)
- Метод:
POST - Маршрут:
/api/v1/fridge/items/merge - Формат данных:
application/json
2.1 Спецификация заголовков (HTTP Headers)
| Заголовок | Обязательный | Описание | Пример значения |
|---|---|---|---|
Content-Type |
Да | Указывает на отправку JSON-пакета | application/json |
Authorization |
Да | Токен авторизации (Access Token) для проверки прав на владение холодильником |
Bearer eyJhbGciOiJIUzI1Ni... |
X-Request-ID |
Да | Сквозной ID запроса для распределенного логирования | req-merge-77aa |
2.2 Спецификация тела запроса (Request Body)
Клиент передает ID записи, которую нужно поглотить (source_item_id), и ID записи, в которую перетечет суммарный вес (target_item_id).
| Поле | Тип | Обязательный | Описание | Пример значения |
|---|---|---|---|---|
source_item_id |
Integer | Да | ID записи инвентаря, которая будет удалена после переноса веса | 1042 |
target_item_id |
Integer | Да | ID целевой записи, баланс которой увеличится на объем исходной | 2085 |
2.2.1 Пример сырого JSON-запроса (Payload):
{
"source_item_id": 1042,
"target_item_id": 2085
}3 Схема обработки запроса пользователя (Mermaid)
Диаграмма описывает логику валидации однородности продуктов, проверку принадлежности к одной семье/офису (home_group_id) и математику схлопывания скрытых отрицательных остатков для домашнего контура.
4 Расшифровка шагов
- Шаг 1 (
User -> APP): Пользователь в интерфейсе холодильника (или в окне задолженностей) решает объединить две записи однородного товара. Например, перетаскивает карточку скрытого минуса яблок (-0.3 кг) на карточку свежей закупки яблок (+1.0 кг). - Шаг 2 (
APP -> API): Мобильное приложение черезDioотправляет запросHTTP POST /api/v1/fridge/items/merge. В теле запроса передаются идентификаторыsource_item_id(что поглощается) иtarget_item_id(куда перетекает вес). - Шаг 3 (
API -> API): Бэкенд-шлюзFastAPIдекодирует JWT-токен. Из него извлекаютсяhome_group_idиaccount_type. Бэкенд запоминает эту ветку для применения специфичных правил (вOFFICEзапрещены отрицательные значения, вHOME— разрешено математическое схлопывание долга). - Шаг 4 (
API -> DB): Бэкенд открывает атомарную SQL-транзакцию вPostgreSQLи блокирует строки запросомSELECT * FROM fridge_inventory WHERE item_id IN ($1, $2) FOR UPDATE. Это защищает данные от параллельного списания во время слияния. - Шаг 5 (
DB -> API): База данных возвращает полные физические структуры обеих записей: их имена, текущие количества (включая скрытые минусы для HOME), единицы измерения (unit) и идентификаторы владельцев группы. - Шаг 6 (
API -> API): Проверка Контекста Группы: Бэкенд сверяетhome_group_idобеих записей сhome_group_idиз токена. Если пользователь попытался подменить ID и слить свои продукты с чужим холодильником, транзакция падает со статусомHTTP 403 Forbidden. - Шаг 7 (
API -> API): Проверка Однородности: Бэкенд сравнивает текстовые наименования и единицы измерения:LOWER(source.product_name) == LOWER(target.product_name) AND source.unit == target.unit. Если пользователь пытается слить яблоки с апельсинами или килограммы со штуками, транзакция откатывается (ROLLBACK) с ошибкойHTTP 422. - Шаг 8 (
API -> API): Если все проверки пройдены успешно, бэкенд разветвляет логику в зависимости отaccount_type. - Шаг 9 (
API -> DB): Ветка OFFICE: Так как в офисе минусов быть не может, система берет два чистых положительных float-значения, суммирует их, записывает результат в целевую строку (target_item_id), а исходную строку (source_item_id) физически удаляет из СУБД. - Шаг 10 (
API -> DB): Ветка HOME: Включается логика схлопывания скрытого долга. Если исходная запись была в скрытом минусе (-0.3 кг), а целевая в плюсе (+1.0 кг), СУБД производит честное алгебраическое сложение:1.0 + (-0.3) = 0.7 кг. Новый чистый остаток записывается в целевую строку, а строка долга удаляется из базы, автоматически гася алерт в интерфейсе. - Шаг 11 (
API -> KAFKA): Атомарная транзакция фиксируется (COMMIT). Бэкенд отправляет асинхронное сообщение в топик Kafkabupar.fridge.events.mergedдля пересчета продуктовых метрик в дата-лейке. - Шаг 12 (
API -> APP): Сервер возвращает статусHTTP 200 OK. Во Flutter-приложении срабатывает триггер: экран холодильника перерисовывается, старая карточка исчезает, а целевая отображает консолидированный вес продукта.
5 Спецификация ответов сервера (Response Body) и ошибок
5.1 Успешный ответ (Success Response)
5.1.1 HTTP 200 OK (Ответ на Шаге 12)
Возвращается после успешного завершения атомарной транзакции слияния партий. Исходная запись удаляется, а целевая обновляется с учетом консолидированного float-объема.
- Заголовки ответа (Response Headers):
Content-Type: application/json
- Тело ответа (Response Body):
{
"status": "SUCCESS_MERGED",
"data": {
"target_item_id": 2085,
"product_name": "Яблоки",
"consolidated_quantity": 0.7,
"unit": "кг",
"account_context": "HOME",
"deleted_source_item_id": 1042,
"timestamp": "2026-07-01T23:28:00Z"
}
}5.2 Спецификация ошибок и вилок исключений (Error Responses)
5.2.1 Ошибка коллизии однородности и единиц измерения (HTTP 422 Unprocessable Entity — Шаг 7)
Выбрасывается, если пользователь пытается объединить разнородные продукты (яблоки с апельсинами) или однородные продукты с несовпадающими единицами измерения (unit), которые нельзя математически сложить напрямую.
{
"error_code": "ERR-MERGE-VIOLATION",
"message": "Ошибка слияния: продукты должны иметь одинаковое наименование и идентичные единицы измерения.",
"details": {
"reason": "Product name or unit token mismatch between source and target rows.",
"source": {
"product_name": "Яблоки",
"unit": "шт"
},
"target": {
"product_name": "Яблоки",
"unit": "кг"
}
}
}5.2.2 Ошибка нарушения контекста безопасности (HTTP 403 Forbidden — Шаг 6)
Выбрасывается, если один из переданных идентификаторов (source_item_id или target_item_id) принадлежит чужой семье или другому офису. Система блокирует попытку межгруппового смешивания данных.
{
"error_code": "ERR-CROSS-GROUP-MERGE",
"message": "Операция отклонена: запрещено объединять продукты из разных холодильников или бизнес-контекстов.",
"details": {
"reason": "Security token home_group_id does not match the database scope for one of the entities."
}
}5.2.3 Ошибка: Запись инвентаря не найдена (HTTP 404 Not Found — Шаг 5)
Выбрасывается, если один из указанных ID физически отсутствует в таблице fridge_inventory (например, запись была параллельно удалена или списана в ноль другим членом семьи).
{
"error_code": "ERR-MERGE-RECORD-NOT-FOUND",
"message": "Одна или обе указанные партии товара не найдены в системе.",
"details": {
"source_id": 1042,
"target_id": 9999
}
}