Метод POST /api/v1/fridge/scan-tag

Документация API: Синхронная авторизация смарт-метки контейнера, привязка кулинарного соединения и инициализация таймера деградации ресурса

Published

July 3, 2026

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

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

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

  1. Распознавание и валидация смарт-метки: Шлюз верхнего уровня проверяет сигнатуру QR-кода на принадлежность кодировки к домену foodapp://container/.
  2. Атомарное связывание ресурсов (Context Coupling): Метод привязывает уникальный физический ID контейнера к конкретному готовому блюду из справочника рецептов (recipe_id) или сырому продукту, определяя точный объем (quantity).
  3. Запуск таймера порчи (Shelf-life tracking): На основе биохимических свойств мастер-карточки продукта (из Периодической таблицы), система выставляет точное время «радиоактивного распада» (порчи) еды с учетом условий хранения (охлаждение или глубокая заморозка).

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

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

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

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

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

Поле Тип Обязательный Описание Пример значения
tag_raw_url String Да Сырая ссылка, считанная сканером с QR-метки на контейнере "foodapp://container/c-772-alpha"
recipe_id Integer Нет ID привязываемого рецепта (если контейнер заполняется готовым блюдом) 1042
quantity Float Да Физический объем или вес закладываемой в контейнер еды 0.5000
storage_mode String Да Режим хранения в холодильнике (CHILLED — полка, FROZEN — морозилка) "CHILLED"

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

{
  "tag_raw_url": "foodapp://container/c-772-alpha",
  "recipe_id": 1042,
  "quantity": 0.5,
  "storage_mode": "CHILLED"
}

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

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

    User->>APP: Сканирует QR-код на контейнере с только что сваренным супом
    APP->>API: HTTP POST /api/v1/fridge/scan-tag (JSON Payload)
    activate API
    
    note over API: Шаг 3: Декодирование JWT, извлечение home_group_id
    note over API: Шаг 4: Проверка валидности префикса URL смарт-метки
    
    API->>DB: Запрос биохимического срока годности из мастер-данных рецепта/продукта
    activate DB
    DB-->>API: Возврат базового срока годности (например, 72 часа при CHILLED)
    deactivate DB
    
    Note over API, DB: Старт атомарной SQL-транзакции (BEGIN)
    API->>DB: INSERT INTO fridge_containers (tag_id, home_group_id, recipe_id, quantity, expires_at) VALUES (...) ON CONFLICT (tag_id) DO UPDATE
    activate DB
    DB-->>API: Успешный Upsert контейнера, запуск таймера хранения
    deactivate DB
    Note over API, DB: Коммит атомарной транзакции (COMMIT)
    
    API->>KAFKA: Отправка события "fridge.container.bound" для пуш-нотификаций о порче
    API-->>APP: HTTP 201 Created (Метка успешно привязана, контейнер в сети)
    deactivate API
    note over APP: Экран обновляется, контейнер №772 горит зеленым цветом, запущен таймер свежести

4 Расшифровка шагов работы метода

  • Шаг 1 (User -> APP): Пользователь переливает приготовленное блюдо (например, Борщ) в физический контейнер, на который наклеен многоразовый QR-стикер. В модуле Fridge мобильного приложения он нажимает кнопку «Привязать метку», вводит вес блюда 0.5 кг, выбирает режим хранения CHILLED и сканирует QR-код камерой.

  • Шаг 2 (APP -> API): Приложение Flutter упаковывает параметры в JSON-пакет и отправляет запрос HTTP POST /api/v1/fridge/scan-tag. В заголовках передается X-Request-ID и токен авторизации сессии.

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

  • Шаг 4 (API -> API): Проверка префикса сигнатуры: Метод проверяет структуру строки tag_raw_url. Строка должна строго начинаться с внутреннего системного префикса foodapp://container/. Если префикс нарушен, запрос немедленно отклоняется.

  • Шаг 5 (API -> DB): Шлюз делает точечный запрос в PostgreSQL к таблице мастер-данных рецептов (master_recipes) или продуктов (food_elements), чтобы извлечь базовые физико-химические константы порчи для Борща.

  • Шаг 6 (DB -> API): База данных возвращает временной лимит деградации белковых соединений блюда. Для Борща при температуре +4°C (режим CHILLED) этот лимит составляет 72 часа. При режиме FROZEN (-18°C) система автоматически применила бы повышающий коэффициент, увеличив срок до 30 дней.

  • Шаг 7 (API -> API): Сервер берет текущую метку времени (current_timestamp) и математически рассчитывает абсолютную точку порчи: expires_at = current_timestamp + 72 hours.

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

    INSERT INTO fridge_containers (tag_id, home_group_id, recipe_id, initial_quantity, current_quantity, storage_mode, expires_at, status)
    VALUES (\$1, \$2, \$3, \$4, \$4, \$5, \$6, 'ACTIVE')
    ON CONFLICT (tag_id) 
    DO UPDATE SET 
        recipe_id = EXCLUDED.recipe_id,
        current_quantity = EXCLUDED.current_quantity,
        storage_mode = EXCLUDED.storage_mode,
        expires_at = EXCLUDED.expires_at,
        status = 'ACTIVE',
        updated_at = NOW();
  • Шаг 9 (DB -> API): База успешно перезаписывает состояние многоразовой метки и возвращает подтверждение транзакции. Выполняется команда COMMIT.

  • Шаг 10 (API -> KAFKA): Шлюз асинхронно публикует событие в топик Kafka bupar.fridge.events.container_bound. Данное сообщение перехватывается микросервисом уведомлений (notification-service) для планирования пуш-нотификации на смартфоны членов группы за 12 часов до истечения срока годности Борща.

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

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

