POST /api/v1/fridge/items/manual-text

Метод ручного текстового ввода ресурсов. Документация микросервисов домена fridge

1 Контракт и валидация данных

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

1.1 Функциональное назначение метода

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

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

  1. Синхронный gRPC-аудит (NLP Check): Транзакционный бэкенд-шлюз FastAPI перенаправляет сырую текстовую строку во внутренний микросервис CensorshipService (метод CheckProduct / ValidateProductText) для проверки на обсценную лексику (мат) и сопоставления текстового ввода с Master Data справочниками.
  2. Контроль полноты обязательных данных: Модель Pydantic проверяет наличие текстовой строки длиной не более 120 символов (примерно два или три продукта)
  3. Прямое добавление (Direct Update): В случае успешного прохождения ИИ-цензуры и валидации структуры, продукт добавляется в (холодильник) таблицу fridge_inventory конкретной группы, не сохраняясь в буферные таблицы чеков и модерации. Также запись добавляется в таблицу чеков purchase_receipts_history

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

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

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

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

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

Поле Тип Обязательный Описание Пример значения
raw_text_input String Да Сырая текстовая строка, введенная пользователем вручную Молоко Домик в деревне 3.2% 1.5 л 1029 тенге

1.2.2.1 Пример сырого JSON-запроса (Payload):

{
  "raw_text_input": "Молоко Домик в деревне 3.2% 1.5 л 1029 тенге"
}

1.3 Спецификация используемой СУБД-схемы склада (purchase_receipt_history)

Для фиксации результатов текстового ввода используется таблица продуктов в PostgreSQL.

CREATE TABLE purchase_receipts_history (
    receipt_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Тот самый UUID чека из confirm
    home_group_id VARCHAR(50) NOT NULL,
    user_id UUID NOT NULL,
    
    -- Сюда пишется итоговый валидированный JSON-пакет расходов со всеми позициями и ценами
    receipt_payload JSONB NOT NULL,
    
    created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW())
);

1.4 Спецификация используемой СУБД-схемы склада (fridge_inventory)

Для фиксации результатов текстового ввода используется таблица продуктов в PostgreSQL.

CREATE TABLE fridge_inventory (
    item_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),   -- Уникальный ID конкретного продукта в холодильнике
    receipt_id UUID,
    home_group_id VARCHAR(50) NOT NULL,
    user_id UUID NOT NULL,
    product_name VARCHAR(255) NOT NULL, -- Каноничное имя ("АПЕЛЬСИНЫ") или сорт ("Апельсины Испания")
    quantity NUMERIC(10, 4) NOT NULL,   -- Физический остаток данной партии (уменьшается при расходе)
    unit VARCHAR(10) NOT NULL,          -- кг, л, шт
    created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc', NOW())
);

-- Индекс для выгрузки актуального холодильника
CREATE INDEX idx_fridge_stock_group ON fridge_inventory(home_group_id);

2 Схема обработки запроса и интеграционный gRPC-контракт

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

На диаграмме представлена внутренняя логика текстового добавления продукта. Бэкенд-шлюз FastAPI выполняет синхронный gRPC-вызов к внутреннему NLP-микросервису цензуры для валидации на ненормативную лексику и сопоставления с глобальным каталогом, после чего производит атомарную операцию вставки в инвентарь PostgreSQL.

sequenceDiagram
    autonumber
    actor User as Пользователь (Экран Ручного ввода)
    participant APP as Мобильное приложение (Flutter)
    participant API as Бэкенд-шлюз (FastAPI)
    participant CP as Сервис Цензуры (gRPC CensorshipService)
    participant DB as Реляционная СУБД (PostgreSQL)
    participant KAFKA as Брокер событий (Kafka)

    User->>APP: Вводит в строку текст, нажимает "Добавить в холодильник"
    APP->>API: HTTP POST /api/v1/fridge/items/manual-text (JSON с raw_text_input)
    activate API
    
    note over API: Шаг 3: Декодирование JWT (Выделение home_group_id и user_id)
    note over API: Шаг 4: Первичная Pydantic-валидация полноты данных (quantity > 0)
    
    API->>CP: gRPC ValidateProductText(raw_text_input="Молоко Домик в деревне 3.2%")
    activate CP
    note over CP: Шаг 6: NLP-сканирование на мат и обсценную лексику
    note over CP: Шаг 7: Лемматизация и сопоставление с Master Data SKU
    CP-->>API: gRPC ValidateProductTextResponse(is_clean=true, matched_category="Молоко")
    deactivate CP
    
    alt is_clean == false (Вилка: Мат обнаружен)
        API-->>APP: HTTP 400 Bad Request (JSON с кодом ERR-PROFANITY-DETECTED)
    else is_clean == true (Успешный контур)
        Note over API, DB: Старт атомарной SQL-транзакции
        API->>DB: INSERT INTO purchase_receipts_history...
        API->>DB: INSERT INTO fridge_inventory...
        activate DB
        DB-->>API: Успешное выполнение INSERT (Запись добавлена/обновлена)
        DB-->>API: Успешное выполнение INSERT (Запись добавлена/обновлена)
        deactivate DB
        Note over API, DB: Коммит атомарной транзакции (COMMIT)
        
        API->>KAFKA: send_to_kafka_async("bpds.fridge.events.manual_add")
        API-->>APP: HTTP 201 Created (Продукт успешно добавлен на склад)
        deactivate API
        note over APP: Поля ввода очищаются, в списке холодильника появляется +1.5 л Молока
    end

