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 — SecureRandom 32 chars alphanumeric
  • AES-256 encryption at rest — через Spring TextEncryptor или custom, ключ из env WEBHOOK_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-Token auth (существующий фильтр)

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: 7
    • app.webhook.attempts-retention-days: 30
    • app.webhook.http-timeout-ms: 5000

Kafka Consumer Groups

  • Новый consumer group aggregator-webhook-dispatcher для всех order.* топиков — чтобы не конфликтовать с уже существующими consumer groups

Verification

  1. mvn compile зелёный
  2. Миграции применяются
  3. Smoke-сценарии:
    • Создать подписку: POST /internal/webhook-subscriptions → получить secret в ответе (один раз)
    • Повторное чтение: GET /internal/webhook-subscriptionssecret отсутствует
    • Пробить заказ в 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 пользователя)