sequenceDiagram
autonumber
actor User as Пользователь (Окно долгов)
participant APP as Мобильное приложение (Flutter)
participant API as Бэкенд-шлюз (FastAPI)
participant DB as Реляционная СУБД (PostgreSQL)
participant KAFKA as Брокер событий (Kafka)
User->>APP: Нажимает "Погасить долг" и вводит новый вес (1.5 кг)
APP->>API: HTTP POST /fridge/virtual-shortages/reconcile (С ID и весом в JSON)
activate API
note over API: Шаг 3: Верификация прав доступа к fridge_item_id
Note over API, DB: Старт атомарной SQL-транзакции
API->>DB: UPDATE fridge_inventory SET quantity = actual_quantity WHERE item_id
activate DB
DB-->>API: Успешное обновление (Запись изменена)
deactivate DB
Note over API, DB: Коммит транзакции (COMMIT)
API->>KAFKA: send_to_kafka_async("bupar.fridge.events.reconciled")
API-->>APP: HTTP 200 OK (Статус RECONCILED, задолженность закрыта)
deactivate API
note over APP: Карточка долга исчезает, на складе появляются +1.5 кг яблок
Метод POST /api/v1/fridge/virtual-shortages/reconcile
Документация API: Ручное обнуление и закрытие виртуальных задолженностей в семейном контуре
1 Функциональное назначение
Метод предназначен для ручного закрытия и калибровки технических отрицательных остатков (виртуальных минусов) в семейном контуре (HOME). Если пользователь увидел на главном экране алерт о задолженности (сформированный Методом 5) и физически пополнил запасы (например, купил яблоки у бабушки на рынке без фискального чека), он может закрыть долг вручную через интерфейс.
Метод решает следующие задачи: 1. Ликвидация технического долга: Переводит отрицательный баланс конкретной записи fridge_item_id в нулевое или положительное состояние на основе переданного пользователем фактического объема. 2. Атомарный пересчет: Корректирует остатки внутри изолированной SQL-транзакции, предотвращая состояние гонки (Race Condition). 3. Обновление стейта UI: После успешного выполнения эндпоинта информер задолженности на стороне Flutter мгновенно гаснет, а на складе появляется реальный чистый объем продукта.
2 Протокол взаимодействия (HTTP Контракт)
- Метод:
POST - Маршрут:
/api/v1/fridge/virtual-shortages/reconcile - Формат данных:
application/json
2.1 Спецификация заголовков (HTTP Headers)
| Заголовок | Обязательный | Описание | Пример значения |
|---|---|---|---|
Content-Type |
Да | Указывает на отправку JSON-пакета | application/json |
Authorization |
Да | Токен авторизации (Access Token). Выделяет home_group_id. |
Bearer eyJhbGciOiJIUzI1Ni... |
X-Request-ID |
Да | Сквозной ID запроса для распределенного логирования | req-reconcile-88ff |
2.2 Спецификация тела запроса (Request Body)
Пользователь передает ID записи, которая находится в минусе, и указывает фактический объем новой закупки. Если пользователь хочет просто обнулить долг (признать, что продуктов нет), он передает actual_quantity: 0.0.
| Поле | Тип | Обязательный | Описание | Пример значения |
|---|---|---|---|---|
fridge_item_id |
Integer | Да | ID записи инвентаря с отрицательным балансом | 1042 |
actual_quantity |
Float | Да | Новый фактический объем товара (>= 0.0) | 1.5000 |
2.2.1 Пример сырого JSON-запроса (Payload):
{
"fridge_item_id": 1042,
"actual_quantity": 1.5
}3 Схема обработки запроса пользователя (Mermaid)
На диаграмме представлена логика закрытия долга: СУБД фиксирует транзакцию, обнуляет или выводит в плюс отрицательный баланс, а бэкенд отправляет событие в Kafka для синхронизации остатков.
4 Расшифровка шагов
- Шаг 1 (
User -> APP): Пользователь тапает по информеру задолженности на главном экране (например, по карточке дефицита яблок в-0.5 кг). Открывается небольшое модальное окно, где пользователь вводит реальный физический вес, который он принёс домой (например, вводит1.5 кг, купленных на рынке), и нажимает «Подтвердить остатки». - Шаг 2 (
APP -> API): Приложение через клиентDioотправляет запросHTTP POST /api/v1/fridge/virtual-shortages/reconcile. В теле JSON уходят два строго типизированных параметра:fridge_item_id(целевой долг) иactual_quantity(значение1.5типа float). - Шаг 3 (
API -> API): БэкендFastAPIпринимает пакет и выполняет проверку безопасности: сверяет, принадлежит ли запрашиваемыйfridge_item_idк той жеhome_group_id, которая зашита в JWT-токене пользователя. Это защищает систему от инъекций, когда злоумышленник пытается обнулить долги чужой семьи. - Шаг 4 (
API -> DB): После успешной проверки прав бэкенд открывает атомарную SQL-транзакцию вPostgreSQL. Чтобы закрыть виртуальный минус и зафиксировать реальный баланс, бэкенд выполняет прямое переопределение веса:UPDATE fridge_inventory SET quantity = $1 WHERE item_id = $2. Математика вычитания долгов здесь не нужна, так как переданный пользователем объем1.5 кг— это уже итоговый чистый остаток, лежащий в холодильнике (старый долг в-0.5 кгпросто аннулируется этой новой физической массой). - Шаг 5 (
DB -> API): База данных фиксирует изменения и возвращает шлюзу подтверждение успешного обновления строки. - Шаг 6 (
API -> API): Бэкенд закрывает транзакцию командойCOMMIT. Старая запись из состояния задолженности переходит в статус легитимного складского запаса. - Шаг 7 (
API -> KAFKA): Бэкенд асинхронно отправляет событие в топик Kafkabupar.fridge.events.reconciled, уведомляя модуль аналитики о том, что техническая задолженность по продукту была успешно ликвидирована вручную. - Шаг 8 (
API -> APP):FastAPIвозвращает успешный статусHTTP 200 OK. Мобильное приложение Flutter ловит ответ, стейт-менеджер убирает карточку долга из реестра (она исчезает с экрана), а в общем списке холодильника пользователь видит чистый положительный объем продукта:Яблоки — 1.5 кг.
5 Спецификация ответов сервера (Response Body) и ошибок
5.1 1. Успешный ответ (Success Response)
5.1.1 HTTP 200 OK (Ответ на Шаге 8)
Возвращается после успешного коммита транзакции ручного переопределения веса и ликвидации технического минуса.
- Заголовки ответа (Response Headers):
Content-Type: application/json
- Тело ответа (Response Body):
{
"status": "RECONCILED",
"data": {
"fridge_item_id": 1042,
"product_name": "Яблоки",
"updated_quantity": 1.5,
"unit": "кг",
"timestamp": "2026-07-01T21:57:00Z"
}
}5.2 2. Спецификация ошибок и вилок исключений (Error Responses)
5.2.1 Ошибка передачи отрицательного фактического веса (HTTP 422 Unprocessable Entity)
Выбрасывается на Шаге 3 бэкенд-шлюзом FastAPI. Пользователь не может закрыть старый минус, введя новое отрицательное число. Значение actual_quantity обязано быть строго >= 0.0.
{
"error_code": "ERR-INVALID-RECONCILE-QUANTITY",
"message": "Фактический объем пополнения не может быть отрицательным.",
"details": [
{
"loc": ["body", "actual_quantity"],
"msg": "Value must be greater than or equal to 0.0",
"type": "value_error.number.not_lt"
}
]
}5.2.2 Ошибка: Попытка закрыть чужую запись или несуществующий ID (HTTP 404 Not Found)
Выбрасывается на Шаге 3, если переданный fridge_item_id отсутствует в базе данных либо принадлежит другой семье (home_group_id не совпадает с данными из JWT-токена). Это предотвращает несанкционированное изменение чужого инвентаря.
{
"error_code": "ERR-SHORTAGE-RECORD-NOT-FOUND",
"message": "Целевая запись задолженности не найдена или у вас нет прав на её изменение.",
"details": {
"requested_item_id": 1042,
"reason": "Entity select returned 0 rows for this group scope context."
}
}