Метод POST /api/v1/integration/shop/webhook

Документация B2B API: Автоматическое зачисление продуктов через фискальный вебхук торговой сети Магазин

Published

July 1, 2026

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

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

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

  1. Синхронный Direct-Inflow (Прямой импорт): В отличие от пользовательского контура (где фото чека уходит на OCR и ручной аудит), “Вендор” передает серверу строго типизированный, очищенный JSON-датасет. Это позволяет системе зачислять продукты в холодильник мгновенно, минуя буферные таблицы pending_receipts.
  2. Сквозная B2B-авторизация: Метод защищен протоколом OAuth2 Client Credentials или выделенным статическим API-ключом, так как запрос инициируется сервером торговой сети, а не мобильным приложением.
  3. Связывание аккаунтов (User Mapping): Идентификация целевого холодильника (home_group_id) происходит по переданному в теле запроса loyalty_card_token (токен карты лояльности Магазин, которую пользователь привязал в своем профиле Flutter).

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

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

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

Заголовок Обязательный Описание Пример значения
Content-Type Да Указывает на передачу строго типизированного JSON-пакета application/json
X-Shop-Token Да Секретный статический ключ авторизации партнера (B2B API Key) shop_live_secret_abc123...
X-Request-ID Да Сквозной ID запроса, генерируемый шиной Magnum для трассировки shop-tr-44bb-99ff

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

Структура данных полностью повторяет фискальный срез чека Magnum. Поля количества товара (quantity) поддерживают тип float/double для корректного зачисления весовых позиций (например, бананов весом 1.695 кг).

Поле Тип Обязательный Описание Пример значения
loyalty_card_token String Да Уникальный токен карты, по которому бэкенд находит user_id и home_group_id shop-card-8822-fa
receipt_number String Да Фискальный номер чека для предотвращения дублирования CH-20260701-09
items Array Да Список приобретенных товарных позиций [..._name: "БАНАН", qty: 1.695]

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

{
  "loyalty_card_token": "shop-card-8822-fa",
  "receipt_number": "CH-20260701-09",
  "purchase_timestamp": "2026-07-01T23:10:00Z",
  "store_id": "shop-almaty-05",
  "items": [
    {
      "product_id": "2110310",
      "product_name": "БАНАН ЭКВАДОР КГ",
      "quantity": 1.6950,
      "price": 595.00,
      "unit": "кг"
    },
    {
      "product_id": "4001020",
      "product_name": "ХЛЕБ АКСАЙ БОРОДИНСКИЙ",
      "quantity": 1.0000,
      "price": 180.00,
      "unit": "шт"
    }
  ]
}

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

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

