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
[DRAFT] Метод POST /api/v1/webhooks/stores/receipt
Документация B2B API: Автоматическое зачисление продуктов через фискальный вебхук торговой сети Магазин
- INV-BACKEND-024 Backlog — Реализация обработчика ReceivePartnerReceipt для добавления продуктов по внешнему методу.
В открытом доступе представлена демонстрационная версия метода. В настоящей публичной документации отображены не все шаги, технические сценарии и приватные эндпоинты для системы цифровых симуляторов бизнес-процессов.
- Полная спецификация метода: Будет доступна только во внутреннем контуре разработки (Confluence / Swagger Enterprise).
1 Функциональное назначение
Метод представляет собой открытый B2B-эндпоинт (вебхук), предназначенный для автоматического пополнения холодильника пользователя товарами, приобретенными в розничной сети или магазине или у продавца с настроенной интеграцией.
Метод решает следующие архитектурные задачи:
- Синхронный Direct-Inflow (Прямой импорт): В отличие от пользовательского контура (где фото чека уходит на OCR), “Вендор” передает серверу строго типизированный, очищенный JSON-датасет. Это позволяет системе зачислять продукты в холодильник обходя буферные таблицы
pending_receipts. - Сквозная B2B-авторизация: Метод защищен протоколом
OAuth2 Client Credentialsили выделенным статическим API-ключом, так как запрос инициируется сервером торговой сети, а не мобильным приложением. - Связывание аккаунтов (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 без ручного вмешательства.
| Шаг | Действие | Параметры / Запросы | Ошибки (Исключения / Статусы) |
|---|---|---|---|
Шаг 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."
}
}