diff --git a/context/00-workspace-layout.md b/context/00-workspace-layout.md index 56501c0..d348a30 100644 --- a/context/00-workspace-layout.md +++ b/context/00-workspace-layout.md @@ -67,7 +67,7 @@ git remote add origin && git push -u origin main | Слой | Где | Статус | Правило для агента | |---|---|---|---| -| Модель данных | `docs/model/data-model-v5.13.dbml` | стабильный, версионируется | единственный источник имён и структур | +| Модель данных | `docs/model/data-model-v5.14.dbml` | стабильный, версионируется | единственный источник имён и структур | | Правила домена | `docs/context/05-domain-rules.md` | курируемый — пишет человек/агент по итогам ревью | **единственный вход алгоритмов в код**; из ТЗ напрямую не кодируем | | Открытые вопросы | `docs/context/07-open-questions.md` | живой реестр | каждое допущение в коде — `ASSUMPTION(OQ-nn)` | | Экспорт ТЗ | `docs/source/tz/` | снимок на дату из `docs/source/tz/_export.md` | читать для контекста; расхождение с §05 → вопрос, не код | diff --git a/context/03-architecture.md b/context/03-architecture.md index fa73c7f..f05707f 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -1,4 +1,4 @@ -# 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.13) +# 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.14) ## Компоненты (= репозитории, см. `00-workspace-layout.md`) @@ -120,7 +120,7 @@ api/src/ # машины статусов, policy — pydantic-модели на входе и выходе services/ # use-cases: оркестрация domain + repositories + outbox + audit_event repositories/ # SQLAlchemy-запросы, в т. ч. запросы-вычисления (сводная ОСВ, шахматка) - models/ # ORM 1:1 с DBML v5.13; имена таблиц/колонок/enum — как в DBML + models/ # ORM 1:1 с DBML v5.14; имена таблиц/колонок/enum — как в DBML schemas/ # pydantic DTO API /api/v1 core/ # config, clients (S3; NATS outbox-publisher и result-consumer), auth (JWT РАИСА / dev-эмитент), # logger, exception_handlers @@ -205,7 +205,7 @@ FastAPI» платформы РАИСА/aurora): дерево импортиро `version_table_schema` и `include_name` Alembic). Следуем шаблону, не `public`. - Alembic берёт URL из настроек, не из `alembic.ini`; ревизии — `api/alembic/versions/`, имя файла стандартное `_.py`. Автогенерация — только черновик: итоговую ревизию сверяет с DBML `api/scripts/check_dbml_vs_orm.py`. -- PK везде `uuid` через `UUIDPrimaryMixin`; имена таблиц, колонок и enum'ов — ровно как в DBML v5.13. +- PK везде `uuid` через `UUIDPrimaryMixin`; имена таблиц, колонок и enum'ов — ровно как в DBML v5.14. ### Инфраструктурные клиенты diff --git a/context/04-data-model-guide.md b/context/04-data-model-guide.md index 32880a8..f9660e8 100644 --- a/context/04-data-model-guide.md +++ b/context/04-data-model-guide.md @@ -1,8 +1,9 @@ -# 04. Как читать модель данных v5.13 +# 04. Как читать модель данных v5.14 -Файлы: `docs/model/data-model-v5.13.dbml` (источник истины), `docs/model/entities-v5.13.md` (смысл таблиц), +Файлы: `docs/model/data-model-v5.14.dbml` (источник истины), `docs/model/entities-v5.14.md` (смысл таблиц; предыдущие +версии `data-model-v5.13.dbml` / `entities-v5.13.md` остаются как история), `docs/model/diagrams/part01…09.svg` (диаграммы v5.12 — на одну версию старше: нет `permission`/`role_permission`, -нет `document`, есть `osv_group_schema`; читать с поправкой на changelog v5.13 в шапке DBML). +нет `document`, есть `osv_group_schema`, нет `related_group_member.kind`; читать с поправкой на changelog v5.13 и v5.14 в шапке DBML). ## Девять частей модели @@ -24,7 +25,9 @@ допустима замена на `bigint identity` при объёмах. - Денежные поля `numeric(18,2)`, проценты `numeric(7,4)`/`numeric(9,4)`, confidence `numeric(5,2)` (0–100). - Временные метки `timestamptz`; отчётные даты `date`. -- Партиальные уникальные индексы (помечены в notes): `related_group_member.legal_entity_id WHERE is_current`, +- Партиальные уникальные индексы (помечены в notes): `related_group_member (legal_entity_id, kind) WHERE is_current` + (v5.14: ЮЛ — не более одной текущей группы каждого вида, т.е. одна ГСК и одна ГСЗ одновременно; `kind` — денормализация + `related_group.kind`), `consolidation_lock.consolidation_version_id WHERE released_at IS NULL`, `document (stored_file_id, document_type, osv_account_code) NULLS NOT DISTINCT`. - Enum'ы PostgreSQL — ровно как в DBML (`consolidation_status`, `document_status`, `audit_action` …). Добавление значения — @@ -51,7 +54,10 @@ ## v4 → v5.13: таблица переименований (обязательна при чтении ТЗ) -| В страницах ТЗ (модель v4 и старше) | В v5.13 | +v5.14 имена таблиц и полей не менял (добавлены `related_group_member.kind` и два значения `audit_action`), поэтому таблица +справедлива и для v5.14. + +| В страницах ТЗ (модель v4 и старше) | В v5.13 / v5.14 | |---|---| | `stored_file.period_year / period_quarter / document_type / osv_account` | `document.period_year / period_quarter / document_type / osv_account_code / is_cumulative` | | `stored_file.status` «Загружен / Обработка / Требуется проверка / Ошибка» | техника: `stored_file.status` UPLOADED/PROCESSING/PROCESSED/ERROR/SUPERSEDED; проверка: `document.status` RECOGNIZED/NEEDS_CHECK/CONFIRMED/ERROR | @@ -73,6 +79,9 @@ ## Открытые места модели (не придумывать, спросить) +- **Закрыто в v5.14:** OQ-32 (членство в ГСК и ГСЗ одновременно) — решение владельца 04.10.2026: ЮЛ в одной ГК и одновременно + в одной ГСК и одной ГСЗ (ANL-030 отменён); OQ-31 (КПП) — не вводится. + - **№2 `osv_entry.parent_entry_id`**: хранить ли все строки ОСВ (п. 11 «Парсинг ОСВ» — да) или только контрагентские; нужно ли дерево. Рекомендация для среза 2: хранить **все** строки с `level` и `parent_entry_id` — это дёшево и позволяет сверять «Σ контрагентов = итог счёта»; вопрос закрывать с аналитиками. diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index 7f2d1e7..9bfb98b 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -1,4 +1,4 @@ -# 05. Правила предметной области (выжимка из ТЗ, имена — v5.13) +# 05. Правила предметной области (выжимка из ТЗ, имена — v5.14) Каждый раздел: источник в ТЗ → правило → где в модели. Где ТЗ молчит или противоречит — пометка **[открыто]** и ссылка на `07-open-questions.md`. @@ -387,31 +387,64 @@ Policy-слой (не права): KA_RF/RKA/CLIENT_MANAGER видят толь `ИНН`, `iD ГК`, `Базовый РФ ГК в CRM`, `Группа компаний`, `Организация`, `ГСК`, `ГСЗ`, `Тип`. Порядок колонок не важен, лишние колонки игнорируются. Нет любой из `ИНН`, `iD ГК`, `Группа компаний`, `Организация`, `Тип` → файл отвергается целиком (`MISSING_COLUMNS`). `Базовый РФ ГК в CRM`, `ГСК`, `ГСЗ` — необязательные. -- Значения: trim; `ИНН` — убрать пробелы; `Тип` — без регистра. +- Значения: trim; `ИНН` — убрать все пробельные символы; `Тип` — без регистра. Пустые строки пропускаются. Число в ячейке + xlsx читается как целое без дробной части (`3528052718.0` → `3528052718`); **ведущий ноль, потерянный ещё в Excel + (`0123456789` → `123456789`), не восстанавливается** — строка получит `INVALID_INN` (ИНН нужно задавать текстом или + использовать csv) **[открыто, OQ-93]**. +- Технические пределы файла (не правила предметной области; нарушение — ошибка файла целиком, §13.3): размер ≤ 20 МиБ + (`IMPORT_MAX_BYTES`, ответ 413), не более 100 000 строк листа/записей csv **включая пустые** (`MAX_IMPORT_ROWS`), суммарный + распакованный размер частей xlsx ≤ 200 МиБ (`MAX_UNCOMPRESSED_BYTES`); допустимы только расширения `xlsx` и `csv`. ### 13.2 Строка → решение +Строки обрабатываются в порядке файла; **порядок проверок внутри строки**: тип → ИНН → дубль ИНН в файле → ГК (пустой +`iD ГК`, конфликт имени) → РФ → ГК (создание/обновление) → ЮЛ → флаги ГСК/ГСЗ. У каждой строки ровно один **итоговый** код +(пять кодов пропуска — `UNSUPPORTED_TYPE`, `INVALID_INN`, `DUPLICATE_IN_FILE`, `LE_IN_OTHER_GROUP`, `GK_NAME_CONFLICT`, — +и `LE_CREATED` / `LE_UPDATED` / `LE_EXISTS`) и ноль или более **сопутствующих** событий той же строки (`RF_*`, `GK_*`, +`GSK_*`/`GSZ_*`, `MEMBER_ADDED`, `UNKNOWN_FLAG`). Счётчик `errors` = число строк с кодом пропуска. + | Шаг | Правило | Код в отчёте | |---|---|---| | Тип | `ЮЛ` / `ИП` → `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` | +| Дубль в файле | второй и далее ИНН в файле пропускается. ИНН считается «встречавшимся» только после успешных проверок типа и формата, поэтому «ФЛ», идущая после ИП с тем же ИНН, получает `UNSUPPORTED_TYPE`, а не `DUPLICATE_IN_FILE` | `DUPLICATE_IN_FILE` | +| РФ | по `regional_office.name` (точное после trim); нет → **создаётся** `regional_office(name, is_head_office=false)` с предупреждением (один раз на имя в файле); пусто → `base_regional_office_id = NULL`. РФ разрешается один раз на ГК — по первой строке ГК и только после проверок пустого `iD ГК` и конфликта имени (отвергнутая строка филиал не создаёт); у **существующей** ГК при `skip` филиал из файла всё равно создаётся, но ГК не меняется. Значения «ЦКБ» и «Головная организация» в выгрузке — **[открыто, OQ-89]**: до решения создаются как обычные филиалы | `RF_CREATED` | +| ГК | **пустой `iD ГК`** → строка пропускается (`GK_NAME_CONFLICT`, сообщение «пустой iD ГК»). По `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_EXISTS` ставится и для неизменной ГК при `update`** (нет различий в имени и РФ — нечего обновлять; это обеспечивает идемпотентность). Разные имена одной ГК в файле (имя первой строки против последующих) → ошибка ГК, строка пропускается. Архивные ГК (`is_archived`) входят в сопоставление и при `update` обновляются **без разархивации** **[открыто, OQ-94]**. `update` пишет снимок `company_group_history` и событие `audit_event` теми же функциями, что ручная правка (PATCH); смена только имени — `GROUP_CHANGE`, смена РФ (в т.ч. вместе с именем) — одно `ASSIGNMENT_CHANGE` | `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]**; сообщение называет имя ЮЛ, независимо от филиала инициатора — импорт доступен только `DIRECTORY_ADMIN` **[открыто, OQ-95]**. `regional_office_id` нового ЮЛ = действующий РФ ГК. `LE_UPDATED` пишет снимок `legal_entity_history(UPDATE)` и `audit_event(LEGAL_ENTITY_CHANGE)` как ручная правка; `LE_EXISTS` — и при `skip`, и при `update` без различий | `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)`, если текущего членства **этого вида** (в этой или другой группе) нет — существующее членство импорт не меняет и не закрывает. Флаги обрабатываются **и при `skip`** (недостающее членство добавляется у существующих ЮЛ). Группа создаётся по самому факту флага, даже если у ЮЛ уже есть членство того же вида в другой группе (возможна пустая группа). Значение, не равное ожидаемому для колонки (в т.ч. «входит в ГСЗ» в колонке `ГСК`), — `UNKNOWN_FLAG` (предупреждение, не ошибка: «не входит»). Отсутствие флага членство **не закрывает** (импорт не удаляет) | `GSK_CREATED`, `GSZ_CREATED`, `MEMBER_ADDED`, `UNKNOWN_FLAG` | + +**Перечень кодов строки (16; совпадает с `ImportCode` в `arm-core/api/src/domain/directory_import.py`):** + +| Код | Роль в строке | Счётчик отчёта | +|---|---|---| +| `UNSUPPORTED_TYPE`, `INVALID_INN`, `DUPLICATE_IN_FILE`, `LE_IN_OTHER_GROUP`, `GK_NAME_CONFLICT` | итоговый, строка пропущена | `errors` | +| `LE_CREATED`, `LE_UPDATED`, `LE_EXISTS` | итоговый | `entities_created`, `entities_updated`, `entities_skipped` | +| `RF_CREATED` | сопутствующий | `offices_created` | +| `GK_CREATED`, `GK_UPDATED`, `GK_EXISTS` | сопутствующий | `groups_created`, `groups_updated`, `groups_skipped` | +| `GSK_CREATED`, `GSZ_CREATED` | сопутствующий (создана группа) | — | +| `MEMBER_ADDED` | сопутствующий | `memberships_created` | +| `UNKNOWN_FLAG` | сопутствующий (предупреждение) | — | + +Допущения по неоднозначностям выгрузки — OQ-87…OQ-91 (в коде — `ASSUMPTION(OQ-nn)`); вопросы, появившиеся при реализации, — +OQ-92 (пустые «Организация»/«Группа компаний» при непустом `iD ГК` импортируются как есть, с пустым именем), OQ-93, OQ-94, OQ-95. ### 13.3 Транзакция, отчёт, след - Один импорт — одна транзакция; ошибки строк не прерывают импорт (строка пропускается), прерывают только ошибки файла. - `dry_run = true` — тот же прогон с откатом, отчёт идентичен. -- Стратегия дублей — параметр `on_existing ∈ {skip, update}` (макет: «Пропустить существующие» — `skip` по умолчанию). + `dry_run = true` — тот же прогон с откатом, отчёт идентичен; **в API `dry_run` по умолчанию `true`** (запись — явным + `dry_run=false`). Номера `GK-`/`GSK-`/`GSZ-` из последовательностей PostgreSQL при откате расходуются (дыры в нумерации + допустимы). Ошибки файла (прерывают импорт до любых записей, ответ 422): `MISSING_COLUMNS` (с перечнем недостающих + колонок) и `UNSUPPORTED_FILE` (расширение не `xlsx`/`csv`, повреждённый xlsx, csv не в UTF-8, превышены лимиты строк и + распакованного размера §13.1); размер файла больше 20 МиБ — 413. +- Стратегия дублей — параметр `on_existing ∈ {skip, update}` (макет: «Пропустить существующие» — `skip` по умолчанию); `update` обновляет у ГК имя и РФ (РФ — только непустым значением из файла), у ЮЛ — имя и тип. - Отчёт: счётчики (`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`). + и регистрируется в `s3_object(bucket_role = uploads, created_by = user:{id})` (`size_bytes`, `sha256`, `content_type` по расширению); имя файла приводится к basename (путь отбрасывается, управляющие и форматирующие символы убираются, длина ≤ 200 символов с сохранением расширения; пустое → `import`). Запись `s3_object` создаётся **до** `put_object` (сбой S3 откатывает импорт); сам объект кладётся до commit транзакции, поэтому при сбое commit в бакете может остаться объект без записи (зачистка — follow-up arm-core). При `dry_run` файл не сохраняется и `DIRECTORY_IMPORT` не пишется. Без подключённого S3 (dev) файл не сохраняется, в лог пишется предупреждение. +- Помимо сводного `DIRECTORY_IMPORT` импорт при `update` пишет **собственные** события изменения объектов — `GROUP_CHANGE`/`ASSIGNMENT_CHANGE` (ГК), `LEGAL_ENTITY_CHANGE` (ЮЛ) с `description = «импорт ЕЦП»`; создания объектов отдельных событий не порождают (только сводное, снимок в `*_history` при создании не пишется — он появляется при первом изменении). +- Повторный импорт того же файла идемпотентен при `skip`: ранее принятые строки получают `GK_EXISTS`/`LE_EXISTS`, новых `MEMBER_ADDED` нет; ранее пропущенные (ошибочные) строки остаются ошибками. +- Лимит тела запроса (20 МиБ на пути импорта) обязан ставить ingress: multipart разбирается до аутентификации (требование к платформе, `arm-core/README.md`, M-1). ### 13.4 Ручные операции справочника (вне импорта) @@ -428,7 +461,7 @@ Policy-слой (не права): KA_RF/RKA/CLIENT_MANAGER видят толь - **ГК:** перезакрепление за другим филиалом — `audit_event(ASSIGNMENT_CHANGE)` + снимок `company_group_history`; архив — `is_archived` (ГК с активными консолидациями архивировать нельзя — проверка появится в Срезе 4). -Все мутации пишут `audit_event` (`GROUP_CHANGE`, `MEMBERSHIP_CHANGE`, `ASSIGNMENT_CHANGE`, `DIRECTORY_IMPORT`) с `before/after`. +Все мутации пишут `audit_event` (`GROUP_CHANGE`, `ASSIGNMENT_CHANGE`, `MEMBERSHIP_CHANGE`, `LEGAL_ENTITY_CHANGE` — v5.14, `DIRECTORY_IMPORT`) с `before/after`. --- diff --git a/context/06-test-data.md b/context/06-test-data.md index 3c3916b..29dac97 100644 --- a/context/06-test-data.md +++ b/context/06-test-data.md @@ -156,10 +156,30 @@ fixtures/ Консолидация_Группа РБ.xlsx # эталон КФО по датам expected_osv_parse/*.json # ожидаемый результат парсера (зафиксировать после ревью) directories/ - legal_entities.csv, company_groups.csv # тестовые справочники группы «Русский бисквит» (Срез 1) + ecp-export-russkiy-biskvit.xlsx # срез выгрузки ЕЦП CRM по ГК «Русский Бисквит» (Срез 1) ``` -`directories/` содержит только тестовые справочники группы (ЮЛ и ГК «Русский бисквит»); до Среза 1 в нём лежит пустой -`.gitkeep`. Справочники **продукта** — план счетов, правила элиминации, права и роли, состав обязательных документов, +`directories/` содержит срез выгрузки ЕЦП CRM по группе «Русский бисквит» (Срез 1, §6.1). + +### 6.1 `directories/ecp-export-russkiy-biskvit.xlsx` — фикстура импорта справочников (Срез 1) + +Срез реальной выгрузки ЕЦП CRM (`docs/source/biq/ecp-export-lagal-entity.xlsx`, лист «Выписки_01.04.26»; формат импорта — +`05-domain-rules.md` §13) по одной ГК. Тот же формат колонок: `ИНН`, `iD ГК`, `Базовый РФ ГК в CRM`, `Группа компаний`, +`Организация`, `ГСК`, `ГСЗ`, `Тип`. Используется тестами импорта `arm-core` (`tests/` Среза 1). + +| Что | Значение | +|---|---| +| Строк данных | 21 (+ строка заголовков) | +| ГК | одна: «Русский Бисквит», `iD ГК` = `1-AZFL6BQ`; базовый РФ — «Санкт-Петербургский РФ» | +| Импортируется | 20 ЮЛ/ИП: 18 ЮЛ + 2 ИП | +| Пропускается | 1 строка типа `ФЛ` (ИНН совпадает с ИП `352819549855`) → `UNSUPPORTED_TYPE` (проверка типа идёт раньше проверки дубля, поэтому `DUPLICATE_IN_FILE` в этом файле не возникает) | +| Члены ГСК (флаг «входит в ГСК») | АО «Русский бисквит» `3528052718` и ООО «Пекарня Счастливочная» `3528104476` → одна ГСК «Русский Бисквит» | +| ГСЗ | флагов нет — ГСЗ не создаётся | +| Кандидаты OQ-25 | ЧКФ, ОФГ, Малечкинская птицефабрика, ТК РБ — в составе ГК, но без флага ГСК | + +Ожидаемый отчёт на пустой БД: `rows = 21`, `groups_created = 1`, `offices_created = 1`, `entities_created = 20`, +`memberships_created = 2`, `errors = 1`. Полная выгрузка (8400 строк, 345 ГК) в `fixtures/` не копируется — лежит в +`docs/source/biq/` (внутренние данные) и нужна для прогона импорта целиком. + Справочники **продукта** — план счетов, правила элиминации, права и роли, состав обязательных документов, региональные офисы — не фикстуры, а seed ядра: `arm-core/api/seed/core/` (обязательные данные: `roles.yaml`, `permissions.yaml`, `role_permissions.yaml`, `regional_office.yaml`, `chart_of_accounts.csv`, `elimination_rules_v1.yaml`, `required_document_rules.yaml`) и `arm-core/api/seed/dev/` (только `python -m scripts.seed --dev`: dev-пользователи и diff --git a/context/08-source-docs-index.md b/context/08-source-docs-index.md index 70da2a9..682de2e 100644 --- a/context/08-source-docs-index.md +++ b/context/08-source-docs-index.md @@ -40,6 +40,7 @@ | ТЗ бизнес-инициативы BIQ-34 «…создание Единой базы финансовых данных клиентов и автоматизация процесса консолидации отчетности» | основное ТЗ АРМ КА: разделы 5.1 (сценарии/подходы), 6.1–6.10 (функции), 7 (нефункциональные), 8.1–8.4 (роли, журнал, хранение), Прил. 2 (сущности), Прил. 3 (статьи ББ и пары счетов), Прил. 4 (ОФР), Прил. 8 (гудвилл), Прил. 9 (корректировки), Прил. 10 (КФО) | «ТЗ 6.4», «Прил. 3», «сноска 29» | | ТЗ BIQ-34649 «…создание Единого хранилища финансовых данных клиентов» | ЕХ: разделы 5.2–5.3, 6.1–6.7, 7, 8.3 | «ЕХ ТЗ 6.2», «ЕХ 7» | | Методика КФО ГСК (ред. 10.03.2026, правки приняты) | методология консолидации, пороги, правило «процент к наименьшему» (сноска 14), подходы | «Методика» | +| `source/biq/ecp-export-lagal-entity.xlsx` — выгрузка ЕЦП CRM (лист «Выписки_01.04.26») | реальная выгрузка справочника клиентов: 8400 строк, колонки `ИНН`, `iD ГК`, `Базовый РФ ГК в CRM`, `Группа компаний`, `Организация`, `ГСК`, `ГСЗ`, `Тип`; 345 ГК. Первоисточник формата импорта `05-domain-rules.md` §13. Добавлена владельцем 04.10.2026; **внутренние данные** (реальные ИНН и названия клиентов) — наружу не передавать, в тесты берётся срез `fixtures/directories/` | «выгрузка ЕЦП», §13 | Конвертировать в markdown (`pandoc --to gfm --wrap=none`) и положить рядом с docx — агенту удобнее искать по тексту. @@ -47,8 +48,8 @@ | Файл | Что | |---|---| -| `data-model-v5.13.dbml` | источник истины; changelog v5.1…v5.13 в шапке; блоки «ИСТОЧНИК» над каждой таблицей | -| `entities-v5.13.md` | описание сущностей по частям 01–09, сценарий «как читать модель», открытые вопросы | +| `data-model-v5.14.dbml` | источник истины; changelog v5.1…v5.14 в шапке; блоки «ИСТОЧНИК» над каждой таблицей (v5.13 остаётся рядом как история) | +| `entities-v5.14.md` | описание сущностей по частям 01–09, таблицы «Что изменилось в v5.14 / v5.13», сценарий «как читать модель», открытые вопросы (v5.13 остаётся как история) | | `diagrams/part01…09.{svg,png}` | диаграммы v5.12 (на шаг старше) | | `data-model-part1_4.dbml` | v4 часть 1 — историческое | diff --git a/context/09-delivery-plan.md b/context/09-delivery-plan.md index eda18db..36683b7 100644 --- a/context/09-delivery-plan.md +++ b/context/09-delivery-plan.md @@ -40,15 +40,37 @@ - DoD: `GET /api/v1/me` возвращает пользователя и его эффективные права; все миграции применяются и откатываются; `make check-full` зелёный в трёх репозиториях. -## Срез 1 — Справочники (ГК / ЮЛ / ГСК / ГСЗ / филиалы) + пользователи и роли +## Срез 1 — Справочники (ГК / ЮЛ / ГСК / ГСЗ / филиалы) + пользователи и роли — **завершён 05.10.2026** -- CRUD ГК (с `code`, `crm_id` nullable, закрепление за РФ, архив, история SCD4), ЮЛ/ИП (ИНН 10/12, алиасы, перевод в - другую ГК с `legal_entity_history.event_type/effective_date`, закрытие), ГСК/ГСЗ и состав (SCD2, одна группа на ЮЛ). -- Импорт из файла (xlsx/csv) с логом ошибок и стратегией дублей; `audit_event(DIRECTORY_IMPORT)`. -- Экран администратора: пользователи, роли, матрица прав (`ROLE_PERMISSION_CHANGE`). -- Policy-слой: видимость ГК по филиалу/ЗО. -- Фикстуры: ГК «Русский бисквит» с двумя ЮЛ (+ кандидаты из OQ-25), ГСК из двух. -- UI: справочник по макету (левая панель ГК, правая — ЮЛ/ГСК/ГСЗ, модалки). +Объём — **вариант Б** по решению владельца 04.10.2026: без UI справочника, с минимальным экраном администратора; цель — +быстрее дойти до Среза 2 (`tabular-parser`). Изначальный состав среза (CRUD + импорт + админка + policy + фикстуры + +UI справочника по макету) разделён: всё, кроме UI справочника, сделано; UI вынесен в отдельный срез. + +**Сделано (по факту):** + +- **API справочников** (`arm-core`, `/api/v1/directories/*`): ГК (`code`, `crm_id` nullable, закрепление за РФ, архив, + история SCD4), ЮЛ/ИП (ИНН 10/12, алиасы, перевод в другую ГК с `effective_date`, закрытие/вывод из ГК, история), + ГСК/ГСЗ и состав (SCD2; ЮЛ — одна ГСК и одна ГСЗ одновременно), филиалы (чтение). Каждая мутация пишет `audit_event`. +- **Импорт из выгрузки ЕЦП CRM** (`POST /directories/import`, право `DIRECTORY_ADMIN`): правила — `05-domain-rules.md` §13, + `dry_run` по умолчанию, `on_existing ∈ {skip, update}`, отчёт с кодами, `audit_event(DIRECTORY_IMPORT)` и файл в S3; + полная выгрузка (8400 строк) обрабатывается за ~1 с. +- **Policy видимости** (§10.1): по `regional_office.is_head_office`, без проверки кода роли; ЗО не фильтрует (OQ-86); + объект вне видимости — 404. +- **Админ-API и минимальный экран `/admin`** («Пользователи и роли»): пользователи, роли, матрица прав + (`USER_CHANGE`, `USER_ROLE_CHANGE`, `ROLE_PERMISSION_CHANGE`), защита `LAST_ADMIN`; вход — dev-форма (SSO РАИСА — позже). +- **DBML v5.14** (`related_group_member.kind`, partial unique `(legal_entity_id, kind)`, `audit_action` + + `LEGAL_ENTITY_CHANGE`/`USER_CHANGE`) и **миграция `0002`** в `arm-core`; `entities-v5.14.md`. +- **Фикстура** `fixtures/directories/ecp-export-russkiy-biskvit.xlsx` (21 строка ГК «Русский Бисквит»; `06-test-data.md` §6.1). +- **Документы:** §13 и §10.1 в `05-domain-rules.md` (сверены с кодом, задача 3.1 change `slice-1-directories`), OQ-31/32 + закрыты, OQ-86…OQ-97 открыты. +- **PR:** arm-core #6, docs #7, fixtures #3 (issue-ы: arm-core #5, docs #6, fixtures #2). + +**Перенесено в будущий срез «UI справочника»** (номер не присвоен; планируется после Среза 2): + +- Экраны справочника по макету: «ГК и организации» [50] (список ГК | карточка с вкладками ЮЛ / ГСК / ГСЗ / Архив), + создание ГК/ЮЛ/ГСК/ГСЗ [51], «ЮЛ вне ГК» [61], вкладка ГСК/ГСЗ [77], изменение ЮЛ [78]–[79], перевод ЮЛ [79-01], + вывод из состава ГК [80], модалка импорта [38]. API для них готов с Среза 1 (см. `10-user-journey.md`). +- Пока UI нет, справочники заполняются импортом ЕЦП и вызовами API (`/docs`). ## Срез 2 — Загрузка файлов + табличный парсер (ОСВ, ББ, ОФР из Excel) — самый рискованный, делать рано diff --git a/context/10-user-journey.md b/context/10-user-journey.md index ea49e1a..ea19e60 100644 --- a/context/10-user-journey.md +++ b/context/10-user-journey.md @@ -42,6 +42,13 @@ └─ Журнал действий ─── категории Справочники / Консолидация / Доступ, таблица событий [63] ``` +**Статус реализации (на 05.10.2026, после Среза 1).** Экраны справочника — [50], [51] (+ [51-01…51-04]), [61], [77]–[80], +модалка импорта [38] — **не реализованы**: по решению владельца 04.10.2026 UI справочника вынесен в отдельный срез +(`09-delivery-plan.md`, «UI справочника»). **API этих экранов есть с Среза 1** (`/api/v1/directories/*`, импорт ЕЦП — +`POST /directories/import`, правила — `05-domain-rules.md` §13); пока справочники заполняются импортом и вызовами API. +Экран администратора `/admin` («Пользователи и роли»: пользователи, роли, матрица прав) — **минимальный, реализован** в +Срезе 1 (в макете его нет — п. 1.6 пути 1). + Чего в макете **нет** (есть в ТЗ, реализуется по ТЗ): пользователи и матрица прав (ТЗ 8.1); экран проверки распознавания для ОСВ/xlsx (строки ОСВ в панели документов — только для чтения, [39]); блок логических ошибок на экране проверки (в снимках не виден, [41] [41-02]); переключатель расчёта гудвилла (решение OQ-27, в снимках [31-03] его нет). @@ -50,6 +57,8 @@ ## Путь 1. Куратор/администратор готовит справочники +> Шаги 1.1–1.5 — экраны макета; **API есть с Среза 1, экраны реализуются отдельным срезом «UI справочника»**. Шаг 1.6 — экран `/admin`, реализован (минимальный). + | Шаг | Экран | Действие | БД | |---|---|---|---| | 1.1 | Справочники → ГК и организации [50] | **Импорт из файла** [38]: файл .xlsx/.csv, РФ, колонки ИНН, Наименование, Тип, Базовый РФ ГК, ID ГК; дубли — «Пропустить существующие». Та же модалка открывается кнопкой **Импорт** на экране консолидаций ГК [34] | `legal_entity`, `company_group`, `audit_event(DIRECTORY_IMPORT)` | @@ -57,7 +66,7 @@ | 1.3 | Карточка ГК → вкладка ГСК/ГСЗ [77] | список: ID ГСК/ГСЗ (GSK-001, GSZ-001), Наименование, Число участников, Дата создания; состав — через форму ЮЛ («Входит в ГСК/ГСЗ»). Меню «…» строки [77-01]: **Изменить ГСК** [77-02] (ID ГСК в CRM, Наименование, Дата создания), **Удалить** | `related_group`, `related_group_member` | | 1.4 | Карточка ГК → ЮЛ → меню «…» [78] | **Изменить ЮЛ** [79] (те же поля, что при создании, + ГК; алиасы с удалением; членство в ГСК/ГСЗ); **История изменений** [79-02] → Журнал действий с фильтром по ГК и категории «Справочники»; **Перевести в другую ГК** [79-01]: ЮЛ, Текущая ГК, Новая ГК, Дата перевода (подсказка: «запись появится в архиве текущей ГК с событием „Переход в другую ГК“»); **Вывести из состава ГК** [80]: ЮЛ, Событие (Закрытие / Переход в другую ГК), Дата закрытия, Комментарий (необязательно) | `legal_entity`, `legal_entity_alias`, `related_group_member`, `legal_entity_history(event_type, effective_date)` | | 1.5 | ЮЛ вне ГК [61] (заголовок экрана — «Юридические лица») | **Создать ЮЛ** (монозаёмщик), поиск по ЮЛ, ИНН или ГК, фильтр «Архив». По снимку таблица показывает **все** ЮЛ с заполненной колонкой ГК, а не только вне ГК — расхождение с названием пункта (OQ-41) | `legal_entity(company_group_id NULL)` | -| 1.6 | (нет в макете) | назначить роли, править матрицу прав | `user_role`, `role_permission`, `audit_event(ROLE_PERMISSION_CHANGE)` | +| 1.6 | `/admin` (нет в макете; минимальный экран, Срез 1) | назначить роли, править матрицу прав | `user_role`, `role_permission`, `audit_event(ROLE_PERMISSION_CHANGE)` | ## Путь 2. КА РФ создаёт КФО и загружает первичку diff --git a/context/KICKOFF-PROMPT.md b/context/KICKOFF-PROMPT.md index b5ee2ac..697dd5f 100644 --- a/context/KICKOFF-PROMPT.md +++ b/context/KICKOFF-PROMPT.md @@ -9,8 +9,8 @@ 1. `CLAUDE.md` (workspace) и `docs/context/00-workspace-layout.md` — как устроена рабочая область, статус аналитики, макет. 2. `docs/context/01-project-overview.md`, `02-glossary.md` — предметная область. -3. `docs/context/03-architecture.md`, `04-data-model-guide.md`, затем **весь** `docs/model/data-model-v5.13.dbml` и - `docs/model/entities-v5.13.md`. +3. `docs/context/03-architecture.md`, `04-data-model-guide.md`, затем **весь** `docs/model/data-model-v5.14.dbml` и + `docs/model/entities-v5.14.md`. 4. `docs/context/05-domain-rules.md` — алгоритмы (особенно §1.0 и §12 — парсеры ищут структуру по содержимому); `06-test-data.md`; `07-open-questions.md`; `10-user-journey.md`; `09-delivery-plan.md`. 5. `arm-core/`, `tabular-parser/`, `scan-gateway/` — три репозитория, развёрнутые из нулевого коммита шаблона команды, @@ -37,7 +37,7 @@ NATS, MinIO, эмулятор ИС Сканирование), контракт ` `07-open-questions.md`; если нужен ответ для среза — делай допущение по правилам из этого файла. Ограничения на всё время работы: -- имена таблиц, колонок и enum'ов — ровно как в DBML v5.13; поля, которых нет в DBML, не изобретать; +- имена таблиц, колонок и enum'ов — ровно как в DBML v5.14; поля, которых нет в DBML, не изобретать; - алгоритмы — только из `05-domain-rules.md`; из страниц ТЗ напрямую не кодировать; - парсеры: никаких координат ячеек, букв колонок, имён листов и номеров строк в коде — структура по содержимому, синонимы в `rules/*.yaml`; фикстуры — регрессия, не спецификация; diff --git a/dev/checks.md b/dev/checks.md index 37176ff..ad141cd 100644 --- a/dev/checks.md +++ b/dev/checks.md @@ -57,7 +57,7 @@ docker compose -f ../docs/dev/docker-compose.yml --project-name arm-ka run --rm Для `arm-core` дополнительно: -- `dbml-check` — `python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.13.dbml` (путь от `api/`; +- `dbml-check` — `python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.14.dbml` (путь от `api/`; другой — `make dbml-check DBML=<путь относительно api/>`): ORM обязана совпадать с DBML; расхождения печатаются построчно, код 1. - Цикл миграций «`alembic upgrade head` → `alembic downgrade base` → `alembic upgrade head`» на чистой БД, пустой diff --git a/model/entities-v5.14.md b/model/entities-v5.14.md new file mode 100644 index 0000000..e9b0aee --- /dev/null +++ b/model/entities-v5.14.md @@ -0,0 +1,144 @@ +# АРМ КА — описание сущностей модели данных v5.14 + +Источник истины по структуре — `data-model-v5.14.dbml` (полный). Диаграммы по частям — `diagrams/` (part01…09 svg/png; отрисованы по v5.13: v5.14 меняет только `related_group_member` — колонку `kind` и индекс, — диаграммы не перерисовывались). Здесь — что каждая таблица значит для бизнеса, как связана с соседями и откуда взялась. + +## Что изменилось в v5.14 (Срез 1, решения владельца 04.10.2026) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `related_group_member.kind` + индекс `(legal_entity_id, kind) WHERE is_current` | ЮЛ состоит **одновременно в одной ГСК и одной ГСЗ** (но только в одной группе каждого вида). Отменяет ANL-030 / v5.8 («только в одной ГСК или ГСЗ»). `kind` — денормализация `related_group.kind` (равенство проверяет ядро при записи, тест на рассинхрон); индекс не выразить через join, триггер не выбран. Закрывает OQ-32 | +| 2 | КПП | **Не вводится.** Поле есть только в макете; у `legal_entity` его нет, импорт его не читает (закрывает OQ-31) | +| 3 | Семантика `company_group.crm_id`, `base_regional_office_id` | Без изменения структуры: `crm_id` = «iD ГК» выгрузки ЕЦП CRM, `base_regional_office_id` = «Базовый РФ ГК в CRM»; флаги «входит в ГСК/ГСЗ» выгрузки → членство в одной неявной ГСК/ГСЗ на ГК (правила — `05-domain-rules.md` §13) | +| 4 | `audit_action` + `LEGAL_ENTITY_CHANGE`, `USER_CHANGE` | Журнал изменений ЮЛ (создание/изменение/закрытие/перевод, алиасы) и пользователей (создание/деактивация администратором) — ТЗ 8.2; в v5.13 для них не было действия (были `GROUP_CHANGE`, `MEMBERSHIP_CHANGE`, `USER_ROLE_CHANGE`) | + +Остальные таблицы и enum — как в v5.13 (43 таблицы). Реализация — миграция `0002` в `arm-core`. + +## Что изменилось в v5.13 (ревью 30.09–01.10) + +| № | Замечание | Решение | +|---|---|---| +| 1 | Вместо RBAC «в коде» — роли **и** права | `permission` + `role_permission` возвращены; права только через роли, прямых назначений пользователю нет. Исключения — заведением роли | +| 2 | `osv_entry.parent_entry_id` не нужен — храним только строки с контрагентами | **Отложено.** Открыт вопрос: хранить ли строки уровня ACCOUNT для сверки «Σ контрагентов = итог счёта»; аналитика «Парсинг ОСВ» п.11 пока говорит «все строки» | +| 3 | `osv_group_schema` не особо нужна | Убрана. Схема шапки — `recognition_job.group_schema jsonb`; тип строки — `osv_entry.group_type` | +| 4 | `financial_statement_value.needs_review` непонятен | Убран: производное от `confidence` / `is_recognized`, порог может меняться | +| 5 | В `recognition_job` не нужны и `result`, и `raw_result` | Оставлен только `result_s3_object_id`. Сырой ответ ИС Сканирование — S3 `raw/{job_id}.json` по конвенции, в БД не регистрируется | +| 6 | `stored_file.fields_modified` — что это | Счётчик «исправлено полей» с карточки документа (v5.7). Убран — `COUNT(is_modified)` по данным | +| 6-бис | `document_type` не предусматривает несколько ОСВ или ББ+ОФР в одном файле | **Разделение файл / документ**: `stored_file` — физический файл, `document` — логическая единица (1:N). Чек-лист, проверки, подтверждение, FK разобранных данных, `consolidation_document`, `ex_snapshot_item` — на `document`. Замена (`superseded_by`) — файл целиком | +| 7 | `file_type`: убрать Р7 | Убран. `xls/xlsx/xlsb → tabular-parser`, `pdf/png/jpg → scan-gateway` | + +**Принципы модели (коротко):** +- Первичка не редактируется: файлы и распознанные значения хранятся как пришли; правки аналитика — рядом (`is_modified`, `original_*`) и в аудите. +- Производные консолидации в работе (сводная ОСВ, пары ВГО, шахматка) **не хранятся** — считаются запросом. Хранятся только ручные решения (корректировки, гудвилл, подтверждение КФО клиента). +- При верификации всё замораживается в Единое хранилище (ЕХ) как самодостаточный срез, который не зависит от таблиц консолидации. +- Единственный писатель в БД — ядро `arm-core`; сервисы распознавания видят только NATS и S3. +- Права — данные (`role_permission`), ограничения по ЗО/филиалу/статусу — policy-слой поверх прав. + +--- + +## Часть 01 — Группы компаний, организации, филиалы + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `regional_office` | Филиал банка: головной офис (ГО, `is_head_office`) и региональные филиалы (РФ). Определяет зону видимости пользователя и закрепление ГК. | ← `company_group.base_regional_office_id`, `legal_entity.regional_office_id`, `app_user.regional_office_id` | ТЗ 8.1 | +| `company_group` | Группа компаний (ГК) — верхний уровень справочника клиентов. Текущее состояние (SCD4). `crm_id` = «iD ГК» из ЕЦП CRM (NULL, если создана вручную Куратором; v5.14 — семантика по выгрузке), внутренний `code`, закрепление за филиалом (`base_regional_office_id` = «Базовый РФ ГК в CRM»), флаг архива. | → `regional_office`; ← `legal_entity`, `related_group`, `consolidation` | ТЗ 6.1, Прил. 2; макет | +| `company_group_history` | Прошлые снимки ГК: каждое изменение — новая строка с `valid_from/valid_to`. | → `company_group` | ТЗ 6.10, 8.2 | +| `legal_entity` | Юридическое лицо / ИП. Натуральный ключ — ИНН. `company_group_id` = текущая ГК; **NULL = монозаёмщик**. `close_date` — закрыто/архивировано. | → `company_group`, `regional_office`; ← `related_group_member`, `stored_file`, `document`, `osv_entry.counterparty_id`, `consolidation_member` | ТЗ 6.1, Прил. 2; ЕХ 6.1 | +| `legal_entity_history` | Прошлые снимки ЮЛ + вид события (создание, изменение, переход в другую ГК, закрытие, вывод из ГК) и дата вступления в силу. | → `legal_entity` | ТЗ 8.2; макет | +| `legal_entity_alias` | Словарь написаний контрагента: как имя встречается в ОСВ и нормализованный ключ для матчинга. Пополняется при ручном сопоставлении. | → `legal_entity` | ТЗ 6.5 | +| `related_group` | ГСК или ГСЗ внутри ГК; `kind` различает. Для ГСК формируется КФО. Иерархия не поддерживается. | → `company_group`; ← `related_group_member`, `consolidation.related_group_id` | ТЗ 6.1, Прил. 2 | +| `related_group_member` | Состав ГСК/ГСЗ — история членства (SCD2). **v5.14:** ЮЛ может одновременно входить в **одну ГСК и одну ГСЗ** (не более одной группы каждого вида): колонка `kind` = `related_group.kind` (денормализация) и частичный уникальный индекс `(legal_entity_id, kind) WHERE is_current`. Решение владельца 04.10.2026, отменяет ANL-030. | → `related_group`, `legal_entity` | ТЗ 6.1; макет [79] | + +## Часть 02 — Пользователи, роли, права + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `app_user` | Пользователь АРМ. Аутентификация внешняя (РАИСА/SSO, `external_id`). Филиал (ГО/РФ), зона ответственности (ДКБ/ДМСБ/ДМ), табельный номер. Видимость данных = права роли + филиал + ЗО. | → `regional_office`; ← `user_role`, все `*_by` поля | ТЗ 8.1, Прил. 2 | +| `role` | Роли: seed из ТЗ 8.1 (КА РФ, КА ГО, РКА, КМ, Куратор ГО, Эксперт, Администратор; `is_system`) + роли, заведённые администратором для исключений. **v5.13:** код роли в бизнес-логике не проверяется — только права. | ← `user_role`, `role_permission` | ТЗ 8.1 | +| `permission` | **v5.13.** Атомарное право «объект × действие»: `DIRECTORY/DOCUMENT/CONSOLIDATION/KFO/EX/EXPORT/USERS/AUDIT` × `VIEW/EDIT/APPROVE/VERIFY/DELETE/EXPORT/ADMIN`. Seed, привязан к коду — администратор новые коды не создаёт. Право говорит «может»; policy-слой (ЗО, филиал, статус КФО) — «эту ли и сейчас ли». | ← `role_permission` | ТЗ Прил. 2, сноска 29 | +| `role_permission` | **v5.13.** Матрица доступов как данные: `effective_permissions(user) = ∪ permissions(roles(user))`. Редактируется администратором на экране «Изменение ролей и прав доступа»; история — `audit_event(ROLE_PERMISSION_CHANGE)`. Прямых прав у пользователя нет. | → `role`, `permission` | ТЗ Прил. 2; макет | +| `user_role` | Назначение ролей пользователю (M:N). История — в аудите. | → `app_user`, `role` | ТЗ 8.1 | + +## Часть 03 — Бухгалтерские справочники и правила + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `chart_of_account` | План счетов с иерархией, тип A/P/AP, признак забалансового. Seed — полный план 1С; нестандартные субсчета клиентов добавляет администратор. | ← `osv_entry.account_id`, `elimination_rule_account`, `required_document_rule` | ТЗ 6.2/6.5, Прил. 3/4 | +| `elimination_rule` | Правило элиминации из Прил. 3 (ББ) / Прил. 4 (ОФР) как данные: пара статей, сторона, мера, режим, НДС, акцизы. Версионируется. | ← `elimination_rule_account`, `correction.rule_id`; версия фиксируется в `consolidation_version` | ТЗ 6.4, Прил. 3/4 | +| `elimination_rule_account` | Счета/субсчета каждой стороны правила, включая исключения («76 кроме 76.7.1»). | → `elimination_rule`, `chart_of_account` | ТЗ Прил. 3/4 | +| `required_document_rule` | Какие документы обязательны по подходу. **v5.13:** сверяется с `document` (тип + счёт + период), а не с файлом: выполнено = есть `document CONFIRMED` у каждого ЮЛ периметра. | → `chart_of_account` | ТЗ 5.1, 6.2, сноска 12 | + +## Часть 04 — Файлы, документы, S3, задания распознавания + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `s3_object` | Реестр всех объектов в S3: бакет-роль, ключ, размер, sha256, срок жизни, ссылка на архив в data lake. Единая точка ретеншна (5 лет по ТЗ 8.4) и дедупа. | ← `stored_file`, `recognition_job` | ТЗ 6.2, 8.4; ЕХ 8.3 | +| `stored_file` | **v5.13: только физический файл** одной загрузки: чей (ЮЛ), формат, hash, кто/когда загрузил, статус обработки (`UPLOADED → PROCESSING → PROCESSED / ERROR / SUPERSEDED`), версия при замене. Что внутри файла — `document`. Замена — файл целиком со всеми документами. | → `legal_entity`, `s3_object`, `recognition_job`; ← `document`, `recognition_job` | ТЗ 6.2, 8.2; UC-01…05 | +| `document` | **v5.13, новая.** Логический документ внутри файла: ББ, ОФР, ОСВ по счёту (`osv_account_code`), расшифровка, прочее. Период, отчётная дата, где в файле (`locator`: лист / страницы), статус проверки (`RECOGNIZED / NEEDS_CHECK / CONFIRMED / ERROR`), результаты проверок периода и контрагента, кто подтвердил. Один файл → N документов (PDF с ББ+ОФР; xlsx с несколькими ОСВ). Создаёт ядро при маппинге результата `recognition_job`. На документ ссылаются разобранные данные, чек-лист, консолидация и ЕХ. | → `stored_file`, `legal_entity`, `app_user`; ← `osv_entry`, `financial_statement_meta`, `consolidation_document`, `ex_snapshot_item` | ТЗ 6.2, 5.1, 8.2; макет (чек-лист, «Подтвердить данные») | +| `recognition_job` | Одна попытка распознать/распарсить файл. **Единственная точка обмена с NATS** (outbox/inbox, дедуп по `result_msg_id`, DLQ). Хранит сервис и версию, **одну** ссылку на нормализованный JSON-результат в S3 (v5.13), метрики, и **схему группировок шапки ОСВ по листам** (`group_schema jsonb`, v5.13 — вместо `osv_group_schema`). | → `stored_file`, `s3_object`; ← `osv_entry`, `financial_statement_meta` | ТЗ 6.2, 6.10; DECISION-4 | + +## Часть 05 — Разобранные ОСВ + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `osv_entry` | Строка ОСВ: документ (счёт + период), владелец, тип строки по схеме шапки (`group_type`, v5.13), уровень и родитель (`parent_entry_id` — **открыт вопрос, замечание 2**), код счёта и резолв в план счетов, сырое имя/ИНН контрагента и резолв в ЮЛ, шесть сумм, тип данных (БУ/валюта/кол-во), «подакцизный» для 90-го. Ручные правки: `is_modified`, `original_values`, `modified_by`. Признак ГСК/ГК/Внешний не хранится — выводится из периметра версии. | → `document`, `recognition_job`, `legal_entity` ×2, `chart_of_account` | ТЗ 6.2/6.5, Прил. 2; «Парсинг ОСВ» | + +## Часть 06 — Распознанные формы ББ/ОФР + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `financial_statement_meta` | Шапка распознанной формы (1:1 с `document` BALANCE/INCOME): ИНН и наименование как в документе, отчётная дата, единица измерения, реквизиты. Ревью формы (`reviewed_by/at`) и акцепт КА непройденных логических проверок с комментарием. Проверки (ТЗ 6.3) не хранятся — считаются на лету. Подтверждение документа — `document.confirmed_by`. | → `document`, `recognition_job`, `app_user`; ← `column`, `line` | ТЗ 6.2/6.3; FNR01; макет | +| `financial_statement_column` | Колонки-периоды формы с датой — нужны для межформенной проверки (1370 на начало года) и гудвилла. | → `meta`; ← `value` | ТЗ 6.3 п.7, 6.7 | +| `financial_statement_line` | Строка формы с кодом ОКУД и наименованием как распознано. Только строки с кодом; порядок и разделы — шаблон формы на фронте. | → `meta`; ← `value` | ТЗ 6.2, Прил. 2 | +| `financial_statement_value` | Число в ячейке (строка × колонка): сумма, уверенность, `is_recognized`, ручная правка с исходным значением. **v5.13:** подсветка «требует проверки» = `confidence < порог OR NOT is_recognized` — считается, флаг `needs_review` убран. | → `line`, `column`, `app_user` | ТЗ 6.2, Прил. 2 | + +## Часть 07 — Консолидация + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `consolidation` | Логическая консолидация: **ГК** + отчётный период. ГСК фиксируется/создаётся при верификации по фактическому периметру. Уникальна по (ГК, год, квартал). | → `company_group`, `related_group`; ← `consolidation_version` | ТЗ 6.10, 7.2 | +| `consolidation_version` | Версия консолидации: номер, статус, сценарий, подход, гудвилл, инициатор и его офис (маршрут), кто утвердил / верифицировал, решение УОБ, отклонения от Методики, версия правил элиминации, ссылка на предыдущую. После VERIFIED — read-only. | → `consolidation`, `app_user`, `regional_office`, self; ← всё в частях 07–08 | ТЗ 5.1, 6.4, 6.10; макет | +| `consolidation_status_history` | Каждый переход статуса: из → в, кто, когда, комментарий (обязателен при возврате). | → `consolidation_version`, `app_user` | ТЗ 6.10, 8.2 | +| `consolidation_lock` | «Взять в работу»: кто редактирует версию, перехват с причиной. Из макета — ANL-036. | → `consolidation_version`, `app_user` | макет | +| `consolidation_member` | Периметр версии — зафиксированный список ЮЛ; `is_head_entity` для гудвилла. | → `consolidation_version`, `legal_entity` | ТЗ 6.1, 6.10 | +| `consolidation_document` | **v5.13:** какие **документы** (не файлы) участвуют в версии. Чек-лист = `required_document_rule` × периметр − `consolidation_document`. Переиспользование одного документа в нескольких версиях/периодах, монозаёмщики из ЕХ. | → `consolidation_version`, `document` | ТЗ Прил. 2; ЕХ 7 | + +*Не хранятся (DECISION-5), считаются запросом по версии:* сводная ОСВ, признак контрагента ГСК/ГК/Внешний, пары ВГО, шахматка, результирующая таблица ВГО. + +## Часть 08 — Корректировки, гудвилл, результат КФО + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `correction` | Ручная корректировка ВГО: пара (правило + компания 1 + компания 2 — естественный ключ), ББ/ОФР, строки и суммы по Дт и Кт, дочерние строки для распределения, причина, обязательный комментарий, статус, номер версии (история — в аудите). КФО = свод − Σ корректировок. | → `consolidation_version`, `elimination_rule`, `legal_entity` ×2, `chart_of_account` ×2, self | ТЗ 6.4, 6.8, Прил. 9 | +| `goodwill` | Строка расчёта гудвилла: головная × дочерняя, доля владения, ФВ по 58.01, УК (1310) и добавочный капитал (1350 — вопрос методологам), гудвилл, «включить в КФО», комментарий. | → `consolidation_version`, `legal_entity` ×2, `app_user` | ТЗ 6.7, Прил. 3 п.2, Прил. 8 | +| `kfo_result` | Шапка результата КФО: когда посчитан, заморожен ли, ЧА и ВБ, выручка, прибыль, расхождения, пороги, обоснование, итог гудвилла. | → `consolidation_version` (1:1); ← `kfo_result_line` | ТЗ 6.4, 6.8, Прил. 10 | +| `kfo_result_line` | Строка консолидированного ББ/ОФР по коду ФСБУ 4/2023: сумма → авто-элиминация → корректировки → гудвилл → итог; «в т.ч. ВГО», источник, расхождение, комментарий. | → `kfo_result`; ← `kfo_result_line_company` | ТЗ 6.4, Прил. 10; макет | +| `kfo_result_line_company` | Разрез строки КФО по компаниям периметра: сумма и доля. | → `kfo_result_line`, `legal_entity` | ЕХ 6.6; макет | +| `client_kfo_line` | Сценарий «подтверждение КФО клиента»: по строке — значение и ВГО от клиента (ручной ввод), значение и ВГО банка, расхождение, причина. | → `consolidation_version`, `app_user` | ТЗ 5.1, 6.10; макет | + +## Часть 09 — Комментарии, аудит, Единое хранилище + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `comment` | Полиморфный комментарий к объекту (документу, ячейке шахматки по ключу пары, строке КФО). | → `app_user`; `object_type + object_id` без FK | ТЗ 6.3/6.4; макет | +| `audit_event` | Журнал действий (append-only): кто, когда, действие, раздел, объект, версия консолидации, before/after, результат, IP/браузер. Покрывает историю правок распознанных данных, корректировок, выгрузок, импорта справочников, входов, **изменений матрицы прав** (v5.13), подтверждения документов, **изменений ЮЛ и пользователей** (v5.14: `LEGAL_ENTITY_CHANGE`, `USER_CHANGE`). | → `app_user`, `consolidation_version` | ТЗ 8.2, Прил. 2; макет | +| `ex_snapshot` | Срез ЕХ: одна ГСК на одну отчётную дату, версия, статус (актуальный/архив), когда и кем верифицирован и записан. Самодостаточен — на консолидацию ссылается только данными. | → `related_group`, `company_group`, `app_user`, self; ← `member`, `item` | ЕХ ТЗ 6.2, 6.4, 5.3 | +| `ex_snapshot_member` | Периметр среза на дату: ЮЛ с ИНН и наименованием на момент верификации. | → `ex_snapshot`, `legal_entity` | ЕХ ТЗ 6.3 | +| `ex_snapshot_item` | Содержимое среза. Два вида: исходный документ → ссылка на **`document`** (v5.13; скачать — через его файл) и всё производное → `payload` jsonb. | → `ex_snapshot`, `document`, `legal_entity` | ЕХ ТЗ 5.2, 6.7 | + +--- + +## Как читать модель по сценарию «сформировать КФО» + +1. **Справочники** (01–03): ГК → ЮЛ → ГСК/ГСЗ; план счетов; правила элиминации; что обязательно загрузить. +2. **Создание консолидации** (07): `consolidation` от ГК + период → `consolidation_version` → `consolidation_member` (периметр). +3. **Загрузка** (04): `stored_file` + `s3_object` → `recognition_job` (queued → NATS → сервис → результат в S3 → done) → ядро создаёт **`document`** на каждый ББ / ОФР / ОСВ по счёту, найденный в файле. +4. **Разбор** (05–06): ОСВ → `osv_entry`; ББ/ОФР → `financial_statement_meta/column/line/value`. Аналитик проверяет, правит, подтверждает **документ**; логические проверки — на лету; сопоставляет контрагентов (→ `osv_entry.counterparty_id`, `legal_entity_alias`). Чек-лист закрывается по `document CONFIRMED`. +5. **Консолидация** (вычисляется): сводная ОСВ, пары ВГО, шахматка — из `osv_entry` × периметр × правила. Аналитик вносит `correction`, заполняет `goodwill`, при сценарии подтверждения — `client_kfo_line`. Результат — `kfo_result(_line)`. +6. **Согласование** (07): `consolidation_status_history` по маршруту; блокировка `consolidation_lock`. Что можно — решает `role_permission` + policy-слой по статусу/ЗО. +7. **Верификация → ЕХ** (09): `ex_snapshot` + `member` + `item` (документы по ссылке, остальное — payload). Версия → read-only. Всё это время — `audit_event`. + +## Открытые вопросы после v5.14 + +- **Замечание 2** (`osv_entry.parent_entry_id`, какие строки ОСВ хранить) — отложено; затрагивает и аналитику «Парсинг ОСВ» п.11. +- Замена по-документно (одна ОСВ из файла с пятью) — сейчас не предусмотрена, замена файлом целиком. Если понадобится — `superseded_by` переезжает в `document`. +- Статус `document.RECOGNIZED` (ручная проверка не требуется по порогу) — подтвердить с аналитиками, нужен ли, или все документы проходят через `NEEDS_CHECK → CONFIRMED`. diff --git a/openspec/changes/slice-1-directories/.openspec.yaml b/openspec/changes/archive/2026-10-05-slice-1-directories/.openspec.yaml similarity index 100% rename from openspec/changes/slice-1-directories/.openspec.yaml rename to openspec/changes/archive/2026-10-05-slice-1-directories/.openspec.yaml diff --git a/openspec/changes/slice-1-directories/design.md b/openspec/changes/archive/2026-10-05-slice-1-directories/design.md similarity index 100% rename from openspec/changes/slice-1-directories/design.md rename to openspec/changes/archive/2026-10-05-slice-1-directories/design.md diff --git a/openspec/changes/slice-1-directories/proposal.md b/openspec/changes/archive/2026-10-05-slice-1-directories/proposal.md similarity index 100% rename from openspec/changes/slice-1-directories/proposal.md rename to openspec/changes/archive/2026-10-05-slice-1-directories/proposal.md diff --git a/openspec/changes/slice-1-directories/tasks.md b/openspec/changes/archive/2026-10-05-slice-1-directories/tasks.md similarity index 90% rename from openspec/changes/slice-1-directories/tasks.md rename to openspec/changes/archive/2026-10-05-slice-1-directories/tasks.md index 64a9bb8..da278dd 100644 --- a/openspec/changes/slice-1-directories/tasks.md +++ b/openspec/changes/archive/2026-10-05-slice-1-directories/tasks.md @@ -11,12 +11,12 @@ ## 2. Документы под v5.14 -- [ ] 2.1 `model/entities-v5.14.md` (копия v5.13 + таблица «Что изменилось в v5.14» наверху, строка `related_group_member` обновлена); +- [x] 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` +- [x] 2.2 `CLAUDE.md` workspace и `docs/repo-templates/*.CLAUDE.md`: ссылки на `data-model-v5.14.dbml`/`entities-v5.14.md`; проверка: ссылки ведут на существующие файлы +- [x] 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` +- [x] 3.1 После merge дочернего change'а arm-core: сверить §13 с фактически реализованными кодами отчёта и допущениями `ASSUMPTION(OQ-8x)` в коде; расхождения — правкой §13 или OQ; проверка: список кодов в §13.2 = перечень в `arm-core/api/src/domain/directory_import.py` diff --git a/openspec/config.yaml b/openspec/config.yaml index fb4f6a8..e4280cf 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -1,12 +1,12 @@ schema: spec-driven context: | - Репозиторий arm-ka-docs — контекст, модель данных (DBML v5.13), правила домена, контракты между сервисами + Репозиторий arm-ka-docs — контекст, модель данных (DBML v5.14), правила домена, контракты между сервисами (docs/contracts/*.schema.json), макет UI и экспорт ТЗ системы АРМ КА (ДКБ, Россельхозбанк). Здесь оформляются СКВОЗНЫЕ изменения: контракт + окружение разработки (docs/dev) + проверки качества (make check) + изменения документов контекста. Код сервисов живёт в соседних репозиториях (../arm-core, ../tabular-parser, ../scan-gateway), у каждого свой openspec/ и свой change; сквозной change ссылается на них. - Источники истины по приоритету: docs/model/data-model-v5.13.dbml → docs/model/entities-v5.13.md → + Источники истины по приоритету: docs/model/data-model-v5.14.dbml → docs/model/entities-v5.14.md → docs/context/05-domain-rules.md → docs/source/tz (черновик, имена v4). Открытые вопросы — docs/context/07-open-questions.md; план срезов — docs/context/09-delivery-plan.md; соглашения шаблона команды — docs/context/03-architecture.md. Язык документов и коммитов — русский, идентификаторы — английские, как в DBML. docs/source/** не редактируется. diff --git a/repo-templates/arm-core.CLAUDE.md b/repo-templates/arm-core.CLAUDE.md index 012b3d7..84acf34 100644 --- a/repo-templates/arm-core.CLAUDE.md +++ b/repo-templates/arm-core.CLAUDE.md @@ -16,7 +16,7 @@ - NATS subjects и схема ключей S3 — `../docs/context/03-architecture.md`. ## Правила репозитория -- ORM 1:1 с `../docs/model/data-model-v5.13.dbml`; `scripts/check_dbml_vs_orm.py` — обязательная проверка CI. +- ORM 1:1 с `../docs/model/data-model-v5.14.dbml`; `scripts/check_dbml_vs_orm.py` — обязательная проверка CI. - Алгоритмы — только из `../docs/context/05-domain-rules.md`; номер § — в docstring. - Вычисляемые объекты консолидации — `backend/app/domain/`, без ORM-таблиц. - Фронт: пользователь слаб во фронте — для TS/React изменений делай ревью сам и объясняй решения в PR.