5.1 Успешный ответ (Success Response)

5.1.1 HTTP 201 Created (Ответ на Шаге 11)

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

  • Заголовки ответа (Response Headers):
    • Content-Type: application/json
  • Тело ответа (Response Body):
{
  "status": "CONTAINER_TAG_BOUND",
  "data": {
    "tag_id": "c-772-alpha",
    "home_group_id": "group-993-beta",
    "content": {
      "recipe_id": 1042,
      "name": "Борщ Домашний",
      "quantity_kg": 0.5
    },
    "storage": {
      "mode": "CHILLED",
      "hours_remaining": 72.0,
      "expires_at": "2026-07-06T18:57:00Z"
    }
  }
}

5.2 Спецификация JSON-Схемы состояния смарт-контейнера (Шаг 11)

Внутренняя структура валидации объекта контейнера, используемая для отрисовки динамических карточек на экранах Flutter.

{
  "\$schema": "https://json-schema.org",
  "title": "SmartContainerState",
  "type": "object",
  "properties": {
    "tag_id": {"type": "string"},
    "recipe_id": {"type": ["integer", "null"]},
    "current_quantity": {"type": "number"},
    "storage_mode": {"type": "string", "enum": ["CHILLED", "FROZEN"]},
    "expires_at": {"type": "string", "format": "date-time"},
    "status": {"type": "string", "enum": ["ACTIVE", "EMPTY", "SPOILED"]}
  },
  "required": ["tag_id", "current_quantity", "storage_mode", "expires_at", "status"]
}

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

5.3.1 Ошибка: Сканирование чужого или невалидного QR-кода (HTTP 400 Bad Request — Шаг 4)

Выбрасывается на Шаге 4, если считанная ссылка ведет на сторонний домен и не распознается внутренней системой кухонной разметки.

{
  "error_code": "ERR-INVALID-CONTAINER-TAG",
  "message": "Ввод отклонен: отсканированный QR-код не является сертифицированной смарт-меткой экосистемы.",
  "details": {
    "scanned_payload": "https://wikipedia.org",
    "action": "Please scan only the dedicated reusable smart-stickers generated within your app profile."
  }
}

5.3.2 Ошибка: Попытка привязать контейнер к чужой группе (HTTP 403 Forbidden — Шаг 3)

Выбрасывается, если многоразовый контейнер в данный момент жестко закреплен за другой семьей/офисом (home_group_id в базе не совпадает с токеном), и не был предварительно очищен (списан через Consume или Waste).

{
  "error_code": "ERR-CONTAINER-OWNED-BY-OTHER-GROUP",
  "message": "Данный контейнер занят инвентарем другой группы. Изменение заблокировано.",
  "details": {
    "tag_id": "c-772-alpha",
    "action": "The current holding group must empty or de-register this smart tag before it can be shared."
  }
}