2.2 Расшифровка шагов пайплайна

  • Шаг 1 (User -> APP): Пользователь на экране ручного импорта ресурсов вбивает текстовое наименование товара, указывает дробный объем 1.5, единицу измерения например литр "л"и цену в одной строке. После завершения ввода он нажимает кнопку «Добавить в холодильник».

  • Шаг 2 (APP -> API): Мобильное приложение упаковывает структуру данных в JSON-тело и отправляет запрос HTTP POST /api/v1/fridge/items/manual-text. В заголовках передается обязательный Access Token сессии и сквозной идентификатор трассировки X-Request-ID.

  • Шаг 3 (API -> API): Бэкенд-шлюз FastAPI принимает входящий пакет, декодирует JWT-токен и извлекает параметры home_group_id и user_id. Метод универсален и работает как для контура HOME, так и для контура OFFICE.

  • Шаг 4 (API -> API): Валидация полноты минимальных данных: Модель Pydantic проверяет тело запроса на наличие обязательных полей. Если строка raw_text_input передана пустой длина текста превышает 120 символов или количество товара меньше 0.0001, бэкенд мгновенно прерывает выполнение операции.

  • Шаг 5 (API -> CP): Бэкенд-шлюз инициирует внутренний синхронный gRPC-вызов к ИИ-микросервису цензуры и сопоставления: gRPC ValidateProductText(raw_text_input=...). Передача данных происходит в бинарном формате по протоколу HTTP/2, что гарантирует минимальные задержки (менее 5 мс).

  • Шаг 6 (CP -> CP): Микросервис CensorshipService запускает NLP-модель классификации текста (на базе трансформера BERT), проверяя строку на наличие нецензурной брани, мата, оскорблений или спама.

  • Шаг 7 (CP -> CP): В случае успешного прохождения цензуры, NLP-модель выполняет лемматизацию и нормализацию строки для сопоставления текста с глобальной Master Data базой (например, преобразует "Молоко Домик в деревне 3.2%" в каноническую категорию "Молоко").

  • Шаг 8 (CP -> API): Микросервис возвращает бэкенд-шлюзу gRPC-ответ ValidateProductTextResponse, содержащий флаг проверки is_clean и очищенное каноническое имя продукта matched_category.

  • Шаг 9 (API -> DB): Шлюз открывает атомарную SQL-транзакцию в PostgreSQL и выполняет команду вставки новой записи:

    INSERT INTO purchase_receipt_history (receipt_draft_id, user_id, home_group_id, activity, timestamp)
    VALUES (\$1, \$2, \$3, \$4, \$5)
  • Шаг 10 (DB -> API): База данных успешно фиксирует запись и возвращает подтверждение обновления строки. Транзакция закрывается командой COMMIT.

  • Шаг 11 (API -> DB): Шлюз открывает атомарную SQL-транзакцию в PostgreSQL и выполняет команду вставки новой записи:

    INSERT INTO fridge_inventory (user_id, home_group_id, product_name, quantity, unit)
    VALUES (\$1, \$2, \$3, \$4, \$5)
  • Шаг 12 (DB -> API): База данных успешно фиксирует запись и возвращает подтверждение обновления строки. Транзакция закрывается командой COMMIT.

  • Шаг 13 (API -> KAFKA): Шлюз асинхронно публикует событие в топик Kafka bpds.fridge.events.manual_add, извещая смежные BI-системы и рекомендательный контур аффинити (Affinity) о добавлении продукта в цифровой холодильник для корректировки весов предпочтений.

  • Шаг 14 (API -> APP): Сервер возвращает мобильному приложению статус успешного завершения операции HTTP 201 Created. В мобильном приложении поля ввода очищаются, экран обновляется, и в списке продуктов холодильника отображается новая строка.

2.3 Спецификация интеграционного gRPC-контракта Цензуры (Внутренний Хелпер)

Описание структуры Protocol Buffers (censorship_integration.proto) для внутреннего синхронного взаимодействия шлюза с переиспользуемым NLP-микросервисом.

syntax = "proto3";

package masterdata.censorship.v1;

service CensorshipService {
  // Синхронная валидация ручного текстового ввода пользователя
  rpc ValidateProductText (ValidateProductTextRequest) returns (ValidateProductTextResponse);
}

message ValidateProductTextRequest {
  string raw_text_input = 1;
}

