[DRAFT] Метод POST /api/v1/webhooks/stores/receipt

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

Published

July 1, 2026

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

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

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

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

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

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

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

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

  • Метод: POST
  • Маршрут: /api/v1/webhooks/stores/receipt
  • Формат данных: 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)

Структура данных полностью повторяет фискальный срез чека Магазин. Поля количества товара (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
    actor Partner as REST-клиент Торговой Сети
    participant Nginx as Nginx Proxy
    participant GW as FastAPI Gateway (backend-api)
    participant Store as store-service (INVENTORY)
    participant Auth as auth-service (SECURITY/IAD)
    participant K as Apache Kafka Cluster
    participant Fridge as fridge-service (INVENTORY)

    Partner->>Nginx: Шаг 1: POST /api/v1/webhooks/stores/receipt (Headers: X-Shop-Token)
    activate Nginx
    Nginx->>GW: Шаг 2: Внутренний прокси сырого JSON вебхука
    activate GW
    GW->>Store: Шаг 3: gRPC: ProcessB2BReceipt(ReceiptRequest)
    activate Store
    
    Store->>Auth: Шаг 4: gRPC: ResolveLoyaltyCard(CardRequest)
    activate Auth
    Auth-->>Store: Возврат user_id, home_group_id и статуса X-Shop-Token
    deactivate Auth
    
    alt Сценарий А: Токен НЕ валиден / Карта лояльности не найдена
        Store-->>GW: gRPC Error: UNAUTHENTICATED / NOT_FOUND
        GW-->>Partner: HTTP 401 Unauthorized / 404 Not Found
    else Сценарий Б: Успешная валидация и маппинг
        Store-->>GW: gRPC Response: ACCEPTED (OK)
        deactivate Store
        GW-->>Partner: Шаг 5: HTTP 200 OK (Контракт исполнен, Fire-and-Forget)
        deactivate GW
        deactivate Nginx
        
        %% АСИНХРОННЫЙ ХВОСТ ПРЯМОГО ЗАЧИСЛЕНИЯ
        Store->>K: Шаг 6: Пуш события в топик: store.inventory.in.receipt.imported
        activate K
        K->>Fridge: Шаг 7: Handler: ProcessTrustedB2BReceipt()
        deactivate K
        activate Fridge
        Note over Fridge: Шаг 8: Прямой транзакционный INSERT<br/>в PostgreSQL Fridge DB на баланс home_group_id
        deactivate Fridge
    end

Процесс автоматического пополнения холодильника через B2B-вебхук (Store-Receipt)

Шаг Действие Параметры / Запросы Ошибки (Исключения / Статусы)
Шаг 1 (Partner -> Nginx) Внешний сервер торговой сети отправляет JSON-датасет фискального чека на B2B-эндпоинт интеграции. HTTP POST /api/v1/webhooks/stores/receipt
Headers: X-Shop-Token: "shop_live_secret_abc123..."
DioException: send timeout
HTTP 400 Bad Request (передан невалидный или сломанный JSON)
Шаг 2 (Nginx -> GW) Прокси-сервер осуществляет инжекцию X-Request-ID и перенаправляет сырой JSON вебхука на API-шлюз. HTTP POST /backend-api/v1/webhooks/stores/receipt
Headers: X-Request-ID: "shop-tr-44bb-99ff"
HTTP 502 Bad Gateway (контейнер backend-api недоступен)
HTTP 504 Gateway Timeout
Шаг 3 (GW -> Store) Шлюз FastAPI валидирует структуру чека и транслирует запрос в инвентарный микросервис. Протокол: gRPC
Метод: ProcessB2BReceipt(ReceiptRequest)
Payload: loyalty_card_token, receipt_number, items
gRPC Status: UNAVAILABLE (сервис store-service отключен)
gRPC Status: INVALID_ARGUMENT (не совпал контракт полей чека)
Шаг 4 (Store -> Auth) Сервис инвентаря запрашивает у компонента безопасности проверку B2B-токена и конвертацию карты лояльности в ID пространства. Протокол: gRPC
Метод: ResolveLoyaltyCard(CardRequest)
Args: shop_token, loyalty_card_token
gRPC Status: DEADLINE_EXCEEDED (таймаут криптографической проверки)
gRPC Status: INTERNAL (сбой внутренней логики безопасности)
Шаг 5 (Сценарий А) (Auth -> Store) Компонент безопасности отклоняет запрос из-за протухшего токена партнера или отсутствия карты в базе данных. gRPC Error: UNAUTHENTICATED (токен заблокирован)
или gRPC Error: NOT_FOUND (карта лояльности не привязана)
gRPC Status: INTERNAL
Шаг 5.1 (Сценарий А) (Store -> Partner) Инвентарный сервис возвращает ошибку на шлюз, и шлюз транслирует партнеру финальный статус отказа. Ответ по цепочке через GW и Nginx:
HTTP 401 Unauthorized (неверный токен)
или HTTP 404 Not Found (карта не зарегистрирована)
HTTP 500 Internal Server Error (ошибка сборки JSON-структуры ответа об ошибке)
Шаг 5 (Сценарий Б) (Auth -> Store) Компонент безопасности подтверждает статус партнера и возвращает целевые user_id и home_group_id. gRPC Response: VALID (OK)
Payload: user_id: "usr-88", home_group_id: "uuid-77"
gRPC Status: DATA_LOSS (база данных IAD вернула пустой UUID)
Шаг 5.1 (Сценарий Б) (Store -> Partner) Сервис инвентаря фиксирует успешное прохождение проверок и мгновенно отпускает партнера по принципу Fire-and-Forget. Ответ по цепочке через GW и Nginx:
HTTP 200 OK
Payload: { "status": "success", "message": "ACCEPTED" }
HTTP 500 Internal Server Error (ошибка маршаллинга успешного ответа на стороне шлюза)
Шаг Действие Параметры / Запросы Ошибки (Исключения / Статусы)
Шаг 6 (Store -> KAFKA) Инвентарный сервис асинхронно публикует событие успешного импорта B2B-чека в брокер сообщений для изоляции транзакционной нагрузки. Топик: store.inventory.in.receipt.imported
Parameters: max.block.ms = 1000
Сбой шины: Kafka: TimeoutException
(Сброс аварийного лога в Promtail для ручной переотправки)
Шаг 7 (KAFKA -> Fridge) Фоновый воркер сервиса холодильника вычитывает доверенное B2B-сообщение из топика и запускает процедуру зачисления позиций. Топик: store.inventory.in.receipt.imported
Handler: ProcessTrustedB2BReceipt()
Kafka: CommitFailedException (сбой фиксации офсета в кластере брокера)
Шаг 8 (Fridge -> DB_Fridge) Проверка идемпотентности: Чтобы избежать дублирования продуктов при повторных отправках кафки, сервис проверяет фискальный номер чека в реестре обработанных документов. SQL-запрос:
SELECT 1 FROM processed_b2b_receipts WHERE store_id = 'shop-almaty-05' AND receipt_number = 'CH-20260701-09' LIMIT 1;
PostgreSQL Exception: ConnectionPoolTimeout
(отказ пула соединений базы данных инвентаря)
Шаг 8 (продолжение) (DB_Fridge -> Fridge) База данных возвращает пустой ответ, подтверждая уникальность фискального документа. Вилка исключений (Дубликат): Если строка найдена, воркер логирует событие B2B_DUPLICATE_DROP и мгновенно прерывает выполнение транзакции. metric: b2b_idempotency_blocks_total (счетчик предотвращенных дубликатов продуктов в логах)
Шаг 8.1 (Fridge -> DB_Fridge) Прямое зачисление продуктов: В рамках единой транзакции воркер регистрирует факт обработки чека и выполняет bulk-insert весовой матрицы товаров на баланс группы. SQL-запросы (Атомарная транзакция):
INSERT INTO processed_b2b_receipts (receipt_number, store_id) VALUES ('CH-20260701-09', 'shop-almaty-05');
INSERT INTO fridge_inventory (user_id, home_group_id, product_name, quantity, unit) VALUES ('usr-88', 'uuid-77', 'БАНАН ЭКВАДОР КГ', 1.6950, 'кг');
PostgreSQL Exception: DeadlockDetected
PostgreSQL Exception: ForeignKeyViolation (не найден UUID пространства)
Шаг 8.2 (DB_Fridge -> Fridge) СУБД фиксирует транзакцию, обновляет складские остатки Multi-Tenancy контура и возвращает статус успешной записи. Ответ СУБД: Статус COMMIT (успешная фиксация транзакции). PostgreSQL Exception: TransactionAborted
Шаг 8.3 (Fridge -> KAFKA) Сервис отправляет системное бизнес-событие в шину данных для уведомления зависимых доменов бэкенда (аналитика, умные рецепты). Топик: fridge.inventory.events.stock_replenished
Payload: home_group_id, items_count
Kafka: BufferExhaustedException
Шаг 9 (Fridge -> App) Реалтайм обновление UI: Чтобы пользователю не пришлось вручную обновлять экран, шлюз по сигналу воркера находит активный WebSocket-стрим семьи по home_group_id и отправляет пуш-ивент. Эндпоинт стрима: /ws/push-stream/{id}
WebSocket Event: "STOCK_INTEGRATION_UPDATED"
WebSocketException: ConnectionResetError (пользователь свернул приложение или потерял сеть)

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

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

4.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"
}

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

4.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."
  }
}

4.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."
  }
}

4.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."
  }
}