Метод POST /api/v1/waste/items

Документация API: Регистрация утилизации продуктов (Waste Control) с поддержкой изолированных теневых логов задолженностей

Published

July 2, 2026

1 Функциональное назначение

Метод предназначен для фиксации факта порчи, сгорания или утилизации продуктов питания и дробных порций блюд во Flutter-интерфейсе.

Метод решает три ключевые задачи:

  1. Калибровка складских остатков: Уменьшает баланс выбранного fridge_item_id на точный float-объем утилизации, поддерживая любые единицы измерения системы (кг, литры, шт) со скриншота интерфейса.
  2. Типизация причин порчи (Waste Control): Фиксирует причину списания («пропало», «истек срок», «сгорело», «не понравилось») для последующего анализа финансового и продуктового мусора командой MLOps.
  3. Обслуживание теневой экономики (Isolated Shadow Debt Ledger):
    • В контексте OFFICE дефицит запрещен. При нехватке веса возвращается жесткий блок.
    • В контексте HOME, если пользователь выбрасывает продукт, которого технически нет на балансе (забыл внести чек ранее), система не уводит физический склад в минус (остаток quantity в холодильнике фиксируется на 0.0). Вместо этого метод создает запись в изолированной таблице теневых долгов fridge_shadow_debts и генерирует уникальный correlation_id (UUID). Это позволяет пользователю позже, находясь во вкладке долгов, вручную привязать этот мусор к новому чеку и скорректировать вес.

2 Протокол взаимодействия (HTTP Контракт)

  • Метод: POST
  • Маршрут: /api/v1/waste/items
  • Формат данных: application/json

2.1 Спецификация заголовков (HTTP Headers)

Заголовок Обязательный Описание Пример значения
Content-Type Да Указывает на передачу строго типизированного JSON-пакета application/json
Authorization Да Токен авторизации (Access Token). Выделяет home_group_id и account_type. Bearer eyJhbGciOiJIUzI1Ni...
X-Request-ID Да Сквозной ID запроса для распределенного логирования req-waste-register-11aa

2.2 Спецификация тела запроса (Request Body)

Поле Тип Обязательный Описание Пример значения
fridge_item_id Integer Да ID списываемой записи инвентаря склада 1042
waste_quantity Float Да Точный float-объем или порция, уходящая в мусор 0.3500
waste_reason String (Enum) Да Каноничный код причины утилизации "REASON_SPOILED"

2.2.1 Перечень допустимых констант waste_reason (Enum):

  • REASON_SPOILED — пропало / сгнило.
  • REASON_EXPIRED — истек срок годности.
  • REASON_BURNT — сгорело при готовке.
  • REASON_DISLIKED — не понравилось по вкусу.

2.2.2 Пример сырого JSON-запроса (Payload — Сценарий: выбросили 0.350 кг испортившихся яблок при их нулевом остатке в приложении):

{
  "fridge_item_id": 1042,
  "waste_quantity": 0.350,
  "waste_reason": "REASON_SPOILED"
}

3 Схема обработки запроса пользователя (Mermaid)

На диаграмме представлена логика фиксации утилизации в мусор. Если физического продукта на складе не хватает (Теневая экономика контура HOME), система сохраняет остаток холодильника равным 0.0, но генерирует сквозной correlation_id и фиксирует технический долг в изолированной таблице fridge_shadow_debts для последующего ручного редактирования пользователем.

sequenceDiagram
    autonumber
    actor User as Пользователь (Экран Waste)
    participant APP as Мобильное приложение (Flutter)
    participant WS as Сервис Утилизации (FastAPI)
    participant DB as База Данных (PostgreSQL)
    participant KAFKA as Брокер событий (Kafka)

    User->>APP: Вводит объем утилизации (0.5 кг) и причину, жмет "Списать"
    APP->>WS: HTTP POST /api/v1/waste/items (JSON с fridge_item_id и весом)
    activate WS
    
    note over WS: Шаг 3: Декодирование JWT (Выделение HOME или OFFICE)
    
    Note over WS, DB: Старт атомарной SQL-транзакции
    WS->>DB: SELECT quantity FROM fridge_inventory WHERE item_id = fridge_item_id FOR UPDATE
    activate DB
    DB-->>WS: Возвращает текущий остаток на складе (например, 0.0 кг)
    deactivate DB
    
    alt Физического остатка ХВАТАЕТ
        WS->>DB: UPDATE fridge_inventory SET quantity = quantity - waste_quantity
        WS->>DB: INSERT INTO waste_history_log (Фиксация чистого акта мусора)
    else Физического остатка НЕ ХВАТАЕТ / Остаток = 0
        alt Контекст аккаунта == OFFICE
            WS-->>APP: HTTP 400 Bad Request (ERR-WASTE-STOCK-SHORTAGE)
        else Context аккаунта == HOME
            note over WS: Шаг 9: Активация теневого лога (Генерация correlation_id)
            WS->>DB: INSERT INTO waste_history_log (Запись акта утилизации с correlation_id)
            WS->>DB: INSERT INTO fridge_shadow_debts (Фиксация задолженности по вводу веса)
        end
    end
    
    Note over WS, DB: Коммит атомарной транзакции (COMMIT)
    
    WS->>KAFKA: send_to_kafka_async("bupar.fridge.events.waste")
    WS-->>APP: HTTP 200 OK (status: "WASTE_RECORDED", correlation_id="uuid-abc")
    deactivate WS
    
    note over APP: Экран обновляется, в реестре долгов появляется строка для ручного редактирования

