BR 2.5 — Aggregator Service (webhook dispatcher)
Источники
Задачи
Миграция БД
- Liquibase changeset
XXX-br-2-5-webhook-subscriptions:CREATE TABLE webhook_subscriptionsсо всеми полями (id, franchise_id, store_id, subscriber_name, webhook_url, secret_encrypted, events (JSONB), is_active, deleted_at, created_at, updated_at)- Unique
uq_webhook_subs (franchise_id, store_id, webhook_url) WHERE is_active = true AND deleted_at IS NULL - Index
(franchise_id) WHERE is_active = true
- Liquibase changeset
XXX-br-2-5-webhook-delivery-attempts:CREATE TABLE webhook_delivery_attemptsсо всеми полями- Index
(subscription_id, created_at DESC)— для GET deliveries - Index
(next_attempt_at) WHERE next_attempt_at IS NOT NULL— для worker
Entity + Repository
-
WebhookSubscription— JPA entity -
WebhookDeliveryAttempt— JPA entity -
WebhookSubscriptionRepository— findByEventTypeAndFranchiseAndStore (для матчинга при получении Kafka-события) -
WebhookDeliveryAttemptRepository— findByNextAttemptAtBefore, findBySubscriptionIdOrderByCreatedAtDesc
Crypto
- Secret generation —
SecureRandom32 chars alphanumeric - AES-256 encryption at rest — через Spring
TextEncryptorили custom, ключ из envWEBHOOK_SECRET_KEY - HMAC-SHA256 подпись при отправке — Java built-in
javax.crypto.Mac
Service
-
WebhookSubscriptionService— CRUD операции, возврат секрета только при создании -
WebhookDispatcher— основной компонент:@KafkaListenerна все топикиorder.*(order.created,order.cooking_started,order.ready,order.paid,order.closed,order.handed_over,order.in_delivery,order.delivered,order.cancelled,order.refunded)- На каждое событие: найти подходящие подписки (по franchise + store + events), создать
WebhookDeliveryAttempt, отправить HTTP POST с HMAC-подписью - При HTTP 2xx: проставить
delivered_at,next_attempt_at=NULL - При не-2xx или timeout: инкрементировать
attempt_number, вычислить next_attempt_at по backoff, логировать error - После 7 попыток без успеха: dead-letter (
next_attempt_at=NULL,delivered_at=NULL)
-
WebhookRetryScheduler—@Scheduled(fixedDelay = 10000)— раз в 10 сек перебираетwhere next_attempt_at <= nowи пытается доставить -
WebhookDeliveryAttemptCleaner—@Scheduled(cron = "0 0 3 * * *")— раз в сутки чистит попытки старше 30 дней
Controllers
-
InternalWebhookSubscriptionController—/internal/webhook-subscriptions(POST, GET list, PATCH, DELETE) -
InternalWebhookDeliveryController—/internal/webhook-subscriptions/{id}/deliveries,/internal/webhook-deliveries/{id}/retry - Все endpoints с
X-Service-Tokenauth (существующий фильтр)
Configuration
- application.yml:
app.webhook.secret-key(AES key, из env)app.webhook.retry.backoff-seconds— массив[10, 30, 120, 600, 3600, 21600, 86400]app.webhook.retry.max-attempts: 7app.webhook.attempts-retention-days: 30app.webhook.http-timeout-ms: 5000
Kafka Consumer Groups
- Новый consumer group
aggregator-webhook-dispatcherдля всехorder.*топиков — чтобы не конфликтовать с уже существующими consumer groups
Verification
mvn compileзелёный- Миграции применяются
- Smoke-сценарии:
- Создать подписку:
POST /internal/webhook-subscriptions→ получитьsecretв ответе (один раз) - Повторное чтение:
GET /internal/webhook-subscriptions→secretотсутствует - Пробить заказ в Order Service → убедиться что подписчик получил POST с HMAC-подписью
- Имитация падения подписчика (отдавать 500) → проверить что
webhook_delivery_attemptsсодержит записи с инкрементирующимсяattempt_number,next_attempt_atследует backoff - Ручной retry dead-letter:
POST /internal/webhook-deliveries/{id}/retry→ создаётся новая попытка
- Создать подписку:
Зависимости
- Shared DTO для Kafka событий — консьюмит топики, определённые в Order Service
Events.md(BR 2.5) - Env переменная
WEBHOOK_SECRET_KEY(32 байта) должна быть задана вenvs/aggregator-service.env(инфра-правка — ASK пользователя)