BR 6.1 → Store Service

Репозиторий: erp-store-service Контракты: API, Data Model, Events

Задачи

Инфраструктура / миграции

  • Liquibase changeset NNN-marketing-slides.xml:
    • Создаёт таблицу marketing_slides (по Data Model)
    • Все индексы (idx_marketing_slides_store, idx_marketing_slides_store_active_order, uq_marketing_slides_store_order)
    • CHECK-constraints: image_mime, source, order_index >= 0, image_size_bytes 1..10485760
  • Liquibase changeset NNN-stores-standby-config.xml:
    • ALTER TABLE stores ADD standby_idle_minutes integer NOT NULL DEFAULT 5
    • ALTER TABLE stores ADD standby_transition_seconds integer NOT NULL DEFAULT 9
  • Liquibase changeset для outbox-таблицы маркетинга (или общая outbox-таблица — посмотреть как сделано в текущем store.table.upserted — там fire-and-forget без outbox; здесь нужен полноценный outbox)
    • Таблица marketing_outbox (id, event_type, payload jsonb, created_at, processed_at)
    • Composite index (processed_at, created_at) для poller’а

Entity / Repository (JPA)

  • MarketingSlide entity (поля по Data Model)
  • MarketingSlideRepository extends JpaRepository
  • MarketingOutbox entity + repository

S3 интеграция

  • Внести erp-marketing-slides бакет в конфиг (application.yml)
  • Сервис MarketingSlideStorageService с методами:
    • upload(storeId, slideId, MultipartFile) → publicUrl
    • delete(objectKey) (для отложенной очистки)
  • Использовать существующий S3Client (если есть для фото ТТ) или поднять новый
  • Public URL должен быть стабильный (не presigned) — https://s3.erp/erp-marketing-slides/{store_id}/{slide_id}.{ext} через bucket policy (read-only public)

Бизнес-логика

  • Сервис MarketingSlideService:
    • list(storeId, activeFilter?) → List<DTO>
    • listActive(storeId) → ActiveBundleDTO (для internal: slides + standby_config из stores)
    • create(storeId, file, title, active) → DTO
      • Валидация MIME/size/dimensions (≥ 1280×720)
      • Проверка лимита 20 активных
      • Upload в S3
      • INSERT в marketing_slides с order_index = MAX + 1
      • INSERT в marketing_outbox (action=created)
      • Один транзакционный блок (если outbox в той же БД, что и slides — это просто @Transactional)
    • update(storeId, slideId, file?, title?, active?) → DTO
      • Если file — старый file_url пишем в marketing_outbox для отложенной очистки (или в отдельную таблицу cleanup queue)
      • INSERT в outbox (action=updated|activated|deactivated)
    • reorder(storeId, List<{id, order}>) → List<DTO>
      • Транзакционно — снимаем uq_marketing_slides_store_order через temp-shift (UPDATE order_index = order_index + 1000 WHERE store_id = ?), затем выставляем правильные
      • INSERT в outbox (action=reordered, slide_id=NULL)
    • delete(storeId, slideId) — soft delete + outbox (action=deleted)
  • Ролевые проверки в MarketingSlideController:
    • marketing.read для GET
    • marketing.write для POST/PUT/PATCH/DELETE
    • Scope-check: store_id из path должен быть в scope пользователя (правила ролевой модели)

HTTP endpoints

  • MarketingSlideController:
    • GET /api/v1/admin/stores/{storeId}/marketing-slides
    • POST /api/v1/admin/stores/{storeId}/marketing-slides (multipart)
    • PUT /api/v1/admin/stores/{storeId}/marketing-slides/{slideId} (multipart или json)
    • PATCH /api/v1/admin/stores/{storeId}/marketing-slides/reorder
    • DELETE /api/v1/admin/stores/{storeId}/marketing-slides/{slideId}
  • InternalMarketingController:
    • GET /internal/stores/{storeId}/marketing-slides/active — slides + standby конфиг

Kafka publisher (outbox poller)

  • Scheduled task (каждые 1 сек или event-driven):
    • SELECT FROM marketing_outbox WHERE processed_at IS NULL ORDER BY created_at LIMIT 100
    • Для каждой записи — KafkaTemplate.send(“marketing.slide.changed”, store_id, payload)
    • На success — UPDATE processed_at = now()
    • На failure — лог + следующий заход (retry бесконечно, идемпотентность через consumer-side dedup по event_id)
  • Конфиг Kafka топика marketing.slide.changed (через kafka-topics-init контейнер в инфре)

Расширения существующих эндпоинтов

  • PATCH /api/v1/stores/{id} — разрешить standby_idle_minutes и standby_transition_seconds в body (только для роли Franchise / scope-owner)
    • Валидация: 1 ≤ idle_minutes ≤ 60, 3 ≤ transition_seconds ≤ 60

Cron / очистка S3

  • Scheduled task раз в сутки (например, 03:00 UTC):
    • SELECT image_url FROM marketing_slides WHERE deleted_at IS NULL
    • Сравнить с listObjects бакета erp-marketing-slides
    • Удалить объекты без ссылки
    • Сохранять deleted_at в последние 7 дней (грация для отката)

Тестирование

  • Юнит-тесты MarketingSlideService:
    • CRUD happy path
    • Reorder в транзакции
    • Лимит 20 активных
    • Валидация MIME/size
    • Soft delete
  • Интеграционный тест: outbox-publisher публикует в Kafka
  • Юнит-тест ролевых проверок в Controller

Seed (для test VPS)

  • См. Seed — отдельная задача

Зависимости

  • User Service миграция permissions — должна задеплоиться до этого сервиса (RBAC-чек)
  • Бакет erp-marketing-slides на MinIO — создать ДО первого деплоя сервиса
  • Kafka topic marketing.slide.changed — добавить в kafka-topics-init

Готово когда

  • Все 5 публичных + 1 internal эндпоинты проходят smoke-тесты на test VPS
  • Создание слайда публикует Kafka-событие, pos-bff его получает (видно в логах)
  • Лимит 20 активных слайдов работает
  • Soft delete работает + cron очистки S3 настроен (можно отдельно проверить вручную)