4 Расшифровка шагов

  • Шаг 1 (User -> APP): Пользователь на экране утилизации выбирает продукт (например, яблоки) и указывает, что выбрасывает 0.5 кг сгнившего товара по причине REASON_SPOILED. При этом физический баланс яблок в приложении равен 0.0 кг (пользователь ранее забыл внести чек покупки). Пользователь нажимает кнопку «Списать».
  • Шаг 2 (APP -> WS): Приложение через клиент Dio отправляет запрос HTTP POST /api/v1/waste/items. В теле JSON уходят идентификатор целевой записи, float-объем мусора и строковый Enum-код причины.
  • Шаг 3 (WS -> WS): Микросервис Утилизации (FastAPI) декодирует Access Token, проверяя уровень прав и извлекая параметр account_type. Бэкенд запоминает бизнес-контекст (HOME или OFFICE).
  • Шаг 4 (WS -> DB): Сервис открывает атомарную SQL-транзакцию в PostgreSQL и блокирует строку инвентаря склада: SELECT quantity FROM fridge_inventory WHERE item_id = $1 FOR UPDATE.
  • Шаг 5 (DB -> WS): База данных возвращает текущий физический объем товара. В нашем кейсе он равен 0.0 кг.
  • Шаг 6 (WS -> WS): Алгоритм бэкенда сверяет остаток с объемом списания. Так как физического веса на складе не хватает для покрытия мусора (0.0 < 0.5), система выполняет разветвление логики.
  • Шаг 7 (WS -> APP): Ветка OFFICE: Строгий корпоративный контур. Бэкенд мгновенно вызывает ROLLBACK транзакции и возвращает мобильному приложению ошибку HTTP 400 Bad Request с кодом ERR-WASTE-STOCK-SHORTAGE (запрещено выбрасывать то, чего нет на балансе предприятия).
  • Шаг 8 (WS -> DB): Ветка HOME (Теневая экономика): База данных оставляет физический баланс холодильника нетронутым (quantity остается равен 0.0 кг, склад не уходит в физический минус). Вместо этого система генерирует уникальный UUID, который становится сквозным correlation_id.
  • Шаг 9 (WS -> DB): Бэкенд выполняет запись в таблицу истории мусора waste_history_log, фиксируя, что 0.5 кг яблок были реально уничтожены, и привязывает к строке сгенерированный correlation_id.
  • Шаг 10 (WS -> DB): Параллельно в рамках этой же транзакции бэкенд делает запись в изолированную таблицу теневого долга: INSERT INTO fridge_shadow_debts (home_group_id, product_name, debt_quantity, unit, correlation_id) VALUES (...). Эта строка фиксирует задолженность по вводу веса.
  • Шаг 11 (WS -> API): SQL-транзакция успешно фиксируется в PostgreSQL командой COMMIT. Данные вступают в силу.
  • Шаг 12 (WS -> KAFKA): Сервис Утилизации отправляет асинхронное сообщение в топик Kafka bupar.fridge.events.waste. Сообщение содержит ID операции и correlation_id для аналитиков MLOps.
  • Шаг 13 (WS -> APP): Сервер возвращает мобильному приложению статус HTTP 200 OK и отдает сгенерированный correlation_id. Во Flutter стейт-менеджер обновляет интерфейс: физический холодильник не изменился, но в специальной вкладке скрытых долгов появляется интерактивная строка: «Выброшено без чека: Яблоки — 0.5 кг», ожидающая последующего ручного сопоставления и редактирования пользователем при импорте новых ресурсов.

