Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
11 KiB
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 — снимки.
Scenario: Неверная длина ИНН
- WHEN создаётся ИП с ИНН из 10 цифр
- THEN ответ 422 с кодом
INVALID_INN, запись не создана
Scenario: Монозаёмщик
- WHEN ЮЛ создано без
company_group_id - THEN оно отдаётся в списке с
outside_group = trueи видно пользователям филиалаregional_office_id
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 = "пекарня счастливочная", повторное добавлениеПекарня Счастливочная, ООО→ 409ALIAS_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 ЮЛ остаётся в прежней ГК, строк истории и аудита нет