User Service — BR 5.1

Контракты

  • Data Model — новая таблица kds_devices + permissions kds.access, kds.settings.edit
  • API — 5 новых endpoint
  • Events — событие user.kds_device.revoked

Что делаем

Миграция Liquibase

  • src/main/resources/db/changelog/0XX_add_kds_devices.xml:
    • CREATE TABLE kds_devices:
      • id uuid PK
      • device_id uuid NOT NULL (UUID с устройства, передаётся в X-Device-Id)
      • franchise_id uuid NOT NULL
      • store_id uuid NOT NULL
      • name varchar(100) NULL
      • last_user_id uuid NULL
      • current_user_id uuid NULL
      • app_version varchar(20) NULL
      • last_seen_at timestamp NULL
      • revoked_at timestamp NULL
      • created_at timestamp NOT NULL
      • updated_at timestamp NOT NULL
    • UNIQUE INDEX uq_kds_devices_franchise_device ON (franchise_id, device_id) WHERE revoked_at IS NULL
    • INDEX idx_kds_devices_franchise_store ON (franchise_id, store_id) WHERE revoked_at IS NULL
    • INDEX idx_kds_devices_last_seen ON (last_seen_at DESC) WHERE revoked_at IS NULL
    • В таблицу permissions (existing roles.permissions или каталог) — добавить:
      • kds.access категория KDS
      • kds.settings.edit категория KDS
    • Регистрация в db.changelog-master.xml

Entities

  • Новая KdsDevice — JPA-entity:
    • все поля из БД
    • boolean isOnline() — computed (last_seen_at != null && last_seen_at > now() - 2 minutes)
    • boolean isRevoked() — computed (revoked_at != null)

Repositories

  • KdsDeviceRepository:
    • Optional<KdsDevice> findByFranchiseIdAndDeviceIdAndRevokedAtIsNull(UUID, UUID) — для регистрации (DUPLICATE check) и heartbeat
    • Page<KdsDevice> findAllByFranchiseIdAndRevokedAtIsNull(...) — list для админки
    • Page<KdsDevice> findAllByFranchiseIdAndStoreIdAndRevokedAtIsNull(...) — фильтр по ТТ
    • Optional<KdsDevice> findByIdAndFranchiseId(UUID, UUID) — для PATCH/DELETE с multi-tenancy

Services

  • Новый KdsDeviceService:
    • register(jwtUser, dto):
      • Проверить permission kds.settings.edit
      • Проверить dto.store_idjwtUser.scope.store_ids (или владелец франшизы)
      • Проверить нет существующей записи с тем же (franchise_id, device_id) и revoked_at IS NULL → 409
      • Создать запись с default name (если не задано)
      • Вернуть DTO
    • heartbeat(deviceId, body):
      • Найти device по device_id
      • Если revoked_at != null → 401 DEVICE_REVOKED
      • Если не найден → 404
      • Обновить last_seen_at = NOW(), current_user_id = body.user_id, app_version = body.app_version
    • list(jwtUser, filters):
      • Фильтрация по franchise_id, опц store_id, online, include_revoked
      • JOIN-lookup current_user.first_name+last_name (через users таблицу)
      • Cross-service lookup store_name (через Store Service GET /internal/stores/{id})
    • rename(jwtUser, deviceId, name) — простой update
    • revoke(jwtUser, deviceId):
      • Проверить permission (только владелец франшизы)
      • Soft-set revoked_at = NOW()
      • Опубликовать событие user.kds_device.revoked
      • Идемпотентно: если уже revoked — 409 ALREADY_REVOKED

Controllers

  • Новый KdsDeviceAdminController:

    • POST /admin/kds/devices/register — JWT с kds.settings.edit
    • GET /admin/kds/devices — JWT с kds.settings.edit
    • PATCH /admin/kds/devices/{id} — JWT с kds.settings.edit
    • DELETE /admin/kds/devices/{id} — JWT с kds.settings.edit + только владелец франшизы
  • Новый KdsDeviceInternalController:

    • POST /internal/kds-devices/{deviceId}/heartbeat — X-Service-Token

KafkaPublisher

  • UserEventPublisher (новый или расширение существующего):
    • publishKdsDeviceRevoked(deviceId, franchiseId, storeId, revokedBy) — топик user.kds_device.revoked, ключ device_id

Permissions seed

  • Liquibase seed скрипт или migration: добавить kds.access и kds.settings.edit в системный каталог permissions франшизы (если используется централизованный список — он есть в RolePermissionEnum или в БД)

Tests

  • Unit:
    • KdsDeviceServiceTest.register_failsIfStoreNotInScope
    • KdsDeviceServiceTest.register_failsOnDuplicate
    • KdsDeviceServiceTest.heartbeat_returnsRevoked
    • KdsDeviceServiceTest.revoke_publishesEvent
  • Integration:
    • KdsDeviceAdminControllerIntegrationTest — register → heartbeat → list → rename → revoke

Зависимости от других сервисов

  • Cross-service lookup store_name через Store Service — нужен endpoint GET /internal/stores/{id} или GET /internal/stores?ids=... (batch). Уже должен быть, проверить в Шаге 6.

Что НЕ делаем

  • Не трогаем существующие endpoints авторизации (/auth/pin-login)
  • Не реализуем consumer на свои же события
  • Не реализуем revocation token blacklist в JWT — полагаемся на pos-bff middleware (он держит подписки и закроет WS при user.kds_device.revoked)

Ссылки