sequenceDiagram
    autonumber
    participant MAG as Шина данных (Magnum)
    participant API as Бэкенд-шлюз (FastAPI)
    participant DB as Реляционная СУБД (PostgreSQL)
    participant KAFKA as Брокер событий (Kafka)
    participant APP as Мобильное приложение (Flutter)

    MAG->>API: HTTP POST /integration/magnum/webhook (JSON чека + API Key)
    activate API
    
    note over API: Шаг 2: Валидация X-Magnum-Token (B2B-авторизация)
    
    API->>DB: SELECT user_id, home_group_id FROM loyalty_cards WHERE token = loyalty_card_token
    activate DB
    DB-->>API: Возвращает идентификаторы пользователя и семьи
    deactivate DB
    
    API->>DB: SELECT 1 FROM processed_b2b_receipts WHERE receipt_number = receipt_number
    activate DB
    DB-->>API: Пустой ответ (Чек уникален, ранее не обрабатывался)
    deactivate DB
    
    Note over API, DB: Старт атомарной SQL-транзакции
    API->>DB: INSERT INTO processed_b2b_receipts (Фиксация номера чека для дедупликации)
    
    loop По каждому товару из массива items (Включая весовые позиции)
        API->>DB: INSERT INTO fridge_inventory ON CONFLICT (home_group_id, product_name, unit) DO UPDATE
    end
    Note over API, DB: Коммит атомарной транзакции (COMMIT)
    
    API->>KAFKA: send_to_kafka_async("bupar.fridge.events.add")
    API-->>MAG: HTTP 200 OK (status: "PROCESSED", items_added: 2)
    
    API->>APP: WebSocket /ws/push-stream/{id} -> Сигнал "STOCK_INTEGRATION_UPDATED"
    deactivate API
    note over APP: Экран холодильника мгновенно перерисовывается, отображая +1.695 кг Бананов

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

  • Шаг 1 (MAG -> API): В момент закрытия фискальной смены или сразу после оплаты клиентом покупки на кассе супермаркета, шина данных розничной сети Magnum отправляет HTTP POST запрос на наш вебхук-эндпоинт. В теле запроса передается полный состав чека, включая весовые и штучные позиции.

  • Шаг 2 (API -> API): Бэкенд-шлюз FastAPI извлекает заголовок X-Shop-Token и сверяет его со значением в безопасном хранилище конфигураций. Если токен невалиден, шлюз мгновенно отсекает запрос со статусом HTTP 401 Unauthorized, защищая систему от спама.

  • Шаг 3 (API -> DB): FastAPI извлекает из JSON поле loyalty_card_token и делает запрос к базе данных: SELECT user_id, home_group_id FROM user_loyalty_cards WHERE partner_code = 'MAGNUM' AND card_token = $1. Так система находит, к какому именно холодильнику привязана данная скидочная карта.

  • Шаг 4 (DB -> API): База данных возвращает user_id и home_group_id конкретной семьи.

  • Шаг 5 (API -> DB): Для защиты от повторных зачислений продуктов при сбоях сети на стороне партнера (Idempotency / Идемпотентность), бэкенд выполняет проверку: SELECT 1 FROM processed_b2b_receipts WHERE partner_code = 'MAGNUM' AND receipt_number = $1.

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

    • Вилка исключений (Дубликат): Если строка найдена, бэкенд прерывает выполнение и возвращает Magnum статус HTTP 409 Conflict, предотвращая дублирование продуктов в холодильнике.
  • Шаг 7 (API -> DB): Бэкенд открывает атомарную SQL-транзакцию в PostgreSQL. Первым действием выполняется логирование фискального номера: INSERT INTO processed_b2b_receipts (partner_code, receipt_number) VALUES ('MAGNUM', $1), что мгновенно блокирует параллельные запросы-дубликаты.

  • Шаг 8 (API -> DB): Запускается цикл прямого зачисления товаров в холодильник семьи. Бэкенд берет точные веса и штуки из JSON-пакета (например, дробное значение 1.695 кг для бананов) и последовательно выполняет DML-команды вставки с апдейтом при коллизии (Upsert):

    INSERT INTO fridge_inventory (user_id, home_group_id, product_name, quantity, unit)
    VALUES (\$1, \$2, \$3, \$4, \$5)
    ON CONFLICT (home_group_id, product_name, unit) 
    DO UPDATE SET quantity = fridge_inventory.quantity + EXCLUDED.quantity;
  • Шаг 9 (API -> API): После успешного прохождения всего массива товаров транзакция фиксируется в БД (COMMIT). Данные становятся доступны для чтения.

  • Шаг 10 (API -> KAFKA): Бэкенд асинхронно отправляет системное бизнес-событие в топик Kafka bridge.fridge.events.add для уведомления смежных микросервисов (аналитика, умные рецепты) о пополнении запасов.

  • Шаг 11 (API -> MAG): Шлюз возвращает серверу Magnum успешный ответ HTTP 200 OK с подтверждением приема данных и количеством зачисленных позиций.

  • Шаг 12 (API -> APP): Чтобы пользователю не приходилось вручную обновлять экран смартфона, FastAPI находит активное WebSocket-соединение семьи по home_group_id и отправляет сигнал "STOCK_INTEGRATION_UPDATED". На стороне Flutter вкладка холодильника мгновенно перерисовывается, отображая свежие бананы и хлеб, купленные в Magnum минуту назад.

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

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

5.1.1 HTTP 200 OK (Ответ на Шаге 11)

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

  • Заголовки ответа (Response Headers):
    • Content-Type: application/json
  • Тело ответа (Response Body):
{
  "status": "PROCESSED",
  "receipt_number": "CH-20260701-09",
  "loyalty_card_token": "shop-card-8822-fa",
  "metrics": {
    "items_processed_count": 2,
    "db_transaction_status": "COMMITTED"
  },
  "timestamp": "2026-07-01T23:10:02Z"
}

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

5.2.1 Ошибка дублирования чека (HTTP 409 Conflict — Шаг 6)

Выбрасывается на Шаге 5, если переданный receipt_number уже присутствует в таблице дедупликации processed_b2b_receipts. Это предотвращает повторное начисление веса продуктов при сбоях и ретраях на стороне Magnum.

{
  "error_code": "ERR-RECEIPT-DUPLICATE",
  "message": "Данный фискальный документ уже был успешно обработан и зачислен ранее.",
  "details": {
    "rejected_receipt_number": "CH-20260701-09",
    "partner_code": "MAGNUM",
    "action": "Idempotency trigger activated. No duplicate database writes executed."
  }
}

5.2.2 Ошибка: Карта лояльности не привязана к профилю (HTTP 422 Unprocessable Entity — Шаг 4)

Выбрасывается на Шаге 4, если loyalty_card_token верный по структуре, но отсутствует в таблице маппинга user_loyalty_cards. Система не может определить, в чей именно холодильник нужно положить продукты.

{
  "error_code": "ERR-LOYALTY-CARD-NOT-MAPPED",
  "message": "Карта лояльности партнера не связана ни с одним активным аккаунтом в экосистеме .",
  "details": {
    "unmapped_token": "mag-card-8822-fa",
    "action": "The user must physically link their Magnum card inside the Flutter mobile app profile."
  }
}

5.2.3 Ошибка B2B-авторизации партнера (HTTP 401 Unauthorized — Шаг 2)

Выбрасывается на Шаге 2 бэкенд-шлюзом FastAPI, если заголовок X-Shop-Token отсутствует, заблокирован или содержит неверный API-ключ партнера.

{
  "error_code": "ERR-B2B-UNAUTHORIZED",
  "message": "B2B API токен партнера не прошел валидацию. Доступ к вебхуку отклонен.",
  "details": {
    "reason": "Invalid or revoked X-Shop-Token credential sequence."
  }
}