User Service — BR 1.4.4
Репо: erp-user-service Контракт: API + Data Model
Фаза 1 — Liquibase changesets (4 файла)
-
018-br-1-4-4-create-franchises.xml:CREATE TABLE franchises (id UUID PK, name VARCHAR(255), type VARCHAR(20) NOT NULL DEFAULT 'corporate', created_at, updated_at)CHECK (type IN ('corporate','individual'))- Seed:
INSERT INTO franchises (id, name, type) VALUES ('00000000-0000-0000-0000-000000000001', 'Тестовая франшиза', 'corporate')(сNOT EXISTSguard)
-
019-br-1-4-4-franchises-fk.xml:ALTER TABLE employees ADD CONSTRAINT fk_employees_franchise FOREIGN KEY (franchise_id) REFERENCES franchises(id)- То же для
legal_entities.franchise_id,roles.franchise_id,employee_stores(если есть поле) и т.д.
-
020-br-1-4-4-role-owner-le.xml:ALTER TABLE roles ADD COLUMN owner_legal_entity_id UUID NULLFK → legal_entities(id) ON DELETE CASCADE(скрытая роль удаляется вместе с ЮЛ партнёра)CREATE INDEX idx_roles_hidden ON roles(owner_legal_entity_id) WHERE owner_legal_entity_id IS NOT NULLCREATE INDEX idx_roles_visible ON roles(franchise_id) WHERE owner_legal_entity_id IS NULL AND deleted_at IS NULL- Удалить старый
uq_roles_name_per_franchiseи пересоздать с исключением скрытых:UNIQUE (franchise_id, LOWER(name)) WHERE deleted_at IS NULL AND owner_legal_entity_id IS NULL
-
021-br-1-4-4-drop-employees-role.xml:ALTER TABLE employees DROP COLUMN roleDROP INDEX IF EXISTS idx_employees_role- CHECK constraint на role — исчезнет автоматически
Фаза 2 — Entity / Repository / DTO
-
Franchiseentity (+ repositoryFranchiseRepositoryсfindById) -
Roleentity — добавитьownerLegalEntityIdполе -
Employeeentity — убрать полеrole -
RoleRepository— методы:findAllByFranchiseIdAndOwnerLegalEntityIdIsNull(для обычных ролей)findByOwnerLegalEntityId(для скрытых ролей при управлении через ЮЛ)
- DTO:
CreateEmployeeRequest,UpdateEmployeeRequest,EmployeeResponse,InternalEmployeeResponse— убратьrole,store_ids(отдельное поле)CreateLegalEntityRequest— добавитьownerPermissions: { mode, permissions }OwnerPermissionsResponse(mode + permissions)ScopeResponse(type + ids)FranchiseResponse(id, name, type)
Фаза 3 — ScopeService + переписывание авторизации
-
ScopeService.computeScope(userId):- Lookup
legal_entities WHERE owner_user_id = userId(один user может владеть не одним ЮЛ в будущем, но пока один) - Если найдено type=franchise →
{ type: all_franchise } - Если найдено type=franchisee →
{ type: legal_entity_ids, legal_entity_ids: [le.id, ...] } - Иначе: SELECT DISTINCT store_id FROM employee_role_stores WHERE employee_id = userId →
{ type: store_ids, store_ids: [...] }
- Lookup
- В
LegalEntityService— заменитьswitch(user.getRole())на проверки черезScopeService:- Владелец франшизы: scope.type == all_franchise → доступ ко всем ЮЛ
- Владелец партнёра: legal_entity_id in scope.legal_entity_ids → только свои
- В
EmployeeService— аналогично: фильтрация employees по scope + permission-check для edit/delete - В
RoleService—GET /rolesфильтруетowner_legal_entity_id IS NULLпо умолчанию
Фаза 4 — Новые controllers и endpoints
-
FranchiseController:GET /api/v1/franchises/{id}— вернуть FranchiseResponse. Проверка:id == user.franchise_id, иначе 404
-
LegalEntityOwnerPermissionsController:GET /api/v1/legal-entities/{id}/owner-permissions— вернуть{ mode, permissions }- Если владелец привязан к системной «Администратор» →
mode=full - Если к скрытой роли →
mode=custom, permissions=[...]
- Если владелец привязан к системной «Администратор» →
PUT /api/v1/legal-entities/{id}/owner-permissions— body{ mode, permissions? }- При смене full→custom: создать скрытую роль, отвязать «Администратор», привязать новую
- При смене custom→full: удалить скрытую роль, привязать «Администратор»
- При custom→custom: обновить permissions скрытой роли
- Форсить минимум:
pos.access,stores.read,employees.read
-
InternalUserController:GET /internal/users/{id}/scope— вернутьScopeResponseчерез ScopeService- Response
/internal/users/{id}/permissions— опционально расширить полемscope
Фаза 5 — Обновление existing endpoints
-
POST /employees— убрать валидациюrole(поле больше не существует в request), убрать ошибкиADMIN_ROLE_FORBIDDEN -
PATCH /employees/{id}— убратьROLE_IMMUTABLE -
GET /employees,GET /employees/{id}— response без поляrole -
POST /legal-entities(type=franchisee):- Принимать поле
owner_permissionsв body - В транзакции: создать LE + Employee-owner + назначить роль (системная «Администратор» при mode=full, скрытая при mode=custom)
- Ошибка
FRANCHISE_TYPE_INDIVIDUALеслиfranchise.type == individual
- Принимать поле
-
/internal/users/validate-credentials|by-email|validate-pin— убратьroleиз response, добавитьscope(через ScopeService + cache) -
/internal/users/validate-pin— заменить enum-проверку на: PIN валиден + в агрегате permissions естьpos.access. ИначеPOS_ACCESS_DENIED
Фаза 6 — Тесты (optional, если время)
-
ScopeServiceunit тесты (3 сценария) -
OwnerPermissionsintegration (full → custom → full) -
FranchiseController(404 при чужой franchise_id)
Выходные критерии
- Миграции применяются на чистой БД и на БД с данными BR 1.4.3
- Все
switch(role)в коде убраны (grep должен быть пуст) - Docker build без ошибок
- Коммит + push в
origin main