[DRAFT] POST /api/v1/purchase/draft-reconcile

Метод ручного редактирования черновика

WarningОграничение публичной документации

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

  • Полная спецификация метода: Будет доступна только во внутреннем контуре разработки (Confluence / Swagger Enterprise).

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

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

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

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

Метод решает следующие критические задачи:

  1. Асинхронный аудит (NLP Check): бэкенд-шлюз перенаправляет сырую текстовую строку во внутренний микросервис Censorship Service для проверки на обсценную лексику (мат).
  2. Контроль полноты обязательных данных: Валидационная модель проверяет наличие текстовой строки длиной не более 120 символов (примерно два или три продукта)
  3. Прямое добавление (Direct Update): В случае успешного прохождения ИИ-цензуры и валидации структуры, продукт добавляется в (холодильник) таблицу fridge_inventory конкретной группы, не сохраняясь в буферные таблицы чеков и модерации.

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

  • Метод: POST
  • Маршрут: /api/v1/fridge/items/upload-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.2.3 Диаграмма последовательности (Sequence Diagram)

sequenceDiagram
    autonumber
    actor User as Мобильный клиент (App)
    participant GW as FastAPI Gateway (backend-api)
    participant K as Apache Kafka Cluster
    participant Censor as censorship-control-worker (SECURITY)
    participant Ollama as Ollama Container (AID)
    participant Redis as In-Memory Redis (SECURITY)
    participant Promtail as Collector: Promtail (TELEMETRY)

    %% СЦЕНАРИЙ А: ПЕРВИЧНЫЙ ВВОД С КЛИЕНТА
    alt Валидация входных данных на шлюзе
        User->>GW: POST /api/v1/purchase/upload-text (raw_text_input, app_lang)
        Note over GW: Шаг 1: Проверка длины <= 120 симв.<br/>Генерация X-Request-ID (trace_id)
    else Ошибка: Превышена длина (длина > 120 симв.)
        GW-->>User: HTTP 422 Unprocessable Entity
        Note over User: Всплывающее окно (Toast / Push):<br/>"Упс! Что-то пошло не так.<br/>Пожалуйста, повторите попытку позже."
        %% НОВЫЙ ШАГ ЛОГИРОВАНИЯ БИЗНЕС-МЕТРИКИ
        Note over GW: Шаг 1.1: Запись бизнес-метрики сбоя в stdout лог
        GW->>Promtail: Отправка структурированного лога (stdout stream)
        Note over Promtail: JSON Payload:<br/>{<br/>&nbsp;&nbsp;"trace_id": "X-Request-ID-12345",<br/>&nbsp;&nbsp;"domain": "INVENTORY",<br/>&nbsp;&nbsp;"error_code": "IAD-VAL-422",<br/>&nbsp;&nbsp;"error_type": "BUSINESS_VALIDATION_ERROR",<br/>&nbsp;&nbsp;"user_id": "usr_9876",<br/>&nbsp;&nbsp;"message": "Length validation failed",<br/>&nbsp;&nbsp;"context": {"max_len": 120, "actual_len": 145}<br/>}

    end
    
    critical Пуш в брокер сообщений
        GW->>K: Пуш в топик: bpds.inventory.in.receipt.upload
        GW-->>User: HTTP 202 Accepted (Запрос принят в обработку)
    option Сбой Kafka / Таймаут записи
        GW-->>User: HTTP 503 Service Unavailable (Fallback на повтор)
        Note over User: Всплывающее окно (Toast / Push):<br/>"Упс! Что-то пошло не так.<br/>Пожалуйста, повторите попытку позже."
    end

    %% АСИНХРОННАЯ ВЫЧИТКА И ОЦЕНКА ЦЕНЗОРА
    K->>Censor: Handler: ProcessUploadText() (Вычитка из receipt.upload)
    
    critical Анализ текста через нейросеть
        Censor->>Ollama: gRPC: EvalTextConfidence(text, lang)
        Ollama-->>Censor: Ответ: is_profane (bool), confidence_score (float)
    option Ошибка Ollama (Таймаут / Падение контейнера)
        Note over Censor: Fallback: Аварийное перенаправление<br/>на ручной разбор в MDM
        Censor->>K: Пуш в топик: bpds.mdm.in.product.process
    end

    critical Проверка лимитов нарушений в кэше
        Censor->>Redis: Вызов Lua-скрипта (Проверка словаря по app_lang + INCR счетчик)
        Redis-->>Censor: Текущий count_violation для user_id
    option Ошибка Redis (Connection Refused)
        Note over Censor: Режим отказоустойчивости:<br/>Игнорируем счетчик нарушений
    end

    %% АЛГОРИТМИЧЕСКОЕ ВЕТВЛЕНИЕ РЕЗУЛЬТАТОВ
    alt Исход 5а: Текст чист + ИИ уверен (Confidence >= 0.85)
        Censor->>K: Пуш в топик: bpds.inventory.out.receipt.parsed
    else Исход 5б: Текст чист + ИИ НЕ уверен (Confidence < 0.85)
        Censor->>K: Пуш в топик: bpds.mdm.in.product.process
        K-->>User: Трансляция draft_data через WebSocket на «Вкладку модерации»
    else Исход 5в / 8б: Обнаружен мат/спам (is_profane == true)
        Note over Censor: Развилка Abuse_Check:<br/>Оценка count_violation из Redis
        Censor->>K: Пуш в топик: bpds.aid.out.profanity.violate (Payload: violation_count)
    end

    %% СЦЕНАРИЙ Б: РАБОТА С ЧЕРНОВИКОМ
    alt Валидация измененного контракта
        User->>GW: POST /api/v1/purchase/draft-reconcile (Отредактированный руками текст)
        Note over GW: Шаг 7: Валидация Pydantic контракта черновика
    else Ошибка: Невалидный JSON / Сломан Pydantic контракт
        GW-->>User: HTTP 400 Bad Request
    end
    
    GW->>K: Повторный пуш в топик: bpds.inventory.in.receipt.upload (Source: USER_EDIT)
    K->>Censor: Повторный цикл цензуры
    Note over Censor: Исход 8а: Отредактировано чисто
    Censor->>K: Пуш в топик: bpds.inventory.out.receipt.parsed

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