message ValidateProductTextResponse {
  bool is_clean = 1;           // Флаг отсутствия мата и обсценной лексики
  string matched_category = 2; // Каноническое имя SKU для вставки в БД
}

3 Спецификация ответов сервера, вилок исключений и таймаутов

3.1 Спецификация успешных ответов сервера (Success Response)

3.1.1 HTTP 201 Created (Ответ на Шаге 12)

Возвращается мобильному приложению после успешного прохождения NLP-цензуры и фиксации Insert операции в реляционной базе данных инвентаря.

  • Заголовки ответа (Response Headers):
    • Content-Type: application/json
  • Тело ответа (Response Body):
{
  "status": "ITEM_MANUALLY_ADDED",
  "data": {
    "home_group_id": "group-772-alpha",
    "product_name": "Молоко",
    "added_quantity": 1.5,
    "unit": "л",
    "timestamp": "2026-07-02T02:02:00Z"
  }
}

3.2 Спецификация ошибок и вилок исключений (Error Responses)

Все ошибки бэкенда возвращаются в едином стандартизированном формате RFC 7807 (Problem Details) или стандартном JSON-формате FastAPI HTTPException. Это позволяет Flutter-клиенту (Dio) однозначно парсить код ошибки и выводить корректный текст в модальных окнах.

3.2.1 Ошибка: Обнаружена обсценная лексика / Мат (HTTP 400 Bad Request — Шаг 8)

Вызывается если внутренний NLP-сервис классифицировал входящую строку как нецензурную брань (is_clean=false). Транзакция в СУБД не открывается, а Мобильное приложение выводит предупреждение (модалка).

{
  "error_code": "ERR-PROFANITY-DETECTED",
  "message": "Ввод отклонен: текст содержит нецензурную брань, оскорбления или недопустимый спам-контент.",
  "details": {
    "raw_input_length": 27,
    "action": "Block input execution and prompt user to correct text naming clear of profanity."
  }
}

3.2.2 Ошибка валидации структуры и полноты данных (HTTP 422 Unprocessable Entity — Шаг 4)

Вызывается бэкенд-шлюзом FastAPI, если в поле количества передан нулевой или отрицательный объем, нарушающий физический смысл учета. Мобильное приложение выводит предупреждение (модалка)

{
  "error_code": "ERR-VALIDATION-FAILED",
  "message": "Переданный JSON-пакет содержит некорректный объем или неверный тип единицы измерения.",
  "details": [
    {
      "loc": ["body", "quantity"],
      "msg": "Физический объем ручного зачисления должен быть строго больше 0.0001",
      "type": "value_error.number.not_gt"
    }
  ]
}

3.2.3 Ошибка авторизации (HTTP 401 Unauthorized — Триггер Interceptor)

Вызывается если Access Token просрочен, подпись невалидна или токен отсутствует в заголовках.

{
  "error_code": "ERR-UNAUTHORIZED",
  "message": "Срок действия сессии истек. Требуется обновление токена.",
  "details": {
    "action": "Trigger queued token refresh mechanism using refresh_token."
  }
}

3.2.4 Ошибка недоступности СУБД/Сервисов (HTTP 503 Service Unavailable)

Вызывается если PostgreSQL находится под высокой нагрузкой или пул соединений переполнен.

{
  "error_code": "ERR-DATABASE-OFFLINE",
  "message": "Сервис временно недоступен. Пожалуйста, повторите попытку позже.",
  "details": {
    "reason": "PostgreSQL connection pool timeout or database node partition."
  }
}

3.3 Политика распределенных таймаутов и отказоустойчивости (Fail-Safe & Deadlines)

Для исключения каскадных сбоев, забивания пула соединений Uvicorn/FastAPI и зависания пользовательского интерфейса, на стыке шлюза и ИИ-микросервиса принудительно настраиваются следующие лимиты:

3.3.1 gRPC Deadline (Таймаут соединения)

  • Значение: timeout = 0.5s (500 миллисекунд).
  • Обоснование: Передача бинарных фреймов по HTTP/2 внутри локального кластера занимает до 5 мс, а инференс легковесной NLP-модели BERT — до 40 мс. Ожидание ответа дольше 500 мс интерпретируется как деградация ИИ-контура.

3.3.2 Аварийная стратегия Fail-Safe (Режим ограниченного доверия)

Если CensorshipService вызывает статус StatusCode.UNAVAILABLE или падает по таймауту 500 мс, FastAPI перехватывает ошибку сети и переключает внутреннюю последовательность на резервный сценарий: 1. Операция не блокируется для конечного человека, чтобы сохранить работоспособность приложения (Offline-First опыт). 2. В системный лог пишется критическое предупреждение [Moderation Warning] для Grafana/ELK. 3. Бэкенд берет исходную сырую строку raw_text_input и выполняет аварийный INSERT в fridge_inventory напрямую без канонизации. Вместо системной категории записывается исходный текст пользователя, а количество выставляется из Pydantic-модели. 4. Пользователю гарантированно возвращается успешный ответ HTTP 201 Created.