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

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

Published

July 2, 2026

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

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

Метод решает три задачи:

  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 Расшифровка шагов

Шаг Действие Параметры / Запросы / DTO Ошибки (Исключения / Статусы)
Шаг 1 (App -> Nginx) Пользователь отправляет форму утилизации с указанием списываемых продуктов, типа пространства и причины списания (например, испортилось/пропало). HTTP POST /api/v1/waste/items
Headers: Authorization: Bearer <JWT>
Payload (CreateWasteItemsRequestDTO):
{ "space_type": "OFFICE", "spent_items": [{ "product_id": "prod-waste-uuid", "qty": 0.8 }], "waste_reason": "SPOILT" }
DioException: send timeout
HTTP 400 Bad Request (передана невалидная структура JSON или неверный enum причины)
HTTP 401 Unauthorized
Шаг 2 (Nginx -> Gateway) Прокси-сервер выполняет базовую маршрутизацию на внутренний шлюз и осуществляет инъекцию сквозного трассировочного ID сессии. HTTP POST (Внутреннее проксирование на FastAPI Gateway)
Added Headers:
X-Request-ID: "trace-waste-control-uuid"
HTTP 502 Bad Gateway (контейнер backend-api API Gateway недоступен или перегружен)
HTTP 504 Gateway Timeout
Шаг 3 (Gateway -> Waste) API-шлюз валидирует параметры запроса через Pydantic-схему WasteItemsValidator и транслирует контракт во внутренний сервис утилизации. HTTP POST /internal/v1/waste/items
Headers: X-Request-ID: "trace-waste-control-uuid", Body: CreateWasteItemsRequestDTO
HTTP 422 Unprocessable Entity
Payload: ValidationErrorResponseDTO
error_code: "WASTE_VALIDATION_FAILED"
Шаг 4 (Waste -> Fridge) Сервис отходов формирует высокопроизводительный gRPC-вызов к контуру холодильника для выполнения пакетной мутации остатков испорченных продуктов. gRPC Запрос:
mutateBalances(BalanceMutationRequest)
Payload: spent_items: [{ "product_id": "prod-waste-uuid", "qty": 0.8 }], space_type: "OFFICE"
gRPC Status: UNAVAILABLE (сервис fridge-service недоступен)
gRPC Status: DEADLINE_EXCEEDED (таймаут обработки транзакции списания)
Шаг 5 (Fridge -> Space_Check) Сервис холодильника принимает gRPC-запрос на списание в отходы и запускает внутреннее ветвление бизнес-логики на основе параметра space_type. Внутренний селектор стратегий списания остатков.
Payload: spent_items: [...], space_type: "OFFICE\|HOME"
RuntimeError: InvalidSpaceType (в коде приложения передан или не обработан неизвестный тип пространства)

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"
    }
  ]
}