Шаг Действие Параметры / Запросы / DTO Ошибки (Исключения / Статусы)
Шаг 1 (Gateway -> T_Results) API-шлюз проверяет длину входящей строки (len(text) <= 120), обогащает метаданными локали приложения, пробрасывает трассировочный ID и публикует событие в брокер. Kafka Message (Topic: bpds.inventory.in.receipt.upload):
Payload: { "raw_text_input": "Строка пользователя...", "app_lang": "ru-RU", "x_request_id": "req-censor-999-uuid" }
FastAPI.ValidationError (строка > 120 символов)
KafkaException: MessageTooLarge
KafkaException: QueueFullException
Шаг 2 (T_Results -> Censor) Фоновый воркер цензуры, подписанный на топик загрузки, вычитывает новую задачу из очереди для проведения лингвистического анализа. Kafka Consumer Poll Request:
Group_ID: "censorship-workers", Получен payload шага 1.
Offset: 452091
KafkaException: CommitFailedException (воркер завис, сработал max.poll.interval.ms)
SerializationException (битый json в пайлоаде)
Шаг 3 (Censor -> Ollama) Воркер отправляет сырой текст в локальный ИИ-контейнер для семантического анализа и определения вероятности наличия скрытого спама, мата или оскорблений. HTTP POST /api/generate (Ollama API):
{ "model": "llama3", "prompt": "Analyze the following text for profanity, hate speech, or spam. Return exact JSON: { 'is_clean': boolean, 'confidence': float }. Text: 'Строка пользователя...'", "stream": false }
ConnectTimeout: Ollama unreachable
HTTP 500 Internal Server Error (сбой аллокации VRAM внутри контейнера Ollama)
ReadTimeout: Inference duration exceeded
Шаг 4 (Censor -> Redis) Параллельно с ИИ воркер проверяет текст по жестким локальным словарям стоп-слов, загруженным под конкретный язык, и выполняет условный инкремент счетчика для пользователя. Redis Command Pipeline:
1. SISMEMBER "dict:profanity:ru-RU" "входящее_слово"
2. MULTI
3. INCRBY "abuse:counter:user_123" 1
4. EXPIRE "abuse:counter:user_123" 86400
5. EXEC
Redis.ConnectionError: Connection refused
Redis.TimeoutError: Command timed out
Redis.ClusterDownException (сбой репликации или сегментации нод Redis памяти)
Шаг 5а (Censor -> T_Clean_Text) Ветка «Успех»: Если стоп-слова не найдены, а ИИ выдал флаг чистоты с высокой степенью уверенности, текст отправляется в топик очищенных данных для дальнейшего парсинга. Kafka Message (Topic: bpds.inventory.out.receipt.parsed):
Payload: { "clean_text_input": "Строка пользователя...", "app_lang": "ru-RU", "x_request_id": "req-censor-999-uuid" }
KafkaException: DeliveryTimeout
KafkaException: NotCoordinatorForException (временный ребаланс брокеров во время пуша)
Шаг 5б (Censor -> T_Draft) Ветка «Подозрение»: Если локальные словари чисты, но ИИ сомневается в контексте (confidence < 0.85), воркер маркирует запись как сомнительный черновик для ручной модерации. Kafka Message (Topic: bpds.mdm.in.product.process):
Payload: { "draft_data": { "raw_id": "uuid", "text": "Строка пользователя..." }, "app_lang": "ru-RU", "x_request_id": "req-censor-999-uuid" }
KafkaException: LeaderNotAvailableException
KafkaException: RecordTooLargeException
Шаг 5в (Censor -> Abuse_Check -> T_Notif) Первичное нарушение: Цензор обнаружил мат. Логический разветвитель проверяет счетчик в Redis. Если Счетчик == 1, генерируется событие первичного предупреждения с учетом языка. Kafka Message (Topic: bpds.aid.out.profanity.violate):
Payload: { "user_id": "user_123", "violation_count": 1, "app_lang": "ru-RU", "notification_type": "FIRST_WARNING_PUSH" }
KafkaException: BrokerNotAvailable
KafkaException: MessageTimedOut (сбой подтверждения доставки от реплик брокера)
Шаг 6 (T_Draft -> App) Мобильное приложение вычитывает из топика черновиков сомнительную запись и отображает её во вкладке модерации на родном языке пользователя для исправления. gRPC / HTTP Stream (вкладка модерации):
Payload: { "draft_id": "uuid-draft-555", "raw_text_input": "Сомнительный текст...", "app_lang": "ru-RU" }
DioException: connection error (клиент потерял сеть)
HTTP 401 Unauthorized (истекла сессия модератора в приложении)
Шаг 7 (App -> T_Results) Пользователь (или модератор) отправляет отредактированную, очищенную версию текста обратно в систему под тем же сквозным языковым контекстом. Kafka Message (Topic: bpds.inventory.in.receipt.upload):
Payload: { "raw_text_input": "Отредактированный чистый текст", "app_lang": "ru-RU", "x_request_id": "req-censor-999-uuid" }
KafkaException: QueueFullException
FastAPI.ValidationError (текст пустой или превысил лимит после редактирования)
Шаг 8а (Censor -> T_Clean_Text) Успешный исход модерации: Повторный цикл проверки цензором подтверждает, что отредактированный текст полностью чист. Запись уходит в финальный топик. Kafka Message (Topic: bpds.inventory.out.receipt.parsed):
Payload: { "clean_text_input": "Отредактированный чистый текст", "app_lang": "ru-RU", "x_request_id": "req-censor-999-uuid" }
KafkaException: DeliveryTimeout
KafkaException: ConcurrentModificationException
Шаг 8б (Abuse_Check -> T_Notif) Повторный мат при модерации: Если в отредактированном тексте снова найден мат, разветвитель фиксирует Счетчик >= 2. Пользователь помечается как ненадежный. Kafka Message (Topic: bpds.aid.out.profanity.violate):
Payload: { "user_id": "user_123", "violation_count": 2, "app_lang": "ru-RU", "notification_type": "BAN_OR_STRICT_ALERT" }
KafkaException: LeaderNotAvailableException
InvalidTopicException (топик заблокирован администратором кафки)

