# directories-api Specification ## Purpose API справочников ядра: группы компаний (ГК), юридические лица и ИП с алиасами и историей, группы связанных компаний и заёмщиков (ГСК/ГСЗ) с составом, филиалы. Все изменения идут через сервисы с историей (SCD4/SCD2) и записью в `audit_event`; имена полей — как в DBML v5.14. ## Requirements ### Requirement: Филиалы доступны для чтения `GET /api/v1/directories/regional-offices` SHALL возвращать список `regional_office` (`id`, `name`, `is_head_office`, `is_active`) любому пользователю с правом `DIRECTORY_VIEW`; создание и изменение филиалов через API в Срезе 1 не предоставляется (seed, импорт). #### Scenario: Список филиалов - **WHEN** пользователь с `DIRECTORY_VIEW` запрашивает список после `seed --dev` - **THEN** ответ 200 содержит Головной офис с `is_head_office = true` и два dev-филиала ### Requirement: Группа компаний — создание, чтение, изменение, архив, история `POST /api/v1/directories/company-groups` (право `DIRECTORY_EDIT`) SHALL создавать ГК с полями `name` (обязательно), `crm_id` (необязательно, уникально), `base_regional_office_id` (необязательно), генерируя `code` вида `GK-NNNNNN` и `valid_from = now`, с записью `audit_event(GROUP_CHANGE)`. `PATCH …/{id}` SHALL изменять `name`, `crm_id`, `base_regional_office_id`, сохраняя снимок прежних значений в `company_group_history` (`valid_from` прежний, `valid_to = now`) и обновляя `valid_from`; смена филиала пишет `ASSIGNMENT_CHANGE`, остальное — `GROUP_CHANGE`. `POST …/{id}/archive` и `…/unarchive` SHALL переключать `is_archived`. `GET …` SHALL отдавать список с поиском по `name`/`crm_id`/`code` (подстрока без регистра), фильтром `is_archived`, пагинацией `{items, total, limit, offset}` (`limit` ≤ 200) и счётчиком `legal_entities_count`; `GET …/{id}` — карточку; `GET …/{id}/history` — снимки по убыванию `valid_from`. Удаление ГК API NOT предоставляет. #### Scenario: Создание и код - **WHEN** создаются две ГК подряд - **THEN** их `code` различаются, имеют вид `GK-` и шесть цифр, `valid_from` заполнен, в `audit_event` две записи `GROUP_CHANGE` с `after_value` #### Scenario: Перезакрепление за филиалом - **WHEN** у ГК меняется `base_regional_office_id` - **THEN** в `company_group_history` появляется строка с прежним филиалом и `valid_to`, у ГК новый `valid_from`, записан `audit_event(ASSIGNMENT_CHANGE)` с `before_value`/`after_value` #### Scenario: Дубликат crm_id - **WHEN** создаётся ГК с `crm_id`, который уже есть - **THEN** ответ 409 `{"detail": {"code": "CRM_ID_EXISTS", "message": …}}`, записи нет ### Requirement: Юридическое лицо / ИП — создание, чтение, изменение `POST /api/v1/directories/legal-entities` (`DIRECTORY_EDIT`) SHALL создавать запись с `inn` (ЮЛ — ровно 10 цифр, ИП — 12; иначе 422 `INVALID_INN`), `entity_type` (`ЮЛ` | `ИП`), `name`, `company_group_id` (NULL = монозаёмщик), `regional_office_id` (по умолчанию — филиал ГК), `crm_id`; `inn` уникален (409 `INN_EXISTS`). `PATCH …/{id}` SHALL менять `name`, `entity_type`, `crm_id`, `regional_office_id` со снимком в `legal_entity_history(event_type = UPDATE)` и `audit_event(LEGAL_ENTITY_CHANGE)`. `GET …` SHALL давать список с фильтрами `company_group_id`, `outside_group` (`company_group_id IS NULL`), `inn` (точно), `q` (подстрока имени/ИНН), `closed` (по `close_date`), пагинацией; `GET …/{id}` — карточку с текущими членствами ГСК/ГСЗ и алиасами; `GET …/{id}/history` — снимки. В списке и карточке SHALL быть `documents_count` — число документов файлов ЮЛ, кроме файлов `SUPERSEDED`. #### Scenario: Неверная длина ИНН - **WHEN** создаётся ИП с ИНН из 10 цифр - **THEN** ответ 422 с кодом `INVALID_INN`, запись не создана #### Scenario: Монозаёмщик - **WHEN** ЮЛ создано без `company_group_id` - **THEN** оно отдаётся в списке с `outside_group = true` и видно пользователям филиала `regional_office_id` #### Scenario: Число документов - **WHEN** у монозаёмщика файл с книгой НПЗ (ББ и ОФР) и заменённый файл с одной ОСВ - **THEN** `documents_count = 2` ### Requirement: Перевод ЮЛ в другую ГК и вывод из состава `POST /api/v1/directories/legal-entities/{id}/move` (`DIRECTORY_EDIT`; тело `company_group_id`, `effective_date` ≥ сегодня) SHALL сменить ГК, записать `legal_entity_history(event_type = MOVE_TO_GROUP, effective_date)`, закрыть все текущие членства ЮЛ в ГСК/ГСЗ прежней ГК (`valid_to = now`, `is_current = false`) и записать `LEGAL_ENTITY_CHANGE` и `MEMBERSHIP_CHANGE`. `POST …/{id}/withdraw` (тело `event`: `CLOSE` | `REMOVE_FROM_GROUP`, `effective_date`, `comment`) SHALL установить `company_group_id = NULL`, при `CLOSE` — ещё `close_date = effective_date`, закрыть членства, записать историю и аудит. `effective_date` раньше сегодняшнего дня → 422. #### Scenario: Перевод закрывает членства - **WHEN** ЮЛ состоит в ГСК старой ГК и переводится в новую ГК - **THEN** членство получает `is_current = false` и `valid_to`, ЮЛ имеет новую `company_group_id`, в истории строка `MOVE_TO_GROUP` с датой перевода #### Scenario: Закрытие - **WHEN** выполнен `withdraw` с `event = CLOSE` - **THEN** `close_date` заполнен, `company_group_id = NULL`, ЮЛ отдаётся в списке с `closed = true`, в истории `CLOSE` ### Requirement: Алиасы ЮЛ `POST /api/v1/directories/legal-entities/{id}/aliases` (`DIRECTORY_EDIT`; тело `alias`) SHALL сохранять `alias` как есть и `normalized_alias` по §3 (нижний регистр, trim, без пунктуации и кавычек, без ОПФ ООО/АО/ПАО/ЗАО/ИП, схлопнутые пробелы); повтор для того же ЮЛ — 409 `ALIAS_EXISTS`; такой же `normalized_alias` у другого ЮЛ — 409 `ALIAS_TAKEN` с указанием `legal_entity_id` владельца. `DELETE …/aliases/{alias_id}` SHALL удалять алиас. Оба пишут `LEGAL_ENTITY_CHANGE`. #### Scenario: Нормализация - **WHEN** добавляется алиас `ООО «Пекарня Счастливочная»` - **THEN** `normalized_alias = "пекарня счастливочная"`, повторное добавление `Пекарня Счастливочная, ООО` → 409 `ALIAS_EXISTS` ### Requirement: ГСК и ГСЗ — группы и состав `POST /api/v1/directories/company-groups/{gk_id}/related-groups` (`DIRECTORY_EDIT`; тело `kind` ∈ {gsk, gsz}, `name`, `created_date`) SHALL создавать `related_group` с `code` `GSK-NNNNNN`/`GSZ-NNNNNN`; `PATCH …/related-groups/{id}` — менять `name`, `created_date`; `DELETE …/related-groups/{id}` (`DIRECTORY_DELETE`) SHALL удалять только группу без текущих членов (иначе 409 `GROUP_NOT_EMPTY`). `POST …/related-groups/{id}/members` (тело `legal_entity_id`) SHALL добавлять текущее членство с `kind` группы, `valid_from = now`, `is_current = true`; ЮЛ MUST принадлежать той же ГК (иначе 422 `DIFFERENT_COMPANY_GROUP`); если у ЮЛ уже есть текущее членство в группе того же вида — 409 `MEMBER_OF_OTHER_GROUP` (с `related_group_id`); повтор в той же группе — 409 `MEMBER_EXISTS`. `DELETE …/members/{legal_entity_id}` SHALL закрывать членство (`valid_to`, `is_current = false`). Все операции пишут `MEMBERSHIP_CHANGE`. `GET …/related-groups` и `GET …/related-groups/{id}` SHALL отдавать группы с числом текущих членов и списком членов. #### Scenario: Одновременно в ГСК и ГСЗ - **WHEN** ЮЛ добавлено в ГСК своей ГК, затем в ГСЗ той же ГК - **THEN** обе операции успешны, у ЮЛ два текущих членства с `kind = gsk` и `kind = gsz` #### Scenario: Вторая ГСК запрещена - **WHEN** ЮЛ с текущим членством в ГСК A добавляют в ГСК B той же ГК - **THEN** ответ 409 `MEMBER_OF_OTHER_GROUP` с идентификатором ГСК A; данные не меняются #### Scenario: Повторное включение после исключения - **WHEN** членство закрыто, затем ЮЛ снова добавляют в ту же группу - **THEN** появляется новая строка `related_group_member` с `is_current = true`, старая остаётся с `valid_to` #### Scenario: Удаление непустой группы - **WHEN** удаляется ГСК с текущими членами - **THEN** ответ 409 `GROUP_NOT_EMPTY` ### Requirement: Права и аудит на всех маршрутах справочников Каждый маршрут SHALL требовать соответствующее право (`DIRECTORY_VIEW` для чтения, `DIRECTORY_EDIT` для изменений, `DIRECTORY_DELETE` для удаления ГСК/ГСЗ и алиасов) и применять политику видимости (`directory-policy`); каждая мутация SHALL выполняться в одной транзакции с записью `audit_event` (`user_id` инициатора, `before_value`/`after_value`, `ip_address`, `user_agent`), откат транзакции отменяет и аудит. #### Scenario: Нет права - **WHEN** пользователь с ролью `EXPERT` (только `DIRECTORY_VIEW`) создаёт ГК - **THEN** ответ 403, записи и аудита нет #### Scenario: Ошибка внутри мутации - **WHEN** при переводе ЮЛ закрытие членства завершается ошибкой БД - **THEN** ЮЛ остаётся в прежней ГК, строк истории и аудита нет