Catalog Service — BR 3.4

Контракты

  • Events — 6 новых publish-топиков
  • APIGET /internal/catalog/full-snapshot

Что делаем

Миграция

  • src/main/resources/db/changelog/027_catalog_outbox.xml — таблица catalog_outbox:
    id uuid PK
    topic varchar(100) NOT NULL
    payload_json jsonb NOT NULL
    status varchar(20) NOT NULL default 'pending'  -- pending / sent / failed
    attempts int NOT NULL default 0
    next_attempt_at timestamp NOT NULL default now()
    last_error text NULL
    created_at timestamp NOT NULL default now()
    sent_at timestamp NULL
    
    INDEX (status, next_attempt_at)  -- для worker'а
    

Kafka publisher

  • com.erp.catalog.entity.CatalogOutboxEntry + CatalogOutboxRepository
  • com.erp.catalog.event.CatalogEventPublisher с методами:
    • publishProductUpserted(Product product) — payload включает готовый category_path (резолвим иерархию категорий до строки)
    • publishProductDeleted(UUID productId, UUID franchiseId, LocalDateTime deletedAt)
    • publishModifierGroupUpserted(ModifierGroup group) — payload: группа + опции + referenced_by_product_ids (список товаров ссылающихся на эту группу)
    • publishModifierGroupDeleted(UUID groupId, UUID franchiseId, LocalDateTime deletedAt, List<UUID> referencedProductIds)
  • Категории не публикуются как отдельные события — при правке категории Catalog Service каскадно публикует publishProductUpserted для всех затронутых товаров (новый category_path).
  • Каждый метод оборачивает событие в стандартную обёртку {event_id, timestamp, version, payload} и пишет в catalog_outbox в той же @Transactional.
  • com.erp.catalog.worker.CatalogOutboxWorker@Scheduled(fixedDelay = 5000):
    • Вычитывает status=pending + next_attempt_at <= now() batch до 100
    • Публикует в Kafka (KafkaTemplate)
    • При успехе — status=sent, sent_at=now
    • При ошибке — attempts++, backoff (10s/30s/2m/10m), после 10 — status=failed
  • KafkaAdmin — декларация топиков (6 штук) с retention 7 дней (168 ч).

Hooks в существующих сервисах

  • ProductService.createProduct — после save(): catalogEventPublisher.publishProductUpserted(product)
  • ProductService.updateProduct — аналогично после save.
  • ProductService.softDeleteProductpublishProductDeleted(...).
  • ProductService.restoreProductpublishProductUpserted(...) (восстановление = upsert).
  • CategoryService.createCategory / updateCategory / deleteCategoryкаскадно перепубликуем все затронутые товары. Helper publishProductUpsertedForCategory(categoryId) — резолвит все товары этой категории + дочерних → publishProductUpserted для каждого. Отдельных catalog.category.* событий нет.
  • ModifierGroupService.createGroup / updateGroup / updateOptions — публикация catalog.modifier_group.upserted с payload группы + опций + referenced_by_product_ids (из product_modifier_groups таблицы связок).
  • ModifierGroupService.softDeletepublishModifierGroupDeleted(...) с referenced_by_product_ids.

Full snapshot endpoint

  • InternalCatalogController.getFullSnapshot(franchiseId) или новый InternalCatalogSyncController:
    GET /internal/catalog/full-snapshot?franchise_id=X
    
    • Собирает products (c готовым category_path) + modifier_groups.
    • Категории отдельно в ответе НЕ возвращаются — category_path уже зашит в каждый product.
    • Включая soft-deleted (поле deleted_at).
    • Возвращает по контракту API.md.
  • Rate limiting — bucket4j с Redis-backend. 1 запрос / 10 сек на franchise_id. При превышении — 429 RATE_LIMITED + Retry-After.
  • Auth — стандартный X-Service-Token.

Per-product expand endpoint

  • InternalCatalogController.expandProduct(productId):
    GET /internal/catalog/products/{id}/expand
    
    • Возвращает развёрнутый список виртуальных PK-продуктов по правилу развёртывания:
      • base — если у товара нет структурных модификаторов; + N записей free_addon для каждого свободного модификатора × если есть структурный, то вариантов может быть M структурных × N addon’ов на каждый.
      • structural_variant — по одному на каждую структурную опцию, имя с суффиксом опции, цена = base_price + структурная доплата.
      • free_addon — по одному на каждую пару (структурный вариант × свободная опция), цена = цена свободной опции.
    • Каждый variant включает sku по формуле {product_id}[:{struct_opt_id}][:+{free_opt_id}], полное name с category_path префиксом.
  • Хелпер ProductExpansionService — чистая логика развёртывания, используется и для expand, и для full-snapshot (в snapshot’е товары возвращаются в оригинальной форме, но адаптер может использовать этот же хелпер локально).

Тесты

  • Unit CatalogEventPublisherTest — проверить payload соответствует контракту (обёртка + все поля).
  • Unit CatalogOutboxWorkerTest — retry-логика, backoff.
  • Integration ProductServiceIT с embedded Kafka — после createProduct в outbox появляется запись, worker её публикует.
  • Integration FullSnapshotIT — полный срез содержит все entity типы.
  • Test: rate-limit (11-й запрос в секунду → 429).

Документация

  • Обновить src/main/resources/README-events.md (если существует) или создать — описание published topics.

Зависимости

  • Нет внешних — все изменения внутри Catalog Service.
  • Kafka — используется существующий конфиг.
  • Redis — уже есть для кэша (используем для bucket4j).

Deploy

После merge — стандартный передеплой catalog-service на VPS через /deploy-all catalog-service.

Ссылки