1.3.1 Диаграмма последовательности: (Этап 2 — парсинг текста и публикация сообщения потребителям)

sequenceDiagram
    autonumber
    
    participant K as Брокер: Apache Kafka Cluster
    participant Worker as purchase-ollama-worker (AID)
    participant Llama as Llama Container: NER Parsing (AID)
    participant Billing as billing-service (INVENTORY)
    participant Fridge as fridge-service (INVENTORY)
    participant Bupar as bupar-audit-service (PMA)

    %% ЭТАП 1: ИИ-РАСПОЗНАВАНИЕ И СИНТЕЗ JSON
    K->>Worker: Шаг 1: Вычитка: bpds.inventory.out.receipt.parsed (Текст + app_lang)
    activate Worker
    note over Worker: Шаг 2: Адаптация системного промпта<br/>под локализацию app_lang
    
    Worker->>Llama: Шаг 3: API: Запрос Structured Outputs (Pydantic-схема)
    activate Llama
    Llama-->>Worker: Шаг 4: Возврат структурированного JSON-массива продуктов
    deactivate Llama
    
    note over Worker: Шаг 5: Валидация паспорта расходов в памяти
    
    Worker->>K: Шаг 6: Пуш в топик: bpds.mdm.out.product.templated
    deactivate Worker

    %% ЭТАП 2: ВЕЕРНАЯ АСИНХРОННАЯ ДИСТРИБУЦИЯ (FAN-OUT)
    par Поток финансовой аналитики чека
        K->>Billing: Шаг 7: Handler: ProcessBillingTransaction()
        activate Billing
        note over Billing: Шаг 7.1: Расчет сумм и shop_id
        Billing->>Billing: Шаг 7.2: SQL: INSERT INTO billing_db
        deactivate Billing
    and Поток зачисления физической еды
        K->>Fridge: Шаг 8: Handler: MutateFridgeBalances()
        activate Fridge
        note over Fridge: Шаг 8.1: Поштучный разбор граммов/литров
        Fridge->>Fridge: Шаг 8.2: SQL: INSERT INTO fridge_db (FIFO Индекс)
        deactivate Fridge
    and Поток трекинга Process Mining
        K->>Bupar: Шаг 9: Handler: AppendAuditLog()
        activate Bupar
        note over Bupar: Шаг 9.1: Запись шага 'PRODUCT_BATCH_PURCHASED'<br/>с индексом case_id
        Bupar->>Bupar: SQL: INSERT INTO bupar_db
        deactivate Bupar
    end

