## Context User-reader уже умеет upsert участника проекта через `PUT /api/project-members` (`project_members_write.py`): валидация раздела/роли/сотрудника и запись в `tMerakomisTeamMember`. Авторизация завязана на `X-Acting-Emp-Id` и `can_write_project_member` (паритет PHP `Rules::isRwMember`). Внешнее приложение знает `project_id`, `emp_id`, `section_id`, `role` и имеет только service API-ключ. Impersonation РП/ГИП для него неудобна и нежелательна. Справочник разделов проекта (`GET /api/project-sections`) уже отдаёт `section_id`; отдельного поля «раздел + подраздел» в модели нет — в TeamMember хранится один `section`. ## Goals / Non-Goals **Goals:** - Дать внешним сервисам upsert участника проекта только по API-ключу. - Переиспользовать существующую бизнес-валидацию и upsert-логику без дублирования SQL. - Явно отделить service-контракт от портального acting-потока (отдельный URL). - Зафиксировать контракт в docs и тестах. **Non-Goals:** - Создание/редактирование сотрудника в `tMerakomisEmp`. - Изменение поведения `PUT /api/project-members` (acting остаётся обязательным). - Расширение `GET /api/project-sections` (parent / full_name) — не требуется для этого сценария. - Отдельный API-ключ или scopes на эндпоинт (пока один `USER_READER_API_KEY`). - Запись разделов проекта (`tMerakomisProjectSection`) или создание команды проекта. ## Decisions ### 1. Отдельный путь `PUT /api/service/project-members` (вариант B) **Выбор:** новый URL под префиксом `/api/service/…`, а не опциональный acting на существующем PUT. **Почему:** - Контракт для интеграций явный: «сервис доверяет ключу», не «человек с правами». - Не ослабляем портальный эндпоинт случайным вызовом без acting. - Проще документировать и аудировать. **Альтернатива (A):** тот же PUT, acting опционален → отвергнута: смешивает модели доверия. ### 2. Переиспользование `ProjectMemberWrite` + `validate_member_write` + `upsert_team_member` **Выбор:** тот же JSON-контракт и те же helper-функции; новый handler только пропускает acting/permissions. **Почему:** один источник правил (раздел ∈ проекта, роль валидна, emp не архивный, у проекта есть team). **Реализация:** тонкий роут в новом модуле (например `project_members_service.py`) или второй handler в `project_members_write.py` с `Depends(require_api_key)` без `parse_acting_emp_id`. Предпочтительно отдельный файл/роутер, чтобы граница service vs portal была видна в `main.py`. ### 3. Авторизация = только API-ключ **Выбор:** при включённом `USER_READER_API_KEY` — обязателен ключ; при пустом ключе (локальная отладка) — как у остальных защищённых эндпоинтов. Проверки `isRwMember` нет. **Почему:** соответствует заявленному сценарию внешнего приложения. Риск компенсируется контролем ключа на стороне деплоя. ### 4. Ответ и ошибки **Выбор:** тот же shape, что у portal PUT: `ok: true` + поля item `GET /api/project-members`. Те же коды валидации (`400`/`404`), без `403 forbidden` из-за прав acting (acting нет). `401` при неверном/отсутствующем ключе. ### 5. Документация Обновить матрицу доступа и раздел записи в `docs/user-reader-api.md` (+ краткая строка в `DEVELOPERS.md`). В сценариях интеграции указать: внешний сервис → service PUT; UI/табель от имени сотрудника → portal PUT. ## Risks / Trade-offs | Risk | Mitigation | |------|------------| | Утечка API-ключа = полный write состава любых проектов | Ключ только на сервере; ротация; в будущем — scopes / отдельный ключ для write | | Клиенты путают portal и service PUT | Разные URL + явная секция в docs; portal по-прежнему требует Acting | | Дублирование логики при копировании кода | Обязательно вызывать существующие validate/upsert, не копировать SQL | | Нет audit «кто добавил» (нет acting) | Принять для v1; при необходимости позже писать в History с маркером `service` | ## Migration Plan 1. Задеплоить user-reader с новым роутом (обратно совместимо). 2. Внешнее приложение переключить на `PUT /api/service/project-members`. 3. Rollback: убрать роут / откатить образ — portal PUT не затронут. ## Open Questions - Нужен ли позже audit/history для service-записей? (вне scope v1) - Нужен ли отдельный write-ключ или scope? (вне scope v1; один общий ключ)