Andrey Epifancev 5682897ed0 OpenSpec change intercompany-matrix: требования шахматки из specs/ в дельты (#114)
Требования AURORA-1537, дописанные в openspec/specs/{audit-trail,consolidated-osv,
consolidations-api,directories-api}, перенесены в дельты change'а; specs/ возвращены к main.
Известные дефекты (AURORA-1651/1652/1653) зафиксированы в design.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:42:30 +04:00

18 KiB
Raw Blame History

directories-api Specification

Purpose

API справочников ядра: группы компаний (ГК), юридические лица и ИП с алиасами и историей, группы связанных компаний и заёмщиков (ГСК/ГСЗ) с составом, филиалы. Все изменения идут через сервисы с историей (SCD4/SCD2) и записью в audit_event; имена полей — как в текущей DBML (v5.18).

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, kpp; inn уникален (409 INN_EXISTS). kpp (DBML v5.17, 05-domain-rules.md §13.5) необязателен у ЮЛ и запрещён у ИП; нормализуется (пробелы убираются, буквы — в верхний регистр) и SHALL быть 9 символов [0-9]{4}[0-9A-Z]{2}[0-9]{3}, иначе 422 INVALID_KPP; пустая строка — «КПП нет»; уникальности у КПП нет. PATCH …/{id} SHALL менять name, entity_type, crm_id, regional_office_id, kpp (kpp: null очищает КПП; смена типа на ИП при непустом итоговом КПП — 422 INVALID_KPP) со снимком в 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. Во всех ответах ЮЛ, кроме снимков истории, SHALL быть company_group_name — текущее имя ГК company_group_id (архивной тоже) или null у монозаёмщика; имена набора ЮЛ — одним запросом. Во всех ответах ЮЛ, кроме снимков истории, SHALL быть memberships — текущие членства ЮЛ в ГСК/ГСЗ (related_group_id, code, kind, name, valid_from; по виду и коду группы; закрытые не отдаются; нет — []); членства набора ЮЛ — одним запросом (#109: фильтр ЮЛ по ГСК/ГСЗ в карточке ГК без запроса карточки каждого ЮЛ). Ответ POST и PATCH — карточка (LegalEntityDetail: поля списка + aliases); поля формы aliases, gsk_id, gsz_id — требование «Форма ЮЛ одним запросом».

Scenario: Неверная длина ИНН

  • WHEN создаётся ИП с ИНН из 10 цифр
  • THEN ответ 422 с кодом INVALID_INN, запись не создана

Scenario: КПП у ИП

  • WHEN создаётся ИП с kpp = "770101001" или ЮЛ с КПП из 8 символов
  • THEN ответ 422 с кодом INVALID_KPP, запись не создана

Scenario: Очистка КПП

  • WHEN у ЮЛ с КПП выполняется PATCH с {"kpp": null}
  • THEN kpp = NULL, прежнее значение — в снимке legal_entity_history(event_type = UPDATE) и в audit_event

Scenario: Монозаёмщик

  • WHEN ЮЛ создано без company_group_id
  • THEN оно отдаётся в списке с outside_group = true и видно пользователям филиала regional_office_id

Scenario: Членства в списке

  • WHEN ЮЛ ГК состоит в ГСК и ГСЗ, другое ЮЛ той же ГК — только в закрытом членстве ГСК
  • THEN в GET …?company_group_id=… у первого memberships — ГСК и ГСЗ, у второго — []

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/legal-entities и PATCH …/{id} SHALL принимать поля формы (макет 51-02, 79): aliases (список строк 1–500 символов, не больше 100 элементов; больше — 422 массивом), gsk_id, gsz_id — и применять их в той же транзакции, что и поля ЮЛ; ошибка на любом шаге откатывает весь запрос. Шаги и порядок ошибок: поля ЮЛ → алиасы → ГСК → ГСЗ. Алиасы и членства SHALL проверяться и аудироваться так же, как в ручках …/aliases и …/related-groups/{id}/members (EMPTY_ALIAS, ALIAS_TAKEN, DIFFERENT_COMPANY_GROUP, MEMBERSHIP_CHANGE, LEGAL_ENTITY_CHANGE по алиасу); два элемента aliases с одним normalized_alias — 409 ALIAS_EXISTS. Группа SHALL быть видимой (иначе 404, как несуществующая) и нужного вида: в gsk_id — ГСК, в gsz_id — ГСЗ (иначе 422 WRONG_GROUP_KIND). В POST не переданное поле — пустой список / без группы. В PATCH не переданное поле SHALL не меняться; aliases: null — 422; aliases — итоговый набор: алиасы сверяются по normalized_alias, совпавшие не трогаются (написание прежнее), недостающие добавляются, лишние удаляются — удаление требует права DIRECTORY_DELETE (иначе 403 «недостаточно прав: требуется DIRECTORY_DELETE», ничего не меняется); gsk_id/gsz_id — итоговая группа вида: null закрывает текущее членство вида, та же группа — без изменений, другая — текущее закрывается, новое открывается. Снимок legal_entity_history(UPDATE) пишется только при изменении полей ЮЛ.

Scenario: Создание с алиасами и группами

  • WHEN ЮЛ ГК создаётся с aliases = ["ООО «Ромашка»", "Ромашка-Юг"], gsk_id и gsz_id групп своей ГК
  • THEN ответ 201 с двумя алиасами и двумя текущими членствами; в аудите — создание ЮЛ, два события по алиасам и два MEMBERSHIP_CHANGE

Scenario: Чужой алиас откатывает создание

  • WHEN в aliases есть алиас, закреплённый за другим ЮЛ
  • THEN ответ 409 ALIAS_TAKEN с legal_entity_id владельца; ни ЮЛ, ни алиасов, ни членств, ни событий аудита

Scenario: Итоговый набор алиасов

  • WHEN у ЮЛ алиасы ООО «Альфа» и Бета, а PATCH администратора передаёт aliases = ["альфа", "Гамма"]
  • THEN ООО «Альфа» остаётся с прежним id и написанием, Бета удалён, Гамма добавлен; снимка истории нет

Scenario: Удаление алиаса без права

  • WHEN пользователь без DIRECTORY_DELETE передаёт aliases без одного из текущих алиасов
  • THEN ответ 403, ни поля ЮЛ, ни алиасы не меняются

Scenario: Смена ГСК и выход из ГСЗ

  • WHEN ЮЛ в ГСК A и ГСЗ C; PATCH с gsk_id = B, gsz_id = null
  • THEN членства в A и C закрыты, открыто членство в B; повтор того же тела ничего не меняет

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 отдавать группы с числом текущих членов и списком членов. Тела POST и PATCH SHALL принимать необязательный crm_id («ID ГСК/ГСЗ в CRM» форм 51-03/51-04/77-02) и отбрасывать его: колонки у related_group нет (arm-core #112); в ответах crm_id всегда null, в аудит и в список изменений PATCH не попадает.

Scenario: crm_id ГСК принимается и не сохраняется

  • WHEN ГСК создаётся с crm_id = "3-9CGSK01", затем PATCH передаёт только crm_id
  • THEN создание — 201 с crm_id = null; PATCH — 200 без изменений и без события аудита

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 ЮЛ остаётся в прежней ГК, строк истории и аудита нет