5 Спецификация ответов сервера (Response Body) и ошибок

5.1 1. Успешные ответы сервера (Success Responses)

5.1.1 HTTP 200 OK (Сценарий А: Физического остатка хватило)

Возвращается, если на складе было достаточное количество товара. Баланс в холодильнике уменьшился, запись ушла в чистую историю мусора.

{
  "status": "WASTE_RECORDED",
  "data": {
    "fridge_item_id": 1042,
    "mutated_quantity": 0.3500,
    "remaining_quantity": 0.6500,
    "unit": "кг",
    "shadow_debt_generated": false,
    "correlation_id": null,
    "timestamp": "2026-07-02T01:12:00Z"
  }
}

5.1.2 HTTP 200 OK (Сценарий Б: Активация теневой экономики / Дефицит веса в HOME)

Возвращается в контуре HOME, если товара не хватило. Физический баланс холодильника заморожен на 0.0, но сгенерирован correlation_id и открыта изолированная строка долга.

{
  "status": "WASTE_RECORDED_WITH_SHADOW_DEBT",
  "data": {
    "fridge_item_id": 1042,
    "product_name": "Яблоки",
    "frozen_fridge_quantity": 0.0000,
    "unit": "кг",
    "shadow_debt_generated": true,
    "shadow_debt_quantity": 0.5000,
    "correlation_id": "8a71d11e-9500-4b11-9a2c-d2b0d7b3dcba",
    "timestamp": "2026-07-02T01:12:05Z"
  }
}

5.2 2. Спецификация структуры теневых таблиц PostgreSQL

Для обеспечения чистой теневой экономики без коллизий автосхлопывания, долги по утилизации изолируются в отдельную таблицу fridge_shadow_debts.

-- Таблица истории физического мусора (Для КБЖУ и ИИ-анализа)
CREATE TABLE waste_history_log (
    id BIGSERIAL PRIMARY KEY,
    home_group_id VARCHAR(50) NOT NULL,
    fridge_item_id INT NOT NULL,
    waste_quantity NUMERIC(10, 4) NOT NULL,
    waste_reason VARCHAR(30) NOT NULL,
    correlation_id UUID DEFAULT NULL, -- Связующий ключ для теневых операций
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Изолированная таблица теневых долгов (Реестр для ручного редактирования)
CREATE TABLE fridge_shadow_debts (
    debt_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    home_group_id VARCHAR(50) NOT NULL,
    product_name VARCHAR(255) NOT NULL,
    debt_quantity NUMERIC(10, 4) NOT NULL, -- Вес выброшенного "воздуха"
    unit VARCHAR(20) NOT NULL,             -- "кг", "шт", "л"
    correlation_id UUID NOT NULL UNIQUE,   -- Точный указатель на запись в waste_history_log
    is_resolved BOOLEAN DEFAULT FALSE,     -- Флаг ручного закрытия пользователем
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Индекс для вывода списка скрытых долгов во вкладку корректировок
CREATE INDEX idx_shadow_debts_group_resolved 
ON fridge_shadow_debts(home_group_id) 
WHERE is_resolved = FALSE;

5.3 3. Вилки исключений и обработка ошибок

5.3.1 Ошибка дефицита на предприятии (HTTP 400 Bad Request — Шаг 7)

Выбрасывается строго в контексте аккаунта OFFICE, если товара на балансе не хватает для покрытия объема утилизации. Теневой лог для корпоративных клиентов заблокирован.

{
  "error_code": "ERR-WASTE-STOCK-SHORTAGE",
  "message": "Утилизация отклонена: в корпоративном контуре запрещено списывать в мусор отсутствующие по накладным позиции.",
  "details": {
    "reason": "Physical inventory quantity underflow in OFFICE mode.",
    "failed_item_id": 1042,
    "requested_waste": 0.5000,
    "available_stock": 0.0000,
    "unit": "кг"
  }
}

5.3.2 Ошибка валидации Pydantic (HTTP 422 Unprocessable Entity)

Выбрасывается бэкенд-шлюзом, если в поле waste_reason передан невалидный строковый код причины, отсутствующий в Enum.

{
  "error_code": "ERR-VALIDATION-FAILED",
  "message": "Передан некорректный тип причины утилизации продуктов.",
  "details": [
    {
      "loc": ["body", "waste_reason"],
      "msg": "Input should be 'REASON_SPOILED', 'REASON_EXPIRED', 'REASON_BURNT' or 'REASON_DISLIKED'",
      "type": "enum_alternative_failed"
    }
  ]
}