ЮЛ: алиасы и ГСК/ГСЗ в POST/PATCH /directories/legal-entities (#84) #85

Merged
aep merged 1 commits from feat/84-le-form-fields into main 2026-10-09 13:03:07 +03:00
Owner

Closes #84

Формы «Добавить юридическое лицо» (51-02) и «Изменить юридическое лицо» (79) отправляют всё одной кнопкой. Теперь POST/PATCH /directories/legal-entities принимают алиасы и ГСК/ГСЗ и применяют их в той же транзакции, что и поля ЮЛ.

Что изменилось в API

  • LegalEntityCreate / LegalEntityUpdate: aliases: string[] (1–500 символов, ≤ 100 элементов), gsk_id, gsz_id.
  • POST — алиасы и членства создаются вместе с ЮЛ; ошибка на любом шаге откатывает весь запрос. Порядок шагов и ошибок: поля ЮЛ → алиасы → ГСК → ГСЗ.
  • PATCH — не переданное поле не меняется:
    • aliases — итоговый набор, сверка по normalized_alias: совпавшие не трогаются (написание прежнее), недостающие добавляются, лишние удаляются; удаление требует DIRECTORY_DELETE, иначе 403 «недостаточно прав: требуется DIRECTORY_DELETE» и откат; aliases: null — 422;
    • gsk_id/gsz_id — итоговая группа вида: null — выйти, та же — no-op, другая — закрыть текущее членство и открыть новое.
  • Ошибки — как у отдельных ручек (EMPTY_ALIAS, ALIAS_TAKEN, DIFFERENT_COMPANY_GROUP, 404 невидимой группы); дубль внутри списка — 409 ALIAS_EXISTS; новая 422 WRONG_GROUP_KIND (в gsk_id передана ГСЗ и наоборот).
  • Ответ POST и PATCH — LegalEntityDetail (надмножество LegalEntityOut: + memberships, aliases).
  • Аудит — как у отдельных ручек: LEGAL_ENTITY_CHANGE по каждому алиасу, MEMBERSHIP_CHANGE по каждому членству; снимок legal_entity_history(UPDATE) — только при изменении полей ЮЛ.

Внутри

  • Логика ручек алиасов и состава ГСК/ГСЗ вынесена в функции модулей (aliases.add_alias/remove_alias/normalize_or_reject, related_groups.add_membership/close_membership/member_payload); форма — services/legal_entity_form.py поверх них, поэтому форма и ручки ведут себя одинаково.
  • PermissionDeniedError в common/exceptions.py — единый 403 «недостаточно прав: требуется <КОД>»; require использует его.
  • Спека openspec/specs/directories-api/spec.md — новое требование «Форма ЮЛ одним запросом» (bounded, правка без change), README — таблица маршрутов и WRONG_GROUP_KIND.

Rulings I made

  • R-1: 403 при удалении алиаса формой — строкой, как у require (PermissionDeniedError), а не доменным объектом: так все 403 в API одного вида (гайд фронта §3.2).
  • R-2: граница aliases ≤ 100 элементов (422 массивом до БД) — по образцу границ входов #26.
  • R-3: проверка права на удаление — после видимости ЮЛ и проверки списка (иначе нечего сравнивать); до любых изменений алиасов, а поля ЮЛ откатываются транзакцией.

Наблюдения

  • O-1: DIRECTORY_DELETE в seed есть только у ADMIN, поэтому ан��литик в модалке 79 может добавлять алиасы, но удаление получит 403. Если аналитик должен удалять алиасы — это правка матрицы прав (данные), не кода; решение владельца.
  • O-2: смена ГК из модалки 79 не входит — это move с effective_date и закрытием всех членств.

Находки

Маркер Находка Статус
O-1 удаление алиасов формой доступно только ADMIN deferred (решение владельца)
O-2 смена ГК из модалки 79 deferred

Проверка

make check-full — exit 0: ruff/mypy чисто; unit 1279 passed; integration 883 passed (новых 18 в tests/test_api_legal_entity_form.py); e2e 1 passed; dbml-check, check-infra, docker-build, web-check — без ошибок.

🤖 Generated with Claude Code

Closes #84 Формы «Добавить юридическое лицо» (51-02) и «Изменить юридическое лицо» (79) отправляют всё одной кнопкой. Теперь `POST`/`PATCH /directories/legal-entities` принимают алиасы и ГСК/ГСЗ и применяют их в той же транзакции, что и поля ЮЛ. ## Что изменилось в API - `LegalEntityCreate` / `LegalEntityUpdate`: `aliases: string[]` (1–500 символов, ≤ 100 элементов), `gsk_id`, `gsz_id`. - **POST** — алиасы и членства создаются вместе с ЮЛ; ошибка на любом шаге откатывает весь запрос. Порядок шагов и ошибок: поля ЮЛ → алиасы → ГСК → ГСЗ. - **PATCH** — не переданное поле не меняется: - `aliases` — итоговый набор, сверка по `normalized_alias`: совпавшие не трогаются (написание прежнее), недостающие добавляются, лишние удаляются; удаление требует `DIRECTORY_DELETE`, иначе 403 «недостаточно прав: требуется DIRECTORY_DELETE» и откат; `aliases: null` — 422; - `gsk_id`/`gsz_id` — итоговая группа вида: `null` — выйти, та же — no-op, другая — закрыть текущее членство и открыть новое. - Ошибки — как у отдельных ручек (`EMPTY_ALIAS`, `ALIAS_TAKEN`, `DIFFERENT_COMPANY_GROUP`, 404 невидимой группы); дубль внутри списка — 409 `ALIAS_EXISTS`; новая 422 `WRONG_GROUP_KIND` (в `gsk_id` передана ГСЗ и наоборот). - Ответ POST и PATCH — `LegalEntityDetail` (надмножество `LegalEntityOut`: + `memberships`, `aliases`). - Аудит — как у отдельных ручек: `LEGAL_ENTITY_CHANGE` по каждому алиасу, `MEMBERSHIP_CHANGE` по каждому членству; снимок `legal_entity_history(UPDATE)` — только при изменении полей ЮЛ. ## Внутри - Логика ручек алиасов и состава ГСК/ГСЗ вынесена в функции модулей (`aliases.add_alias/remove_alias/normalize_or_reject`, `related_groups.add_membership/close_membership/member_payload`); форма — `services/legal_entity_form.py` поверх них, поэтому форма и ручки ведут себя одинаково. - `PermissionDeniedError` в `common/exceptions.py` — единый 403 «недостаточно прав: требуется <КОД>»; `require` использует его. - Спека `openspec/specs/directories-api/spec.md` — новое требование «Форма ЮЛ одним запросом» (bounded, правка без change), README — таблица маршрутов и `WRONG_GROUP_KIND`. ## Rulings I made - R-1: 403 при удалении алиаса формой — строкой, как у `require` (`PermissionDeniedError`), а не доменным объектом: так все 403 в API одного вида (гайд фронта §3.2). - R-2: граница `aliases` ≤ 100 элементов (422 массивом до БД) — по образцу границ входов #26. - R-3: проверка права на удаление — после видимости ЮЛ и проверки списка (иначе нечего сравнивать); до любых изменений алиасов, а поля ЮЛ откатываются транзакцией. ## Наблюдения - O-1: `DIRECTORY_DELETE` в seed есть только у `ADMIN`, поэтому ан��литик в модалке 79 может добавлять алиасы, но удаление получит 403. Если аналитик должен удалять алиасы — это правка матрицы прав (данные), не кода; решение владельца. - O-2: смена ГК из модалки 79 не входит — это `move` с `effective_date` и закрытием всех членств. ## Находки | Маркер | Находка | Статус | |---|---|---| | O-1 | удаление алиасов формой доступно только ADMIN | deferred (решение владельца) | | O-2 | смена ГК из модалки 79 | deferred | ## Проверка `make check-full` — exit 0: ruff/mypy чисто; unit 1279 passed; integration 883 passed (новых 18 в `tests/test_api_legal_entity_form.py`); e2e 1 passed; dbml-check, check-infra, docker-build, web-check — без ошибок. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
aep added 1 commit 2026-10-09 12:56:03 +03:00
Формы «Добавить/Изменить юридическое лицо» (макет 51-02, 79) отправляют всё
одним запросом. POST/PATCH /directories/legal-entities принимают `aliases`,
`gsk_id`, `gsz_id` и применяют их в той же транзакции, что и поля ЮЛ.

- POST: алиасы и членства создаются вместе с ЮЛ; ошибка откатывает всё.
- PATCH: `aliases` — итоговый набор (сверка по normalized_alias; удаление —
  с правом DIRECTORY_DELETE, иначе 403), `gsk_id`/`gsz_id` — итоговые группы
  (`null` — выйти, другая — сменить); не переданное поле не меняется.
- Новая ошибка 422 WRONG_GROUP_KIND; дубль в списке — 409 ALIAS_EXISTS.
- Ответ POST и PATCH — LegalEntityDetail (надмножество LegalEntityOut).
- Логика ручек алиасов и состава ГСК/ГСЗ вынесена в функции модулей
  (add_alias/remove_alias, add_membership/close_membership) — форма и ручки
  ведут себя одинаково; PermissionDeniedError — единый 403 «недостаточно прав».
- Спека directories-api и README обновлены.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aep merged commit b4c7a15e99 into main 2026-10-09 13:03:07 +03:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RSHB/arm-core#85
No description provided.