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}
Подписчик должен:
- Проверить
X-Timestamp— не старше 5 минут (защита от replay) - Проверить
X-Signature— HMAC-SHA256 тела с известным емуsecret - Обработать и ответить
HTTP 2xx(любой 2xx считается успехом) - На ошибку — 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)
- Возвращается подписчику только один раз при создании — потом только
rotateendpoint (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