Webhook-подписки (внешние интеграции)

Источник требований

Механизм подписки внешних систем (локальные POS-фронты вроде KOALa, кухонные дашборды, отдельные мобильные приложения, аналитические сервисы) на реал-тайм события о заказах из Order Service.

Webhook-подписки — это «исходящая» интеграция. Входящие интеграции (внешняя система создаёт заказ у нас) идут напрямую в публичный API Order Service (POST /api/v1/orders + JWT).


Отличие от агрегаторов

У нас есть два похожих кейса «внешней интеграции»:

КейсСущностьСервис-владелец
Маркетплейс агрегации (Яндекс.Еда, Market Delivery)OAuth2-binding, full lifecycle (pull меню, push заказов, push статусов)Aggregator Service
Внешний POS / клиент (KOALa, KDS)Webhook-подписка (только push событий вовне)Aggregator Service (тот же, переиспользуем retry/log-инфру)

Подробнее различие — в спеке Агрегаторов.

Сервис-владелец webhook-подписок — Aggregator Service (:3013), потому что там уже есть паттерны retry/dead-letter/логирования взаимодействий с внешними системами. Заводить отдельный микросервис для этого — избыточно.


Сущности

webhook_subscription

Подписка конкретной внешней системы на события конкретной франшизы / ТТ.

ПолеОбязательностьОписание
idОбязательно (auto)UUID, первичный ключ
franchise_idОбязательноМультитенантность
store_idНеобязательноNULL = подписка на все ТТ франшизы. UUID = только одна ТТ
subscriber_nameОбязательноЧеловекочитаемое имя: koala, kitchen-display, analytics. Макс 100 символов
webhook_urlОбязательноHTTPS URL куда POST-ить события
secret_encryptedОбязательноСекрет для HMAC-SHA256 подписи тела (хранится зашифрованно AES at rest)
eventsОбязательноМассив типов событий, на которые подписчик хочет получать уведомления (см. §Типы событий)
is_activeОбязательноDefault true. Деактивация через UI/SQL временно отключает доставку без удаления
created_at / updated_atОбязательно (auto)

webhook_delivery_attempt

Лог попыток доставки события. Нужен для аудита и retry-логики.

ПолеОбязательностьОписание
idОбязательно (auto)UUID
subscription_idОбязательноFK → webhook_subscription.id
event_idОбязательноUUID события из Kafka (для идемпотентности и дедупа)
event_typeОбязательноНапример order.paid
payload_hashОбязательноSHA256 тела запроса — для дедупа при повторе
attempt_numberОбязательно1..N, инкрементируется на каждой попытке
http_statusНеобязательноHTTP-ответ подписчика (NULL если сетевая ошибка)
errorНеобязательноТекст ошибки при неуспехе
next_attempt_atНеобязательноКогда следующая попытка (NULL если успех или мёртвое письмо)
delivered_atНеобязательноКогда первый успешный 2xx
created_atОбязательно (auto)

Типы событий

В Фазе 1 MVP (BR 2.5) публикуются следующие события заказов:

event_typeКогдаКомментарий
order.createdСоздан заказ (status=new)Включает items, total, order_type, customer_id (nullable), requires_kitchen
order.cooking_startedПереход new → acceptedЗаказ ушёл на кухню
order.readyПереход accepted → readyКухня отметила готовность
order.paidВыставлено paid_at (статус не меняется)Включает payment_method, paid_amount
order.closedПереход → closedЗаказ выдан клиенту
order.cancelledПереход → cancelledВключает cancel_reason
order.handed_overПереход ready → handed_overПередан курьеру
order.in_deliveryПереход handed_over → in_deliveryКурьер начал путь
order.deliveredПереход in_delivery → deliveredКлиент получил
order.refundedСоздана запись RefundRecordВключает amount, full/partial, fiscal_data

Подписчик выбирает интересные ему события в поле events. Неподписанные события к нему не отправляются.

Схема payload’а конкретного события — описана в Aggregator Service · Events (Шаг 2 BR 2.5).


Формат webhook-запроса

Исходящий POST от Aggregator Service к подписчику:

POST {webhook_url}
Content-Type: application/json
X-Event-Type: order.paid
X-Event-Id: {uuid из Kafka события}
X-Timestamp: {unix-seconds}
X-Signature: {base64(HMAC-SHA256(body, secret))}

{JSON payload — см. Events spec}

Подписчик должен:

  1. Проверить X-Timestamp — не старше 5 минут (защита от replay)
  2. Проверить X-Signature — HMAC-SHA256 тела с известным ему secret
  3. Обработать и ответить HTTP 2xx (любой 2xx считается успехом)
  4. На ошибку — HTTP 4xx/5xx → мы ретраим

Бизнес-правила

Уникальность подписки

  • Уникальность (franchise_id, store_id, webhook_url) среди активных (is_active=true). Deleted / deactivated не считаются.
  • Один subscriber может иметь несколько подписок (например, kitchen-display для разных ТТ — отдельные подписки).

Retry-политика

  • Backoff: 10s → 30s → 2m → 10m → 1h → 6h → 24h → после 7 неуспешных попыток — перевод в «dead_letter» (next_attempt_at = NULL, delivered_at = NULL)
  • Dead letter можно «оживить» вручную через internal endpoint (POST /internal/webhook-deliveries/{id}/retry)

Retention лога

  • webhook_delivery_attempt хранится 30 дней — достаточно для аудита, не забивает БД
  • Cron чистит старше 30 дней раз в сутки

Secret — обязательная защита

  • Секрет генерируется при создании подписки (минимум 32 символа из crypto-random)
  • Возвращается подписчику только один раз при создании — потом только rotate endpoint (Phase 2)
  • Хранится зашифрованно at rest (AES-256 с ключом из environment)

MVP: управление — через SQL / internal endpoint

  • Admin-UI для управления подписками — отложено, Phase 2
  • В MVP: оператор регистрирует подписку через internal endpoint POST /internal/webhook-subscriptions (service-token auth) или напрямую в БД
  • Список подписок, их статус — через GET /internal/webhook-subscriptions?franchise_id=...

Ролевая матрица (для будущего Admin-UI)

ДействиеФраншизаФранчайзиМенеджер ТТКассир
Просмотр списка подписок✅ (свои ТТ)
Создать подписку
Редактировать / деактивировать
Увидеть лог доставки✅ (свои ТТ)
Ручной retry мёртвого письма

Permissions (добавляются в Ролевую модель в BR 2.5):

  • Просмотр — integrations.read
  • CRUD — integrations.manage

Что НЕ входит в Фазу 1 MVP

  • Admin-UI для управления подписками — через SQL/internal endpoint
  • Secret rotation endpoint — Phase 2
  • Filter events по store_id / по заказчику — в MVP только grubbing фильтр по типу события
  • Delivery метрики — Grafana dashboard подписок — Phase 2
  • Consumer confirmation (подписчик подтверждает что событие обработано → мы знаем «точно дошло до бизнес-логики») — Phase 2

Связи с другими модулями

  • Заказы — источник событий (каждая смена статуса → событие → всем заинтересованным подписчикам)
  • Агрегаторы доставки — соседний тип интеграции (аггрегатор вместо внешнего POS); тот же сервис-владелец (Aggregator Service)
  • Aggregator Service — сервис-владелец, новые таблицы и endpoint’ы — в его контрактах (Шаг 2 BR 2.5)
  • Order Service · Events — Kafka-события, на которые подписывается Aggregator Service для превращения в webhook

Ссылки