Процесс ИИ-распознавания (NER) и веерной асинхронной дистрибуции данных чека

1.3.2 Часть 2: Асинхронное NER-распознавание и веерная дистрибуция (Fan-Out Pipeline)

Шаг Действие Параметры Ошибки (Исключения / Статусы)
Шаг 1 (KAFKA -> WORKER) ИИ-воркер purchase-ollama-worker вычитывает очищенный и проверенный текст чека из топика успешной цензуры. Топик: bpds.inventory.out.receipt.parsed
Context: text, app_lang
Kafka: RecordDeserializationException
Шаг 2 (WORKER -> WORKER) Внутреннее действие: Воркер динамически адаптирует системный промпт LLM под переданный язык приложения (app_lang). Настройка контекста локализации промпта TemplateRenderingError (ошибка синтаксиса промпта)
Шаг 3 (WORKER -> LLAMA) Воркер отправляет запрос в контейнер Llama для извлечения сущностей (Named Entity Recognition). Используется режим JSON Structured Outputs. API Запрос с Pydantic-схемой ожидаемого массива продуктов Ошибка LLM контейнера: Ollama: Timeout / OutOfMemory
Действие: Перенаправление чека в DLQ (Dead Letter Queue)
Шаг 4 (LLAMA -> WORKER) Нейросеть возвращает строго валидированный, структурированный JSON-массив с распознанными продуктами, объемами и ценами. Result: [{"name": "...", "weight_g": 500, "price": 120.0}, ...] ValidationError (если LLM нарушила Pydantic-контракт схемы)
Шаг 5 (WORKER -> WORKER) Внутреннее действие: Воркер производит финальную валидацию сформированного паспорта расходов в оперативной памяти. Проверка полей: validate_receipt_passport() InvalidPassportDataException
Шаг 6 (WORKER -> KAFKA) Воркер публикует валидный паспорт чека в топик веерного распределения для транзакционных сервисов. Топик: bpds.mdm.out.product.templated
Payload: Structured JSON Array
Kafka: PreNotAvailable (брокер не отвечает)
Шаг 7 (KAFKA -> BILLING) Параллельный поток 1 (Fan-Out): Сервис billing-service вычитывает паспорт чека для формирования финансовой аналитики. Хэндлер: ProcessBillingTransaction() Kafka: CommitFailedException
Шаг 7.1 (BILLING -> BILLING) Внутреннее действие Billing: Сервис рассчитывает финальные суммы, налоги и сопоставляет идентификатор магазина (shop_id). Расчет транзакции UnknownShopIdentifierException
Шаг 7.2 (BILLING -> SQL) Финансовая транзакция атомарно записывается в базу данных биллинга. SQL INSERT INTO billing_db (transaction_id, shop_id, total_amount) PostgreSQL: unique_violation (transaction_id)
Шаг 8 (KAFKA -> FRIDGE) Параллельный поток 2 (Fan-Out): Сервис fridge-service параллельно забирает то же сообщение для зачисления физической еды пользователю. Хэндлер: MutateFridgeBalances() Kafka: MessageDeserializationError
Шаг 8.1 (FRIDGE -> FRIDGE) Внутреннее действие Fridge: Сервис производит поштучный и пограммовый разбор позиций чека для инвентаризации холодильника. Парсинг объемов (граммы / литры) InvalidMeasurementUnitException
Шаг 8.2 (FRIDGE -> SQL) Продукты записываются в базу данных холодильника с автоматическим расчетом и присвоением FIFO-индекса для контроля порчи. SQL INSERT INTO fridge_db (product_id, quantity, fifo_index, expiry_date) PostgreSQL: lock_not_available (блокировка строки)
Шаг 9 (KAFKA -> BUPAR) Параллельный поток 3 (Fan-Out): Сервис аудита процессов bupar-audit-service вычитывает событие для Process Mining трекинга. Хэндлер: AppendAuditLog() Kafka: BrokerTransportError
Шаг 9.1 (BUPAR -> SQL) Сервис фиксирует прохождение этапа бизнес-процесса, записывая событие, привязанное к сквозному индексу цепочки. SQL INSERT INTO bupar_db (case_id, activity, timestamp)
Data: activity='PRODUCT_BATCH_PURCHASED'
PostgreSQL: foreign_key_violation (case_id)

