Merge pull request 'Срез 1: DBML v5.14, §13 импорт из ЕЦП CRM, §10.1 видимость, OQ-86…91' (#7) from feat/6-slice-1-directories into main

Reviewed-on: #7
This commit is contained in:
aep 2026-10-05 09:59:29 +03:00
commit f9c757ae97
8 changed files with 1556 additions and 3 deletions

View File

@ -359,6 +359,80 @@ Policy-слой (не права): KA_RF/RKA/CLIENT_MANAGER видят толь
допустимом статусе и при отсутствии чужой активной `consolidation_lock`; ЗО (`business_segment`) — фильтр видимости.
Матрица в ТЗ помечена «будет разработана» (сноска 29) — seed считать черновиком, редактируется администратором в UI.
### 10.1 Видимость справочников (Срез 1, без проверки кода роли)
Правило формулируется через данные, а не через роль (инвариант «права — данные»):
- Пользователь **головного офиса** (`app_user.regional_office → regional_office.is_head_office = true`) видит все ГК и все ЮЛ.
- Пользователь **филиала** видит ГК с `company_group.base_regional_office_id = app_user.regional_office_id`, их ЮЛ, ГСК/ГСЗ,
а также монозаёмщиков своего филиала (`legal_entity.company_group_id IS NULL AND legal_entity.regional_office_id = …`).
- Это совпадает с §10: KA_RF / RKA / CLIENT_MANAGER — пользователи филиалов, KA_GO / CURATOR_GO / EXPERT / ADMIN — ГО.
- ЗО (`business_segment`) в Срезе 1 **не фильтрует**: у ГК и ЮЛ нет поля сегмента (DBML v5.14), сегмент есть только у пользователя
**[открыто, OQ-86]**.
- Право (`DIRECTORY_VIEW`, `DIRECTORY_EDIT`, …) отвечает «может ли вообще», policy — «какие именно записи»; обе проверки
обязательны на каждом маршруте справочников. Запись вне видимости → 404 (не 403), чтобы не раскрывать существование.
---
## §13. Импорт справочников из выгрузки ЕЦП CRM (Срез 1; формат — `docs/source/biq/ecp-export-lagal-entity.xlsx`)
Источник: реальная выгрузка ЕЦП CRM (лист «Выписки_01.04.26», 8400 строк, 345 ГК, 53 значения «Базовый РФ»), ТЗ 6.1 (ГК из
ЕЦП CRM, создание вручную Куратором), макет [38]. Импорт запускает пользователь с правом `DIRECTORY_ADMIN`
(seed: `ADMIN`); результат — отчёт + `audit_event(DIRECTORY_IMPORT)` + файл в S3.
### 13.1 Формат файла
- xlsx (первый лист, первая непустая строка — заголовки) или csv (UTF-8, разделитель `;` или `,` — определяется по строке
заголовков). Заголовки сопоставляются по нормализованному тексту (нижний регистр, без пробелов, `ё` = `е`):
`ИНН`, `iD ГК`, `Базовый РФ ГК в CRM`, `Группа компаний`, `Организация`, `ГСК`, `ГСЗ`, `Тип`. Порядок колонок не важен,
лишние колонки игнорируются. Нет любой из `ИНН`, `iD ГК`, `Группа компаний`, `Организация`, `Тип` → файл отвергается целиком
(`MISSING_COLUMNS`). `Базовый РФ ГК в CRM`, `ГСК`, `ГСЗ` — необязательные.
- Значения: trim; `ИНН` — убрать пробелы; `Тип` — без регистра.
### 13.2 Строка → решение
| Шаг | Правило | Код в отчёте |
|---|---|---|
| Тип | `ЮЛ` / `ИП` → `entity_type`; иное (`нерез.`, `ФЛ`, `нет инн`, пусто) → строка пропускается **[открыто, OQ-87]** | `UNSUPPORTED_TYPE` |
| ИНН | ЮЛ — ровно 10 цифр, ИП — 12; иначе пропуск (контрольные разряды в Срезе 1 не проверяются **[открыто, OQ-88]**) | `INVALID_INN` |
| Дубль в файле | второй и далее ИНН в файле пропускается (в выгрузке: ИП и «ФЛ» с одним ИНН) | `DUPLICATE_IN_FILE` |
| РФ | по `regional_office.name` (точное после trim); нет → **создаётся** `regional_office(name, is_head_office=false)` с предупреждением; пусто → `base_regional_office_id = NULL`. Значения «ЦКБ» и «Головная организация» в выгрузке — **[открыто, OQ-89]**: до решения создаются как обычные филиалы | `RF_CREATED` |
| ГК | по `company_group.crm_id = iD ГК`: нет → создать (`code` генерируется `GK-NNNNNN`, `name`, `base_regional_office_id`, `valid_from = now`); есть → при стратегии `update` обновить `name`/`base_regional_office_id` c снимком в `company_group_history` и `audit_event(GROUP_CHANGE / ASSIGNMENT_CHANGE)`, при `skip` — оставить. Разные имена одной ГК в файле → ошибка ГК | `GK_CREATED`, `GK_UPDATED`, `GK_EXISTS`, `GK_NAME_CONFLICT` |
| ЮЛ | по `legal_entity.inn`: нет → создать (`entity_type`, `name`, `company_group_id`, `regional_office_id = base RF ГК`, `valid_from = now`); есть в этой же ГК → `update`: обновить `name`/`entity_type` со снимком `legal_entity_history(UPDATE)`, `skip`: оставить; есть **в другой ГК** → строка пропускается, перевод между ГК только явной операцией (§13.4) **[открыто, OQ-90]** | `LE_CREATED`, `LE_UPDATED`, `LE_EXISTS`, `LE_IN_OTHER_GROUP` |
| ГСК / ГСЗ | флаг = нормализованное `входит в гск` / `входит в гсз`; любое другое непустое значение → предупреждение и «не входит». Если у ГК есть хотя бы один член с флагом — создаётся (если нет) **одна** группа вида `gsk`/`gsz` с `name` = имя ГК и `code` `GSK-NNNNNN`/`GSZ-NNNNNN` **[открыто, OQ-91]**; член добавляется записью `related_group_member(kind, valid_from = now, is_current)`, если текущей нет. Отсутствие флага членство **не закрывает** (импорт не удаляет) | `GSK_CREATED`, `MEMBER_ADDED`, `UNKNOWN_FLAG` |
### 13.3 Транзакция, отчёт, след
- Один импорт — одна транзакция; ошибки строк не прерывают импорт (строка пропускается), прерывают только ошибки файла.
`dry_run = true` — тот же прогон с откатом, отчёт идентичен.
- Стратегия дублей — параметр `on_existing ∈ {skip, update}` (макет: «Пропустить существующие» — `skip` по умолчанию).
- Отчёт: счётчики (`rows`, `groups_created/updated/skipped`, `entities_created/updated/skipped`, `memberships_created`,
`offices_created`, `errors`) и построчный список `{row, inn, code, message}`. Отчёт возвращается в ответе и пишется в
`audit_event(action = DIRECTORY_IMPORT, section = DIRECTORIES, object_type = 'directory_import', after_value = отчёт,
description = имя файла, user_id = инициатор)`. Файл сохраняется в S3 как `uploads/imports/{audit_event_id}/{original_name}`
и регистрируется в `s3_object(bucket_role = uploads, created_by = user:{id})`; при `dry_run` файл не сохраняется.
- Повторный импорт того же файла идемпотентен при `skip` (все строки `*_EXISTS`).
### 13.4 Ручные операции справочника (вне импорта)
- **Перевод ЮЛ в другую ГК** (макет [79-01]): `legal_entity.company_group_id := новая`, снимок в `legal_entity_history(event_type =
MOVE_TO_GROUP, effective_date = дата перевода ≥ сегодня, OQ-21)`, все текущие членства ЮЛ в ГСК/ГСЗ старой ГК закрываются
(`valid_to`, `is_current = false`), `audit_event(MEMBERSHIP_CHANGE)`.
- **Вывод из состава ГК** (макет [80]): событие `CLOSE` (`close_date`, ЮЛ остаётся с `company_group_id` для истории? — нет:
`company_group_id := NULL`, `close_date := дата`) или `REMOVE_FROM_GROUP` (`company_group_id := NULL`, без `close_date`);
членства закрываются; `legal_entity_history(event_type, effective_date)`.
- **ГСК/ГСЗ:** ЮЛ может входить одновременно в одну ГСК и одну ГСЗ (v5.14), но только в группы **своей** ГК; добавление в
группу другой ГК → ошибка. Удаление группы — только пустой (без текущих членов) **[OQ-34]**.
- **Алиасы** (§3): `legal_entity_alias(alias, normalized_alias)`; `normalized_alias` уникален в пределах ЮЛ; конфликт
с алиасом другого ЮЛ (OQ-28/ANL-028) — ошибка с указанием ЮЛ-владельца.
- **ГК:** перезакрепление за другим филиалом — `audit_event(ASSIGNMENT_CHANGE)` + снимок `company_group_history`; архив —
`is_archived` (ГК с активными консолидациями архивировать нельзя — проверка появится в Срезе 4).
Все мутации пишут `audit_event` (`GROUP_CHANGE`, `MEMBERSHIP_CHANGE`, `ASSIGNMENT_CHANGE`, `DIRECTORY_IMPORT`) с `before/after`.
---
---
## §11. Маппинг JSON ИС Сканирование (FNR01/FNR02) → формы

View File

@ -30,14 +30,14 @@
| OQ-22 | Ретеншн `results/` в S3 (ARCH-010) | DBML | срез 2 | допущение: как `uploads/` |
| OQ-23 | Расшифровки (`DISCLOSURE`): разбирать или только хранить (ANL-040) | DBML | — | только хранить |
| OQ-24 | Форма 276-ОД — состав и шаблон | FNR03 3.4 | — | вне scope |
| OQ-25 | Состав ГСК для тестовой группы «Русский бисквит»: входит ли ОАО «ЧКФ», ООО «ОФГ», «Лидер» | фикстуры | тесты консолидации | спросить аналитиков; до ответа — периметр из двух компаний |
| OQ-25 | Состав ГСК для тестовой группы «Русский бисквит»: входит ли ОАО «ЧКФ», ООО «ОФГ», «Лидер» | фикстуры | тесты консолидации | **частично закрыт по выгрузке ЕЦП (04.10.2026):** ГК «Русский Бисквит» (`1-AZFL6BQ`, Санкт-Петербургский РФ) — 21 запись; флаг «входит в ГСК» только у АО «Русский бисквит» и ООО «Пекарня Счастливочная»; ЧКФ, ОФГ, Малечкинская птицефабрика, ТК РБ, Регион Финанс и др. — в ГК, но **не в ГСК** (срез — `fixtures/directories/ecp-export-russkiy-biskvit.xlsx`). Периметр тестовой консолидации — две компании; OQ-25 остаётся про то, верен ли состав ГСК в CRM |
| OQ-26 | Единицы: ББ/ОФР в тыс. руб., ОСВ в руб. — правило округления при элиминации (до тыс., банковское?) | — | срез 6 | допущение: считать в рублях, округлять до тыс. руб. на выходе в `kfo_result_line` (ROUND_HALF_UP) |
| OQ-27 | Поля ТЗ 5.1 (сценарий, подход, гудвилл) не в модалке создания консолидации | макет [42] vs ТЗ 5.1 | срез 4, 6 | **решено (владелец макета, 03.10)**: `approach = BASIC` всегда в первой очереди, EXTENDED позже — определять по набору загруженных документов; `goodwill_enabled` — переключатель на вкладке «Гудвилл»; `scenario = CONFIRM` включается через меню «…» консолидации в списке |
| OQ-28 | ~~Нет экрана проверки распознавания~~ — есть, открывается кликом по документу в панели «Первичные документы» (снимок `41`) | макет [41] | срез 3 | **закрыт**: реализовать по макету; для ОСВ (xlsx) левая панель — уточнить |
| OQ-29 | Макет: нет экранов пользователей / ролей / матрицы прав (ТЗ 8.1, `role_permission`); в журнале макета роли «Аналитик ДКБ», «Кредитный аналитик», «Утверждающий РФ», «Верификатор ГО», «Система распознавания / Сервис» (ANL-034, снимок `43-02`) | макет vs ТЗ 8.1 | срез 1 | допущение: минимальный экран администратора по §10 правил; роли — коды ТЗ 8.1, названия макета не используются |
| OQ-30 | Макет: шахматка и КФО показаны в тыс. руб. с 2 знаками, ОСВ — в рублях; где и как округлять (связано с OQ-26) | макет [30] [31-01] | срез 5–6 | допущение: хранить и считать в рублях, переключатель единиц — только отображение |
| OQ-31 | КПП ЮЛ: в макете колонка и поле форм (`50`, `61`, `51-02`, `79`), в DBML v5.13 у `legal_entity` поля нет. Нужен ли КПП (и в импорте, и для ИП, у которых его нет)? | макет [50] [51-02] | срез 1, DBML v5.14 | допущение: не вводить до решения; показывать «—» |
| OQ-32 | ЮЛ в макете состоит одновременно в ГСК и ГСЗ («Входит в ГСК» и «Входит в ГСЗ» заполнены, [79]); ТЗ 6.1 / ANL-030 — только в одной. Что верно? | макет [79] [51-02] | срез 1 (partial unique `related_group_member.legal_entity_id`) | допущение: по ТЗ — одна группа; форма допускает выбор только одного поля |
| OQ-31 | КПП ЮЛ: в макете колонка и поле форм (`50`, `61`, `51-02`, `79`), в DBML v5.13 у `legal_entity` поля нет. Нужен ли КПП (и в импорте, и для ИП, у которых его нет)? | макет [50] [51-02] | срез 1, DBML v5.14 | **закрыт решением владельца 04.10.2026: КПП не нужен** (DBML v5.14 без поля; импорт его не читает; формы — без КПП) |
| OQ-32 | ЮЛ в макете состоит одновременно в ГСК и ГСЗ («Входит в ГСК» и «Входит в ГСЗ» заполнены, [79]); ТЗ 6.1 / ANL-030 — только в одной. Что верно? | макет [79] [51-02] | срез 1 (partial unique `related_group_member.legal_entity_id`) | **закрыт решением владельца 04.10.2026: ЮЛ только в одной ГК, одновременно — в одной ГСК и одной ГСЗ.** DBML v5.14: `related_group_member.kind` + partial unique `(legal_entity_id, kind) WHERE is_current` (отменяет ANL-030); выгрузка ЕЦП это подтверждает (флаги «входит в ГСК» и «входит в ГСЗ» одновременно) |
| OQ-33 | «ID ГСК/ГСЗ в CRM» в формах (`51-03`, `51-04`, `77-02`; пример «3-9CGSK01» в формате ЕЦП CRM): у `related_group` в DBML только внутренний `code` (GSK-001). Есть ли у ГСК/ГСЗ идентификатор в CRM? | макет [77-02] | срез 1, DBML | допущение: поле формы = `related_group.code`, генерируется системой; `crm_id` не вводить |
| OQ-34 | «Удалить» ГСК в меню строки (`77-01`): физическое удаление или закрытие с историей? В DBML у `related_group` нет флага/истории удаления | макет [77-01] | срез 1 | допущение: удаление только пустой группы (без текущих участников), факт — в `audit_event(MEMBERSHIP_CHANGE)` |
| OQ-35 | База процента отклонения: в таблице корректировки % считается к сумме стороны ЮЛ-1 (−79,77 % для 1 000 ↔ 202,32, `31-02-02`), в «Подтверждении КФО клиента» — к версии банка (ст. 2, `31-06`); DBML и Методика (сноска 14) — к **меньшей** сумме. Что считать правильным для `correction.deviation_percent` и `client_kfo_line.deviation_percent`? | макет [31-02] [31-06] vs DBML, Методика | срезы 5–6 | допущение: к меньшей (Методика); макет считать неточным |
@ -91,6 +91,18 @@
| OQ-83 | Контейнеры профиля `services` подписаны на те же subject и queue group общего NATS, что и экземпляры в интеграционных тестах сервисов (`recognition.result` / `arm-core`, `*.request`): NATS может отдать сообщение теста контейнеру и наоборот; интеграционные тесты `scan-gateway` и `arm-core` одновременно тоже делят NATS. Сделать queue group и subject-префикс настраиваемыми для тестов (например `NATS_TEST_PREFIX`) или поднимать отдельный NATS на тесты? | scan-gateway: п. 11 таблицы предложений (T14.1, T11.8); docs: O-11 (задача 3 фазы 2; подтверждено ревью для tabular-parser и scan-gateway), `dev/README.md` | срез 0 (процесс), интеграционные тесты | допущение принято: в `dev/README.md` предупреждение и команда `stop` для контейнеров профиля; не гонять `make test-integration` двух сервисов одновременно |
| OQ-84 | Запуск сервисов из venv без Nexus (`make venv` + `python run_server.py` из `api/` с `.env` из `docs/dev/.env.example`, healthcheck по `localhost`) в Срезе 0 не проверялся ни в одном из трёх сервисов. Известно по чтению конфигов (design #44): `.env.example` в нынешнем виде отвергнут `scan-gateway` (`OPENBAO__DATABASE__*`) и `tabular-parser` (`OPENBAO__DATABASE__*`, `OPENBAO__SMB__*` — `extra_forbidden`), в нём нет `OPENBAO__MINIO__MINIO_SECURE=false` (по умолчанию `true` — https к http-MinIO). Прогнать сценарий, зафиксировать набор переменных и портов (`SERVER__PORT`, `SCAN_HOTFOLDER_PATH` для `scan-gateway`), решить, нужен ли отдельный `.env.example` на сервис, и снять пометку «черновик» с раздела `dev/README.md`? | spec `dev-environment`, сценарий «Запуск из venv против compose без Nexus»; docs: design #43, `dev/README.md` («Запуск сервисов из venv без Nexus»); финальное ревью фазы 2: I-1 | срез 0 (процесс, окружение разработки); кандидат в follow-up issue | вопрос: допущение Среза 0 — раздел README остаётся черновиком, сценарий не подтверждён (проверка требовала `.venv` внутри сервисных репозиториев, в фазе 2 доступных только для чтения) |
| OQ-85 | **Возможен ли JetStream на платформе** (серверный дедуп по `Nats-Msg-Id`, durable consumer'ы, redelivery вместо ack и повторной публикации на уровне приложения)? Сейчас NATS без JetStream (ответ платформы 03.10.2026); Срез 0 строит доставку на ack `processing`, повторной публикации ядром и дедупе по `recognition_job.result_msg_id`. Если JetStream подключить можно — меняются только publisher/subscriber (`core/nats.py` в трёх сервисах), outbox/inbox и контракт остаются; исчезают потеря результата при простое ядра и риск повторной обработки. | решение человека 04.10.2026: «узнать у платформы и проверить позже»; `design.md` slice-0-skeleton (D-3, Open Questions п. 4); OQ-72, OQ-73 | инфраструктура NATS (платформа); `core/nats.py`, воркеры сервисов | **вопрос к платформе (поставить напоминание, проверить после ревью Среза 0 / до Среза 3)**: допущение — без JetStream |
| OQ-86 | Фильтр видимости по ЗО (`business_segment`): сегмент есть только у `app_user`, у ГК/ЮЛ поля нет (DBML v5.14). Откуда брать сегмент ГК (из ЕЦП CRM? из РФ?), нужен ли вообще фильтр по ЗО в первой очереди? | §10.1 правил; DBML v5.14 | срез 1 (policy), DBML v5.15 | допущение: в Срезе 1 ЗО не фильтрует; видимость — только по филиалу (ГО видит всё) |
| OQ-87 | Выгрузка ЕЦП содержит типы `нерез.` (354), `ФЛ` (180), `нет инн` (2); enum `entity_type` — только ЮЛ/ИП. Нужны ли нерезиденты и физлица в справочнике (контрагенты ОСВ, поручители)? | `docs/source/biq/ecp-export-lagal-entity.xlsx`; §13.2 | срез 1 (импорт), DBML | допущение: строки пропускаются с кодом `UNSUPPORTED_TYPE`, enum не расширяется |
| OQ-88 | Проверять ли контрольные разряды ИНН при импорте и вводе (сейчас только длина 10/12); в выгрузке 279 нечисловых значений (`LV…`, `РНН…`, `нерез.`) | §13.2 | срез 1 | допущение: только длина; нечисловые → `INVALID_INN` |
| OQ-89 | «Базовый РФ ГК в CRM» в выгрузке содержит `ЦКБ` (1370 строк) и `Головная организация`: это головной офис (`regional_office.is_head_office`) или отдельные подразделения? Полный список — 53 значения, из них 51 «… РФ» | `ecp-export-lagal-entity.xlsx`; §13.2 | срез 1 (импорт, policy видимости) | допущение: создаются как обычные филиалы с именем из файла; маппинг на ГО — после ответа (миграция данных одной командой) |
| OQ-90 | Один ИНН в нескольких ГК выгрузки (215 случаев, напр. `7733288488` в `1-5AL0CC8` и `1-5A6WZN2`): какая ГК основная? `legal_entity.company_group_id` — одна | `ecp-export-lagal-entity.xlsx`; §13.2 | срез 1 (импорт) | допущение: первая встретившаяся ГК выигрывает, остальные строки — `LE_IN_OTHER_GROUP` в отчёте; перевод — вручную |
| OQ-91 | В выгрузке ЕЦП только флаги «входит в ГСК/ГСЗ» без идентификатора группы: у ГК предполагается одна ГСК и одна ГСЗ (в макете у «РМ Агро» две ГСК). Как ЕЦП кодирует несколько ГСК в одной ГК и даёт ли ГСК имя/ID (OQ-33)? | `ecp-export-lagal-entity.xlsx`; §13.2; макет [77] | срез 1 (импорт), срез 4 | допущение: импорт создаёт не более одной ГСК и одной ГСЗ на ГК с именем ГК; разделение — вручную Куратором/КА |
| OQ-92 | В выгрузке ЕЦП есть строки с непустым `iD ГК`, но пустыми «Организация»/«Группа компаний» (напр. строка 4034, ГК `1-KCKKHRK`). Импорт сейчас создаёт сущность с пустым именем. Пропускать такие строки или брать имя из других строк той же ГК? | `ecp-export-lagal-entity.xlsx`; §13.2; arm-core T9.6 | срез 1 (до боевого импорта) | допущение: импортируются как есть; исправление — повторный импорт с `update` |
| OQ-93 | ИНН в xlsx бывает числом — ведущий ноль теряется ещё в Excel (`0123456789` → `123456789`, 9 цифр → `INVALID_INN`). Считать ли такие ИНН восстанавливаемыми (дополнять нулём слева до 10/12)? | §13.1; arm-core T9.3 | срез 1 | допущение: не дополняем, строка → `INVALID_INN` |
| OQ-94 | Импорт с `on_existing = update` обновляет и архивные ГК (`is_archived`), не разархивируя их. Должен ли импорт разархивировать ГК, которая снова пришла из ЕЦП, или пропускать её? | §13.2; arm-core T10.7 | срез 1 | допущение: обновляется без разархивации |
| OQ-95 | Код `LE_IN_OTHER_GROUP` в отчёте импорта называет ЮЛ (ИНН, имя) из ГК, которую филиальный пользователь с `DIRECTORY_ADMIN` видеть не должен. Допустимо ли раскрытие в отчёте импорта (право по seed только у ADMIN ГО)? | §13.3; §10.1; arm-core T10.9 | срез 1 | допущение: отчёт импорта вне policy видимости, право даётся только ГО |
| OQ-96 | Граница «`effective_date` не раньше сегодня» (OQ-21) считается по UTC: для МСК правило сдвигается на 3 часа после полуночи. Какой часовой пояс — «банковский» МСК или UTC? | §13.4; arm-core R-9 | срез 1 | допущение: UTC |
| OQ-97 | Право `DIRECTORY_DELETE` по матрице §10 только у ADMIN: удалять алиасы и пустые ГСК/ГСЗ могут только администраторы, а не КА. Расширить право на KA_RF/KA_GO/CURATOR_GO? | §10; arm-core O-6 | срез 1 (seed) | допущение: матрица не меняется |
Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения;
`DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4.

1311
model/data-model-v5.14.dbml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
schema: spec-driven
created: 2026-10-04
skip_specs: true

View File

@ -0,0 +1,78 @@
# Design: Срез 1 (сквозное) — DBML v5.14, импорт ЕЦП, видимость
## Context
См. `proposal.md`. Факты, на которых стоит дизайн:
- Выгрузка ЕЦП (`docs/source/biq/ecp-export-lagal-entity.xlsx`, лист «Выписки_01.04.26»): 8400 строк, колонки `ИНН`,
`iD ГК`, `Базовый РФ ГК в CRM`, `Группа компаний`, `Организация`, `ГСК`, `ГСЗ`, `Тип`; 345 ГК; 53 значения РФ (51 «… РФ»,
плюс «ЦКБ» 1370 строк и «Головная организация»); типы ЮЛ 7395, ИП 469, `нерез.` 354, `ФЛ` 180, `нет инн` 2; ИНН: 10
цифр у ЮЛ, 12 у ИП, 279 нечисловых; 215 ИНН встречаются в нескольких ГК; флаги ГСК/ГСЗ — строки «входит в ГСК» /
«входит в ГСЗ» (плюс мусор: `0`, ИНН в колонке флага); идентификатора ГСК/ГСЗ нет; у 213 ГК есть члены ГСК (до 83).
- Решения владельца 04.10.2026: без UI справочника; КПП не нужен; ЮЛ в одной ГК, одновременно в одной ГСК и одной ГСЗ;
экран администратора минимальный.
- Инварианты: права — данные, код роли в логике не проверяется; первичка не мутируется; append-only аудит; поля вне DBML не вводить.
## Goals / Non-Goals
**Goals:** одна версия DBML, которую реализует миграция 0002; правила импорта, по которым можно написать тесты на реальной
выгрузке; правило видимости без кода роли. **Non-Goals:** см. proposal.
## Decisions
### D-1. v5.14: денормализованный `kind` в `related_group_member`
Уникальность «одна группа каждого вида на ЮЛ» нельзя выразить индексом через join; варианты — триггер или денормализация.
Выбрана денормализация (`kind` = `related_group.kind`, равенство проверяет ядро при вставке, тест на рассинхрон), индекс
`(legal_entity_id, kind) WHERE is_current`. *Альтернатива* — триггер: сложнее переносить, невидим в ORM. *Цена ошибки:*
убрать колонку и индекс миграцией.
### D-2. Версия DBML — новый файл, старый остаётся
`data-model-v5.14.dbml` рядом с v5.13 (история версий — файлы, changelog в шапке); `entities-v5.14.md` — копия с таблицей
изменений наверху; ссылки в CLAUDE.md репозиториев и `Makefile`/`DBML_VERSION` arm-core переводятся на v5.14 в дочернем change.
### D-3. Импорт — правила §13 как данные для тестов, строки не блокируют файл
Построчные коды (`UNSUPPORTED_TYPE`, `INVALID_INN`, `DUPLICATE_IN_FILE`, `RF_CREATED`, `GK_*`, `LE_*`, `GSK_CREATED`,
`MEMBER_ADDED`, `UNKNOWN_FLAG`) фиксируются в §13.2 и становятся контрактом отчёта и тестов `arm-core`; файл отвергается
только при отсутствии обязательных колонок или нечитаемости. Одна транзакция, `dry_run`, стратегия `on_existing ∈ {skip, update}`.
### D-4. Неоднозначности выгрузки — осторожные допущения + OQ, не угадывание
`нерез.`/`ФЛ` — пропуск (OQ-87); ИНН только по длине (OQ-88); «ЦКБ»/«Головная организация» — обычные филиалы (OQ-89);
ИНН в нескольких ГК — первая выигрывает, остальные в отчёт (OQ-90); одна неявная ГСК/ГСЗ на ГК с именем ГК (OQ-91).
Каждое допущение помечается `ASSUMPTION(OQ-nn)` в коде импортёра, все обратимы одной командой или повторным импортом с `update`.
### D-5. Видимость — по `regional_office.is_head_office`, без ролей
ГО видит всё, филиал — ГК своего РФ и своих монозаёмщиков; ЗО не фильтрует (OQ-86). Запись вне видимости — 404.
### D-6. Выгрузка хранится в `docs/source/biq/`, срез — в fixtures
Полная выгрузка — первоисточник формата и прогон «на всём объёме» (помечена как внутренняя: реальные ИНН); для unit-тестов —
21 строка ГК «Русский Бисквит» в `fixtures/directories/`. `fixtures/README` и `06-test-data.md` §6 обновляются.
## Risks / Trade-offs
- [Импорт всей выгрузки создаст 53 филиала и ~345 ГК в dev-БД] → `dry_run` первым, отчёт показывает `RF_CREATED`.
- [Денормализованный `kind` разойдётся с группой] → проверка в сервисе + тест; индекс всё равно защищает уникальность.
- [Допущения D-4 окажутся неверны] → данные исправимы повторным импортом с `update` или одной миграцией данных; OQ отслеживаются.
## Migration Plan
Миграция 0002 (arm-core): `ALTER TABLE related_group_member ADD kind` (заполнение из `related_group.kind` для существующих строк —
в dev их нет), замена индекса. Откат — обратная замена индекса и `DROP COLUMN`.
## Open Questions
OQ-86…OQ-91 (см. `07-open-questions.md`) — ни один не блокирует: приняты допущения D-4/D-5. Вопросы владельцу: «ЦКБ» и
«Головная организация» = ГО? нужны ли нерезиденты/ФЛ?
## Decisions made without discussion
| № | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
| 1 | 2026-10-04 | D-1: `kind` в `related_group_member` вместо триггера | индекс виден в ORM и сверке с DBML | миграция: убрать колонку |
| 2 | 2026-10-04 | D-2: новый файл v5.14, v5.13 остаётся | правило «новая версия DBML с changelog» | нет |
| 9 | 2026-10-04 | v5.14: два новых значения `audit_action` — `LEGAL_ENTITY_CHANGE`, `USER_CHANGE` | без них события ЮЛ/пользователей пришлось бы писать под чужим действием (`GROUP_CHANGE`) | удалить значения enum (миграция) |
| 3 | 2026-10-04 | D-3: коды отчёта импорта фиксированы в §13.2; строки не блокируют файл; `on_existing` skip/update; `dry_run` | макет [38] «Пропустить существующие»; реальная выгрузка грязная | добавить/переименовать коды |
| 4 | 2026-10-04 | D-4: пять допущений по неоднозначностям выгрузки (OQ-87…OQ-91) | нельзя угадывать; все обратимы | повторный импорт / миграция данных |
| 5 | 2026-10-04 | D-5: видимость по `is_head_office`, ЗО не фильтрует | права — данные; у ГК нет сегмента | добавить поле сегмента (DBML v5.15) и фильтр |
| 6 | 2026-10-04 | D-6: полная выгрузка в `docs/source/biq`, срез в fixtures | первоисточник формата vs тестовые данные | перенос файла |
| 7 | 2026-10-04 | Срез 1 без UI справочника, экран администратора минимальный | решение владельца 04.10.2026 | UI — отдельный срез |
| 8 | 2026-10-04 | Записи вне видимости отвечают 404, не 403 | не раскрывать существование чужих ГК | одна константа |

View File

@ -0,0 +1,53 @@
# Proposal: Срез 1 (сквозное) — DBML v5.14, правила импорта из ЕЦП CRM, политика видимости, OQ
**Срез:** 1 «Справочники» (`docs/context/09-delivery-plan.md`), сокращённый по решению владельца 04.10.2026: без UI
справочника, минимальный экран администратора — цель быстрее дойти до Среза 2 (`tabular-parser`).
**Репозиторий:** arm-ka-docs. **Issue:** RSHB/arm-ka-docs#6. **Дочерние change'и:** `arm-core/openspec/changes/slice-1-directories`
(RSHB/arm-core#5), фикстура — RSHB/arm-ka-fixtures#2.
## Why
Срез 1 требует двух решений, которые нельзя принять в коде молча: модель данных (ЮЛ одновременно в одной ГСК и одной
ГСЗ — DBML v5.13 это запрещает) и правила импорта реальной выгрузки ЕЦП CRM, формат которой появился только сейчас
(`docs/source/biq/ecp-export-lagal-entity.xlsx`, 8400 строк) и отличается от макета [38] и от предположений ТЗ. По
CLAUDE.md изменение модели идёт только через новую версию DBML с changelog, алгоритмы — только через `05-domain-rules.md`.
## What Changes
- **DBML v5.14** (`docs/model/data-model-v5.14.dbml`, v5.13 остаётся в истории): `related_group_member.kind`
(денормализация `related_group.kind`) и частичный уникальный индекс `(legal_entity_id, kind) WHERE is_current` вместо
`legal_entity_id` — ЮЛ может состоять одновременно в одной ГСК и одной ГСЗ (решение владельца, отменяет ANL-030);
КПП не вводится; `audit_action` + `LEGAL_ENTITY_CHANGE`, `USER_CHANGE` (ТЗ 8.2 требует журнал изменений ЮЛ и пользователей,
в v5.13 для них не было действия); уточнена семантика `company_group.crm_id` / `base_regional_office_id` по выгрузке ЕЦП.
`entities-v5.14.md`, `04-data-model-guide.md`, ссылки в `CLAUDE.md` репозиториев — на v5.14.
- **`05-domain-rules.md`:** новый **§13 «Импорт справочников из выгрузки ЕЦП CRM»** (формат файла, построчные правила,
коды отчёта, транзакция, след в `audit_event` и S3, ручные операции справочника: перевод в другую ГК, вывод из состава,
алиасы, ГСК/ГСЗ); **§10.1 «Видимость справочников»** — policy через данные (`regional_office.is_head_office`), без кода роли;
ЗО не фильтрует (OQ-86).
- **`07-open-questions.md`:** OQ-31 и OQ-32 закрыты решением владельца; OQ-25 частично закрыт по выгрузке (состав ГК
«Русский Бисквит»); новые OQ-86…OQ-91 (ЗО-фильтр, типы `нерез.`/`ФЛ`, контрольные разряды ИНН, «ЦКБ»/«Головная
организация», ИНН в нескольких ГК, одна ГСК/ГСЗ на ГК в выгрузке).
- **Выгрузка ЕЦП** `docs/source/biq/ecp-export-lagal-entity.xlsx` закоммичена как первоисточник формата (добавлена
владельцем); срез по ГК «Русский Бисквит» — фикстура `fixtures/directories/ecp-export-russkiy-biskvit.xlsx`.
- **`09-delivery-plan.md`:** Срез 1 по факту (без UI справочника; минимальный экран администратора; импорт из ЕЦП);
**`10-user-journey.md`:** пометка, что экраны справочника (50, 51, 61, 77–80) реализуются позже, API — в Срезе 1.
- **`08-source-docs-index.md`:** строка про выгрузку ЕЦП.
## Capabilities
Change документный: поведение описывается спеками дочернего change'а в `arm-core` (`directories-api`, `directory-import`,
`directory-policy`, `users-admin`). Здесь — `skip_specs: true`.
## Impact
- Таблицы DBML: `related_group_member` (новая колонка, новый индекс) — миграция 0002 в `arm-core`; остальные таблицы
частей 01–02 без изменений структуры.
- Правила: §13 — новый; §10.1 — уточнение §10; §3 (алиасы) — без изменений.
- Потребители: `arm-core` (ORM, миграция, `check_dbml_vs_orm.py` на v5.14, импорт, policy), `fixtures` (срез выгрузки).
## Non-goals
- UI справочника по макету (экраны 50, 51, 61, 77–80) — отдельный срез по решению владельца.
- Расширение `entity_type` (нерезиденты, ФЛ), контрольные разряды ИНН, маппинг «ЦКБ» на ГО — после ответов на OQ-87…OQ-89.
- Онлайн-интеграция с ЕЦП CRM — только файл.
- Контракт сервисов распознавания — не затрагивается.

View File

@ -0,0 +1,22 @@
# Tasks: Срез 1 (сквозное, docs)
Репозиторий — arm-ka-docs. Задачи ≤ половины дня. Дочерний change — `arm-core/openspec/changes/slice-1-directories`.
## 1. Модель и правила (сделано в brainstorming, проверить)
- [x] 1.1 `model/data-model-v5.14.dbml` — changelog v5.14, `related_group_member.kind`, индекс `(legal_entity_id, kind)`; проверка: 43 таблицы, 27 enum, diff с v5.13 только в этих местах
- [x] 1.2 `context/05-domain-rules.md` — §13 импорт из ЕЦП, §10.1 видимость; проверка: все коды отчёта §13.2 перечислены один раз
- [x] 1.3 `context/07-open-questions.md` — OQ-31/32 закрыты, OQ-25 уточнён, OQ-86…OQ-91 добавлены
- [x] 1.4 `source/biq/ecp-export-lagal-entity.xlsx` закоммичен (первоисточник формата, добавлен владельцем)
## 2. Документы под v5.14
- [ ] 2.1 `model/entities-v5.14.md` (копия v5.13 + таблица «Что изменилось в v5.14» наверху, строка `related_group_member` обновлена);
`context/04-data-model-guide.md` — файлы v5.14, строка про partial unique `(legal_entity_id, kind)`, таблица «Открытые места» (OQ-32 закрыт);
проверка: `grep -n "v5.13"` оставляет только исторические упоминания (changelog, диаграммы v5.12)
- [ ] 2.2 `CLAUDE.md` workspace и `docs/repo-templates/*.CLAUDE.md`: ссылки на `data-model-v5.14.dbml`/`entities-v5.14.md`; проверка: ссылки ведут на существующие файлы
- [ ] 2.3 `context/09-delivery-plan.md` Срез 1 по факту; `context/10-user-journey.md` — пометка про экраны справочника; `context/08-source-docs-index.md` — строка про выгрузку ЕЦП; `context/06-test-data.md` §6 — `directories/ecp-export-russkiy-biskvit.xlsx`; проверка: `openspec validate slice-1-directories --strict`
## 3. Закрытие
- [ ] 3.1 После merge дочернего change'а arm-core: сверить §13 с фактически реализованными кодами отчёта и допущениями `ASSUMPTION(OQ-8x)` в коде; расхождения — правкой §13 или OQ; проверка: список кодов в §13.2 = перечень в `arm-core/api/src/domain/directory_import.py`

Binary file not shown.