meraproject/openspec/changes/project-members-write/design.md
keboss-m 5c21d25d45 Initial commit: Merakomis portal, Docker stack and user-reader API.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 11:04:05 +03:00

9.2 KiB
Raw Permalink Blame History

Design: Project Members Write

Context

services/user-reader уже читает составы через GET /api/project-members (join tMerakomisTeamMembertMerakomisProjecttMerakomisDSection) и пишет табель (labor-api-write). Запись составов в PHP CMS идёт через themes.merakomis.team.member + generic CRUD merakomisControllerTable; отдельного REST-action нет.

Модель данных:

tMerakomisProject.team ──► tMerakomisTeam
tMerakomisProjectSection (project_id, section_id) ──► допустимые разделы
tMerakomisTeamMember (team, emp, section, role, active, text)
  UNIQUE (team, emp)

Права в PHP: Rules::isRwMember($team_id, $member_id) — admin, РП проекта, ГИП в проекте, директор отдела (для подчинённых при редактировании конкретного member).

Валидация в PHP Member::beforeUadd: обязательны emp, role, section (ненулевые).

Goals / Non-Goals

Goals:

  • Внешний сервис добавляет/обновляет участника команды через PUT /api/project-members с тем же auth-паттерном, что табель
  • Поведение 1:1 с CMS: upsert по (team, emp), валидация section по проекту, права isRwMember
  • Вспомогательные read-эндпоинты для построения UI без знания внутренних id ролей/разделов
  • Расширение GET /api/labor/permissions флагом can_write_member

Non-Goals:

  • Full-sync всего состава проекта одним запросом
  • Delta-инкремент составов
  • Физическое DELETE из tMerakomisTeamMember
  • Автодобавление всех сотрудников (Project::checkAddAllEmps / is_all_emp)
  • Изменение PHP CMS или React UI

Decisions

1. Один upsert-эндпоинт PUT /api/project-members

Решение: единый PUT с телом { project_id, emp_id, section_id, role, active?, text? }.

Почему: unique (team, emp) в БД; интегратору не нужен member_id для первичного добавления. Если запись есть — UPDATE, иначе INSERT (team из project.team).

Альтернатива: REST POST/PATCH/DELETE по member_id — ближе к CMS, но требует предварительного GET для id; отклонено для v1.

2. Деактивация вместо удаления

Решение: active: false в теле PUT; физический DELETE не экспонировать.

Почему: CMS использует switcher active; сохраняется история; GET /api/project-members?active_only=true уже фильтрует неактивных.

3. Новый модуль project_members_write.py + project_members_read.py

Решение: отдельные файлы, не раздувать labor_write.py.

Почему: другая доменная область (TeamMember vs Time), другие права; labor.py остаётся read-only агрегатами.

4. Порт Rules::isRwMember в labor_permissions.py

Решение: функции can_write_project_member(cur, db, acting_emp_id, project_id, member_id=None, target_emp_id=None).

Логика:

Условие Разрешено
is_admin(acting) да
acting == project.director (РП) да
роль acting в проекте == MAIN_ENGINEER (2) да
при update: target_emp_id in get_sub_emp_ids(acting) да
при create (member_id=None): get_sub_emp_ids(acting) непустой (директор отдела) да
иначе нет

Порт getRoleInProject: SELECT role FROM TeamMember WHERE emp=acting AND team=project.team LIMIT 1.

Альтернатива: только admin + РП — проще, но ломает сценарий ГИП/директора отдела из CMS.

5. Валидация section_id через tMerakomisProjectSection

Решение: перед INSERT/UPDATE проверять, что section_id ∈ разрешённых для project_id. Если у проекта нет записей в ProjectSection — отклонять запись с 400 (как CMS: нельзя выбрать раздел).

Порт ProjectSection::get($project_id) → helper _project_section_ids(cur, db, project_id).

6. Справочник ролей — статический dict в Python

Решение: member_roles.py с константами из eMemberRole.php (id → name, archive flag); GET /api/member-roles отдаёт неархивные + текущую роль при edit.

Почему: справочник редко меняется; не тянуть PHP. При изменении ролей — обновить dict (допустимый trade-off).

7. Auth на write-маршрутах

Решение: require_api_key + require_acting_emp_id (переиспользовать из labor_identity.py если есть, иначе тот же Depends).

Read вспомогательных эндпоинтов (project-sections, member-roles) — только API-ключ, без Acting (как GET /api/project-members).

8. Ответ PUT — тот же shape, что элемент GET /api/project-members

Решение: после upsert выполнить SELECT с теми же join'ами и вернуть один объект + ok: true.

Почему: клиент сразу получает member_id, step_name, section_name без повторного GET.

Architecture

Внешний сервис
  │ X-Api-Key
  │ X-Acting-Emp-Id
  ▼
project_members_write.py ──► labor_permissions.can_write_project_member
         │                   project_members_read._project_section_ids
         ▼
    MySQL: tMerakomisTeamMember (+ join Project для team_id)

Расширение merakomis_schema.py:

PROJECT_SECTION_TABLE = "tMerakomisProjectSection"
TEAM_MEMBER_FIELDS += "text"  # если есть в БД

API Contract (кратко)

PUT /api/project-members

Поле Обязательно Описание
project_id да id проекта
emp_id да id сотрудника
section_id да id из GET /api/project-sections
role да id из GET /api/member-roles
active нет default true
text нет комментарий, default ""

Ошибки: 400 validation, 403 forbidden, 404 project/emp not found, 409 duplicate conflict (не ожидается при upsert).

GET /api/project-sections?project_id=

items[]: { section_id, section_name } — join tMerakomisProjectSection + tMerakomisDSection.

GET /api/member-roles?role=

items[]: { id, title } — неархивные роли; query role включает архивную текущую при edit.

GET /api/labor/permissions (расширение)

Новые query: project_id (уже есть), опционально member_id, target_emp_id.

Новое поле ответа: can_write_member: bool.

Risks / Trade-offs

Риск Митигация
Статический список ролей устареет Вынести в конфиг/таблицу в follow-up; документировать sync с eMemberRole.php
isRwMember сложнее, чем табель Unit-тесты на матрицу ролей; сверка с PHP на staging
Race при параллельном upsert (team, emp) UNIQUE в БД; catch duplicate → retry as UPDATE
Запись section не из ProjectSection Жёсткая валидация до INSERT
Нет инвалидации кэша TeamMember не входит в tMerakomisTimeCache; кэш часов не затрагивается

Migration Plan

  1. Деплой user-reader с новыми эндпоинтами (обратно совместимо — только добавления)
  2. Обновить документацию и выдать API-ключ интегратору
  3. Staging: сравнить upsert с ручным добавлением в CMS для РП/ГИП/директора отдела
  4. Rollback: откат образа user-reader; данные в MySQL остаются (записи не удаляются)

Open Questions

  1. Нужен ли GET /api/project-members с active_only=false по умолчанию для интеграции — нет изменений в v1
  2. Проверять ли emp.archive / emp.removed при добавлении — да, отклонять 400 (как фильтр в форме CMS)
  3. Поле portal в TeamMember — писать 0 (default), как в time-entries