1.4 Спецификация используемой СУБД-схемы склада (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.5 Спецификация используемой СУБД-схемы склада (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 NEW Схема обработки запроса и интеграционный gRPC-контракт

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

На диаграммах (Этап 1 и Этап 2) представлена асинхронная логика текстового добавления продуктов из чека. Бэкенд-шлюз FastAPI выполняет первичную валидацию длины строки, логирует бизнес-метрики сбоев через Promtail и мгновенно возвращает клиенту статус HTTP 202 Accepted, отправляя задачу в брокер сообщений Apache Kafka Cluster.

Вся тяжелая работа по оценке текста, обращению к LLM и дистрибуции данных изолирована в асинхронных воркерах: 1. censorship-control-worker производит проверку на спам и нецензурную лексику через локальную нейросеть Ollama, контролируя лимиты нарушений в кэше Redis по стратегии Fail-Close. 2. purchase-ollama-worker выполняет именованное распознавание сущностей (NER) через контейнер Llama в режиме Structured Outputs и запускает веерную рассылку (Fan-Out) готового паспорта чека в независимые СУБД биллинга, балансов холодильника (с FIFO-индексами) и логов процессного майнинга.

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

Описание структуры Protocol Buffers (censorship_control.proto) для внутреннего асинхронного взаимодействия воркера цензуры с локальным контейнером нейросети Ollama/Llama.

syntax = "proto3";

package security.censorship.v1;

service CensorshipControlService {
  // Асинхронная оценка текста чека на ненормативную лексику и спам
  rpc EvalTextConfidence (EvalTextConfidenceRequest) returns (EvalTextConfidenceResponse);
}

message EvalTextConfidenceRequest {
  string text = 1;      // Очищенный текстовый ввод из чека
  string lang = 2;      // Целевой язык локализации (app_lang) для адаптации промпта
}

message EvalTextConfidenceResponse {
  bool is_profane = 1;       // Флаг обнаружения мата, спама или запрещенного контента
  float confidence_score = 2; // Уровень уверенности нейросети в результате (0.00 - 1.00)
}

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

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

3.1.1 HTTP 202 Accepted (Ответ на Части А, Шаге 3)

Возвращается мобильному приложению шлюзом FastAPI Gateway мгновенно после успешной валидации длины строки и публикации сырого текста чека в топик брокера сообщений. Это означает, что запрос принят в асинхронный пайплайн обработки.

  • Заголовки ответа (Response Headers):
    • Content-Type: application/json
  • Тело ответа (Response Body):
{
  "status": "RECEIPT_UPLOAD_ACCEPTED",
  "data": {
    "trace_id": "req-err-dir-99aa",
    "message": "Текст чека успешно отправлен на цензурирование и NER-парсинг.",
    "submitted_at": "2026-07-02T02:02:00Z"
  }
}

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

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

3.2.1 Ошибка: Обнаружена обсценная лексика / Спам (Асинхронный Исход 5в / Шаг 9)

Формируется асинхронным воркером censorship-control-worker, если локальная нейросеть Ollama классифицировала текст чека как вредоносный (is_profane == true). Запрос терминируется, событие пушится в топик нарушений, а мобильное приложение получает уведомление.

{
  "error_code": "ERR-PROFANITY-DETECTED",
  "message": "Ввод отклонен: текст чека содержит нецензурную брань, оскорбления или недопустимый спам-контент.",
  "details": {
    "violation_count_current": 3,
    "action": "Block further parsing pipeline, route incident to security log, and notify user to fix input."
  }
}

3.2.2 Ошибка валидации длины текста чека (HTTP 422 Unprocessable Entity — Часть А, Шаг 1.1)

Вызывается бэкенд-шлюзом FastAPI на самом раннем этапе до отправки в брокер, если сырая строка текста чека превышает лимит в 120 символов, что блокирует перегрузку контекста LLM. Лог ошибки параллельно уходит в Promtail.

{
  "error_code": "IAD-VAL-422",
  "message": "Превышена максимальная допустимая длина текстового ввода для распознавания чека.",
  "details": [
    {
      "loc": ["body", "raw_text_input"],
      "msg": "Длина текста чека должна быть строго меньше или равна 120 символам.",
      "type": "value_error.string.max_length"
    }
  ]
}

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

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

{
  "error_code": "IAD_JWT_ACCESS_EXPIRED",
  "message": "Срок действия токена доступа истек или сессия невалидна. Требуется обновление.",
  "details": {
    "action": "Trigger transparent token refresh pipeline using refresh_token underneath Dio Interceptor."
  }
}

3.2.4 Ошибка недоступности брокера сообщений или подсистем безопасности (HTTP 503 Service Unavailable)

Вызывается шлюзом FastAPI в двух случаях: если кластер Apache Kafka недоступен при попытке записи (Часть А, Шаг 2), либо если произошел критический сбой связи с Redis в режиме Fail-Close во время проверки черного списка.

{
  "error_code": "ERR-INFRASTRUCTURE-OFFLINE",
  "message": "Серверная подсистема временно недоступна. Пожалуйста, повторите попытку позже.",
  "details": {
    "reason": "Kafka broker transport timeout or Redis Security perimeter failure (Fail-Close enforcement)."
  }
}

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

Для исключения каскадных сбоев, перегрузки памяти контейнеров ИИ, зависания топиков брокера Apache Kafka и предотвращения утечек данных, в рамках асинхронного пайплайна принудительно настраиваются следующие лимиты и режимы отказоустойчивости:

3.3.1 Взаимодействие на шлюзе и gRPC Deadlines (Таймауты инференса)

  • Таймаут публикации в Kafka (Шлюз): max.block.ms = 1000 (1 секунда). Если за секунду шлюз FastAPI не смог опубликовать событие в топик первичного приема чеков, контур считает шину перегруженной и мгновенно возвращает HTTP 503 Service Unavailable.
  • gRPC Deadline (Воркер -> Ollama): timeout = 4.0s (4000 миллисекунд). Извлечение структурированных сущностей (NER) большой языковой моделью Llama требует времени на генерацию токенов. Ожидание инференса дольше 4 секунд интерпретируется как критическое зависание ИИ-контейнера.

3.3.2 Аварийная стратегия Fail-Safe и отказоустойчивость воркеров

Если асинхронные подсистемы вызывают сбои, поведение системы жестко разделяется по зонам ответственности (Безопасность против Бизнес-логики):

3.3.2.1 1. Зона безопасности: Взаимодействие Censor -> Redis Blacklist

  • Применяется бескомпромиссная стратегия Fail-Close.
  • Если база данных Redis недоступна (ошибка связи или таймаут подключения ConnectionRefusedError), воркер цензуры не имеет права рисковать периметром.
  • Цепочка обработки чека аварийно прерывается на Шаге 6. Запрос полностью аннулируется, а в лог Promtail отправляется критический алерт безопасности CRITICAL: Security subsystem failure.

3.3.2.2 2. Зона ИИ-аналитики: Взаимодействие Censor/Worker -> Ollama (Llama)

  • Применяется стратегия Fail-Safe с переводом на ручной разбор (MDM Fallback).
  • Если контейнер Ollama на Шаге 5 падает по таймауту 4 сек или выбрасывает сетевую ошибку, обработка чека не прерывается окончательно, чтобы не потерять данные пользователя.
  • Система перехватывает исключение и осуществляет аварийный сброс задачи в резервный топик модерации: bpds.mdm.in.product.process.
  • Сырой текст чека транслируется через WebSocket на «Вкладку модерации» мобильного приложения, позволяя пользователю или оператору провести ручной разбор и канонизацию позиций без потери транзакции.