Метод POST /api/v1/fridge/virtual-shortages/reconcile

Документация API: Ручное обнуление и закрытие виртуальных задолженностей в семейном контуре

Published

July 1, 2026

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 для синхронизации остатков.

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 кг яблок

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): Бэкенд асинхронно отправляет событие в топик Kafka bupar.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."
  }
}