User Service — BR 1.4.3

Репо: erp-user-service (C:\Users\A211\Desktop\erp-user-service) Контракт: API + Data Model

Фаза 1 — Миграции (Flyway SQL)

  • Миграция: создать roles (id, franchise_id, name, description, is_system, deleted_at, created_at, updated_at) + UNIQUE(franchise_id, LOWER(name)) WHERE deleted_at IS NULL + indexes
  • Миграция: создать role_permissions (role_id FK CASCADE, permission_key varchar(100), granted bool) + PK (role_id, permission_key)
  • Миграция: создать employee_roles (employee_id FK CASCADE, role_id FK RESTRICT, created_at) + PK + indexes
  • Миграция: создать employee_role_stores (employee_id, role_id, store_id) + PK + composite FK → employee_roles CASCADE + index (store_id)
  • Миграция: salary_formulas — добавить role_id UUID NULL FK roles(id), пересоздать уникальные индексы (drop old uq_salary_formulas_role, create (role_id) UNIQUE WHERE employee_id IS NULL). Колонка role (varchar) — удалить (все записи будут пустые после миграции данных ниже)
  • Миграция: индексы для запросов — idx_employee_roles_role, idx_employee_roles_employee, idx_employee_role_stores_store

Фаза 2 — Entity + Repository + DTO

  • JPA Entity: Role, RolePermission (EmbeddedId), EmployeeRole (EmbeddedId), EmployeeRoleStore (EmbeddedId)
  • Repositories: RoleRepository, RolePermissionRepository, EmployeeRoleRepository, EmployeeRoleStoreRepository
  • Методы в RoleRepository: findByFranchiseIdAndDeletedAtIsNull, findByFranchiseIdAndNameIgnoreCase, countEmployeesByRoleId(roleId), и т.п.
  • DTOs: RoleResponse, RoleListItememployee_count), CreateRoleRequest, UpdateRoleRequest, PermissionCatalog

Фаза 3 — Permission catalog (константа)

  • Класс PermissionCatalog с константами BACKOFFICE_SECTIONS (16 разделов с лейблами и наличием edit) и POS_OPERATIONS (14 операций)
  • Метод isValidKey(String key): boolean для валидации при сохранении роли
  • Метод all(): Set<String> для seed системной роли

Фаза 4 — Roles CRUD Service + Controller

  • RoleService.list(filters, pagination) с агрегацией employee_count (JOIN employee_roles)
  • RoleService.get(id) + проверка franchise_id из JWT
  • RoleService.create(request) — валидация name unique, permission keys, Edit-implies-Read, сохранение в транзакции (role + role_permissions + optional salary_formula)
  • RoleService.update(id, patch) — частичное обновление; для is_system=true разрешить только name/description
  • RoleService.delete(id) — проверка ROLE_IN_USE; soft delete (set deleted_at)
  • RoleService.restore(id) — проверка NAME_DUPLICATE с активными + unset deleted_at
  • RoleService.getPermissionCatalog() — из константы с i18n-лейблами
  • RoleController с эндпоинтами GET/POST/PATCH/DELETE /api/v1/roles, POST /{id}/restore, GET /permission-catalog
  • Exception handling: NAME_DUPLICATE, UNKNOWN_PERMISSION_KEY, ROLE_IN_USE, SYSTEM_ROLE_PROTECTED, ROLE_NOT_FOUND

Фаза 5 — Изменения в EmployeeService

  • CreateEmployeeRequest + UpdateEmployeeRequest — добавить поле roles: List<EmployeeRoleAssignment> (role_id + store_ids[])
  • EmployeeService.create — валидировать role_ids (same franchise, not deleted), store_ids (subset of multi-tenancy scope), сохранить в employee_roles + employee_role_stores в одной транзакции
  • EmployeeService.update — идемпотентно заменить связи (delete old → insert new)
  • EmployeeResponse — в detail-ответе добавить roles: [{ id, name, store_ids }]
  • Errors: ROLE_NOT_FOUND, ROLE_STORE_OUT_OF_SCOPE

Фаза 6 — Internal API

  • Новый GET /internal/users/{id}/permissions — возвращает { user_id, role_ids[], permissions[] } (агрегат granted=true со всех ролей сотрудника)
  • Обновить POST /internal/users/validate-credentials, GET /internal/users/by-email, POST /internal/users/validate-pin — добавить role_ids[] и permissions[] в response
  • Переиспользовать общий helper PermissionAggregator.forUser(employeeId)

Фаза 7 — Seed системной роли

  • FranchiseSeedService (новый или расширение существующего DataSeeder): при создании франшизы вызывать createSystemAdministratorRole(franchiseId):
    • roles запись с name="Администратор", is_system=true
    • все ключи из PermissionCatalog.all() в role_permissions с granted=true
  • В application-dev.yml / application-test.yml — убедиться что seed выполняется на пустой БД

Фаза 8 — Разовый скрипт миграции данных (SQL-файл)

  • Flyway миграция V{NN}__BR_1.4.3_migrate_existing_employees.sql:
    1. INSERT INTO roles — системная роль «Администратор» для каждой существующей франшизы (если не создана seed’ом)
    2. INSERT INTO role_permissions — все ключи для этой роли
    3. INSERT INTO employee_roles — привязка всех активных admin_franchise к системной роли
    4. UPDATE legal_entities SET owner_user_id = NULL WHERE owner_user_id IN (SELECT id FROM employees WHERE role='admin_franchisee')
    5. DELETE FROM employees WHERE role IN ('admin_franchisee', 'manager', 'cashier')

Фаза 9 — Тесты

  • Unit: RoleService — все сценарии CRUD + валидация
  • Unit: PermissionAggregator — агрегат granted=true с учётом нескольких ролей
  • Integration: POST /roles → GET list → назначение сотруднику → GET /internal/users/{id}/permissions → проверка агрегата
  • Integration: DELETE /roles/{id} → ROLE_IN_USE при назначенных сотрудниках
  • Integration: SYSTEM_ROLE_PROTECTED при попытке изменить права системной

Выходные критерии

  • Все миграции применяются на чистой БД и на БД с существующими данными
  • Linting/сompile без ошибок
  • Unit-тесты проходят
  • Integration-тесты проходят на Testcontainers
  • Сборка Docker-образа без ошибок