6 Расшифровка шагов работы метода

  • Шаг 1 (User -> APP): Пользователь берет с полки фабричный маркированный товар (например, пачку творога) и наводит камеру мобильного приложения на штрихкод EAN-13 или квадратный код маркировки DataMatrix. Модуль Pantry захватывает картинку и декодирует её в сырую строку данных.

  • Шаг 2 (APP -> API): Приложение Flutter отправляет запрос методом HTTP POST /api/v1/pantry/scan-barcode на бэкенд-шлюз. В теле запроса передается извлеченная символьная последовательность barcode_raw_string.

  • Шаг 3 (API -> API): Бэкенд-шлюз FastAPI декодирует JWT-токен, извлекая идентификаторы сессии группы. Проверяется валидность токена и права доступа пользователя на модификацию складских остатков.

  • Шаг 4 (API -> API): Выделение GTIN: Специализированный парсер анализирует строку. Если отсканирован код DataMatrix, система вычленяет из него глобальный номер товарной продукции GTIN (первые 14 цифр после префикса применения 01). Если это стандартный штрихкод, строка сразу преобразуется в чистый 13-значный EAN-код.

  • Шаг 5 (API -> DB): Шлюз выполняет высокоскоростной индексный запрос в локальную базу данных PostgreSQL, проверяя таблицу product_barcodes. Если данный GTIN уже сканировался кем-то в системе ранее, база мгновенно возвращает кэшированную связь с элементом Периодической таблицы (element_id), минуя внешние шлюзы (переход к Шагу 11).

  • Шаг 6 (API -> REG): В случае отсутствия записи в локальном кэше (Cache Miss), шлюз инициирует синхронный защищенный запрос к API национальной системы маркировки («Честный Знак», ГС1 РУС) для получения официального реестрового паспорта товара.

  • Шаг 7 (REG -> API): Сервер прослеживаемости возвращает верифицированную карточку SKU, содержащую бренд, каноническое наименование товара и точный вес нетто (например, 200 г).

  • Шаг 8 (API -> DB): Шлюз открывает атомарную SQL-транзакцию в PostgreSQL. Запускается внутренний триграммный поиск для автоматического сопоставления текстового имени из реестра (например, "Творог рассыпчатый 5% 200г") с базовым элементом таблицы продуктов — изотопом творога Cc (Лакто-элементы).

  • Шаг 9 (API -> DB): Выполняется команда записи нового маппинга для предотвращения повторных внешних сетевых запросов:

    INSERT INTO product_barcodes (gtin, element_id, manufacturer_sku_name, standard_weight_g)
    VALUES (\$1, \$2, \$3, \$4)
    ON CONFLICT (gtin) DO NOTHING;
  • Шаг 10 (DB -> API): База данных подтверждает запись связей. Транзакция закрывается командой COMMIT.

  • Шаг 11 (API -> APP): Сервер возвращает статус HTTP 200 OK (или 201 Created при первой регистрации товара) и полную физико-химическую карточку изотопа. Во Flutter-приложении поля веса заполняются автоматически на основе данных производителя (200 г), а продукт привязывается к своей ячейке в Периодической системе.

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

7.1 Успешный ответ (Success Response)

7.1.1 HTTP 200 OK (Ответ на Шаге 11 — Успешное распознавание)

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

  • Заголовки ответа (Response Headers):
    • Content-Type: application/json
  • Тело ответа (Response Body):
{
  "status": "BARCODE_IDENTIFIED",
  "gtin": "04601234567890",
  "product_details": {
    "brand": "Простоквашино",
    "commercial_name": "Творог рассыпчатый 5%",
    "packaged_weight_g": 200.0
  },
  "mapped_periodic_element": {
    "symbol": "Cc",
    "element_name": "Творог",
    "group_name": "Лакто-Элементы",
    "macros_per_100g": {
      "calories": 121.0,
      "proteins": 16.5,
      "fats": 5.0,
      "carbs": 3.0
    }
  }
}

7.2 Спецификация JSON-Схемы паспорта товара из Реестра Маркировки (Шаг 7)

Структура данных, возвращаемая государственным/международным API-шлюзом прослеживаемости товаров.

{
  "\$schema": "https://json-schema.org",
  "title": "NationalRegistrySkuPassport",
  "type": "object",
  "properties": {
    "gtin": {"type": "string"},
    "product_name_ru": {"type": "string"},
    "brand_name": {"type": "string"},
    "net_mass_grams": {"type": "number"},
    "inner_contents_category": {"type": "string"}
  },
  "required": ["gtin", "product_name_ru", "net_mass_grams"]
}

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

7.3.1 Ошибка: Сканируемый штрихкод поврежден или имеет неверный формат (HTTP 400 Bad Request — Шаг 4)

Выбрасывается на Шаге 4, если считанная лазером/камерой строка не проходит базовую валидацию контрольной суммы EAN-13 или содержит некорректные спецсимволы GS1.

{
  "error_code": "ERR-INVALID-BARCODE-STRUCTURE",
  "message": "Ввод отклонен: отсканированная последовательность знаков повреждена или не является валидным кодом EAN/DataMatrix.",
  "details": {
    "raw_length": 8,
    "action": "Ensure the barcode on the package is clean, well-lit, and scan the code completely."
  }
}

7.3.2 Ошибка: Товар отсутствует во всех национальных базах данных (HTTP 404 Not Found — Шаг 6)

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

{
  "error_code": "ERR-SKU-NOT-FOUND-IN-REGISTRY",
  "message": "Штрихкод отсутствует в глобальных справочниках маркировки. Перенаправление на ручной ввод.",
  "details": {
    "unknown_gtin": "04601234567890",
    "action": "Redirecting user to manual text entry screen to manually map this new product to a Periodic Element."
  }
}