diff --git a/Makefile b/Makefile index ebafae0..098ecde 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ VENV := .venv PY := $(if $(wildcard $(VENV)/bin/python),$(CURDIR)/$(VENV)/bin/python,python3) CHANGE ?= slice-0-skeleton # Текущая версия модели данных — по ней рисуются диаграммы (make diagrams) -DBML ?= model/data-model-v5.17.dbml +DBML ?= model/data-model-v5.18.dbml .PHONY: venv validate-contracts validate-openspec test-snapshot check diagrams diff --git a/context/00-workspace-layout.md b/context/00-workspace-layout.md index 778b1ac..34edae7 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.17.dbml` | стабильный, версионируется | единственный источник имён и структур | +| Модель данных | `docs/model/data-model-v5.18.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 f248bd0..42c5046 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -1,4 +1,4 @@ -# 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.17) +# 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.18) ## Компоненты (= репозитории, см. `00-workspace-layout.md`) @@ -147,7 +147,7 @@ advisory-lock по `(ЮЛ, sha256)` (двойной клик не проскак `consolidation_documents` (привязка/отвязка), `consolidated_osv`, `counterparty_mapping`, `osv_export`; домен — `policy` (`ensure_version_editable`), `checklist`, `consolidation_periods`, `consolidated_osv`. - **Единственное место смены статуса** — `consolidation_status.transition`: ручной `POST …/status` и автоматические - переходы (загрузка с версией, маппинг, подтверждение документа) зовут её же, поэтому история и аудит одинаковы. Версия к + переходы (загрузка в консолидацию, маппинг, подтверждение документа) зовут её же, поэтому история и аудит одинаковы. Версия к моменту вызова уже под `FOR UPDATE` у вызывающего. - **Порядок блокировок: файл → документ → advisory-lock ГК → версии (по возрастанию `id`).** Подтверждение документа берёт файл, документ и затем все версии «в работе» ГК ЮЛ (`lock_versions_for_entity` блокирует надмножество — член ИЛИ та же ГК — и @@ -232,7 +232,7 @@ api/src/ # машины статусов, policy — pydantic-модели на входе и выходе services/ # use-cases: оркестрация domain + repositories + outbox + audit_event repositories/ # SQLAlchemy-запросы, в т. ч. запросы-вычисления (сводная ОСВ, шахматка) - models/ # ORM 1:1 с DBML v5.17; имена таблиц/колонок/enum — как в DBML + models/ # ORM 1:1 с DBML v5.18; имена таблиц/колонок/enum — как в DBML schemas/ # pydantic DTO API /api/v1 core/ # config, clients (S3; NATS outbox-publisher и result-consumer), auth (JWT РАИСА / dev-эмитент), # logger, exception_handlers @@ -320,7 +320,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.17. +- PK везде `uuid` через `UUIDPrimaryMixin`; имена таблиц, колонок и enum'ов — ровно как в DBML v5.18. ### Инфраструктурные клиенты diff --git a/context/04-data-model-guide.md b/context/04-data-model-guide.md index 8825a47..a6ca68b 100644 --- a/context/04-data-model-guide.md +++ b/context/04-data-model-guide.md @@ -1,8 +1,8 @@ -# 04. Как читать модель данных v5.17 +# 04. Как читать модель данных v5.18 -Файлы: `docs/model/data-model-v5.17.dbml` (источник истины), `docs/model/entities-v5.17.md` (смысл таблиц; предыдущие -версии `data-model-v5.13…v5.16.dbml` и `entities-v5.13…v5.16.md` остаются как история), -`docs/model/diagrams/part01…09.{svg,png}` (диаграммы v5.17 по частям из таблицы ниже; перерисовываются из DBML командой +Файлы: `docs/model/data-model-v5.18.dbml` (источник истины), `docs/model/entities-v5.18.md` (смысл таблиц; предыдущие +версии `data-model-v5.13…v5.17.dbml` и `entities-v5.13…v5.17.md` остаются как история), +`docs/model/diagrams/part01…09.{svg,png}` (диаграммы v5.18 по частям из таблицы ниже; перерисовываются из DBML командой `make diagrams` в корне репозитория docs — скрипт `scripts/gen_model_diagrams.py`, нужен npx; при новой версии DBML обновить `DBML` в Makefile, а новую таблицу — добавить в раскладку частей скрипта и в эту таблицу). @@ -20,6 +20,8 @@ | 08 Корректировки и КФО | `correction`, `goodwill`, `kfo_result`, `kfo_result_line`, `kfo_result_line_company`, `client_kfo_line` | ручные решения и результат | | 09 Комментарии, аудит, ЕХ | `comment`, `audit_event`, `ex_snapshot`, `ex_snapshot_member`, `ex_snapshot_item` | полиморфные комментарии, append-only журнал, замороженные срезы | +**v5.18:** `stored_file.consolidation_id` — в какую консолидацию (период ГК) загружен файл; файлы, загруженные в консолидацию, — основа рабочего стола консоли, общего для её версий (полный состав стола, с файлами документов, привязанных к версиям, — `05-domain-rules.md` §9.5 п. 11). Что использует версия — `consolidation_document`. + ## Соглашения - PK везде `uuid`; натуральные ключи — `UNIQUE` (ИНН, код счёта, crm_id, код ГСК). Для `osv_entry` и `audit_event` @@ -58,9 +60,9 @@ ## v4 → v5.13: таблица переименований (обязательна при чтении ТЗ) v5.14 имена таблиц и полей не менял (добавлены `related_group_member.kind` и два значения `audit_action`), v5.15 — тоже -(добавлено значение `osv_data_type.NU`), v5.16 — тоже (колонка `osv_entry.counterparty_match_kind`), v5.17 — тоже (колонка `legal_entity.kpp`), поэтому таблица справедлива и для v5.14…v5.17. +(добавлено значение `osv_data_type.NU`), v5.16 — тоже (колонка `osv_entry.counterparty_match_kind`), v5.17 — тоже (колонка `legal_entity.kpp`), v5.18 — тоже (колонка `stored_file.consolidation_id`), поэтому таблица справедлива и для v5.14…v5.18. -| В страницах ТЗ (модель v4 и старше) | В v5.13 / v5.14 / v5.15 / v5.16 / v5.17 | +| В страницах ТЗ (модель v4 и старше) | В v5.13 / v5.14 / v5.15 / v5.16 / v5.17 / v5.18 | |---|---| | `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 | diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index 3fe6fd6..a13267a 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -1,4 +1,4 @@ -# 05. Правила предметной области (выжимка из ТЗ, имена — v5.17) +# 05. Правила предметной области (выжимка из ТЗ, имена — v5.18) Каждый раздел: источник в ТЗ → правило → где в модели. Где ТЗ молчит или противоречит — пометка **[открыто]** и ссылка на `07-open-questions.md`. @@ -567,7 +567,7 @@ NEEDS_CHECK`; пройдено/акцептовано + нажато «Подт | Событие | Переход | Кто (`changed_by`) | |---|---|---| -| загрузка файла с `consolidation_version_id`, принят ≥ 1 файл | INITIATED → DOCUMENTS_PROCESSING | загрузивший | +| загрузка файла в консолидацию (`POST /consolidations/{id}/legal-entities/{le}/files`), принят ≥ 1 файл | INITIATED → DOCUMENTS_PROCESSING у **каждой** версии консолидации «в работе», где ЮЛ в периметре | загрузивший | | маппинг создал документ `NEEDS_CHECK`, подходящий версии | DOCUMENTS_PROCESSING → DATA_CORRECTION | `stored_file.uploaded_by` (нет — переход пропускается, лог) | | подтверждение документа, привязанного к версии | INITIATED / DOCUMENTS_PROCESSING → DATA_CORRECTION | подтвердивший | | `POST …/status {CANCELLED, comment}` (комментарий обязателен, 422 `COMMENT_REQUIRED`) | любой «в работе» → CANCELLED | пользователь с блокировкой | @@ -608,7 +608,8 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; ` ### 9.5 Первичные документы: загрузка, маппинг, замена, удаление, подтверждение (Срез 2c) -1. **Загрузка** (`POST /legal-entities/{id}/files`, право `DOCUMENT_EDIT`, ЮЛ в видимости): тип файла по расширению и +1. **Загрузка** (`POST /legal-entities/{id}/files` — без контекста, `stored_file.consolidation_id = NULL`; в консолидацию — + п. 8; право `DOCUMENT_EDIT`, ЮЛ в видимости): тип файла по расширению и сигнатуре (`xls` — BIFF8, `xlsx`/`xlsb` — zip, `pdf`, `png`, `jpg`); лимит `UPLOAD_MAX_BYTES` (50 МиБ); дедуп по `sha256` в пределах ЮЛ среди файлов не `SUPERSEDED`. Принятый файл: объект S3 `uploads/{file_id}/{job_id}/{имя}`, `stored_file(UPLOADED, version_no = 1)`, `recognition_job(queued)`, `audit_event(FILE_UPLOAD)`. Тип документа и период @@ -641,7 +642,7 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; ` §9.3. 3. **Автозамена.** После успешного маппинга другие файлы того же ЮЛ (не `SUPERSEDED`), содержащие документ с теми же `document_type`, `osv_account_code`, `period_year`, `period_quarter`, переводятся в `SUPERSEDED` - (`superseded_by_file_id` = новый файл); `version_no` нового = максимум заменённых + 1. Заменяется и файл с `CONFIRMED` + (`superseded_by_file_id` = новый файл); `version_no` нового = максимум заменённых + 1. Новый файл без контекста (`consolidation_id IS NULL`) наследует `consolidation_id` первого (по `id`) заменённого файла, у которого он задан; файл, загруженный в консолидацию, сохраняет свою (v5.18). Заменяется и файл с `CONFIRMED` документом (решение владельца 05.10.2026); документ без года или квартала в замене не участвует. Документы и строки заменённых файлов сохраняются, но по умолчанию не показываются. Если новый файл содержит лишь часть документов заменяемого, заменяемый уходит в `SUPERSEDED` целиком (OQ-06: замена файлом целиком; частный случай — OQ-115). Для форм @@ -680,10 +681,15 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; ` 7. **Порядок блокировок** (против взаимоблокировок): задание → файл → документы (`FOR NO KEY UPDATE`) → advisory-lock ГК → версии консолидации (`FOR UPDATE`, по возрастанию `id`); автозамена дополнительно берёт advisory-lock по ЮЛ. Подробности — `03-architecture.md`. -8. **Загрузка в контексте версии** (`?consolidation_version_id=`, Срез 4): до приёма — версия видима (404), «в работе» - (409 `VERSION_NOT_EDITABLE`), ЮЛ — член периметра (422 `ENTITY_NOT_IN_PERIMETER`); после приёма хотя бы одного файла - INITIATED → DOCUMENTS_PROCESSING (§9.1); сбой этого перехода не отменяет принятые файлы. Блокировка версии для загрузки не - нужна. +8. **Загрузка в консолидацию** (`POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files`, + `DOCUMENT_EDIT`; v5.18, решения владельца 09.10.2026, RSHB/arm-ka-docs#90): до приёма — консолидация видима (404 + `NOT_FOUND`), ЮЛ видимо (404), есть версия консолидации «в работе» (409 `CONSOLIDATION_NOT_EDITABLE`), ЮЛ — член + периметра хотя бы одной такой версии (422 `ENTITY_NOT_IN_PERIMETER`). Принятый файл получает + `stored_file.consolidation_id`; `FILE_UPLOAD.after_value.consolidation_id`. После приёма хотя бы одного файла, отдельной + транзакцией, каждая версия консолидации «в работе» с этим ЮЛ в периметре и статусом INITIATED переходит в + DOCUMENTS_PROCESSING (§9.1; версии блокируются `FOR UPDATE` по возрастанию `id`, статус перечитывается); сбой перехода не + отменяет принятые файлы. Блокировка версии для загрузки не нужна. Query `consolidation_version_id` у + `POST /legal-entities/{id}/files` (Срез 4) удалён. 9. **Правка формы ББ/ОФР** (`PATCH /documents/{id}/statement`, `DOCUMENT_EDIT`, Срез 3b; решения владельца 07.10.2026). Правятся **суммы** ячеек (`values[] {line_id, column_id, amount}`) и три поля шапки: `inn` (10 или 12 цифр, иначе 422 `INVALID_INN`, в том числе `null` — очистить ИНН нельзя; пересчитывается `counterparty_check`), `report_date` (пересчитываются `period_year`, `period_quarter`, @@ -703,6 +709,15 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; ` `POST/GET /documents/{id}/comments` (таблица `comment`, `object_type = 'document'`); добавлять можно к документу в любом статусе; правки и удаления нет; при удалении файла (`DELETE /files/{id}`, п. 5) комментарии его документов удаляются вместе с ними (связь полиморфная, FK нет — удаляет сервис). +11. **Рабочий стол консоли** (`GET /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files`, + `DOCUMENT_VIEW`; 404 на невидимые консолидацию или ЮЛ; `limit`, `offset`, `include_superseded`): файлы ЮЛ с + `consolidation_id` этой консолидации **плюс** файлы ЮЛ, у которых есть документ, привязанный (`consolidation_document`) + к любой версии этой консолидации (подхваченные ОСВ за `Q1` и файлы, загруженные без контекста); новые первыми + (`uploaded_at` desc, `id`); `SUPERSEDED` — только при `include_superseded=true` (скрыт по умолчанию). Карточка файла та же, что в + `GET /legal-entities/{id}/files` (`FileOut` + `consolidation_id`). Рабочие версии одного периода видят один стол; + верифицированная версия заморожена привязками и `ex_snapshot` (Срез 7), отдельного «замороженного списка» в панели + нет. Панель [39] из карточки версии показывает стол консоли; переключатель «Все документы организации» — + `GET /legal-entities/{id}/files` с подписью консолидации у файла. --- diff --git a/context/07-open-questions.md b/context/07-open-questions.md index ab7f2a3..2b820be 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -164,7 +164,8 @@ | OQ-156 | Ответ ИС Сканирование склеивает соседние строки формы в одну: код `1500\n1700`, наименование `Итого по разделу V\n\nБАЛАНС`, суммы `917 775\n2 371 333` (ББ РБ и Счастливочной за 1 кв. 2025, `fixtures/ocr/Бухгалтерская отчетность 1 кв.25.json`, `БО 1 кв 2025.json`). Нормализатор отбрасывал строку как нечисловой код — в документе не было 1500 и 1700 | прогон 1 кв. 2025 на реальных ответах ИС (arm-ka-fixtures#16) | scan-gateway `rows.py`, §11.1 п. 4 | **решено** (владелец 08.10.2026): разлеплять, если в каждой непустой ячейке значений столько же частей, сколько кодов; иначе — как раньше; предупреждение «строк с несколькими кодами разлеплено: N», `needs_check`; реализация — RSHB/arm-scan-gateway#16 | | OQ-157 | Чистый документ (без замечаний) требовал ручного «Подтвердить» (`RECOGNIZED` → `CONFIRMED`), хотя строки ОСВ не правятся (OQ-114) и ТЗ 03_9 п. 4 / 10_10 даёт «Загружен» сразу при отсутствии замечаний; на комплекте 1 кв. 2025 — 16 пустых кликов | прогон владельца 08.10.2026 | arm-core `services/auto_confirm.py`, `result_mapping`, `primary_files.delete`; §9.3, §9.5 п. 4–5 | **решено** (владелец 08.10.2026): чистый документ при маппинге сразу `CONFIRMED` системой (`confirmed_by` NULL), переход версии — от загрузившего; удаление автоподтверждённого файла — пока версии «в работе»; RSHB/arm-core#64, change `auto-confirm-clean-documents` | | OQ-158 | В «Осталось N» (строки свода без ЮЛ и без ИНН) попадают строки аналитики, которые не являются контрагентами-организациями: «Ведомость в счет з/платы», подотчётные лица, физлица и т. п. — на РБ 2025 К1 таких большинство из 1291 строки без ИНН, и довести баннер до нуля сопоставлением с ЮЛ нельзя. Нужно ли отличать такие строки (признак «не организация» вручную / по словарю / по счёту) и исключать их из «Осталось N» и предупреждения [31-00] | жалоба владельца на «Указать» в сводной ОСВ (08.10.2026), RSHB/arm-core#74 | arm-core `domain/consolidated_osv.needs_mapping`, §3, экраны [30], [32-osv-no-inn], [31-00] | открыт: до решения такие строки считаются несопоставленными (EXTERNAL), переход к следующему шагу ими не блокируется — блокировки в API нет | -| OQ-159 | Note DBML `recognition_job.reply_deadline` «таймаут ожидания (OCR — 5 мин)» расходится с решением владельца 09.10.2026: срок ответа OCR — 20 мин (`DEFAULT_REPLY_TIMEOUT` в arm-core, `SCAN_TIMEOUT_SECONDS=1200` в scan-gateway). Поправить note при следующей версии DBML | решение владельца 09.10.2026 (не выпускать версию DBML ради note); arm-core #87, arm-scan-gateway #26, arm-ka-docs #84 | DBML `recognition_job`, `05-domain-rules.md` §9.4 | открыт: код и §9.4 — 20 мин; note DBML правится с ближайшей версией модели | +| OQ-159 | Note DBML `recognition_job.reply_deadline` «таймаут ожидания (OCR — 5 мин)» расходится с решением владельца 09.10.2026: срок ответа OCR — 20 мин (`DEFAULT_REPLY_TIMEOUT` в arm-core, `SCAN_TIMEOUT_SECONDS=1200` в scan-gateway). Поправить note при следующей версии DBML | решение владельца 09.10.2026 (не выпускать версию DBML ради note); arm-core #87, arm-scan-gateway #26, arm-ka-docs #84 | DBML `recognition_job`, `05-domain-rules.md` §9.4 | **закрыт 09.10.2026:** note исправлен в DBML v5.18 (docs #90) | +| OQ-160 | Целевая модель версий консолидации (владелец, 09.10.2026): несколько рабочих версий одного периода с **разным периметром** делят первичку (рабочий стол консоли, `stored_file.consolidation_id`); после `VERIFIED` новая версия (`previous_version_id`) может менять документы при **том же** периметре. Ядро сейчас создаёт только версию 1 (`POST /consolidations`), «новая версия» и параллельные версии не реализованы. Нужны правила: создание второй рабочей версии (обязательно другой периметр?), создание версии после `VERIFIED` (копия периметра, подхват привязок), что видит панель [39] у верифицированной версии до Среза 7 (ЕХ) | brainstorming docs #90, `openspec/changes/console-files/design.md` R-6, R-7 | срез «версии консолидации» (после Среза 4), Срез 7 | открыт: модель файлов совместима; реализация — отдельный срез | Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения; `DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4. diff --git a/context/08-source-docs-index.md b/context/08-source-docs-index.md index 4fa1206..f3129f2 100644 --- a/context/08-source-docs-index.md +++ b/context/08-source-docs-index.md @@ -48,9 +48,9 @@ | Файл | Что | |---|---| -| `data-model-v5.17.dbml` | источник истины; changelog v5.1…v5.17 в шапке; блоки «ИСТОЧНИК» над каждой таблицей (v5.13…v5.16 остаются рядом как история) | -| `entities-v5.17.md` | описание сущностей по частям 01–09, таблицы «Что изменилось» по версиям, сценарий «как читать модель», открытые вопросы (v5.13…v5.16 остаются как история) | -| `diagrams/part01…09.{svg,png}` | диаграммы v5.17 по частям; перерисовываются `make diagrams` (`scripts/gen_model_diagrams.py`) | +| `data-model-v5.18.dbml` | источник истины; changelog v5.1…v5.18 в шапке; блоки «ИСТОЧНИК» над каждой таблицей (v5.13…v5.17 остаются рядом как история) | +| `entities-v5.18.md` | описание сущностей по частям 01–09, таблицы «Что изменилось» по версиям, сценарий «как читать модель», открытые вопросы (v5.13…v5.17 остаются как история) | +| `diagrams/part01…09.{svg,png}` | диаграммы v5.18 по частям; перерисовываются `make diagrams` (`scripts/gen_model_diagrams.py`) | | `source/tz/data-model-part1_4.dbml` (лежит не в `model/`, а в экспорте ТЗ) | v4 часть 1 — историческое | ## Макет UI diff --git a/context/09-delivery-plan.md b/context/09-delivery-plan.md index 34e71d8..6c96c0e 100644 --- a/context/09-delivery-plan.md +++ b/context/09-delivery-plan.md @@ -166,6 +166,9 @@ Change'и: `arm-core/openspec/changes/slice-2c-osv-ingest` (issue arm-core#14, **Статус:** API готов (Срезы 2c, 3b, arm-core #42); UI — у отдельного исполнителя фронта по `dev/frontend-api-guide.md`. - Панель «Первичные документы», окно загрузки, таблица ОСВ с деревом строк и подсветкой нераспознанных контрагентов. +- **Доработка 09.10.2026 (docs #90, DBML v5.18):** файлы в контексте консолидации — `stored_file.consolidation_id`, маршруты + `POST/GET /consolidations/{cid}/legal-entities/{le}/files`, панель [39] показывает рабочий стол консоли; спецификация — + `openspec/changes/console-files/`; дочерние PR arm-core и arm-web. Целевая модель версий — OQ-160. **DoD среза 2 целиком:** все реальные файлы и вся синтетика проходят **без единого правила под конкретный файл или координату**; в коде парсера нет литералов букв колонок/номеров строк/имён листов (проверяется тестом-линтером). diff --git a/context/10-user-journey.md b/context/10-user-journey.md index 7b5d189..7fb3d3b 100644 --- a/context/10-user-journey.md +++ b/context/10-user-journey.md @@ -76,7 +76,7 @@ | 2.2 | Отчётность → Группы компаний [03] → **Консолидации →** [34] | [03]: таблица ГК (Группа компаний, CRM ID, Основной РФ, Организаций), поиск по ГК, CRM ID или РФ. [34]: таблица консолидаций ГК: Год, Квартал, Версия (дерево версий под периодом), Статус (бейдж), ГСК, Дата создания, Дата верификации, Организаций; кнопки в строке **Взять в работу**, **К отчетности** (по снимку неактивна у «Инициация» и «Верификация РФ»; решение владельца 07.10.2026 — доступна во всех статусах, OQ-43), меню «…»; чекбокс «Только в работе». Выбор строки открывает панель **Организации** (Ответственный / Утверждён в РФ / Верифицирован в ГО; таблица Наименование, Документы, ОФР, Бух. баланс, ОСВ — перечень счетов, ИНН, Тип). **API:** `GET /company-groups/{id}/consolidations?in_work_only=` (периоды и версии, блокировка), `GET /consolidations/versions/{id}` (карточка: члены — тип ЮЛ `entity_type`, самоописывающие ячейки чек-листа ББ/ОФР, счета загруженных ОСВ `osv_accounts`; история; «Ответственный» — `lock.locked_by_name` при активной блокировке, иначе `initiator.name`; «Утверждён в РФ» / «Верифицирован в ГО» и колонка «ГСК» — Срез 7) | `consolidation_version`, `consolidation_lock`, `consolidation_member`, `document` | | 2.3 | [34] → **Взять в работу** [35] | «Консолидация … будет закреплена за вами для редактирования». **API:** `POST …/versions/{id}/lock` (перехват — `takeover_reason`), `DELETE …/lock`; ручной статус — `POST …/status` | `consolidation_lock`, `audit_event(CONSOLIDATION_LOCK)` | | 2.4 | [34] панель Организации → **Редактировать состав** [36] | фильтр по ГСК, «Показать все ЮЛ в ГК», чекбоксы, Сохранить. **API:** `PUT …/versions/{id}/members` (`{legal_entity_ids}`), нужны статус «в работе» и своя блокировка | `consolidation_member` | -| 2.5 | строка организации → **Документы** → drawer «Первичные документы» [39] | список: документ (имя, формат, дата; подпись типа у прочих — «Прочий документ», «Бухгалтерская отчетность»), Период («2024 К1», «2025 · 9 мес.»), Проверка (✓ / ✗ / — Период, Контрагент), Статус (Загружен / Обработка / Требуется проверка / Ошибка); иконки: комментарий (поповер «Комментарий к документу …», [39-01]) и удалить; **Редактировать документы** — в снимке неактивна; **Загрузить документы** [40]: модалка с зоной «Перетащите файлы сюда» / **Выбрать документы**, Загрузить; **тип документа и период в модалке не выбираются** — определяются при распознавании. **API:** загрузка — `POST /legal-entities/{id}/files?consolidation_version_id=`; документы и подтверждение — `/files`, `/documents` (Срез 2c); подтверждённый документ привязывается к версиям «в работе» автоматически | `stored_file(UPLOADED)`, `s3_object`, `recognition_job(queued)`, `document.period_check/counterparty_check` → `DOCUMENTS_PROCESSING` | +| 2.5 | строка организации → **Документы** → drawer «Первичные документы» [39] | список: документ (имя, формат, дата; подпись типа у прочих — «Прочий документ», «Бухгалтерская отчетность»), Период («2024 К1», «2025 · 9 мес.»), Проверка (✓ / ✗ / — Период, Контрагент), Статус (Загружен / Обработка / Требуется проверка / Ошибка); иконки: комментарий (поповер «Комментарий к документу …», [39-01]) и удалить; **Редактировать документы** — в снимке неактивна; **Загрузить документы** [40]: модалка с зоной «Перетащите файлы сюда» / **Выбрать документы**, Загрузить; **тип документа и период в модалке не выбираются** — определяются при распознавании. **API (v5.18, docs #90):** список — `GET /consolidations/{cid}/legal-entities/{id}/files` (рабочий стол консоли: файлы, загруженные в эту консолидацию, и файлы с документами, привязанными к её версиям; переключатель «Все документы организации» — `GET /legal-entities/{id}/files`, у файла подпись консолидации); загрузка — `POST /consolidations/{cid}/legal-entities/{id}/files` (только в версии «в работе»); документы и подтверждение — `/files`, `/documents` (Срез 2c); подтверждённый документ привязывается к версиям «в работе» автоматически | `stored_file(UPLOADED, consolidation_id)`, `s3_object`, `recognition_job(queued)`, `document.period_check/counterparty_check` → `DOCUMENTS_PROCESSING` | | 2.6 | [34] → **Скачать чек-лист** [37] | матрица Организация × (Бух. баланс, ОФР, ОСВ 01 58 60 62 66 67 76 90, Прочие); легенда: Загружен / Обработка / Требуется проверка / Ошибка загрузки / Отсутствует; Скачать CSV. **API:** `GET …/versions/{id}/checklist` (`rules[]`, `rows[].cells[]`: `document_type`, `account`, `status`, `missing_quarters`, `document_ids`); «Скачать CSV» — `?format=csv` (Срез 3b) | `required_document_rule × consolidation_member − document(CONFIRMED)` | | 2.7 | drawer [39] → клик по **PDF**-документу → **Проверка распознавания** [41] | слева «Исходный документ» постранично с масштабом; справа «Результат распознавания»: шапка формы (организация, ИНН, ОКПО/ОКОПФ/ОКФС, адрес, аудит, ОКУД, отчётная дата, ОКЕИ, единица измерения) и таблица Пояснения / Наименование показателя / Код / колонки-даты (в таблице есть строки без кода «в том числе …» и строка «Гудвилл 1105» ФСБУ 4/2023 — OQ-36, OQ-37); выпадающий список **Документ** для перехода между документами ЮЛ; **Добавить комментарий**; **Редактировать** [41-02] → кнопки **Сохранить** / **Отменить** (значения таблицы редактируются на месте); **Подтвердить данные**. Блок логических ошибок — по ТЗ (в снимках не виден). Для ОСВ (xlsx) экран в макете не открывается — строки readonly, OQ-42 | `financial_statement_meta/column/line/value.is_modified`, `osv_entry.is_modified`, `document(CONFIRMED)`, `comment`, `audit_event(RECOGNIZED_DATA_EDIT, DOCUMENT_CONFIRM)` → `DATA_CORRECTION` | diff --git a/context/KICKOFF-PROMPT.md b/context/KICKOFF-PROMPT.md index 9fb29fa..3c958d5 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.17.dbml` и - `docs/model/entities-v5.17.md`. +3. `docs/context/03-architecture.md`, `04-data-model-guide.md`, затем **весь** `docs/model/data-model-v5.18.dbml` и + `docs/model/entities-v5.18.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.17; поля, которых нет в DBML, не изобретать; +- имена таблиц, колонок и enum'ов — ровно как в DBML v5.18; поля, которых нет в DBML, не изобретать; - алгоритмы — только из `05-domain-rules.md`; из страниц ТЗ напрямую не кодировать; - парсеры: никаких координат ячеек, букв колонок, имён листов и номеров строк в коде — структура по содержимому, синонимы в `rules/*.yaml`; фикстуры — регрессия, не спецификация; diff --git a/dev/api-contract-files-directories.md b/dev/api-contract-files-directories.md index 207d6b4..1cab3d1 100644 --- a/dev/api-contract-files-directories.md +++ b/dev/api-contract-files-directories.md @@ -23,8 +23,10 @@ | Метод и путь | Право | Запрос | Ответ | Ошибки | Экран | |---|---|---|---|---|---| -| `GET /legal-entities/{legal_entity_id}/files` | VIEW | query `limit`, `offset`, `include_superseded` (bool, по умолчанию `false` — заменённые скрыты) | `Page[FileOut]`, новые первыми (по убыванию `uploaded_at`) | 404 | [39] drawer «Первичные документы» | -| `POST /legal-entities/{legal_entity_id}/files` | EDIT | multipart, поле `files` (повторяется для каждого файла, не `files[]`); query `consolidation_version_id?` | 201 `UploadResult` (принят хотя бы один файл) / 422 `UploadResult` (ни один) | 404 (ЮЛ; версия — `VERSION_NOT_FOUND`), 409 `VERSION_NOT_EDITABLE`, 422 `ENTITY_NOT_IN_PERIMETER`, 503 `STORAGE_UNAVAILABLE` | [40] | +| `GET /legal-entities/{legal_entity_id}/files` | VIEW | query `limit`, `offset`, `include_superseded` (bool, по умолчанию `false` — заменённые скрыты) | `Page[FileOut]`, новые первыми (по убыванию `uploaded_at`) | 404 | переключатель «Все документы организации» [39] / страница ЮЛ | +| `GET /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` | VIEW | query `limit`, `offset`, `include_superseded` (bool, по умолчанию `false`) | `Page[FileOut]` — рабочий стол консоли для ЮЛ (файлы с этим `consolidation_id` + файлы с документами, привязанными к версиям консолидации), новые первыми; `SUPERSEDED` — только при `include_superseded=true` (скрыт по умолчанию) | 404 | [39] drawer внутри версии (v5.18, docs #90) | +| `POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` | EDIT | multipart, поле `files` (повторяется) — загрузка в консолидацию | 201 `UploadResult` (принят хотя бы один файл) / 422 `UploadResult` (ни один) | 404 (консолидация, ЮЛ), 409 `CONSOLIDATION_NOT_EDITABLE`, 422 `ENTITY_NOT_IN_PERIMETER`, 503 `STORAGE_UNAVAILABLE` | [40] | +| `POST /legal-entities/{legal_entity_id}/files` | EDIT | multipart, поле `files` (повторяется) — без контекста (`consolidation_id = null`); query `consolidation_version_id` удалён (v5.18) | 201 / 422 `UploadResult` | 404, 503 `STORAGE_UNAVAILABLE` | страница ЮЛ, монозаёмщики | | `GET /files/{file_id}` | VIEW | — | `FileOut` | 404 | опрос строки файла | | `DELETE /files/{file_id}` | EDIT | — | 204 | 404, 409 `FILE_HAS_CONFIRMED_DOCUMENTS`, 409 `FILE_SUPERSEDES_OTHERS` | [39] иконка «удалить» | | `GET /files/{file_id}/content` | VIEW | query `disposition` (`attachment` по умолчанию / `inline`) | поток исходного файла | 404 (`FILE_CONTENT_NOT_FOUND` — объекта нет в хранилище), 503 | [41] «Исходный документ» (см. контракт 3b) | @@ -32,20 +34,21 @@ | `GET /documents/{document_id}/entries` | VIEW | query `limit` (≤ 2000, по умолчанию 500), `offset` | `Page[EntryOut]` по возрастанию `row_number`; дерево — по `parent_entry_id` | 404 | строки ОСВ документа (только чтение) | | `POST /documents/{document_id}/confirm` | EDIT | — | `DocumentOut` | 404, 409 `ALREADY_CONFIRMED` / `FILE_SUPERSEDED` / `DOCUMENT_NOT_CONFIRMABLE`; для ББ/ОФР — `PERIOD_REQUIRED`, `CHECKS_NOT_PASSED` | «Подтвердить данные» | -Фильтра «файлы в контексте версии консолидации» **нет** (решение владельца 07.10.2026, arm-core #42): drawer показывает все -документы ЮЛ, период — в колонке «Период». Подходит ли документ версии, видно по ячейкам чек-листа версии (`document_ids`). +~~Фильтра «файлы в контексте версии консолидации» **нет** (решение владельца 07.10.2026, arm-core #42): drawer показывает все +документы ЮЛ, период — в колонке «Период».~~ (пересмотрено 09.10.2026, docs #90: drawer внутри версии — рабочий стол консоли, §9.5 п. 11.) +Подходит ли документ версии, видно по ячейкам чек-листа версии (`document_ids`). ## 3. Схемы: файлы и документы **`UploadResult`** `{accepted[], rejected[]}`: `accepted[]` — `{file_id, job_id, name}`; `rejected[]` — `{name, code, message, file_id?}`, где `code` — `UNSUPPORTED_FILE` (тип не из списка или сигнатура не совпала), `FILE_TOO_LARGE` (больше `UPLOAD_MAX_BYTES`, по умолчанию 50 МиБ), `DUPLICATE_FILE` (такой файл у ЮЛ уже есть — `file_id` указывает на него), `STORAGE_UNAVAILABLE`. **Два разных тела 422**: при -отказе всех файлов — `UploadResult` (смотрите `rejected[]`); при ЮЛ вне периметра версии — доменная ошибка +отказе всех файлов — `UploadResult` (смотрите `rejected[]`); при ЮЛ вне периметра всех версий консолидации «в работе» — доменная ошибка `{"detail": {"code": "ENTITY_NOT_IN_PERIMETER", "message": …}}`. Различайте по наличию `detail`. **`FileOut`** `{id, legal_entity_id, original_name?, file_type, status (FileStatus), version_no, size_bytes?, content_hash?, uploaded_at?, uploaded_by?, superseded_by_file_id?, job (JobBrief | null), documents[] (DocumentBrief)}` — поля с `?` могут быть -`null`. **`JobBrief`** `{id, status (RecognitionStatus), error_code?, error_message?, mapping_error?, warnings[], +`null`. `consolidation_id: uuid | null` — консолидация, в которую загружен файл (v5.18); `null` — загружен без контекста. **`JobBrief`** `{id, status (RecognitionStatus), error_code?, error_message?, mapping_error?, warnings[], mapping_warnings[], finished_at?}` — причина ошибки сервиса распознавания (`error_*`) или маппинга (`mapping_error`). **`DocumentBrief`** `{id, document_type, osv_account_code?, period_year?, period_quarter?, is_cumulative?, status (DocumentStatus), period_check?, counterparty_check?, locator?, comments_count}` — порядок: тип, счёт, `locator`. @@ -64,13 +67,14 @@ opening_debit?, opening_credit?, turnover_debit?, turnover_credit?, closing_debi нюанс 17). - «Проверка»: «Контрагент» — `counterparty_check` (`MATCH` ✓, `MISMATCH` ✗, `UNKNOWN`/`null` —); «Период» — `period_check` сейчас всегда `UNKNOWN` → показывайте «—» (OQ-120). +- Подпись консолидации: «<год> К<квартал>» из списка консолидаций ГК (`GET /company-groups/{id}/consolidations`) по `FileOut.consolidation_id`; `null` — «вне консолидации». - «Статус» (подписи — как в чек-листе и его CSV, `context/10-user-journey.md` «Словарь UI»): файл `UPLOADED`/`PROCESSING` — «Обработка»; файл `ERROR` — «Ошибка» (причина — `job.error_message` / `job.mapping_error`; задание `timeout`/`dlq` — тоже ошибка, причина в `job.error_message`); иначе по документу: `NEEDS_CHECK` и `RECOGNIZED` (старые и формы после правки) — «Требуется проверка», `CONFIRMED` (в т.ч. автоподтверждённый) — «Загружен», `ERROR` — «Ошибка». - Комментарий — у **документа** (`comments_count`, `GET/POST /documents/{id}/comments`, контракт 3b); у файла без документов комментария нет. -- Опрос во время распознавания: перечитывайте `GET /legal-entities/{id}/files` (или `GET /files/{id}` для одного файла), пока +- Опрос во время распознавания: перечитывайте активный список (стол консоли `GET /consolidations/{cid}/legal-entities/{id}/files` или файлы ЮЛ `GET /legal-entities/{id}/files`; или `GET /files/{id}` для одного файла), пока файл в `UPLOADED`/`PROCESSING`; разумный интервал — 3–5 с. - Клик по строке ББ/ОФР-документа (PDF и Excel) — экран [41] (`GET /documents/{id}/statement`, контракт 3b); строки ОСВ — только чтение (`GET /documents/{id}/entries`). diff --git a/dev/api-contract-slice-4.md b/dev/api-contract-slice-4.md index c88c583..0a41ac8 100644 --- a/dev/api-contract-slice-4.md +++ b/dev/api-contract-slice-4.md @@ -47,7 +47,7 @@ | `GET /consolidations/versions/{version_id}/osv/export` | VIEW | те же фильтры, что у `…/osv`, без `limit`/`offset` | файл xlsx (`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`) | 404 | «Экспорт» [30] | | `GET /consolidations/versions/{version_id}/counterparty-candidates` | VIEW | query: `q` (≤ 500), `scope` (`group` по умолчанию / `all`), `entry_id` | `CandidateOut[]` (до 100) | 404, 422 `QUERY_TOO_SHORT` (`scope=all` и `q` короче 3 символов после обрезки) | [32-osv-map-counterparty] | | `POST /consolidations/versions/{version_id}/osv/entries/{entry_id}/counterparty` | EDIT | `MapCounterpartyIn {legal_entity_id, move_alias?=false}` | 200 `MapCounterpartyOut` | 404, 409 `VERSION_NOT_EDITABLE` / `LOCK_REQUIRED` / `LOCKED_BY_OTHER`, 409 `ALIAS_TAKEN` (`owners[] {legal_entity_id, name}`) | [32-osv-map-counterparty] «Связать» | -| `POST /legal-entities/{legal_entity_id}/files?consolidation_version_id=` | `DOCUMENT_EDIT` | multipart `files`; параметр версии необязателен | как в Срезе 2c: 201/422 `{accepted[], rejected[]}` | 404 (ЮЛ или версия), 409 `VERSION_NOT_EDITABLE`, 422 `ENTITY_NOT_IN_PERIMETER` | [39] / [40] | +| `POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` | `DOCUMENT_EDIT` | multipart `files` (v5.18, docs #90; прежний query `consolidation_version_id` у `POST /legal-entities/{id}/files` удалён) | как в Срезе 2c: 201/422 `{accepted[], rejected[]}` | 404 (консолидация или ЮЛ), 409 `CONSOLIDATION_NOT_EDITABLE`, 422 `ENTITY_NOT_IN_PERIMETER` | [39] / [40] | Остальные маршруты, влияющие на версию: `POST /documents/{id}/confirm` (Срез 2c) привязывает подтверждённый документ ко всем версиям «в работе» своей ГК и может перевести версию в `DATA_CORRECTION`; `DELETE /files/{id}` и автозамена — снимают привязки @@ -177,7 +177,7 @@ alias_moved_from (uuid | null)}`. | [42] Новая консолидация | справочники: ГК → её ГСК (`…/company-groups/{id}/related-groups`, `kind = gsk`) → ЮЛ ГК и члены ГСК (нюанс 19) → `POST /consolidations` с `legal_entity_ids` | | [34] Консолидации ГК | `GET /company-groups/{id}/consolidations` (+ `in_work_only`), клик по строке — `GET …/versions/{id}` (панель «Организации»: `members[]` — тип, ячейки ББ/ОФР, `osv_accounts`; «Ответственный» — нюанс 20); «Взять в работу» — `POST …/lock`; меню «…» → «Отмена КФО» — `POST …/status {CANCELLED, comment}` | | [36] Состав | `GET …/versions/{id}` (члены), все ЮЛ ГК — справочники, «Сохранить» — `PUT …/members` | -| [39]/[40] Документы организации | `GET /legal-entities/{id}/files` (все файлы ЮЛ, опрос во время распознавания — им же или `GET /files/{id}`; схема — `dev/api-contract-files-directories.md`), `POST /legal-entities/{id}/files?consolidation_version_id=` | +| [39]/[40] Документы организации | `GET /consolidations/{cid}/legal-entities/{id}/files` (рабочий стол консоли; «Все документы организации» — `GET /legal-entities/{id}/files`; опрос во время распознавания — им же или `GET /files/{id}`; схема — `dev/api-contract-files-directories.md`), `POST /consolidations/{cid}/legal-entities/{id}/files` | | [37] Чек-лист | `GET …/checklist`; «Скачать CSV» — `GET …/checklist?format=csv` (Срез 3b) | | [30] ОСВ | `GET …/osv` с фильтрами, `GET …/osv/summary`, «Экспорт» — `GET …/osv/export` | | [32-osv-no-inn], [32-osv-map-counterparty] | `GET …/osv?unmapped=true`, `GET …/counterparty-candidates`, «Связать» — `POST …/osv/entries/{entry_id}/counterparty` | diff --git a/dev/checks.md b/dev/checks.md index 683682b..caf5c65 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.17.dbml` (путь от `api/`, по умолчанию — переменная `DBML` Makefile, она указывает на текущую версию модели; +- `dbml-check` — `python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.18.dbml` (путь от `api/`, по умолчанию — переменная `DBML` Makefile, она указывает на текущую версию модели; другой — `make dbml-check DBML=<путь относительно api/>`): ORM обязана совпадать с DBML; расхождения печатаются построчно, код 1. - Цикл миграций «`alembic upgrade head` → `alembic downgrade base` → `alembic upgrade head`» на чистой БД, пустой diff --git a/dev/frontend-api-guide.md b/dev/frontend-api-guide.md index 39d7156..08eff45 100644 --- a/dev/frontend-api-guide.md +++ b/dev/frontend-api-guide.md @@ -17,8 +17,8 @@ | `context/10-user-journey.md`, `mockup/INDEX.md` | пользовательский путь; номера экранов `[NN]` — снимки макета | Если этот гайд и OpenAPI расходятся в **схемах и полях** — прав OpenAPI работающего стенда (`/openapi.json`), а расхождение — -повод для issue в `RSHB/arm-ka-docs`. Текстовые описания ошибок в OpenAPI местами неточны (например, «`VERSION_NOT_FOUND`» у -загрузки — на деле код `NOT_FOUND`; у загрузки 422 `ENTITY_NOT_IN_PERIMETER` приходит объектом `detail`, а не `UploadResult`): +повод для issue в `RSHB/arm-ka-docs`. Текстовые описания ошибок в OpenAPI местами неточны (например, до v5.18 «`VERSION_NOT_FOUND`» у +загрузки с query `consolidation_version_id` — на деле код `NOT_FOUND`; у загрузки 422 `ENTITY_NOT_IN_PERIMETER` приходит объектом `detail`, а не `UploadResult`): коды ошибок сверяйте по §9. --- @@ -29,6 +29,7 @@ | Когда, PR | Что изменилось в API | Что сделать в клиенте | |---|---|---| +| 09.10, docs #90 (DBML v5.18; arm-core и arm-web — дочерние PR) | **Файлы в контексте консолидации.** Новые маршруты `POST /consolidations/{cid}/legal-entities/{le}/files` (загрузка в консоль: 404; 409 `CONSOLIDATION_NOT_EDITABLE` — нет версии «в работе»; 422 `ENTITY_NOT_IN_PERIMETER`) и `GET /consolidations/{cid}/legal-entities/{le}/files` (рабочий стол консоли для ЮЛ: файлы консоли + файлы с документами, привязанными к её версиям; `include_superseded`). Query `consolidation_version_id` у `POST /legal-entities/{id}/files` **удалён**; `FileOut.consolidation_id: uuid \| null`. Отменяет решение 07.10 «фильтра файлов ЮЛ в контексте версии нет» | панель [39] — стол консоли (`current_consolidation_id` из `ConsolidationPeriodOut.consolidation_id` строки списка / карточки версии `consolidation_id`), переключатель «Все документы организации» → `GET /legal-entities/{id}/files` с подписью консолидации у файла; загрузка [40] — новым маршрутом, кнопка недоступна, если версия не «в работе» | | 09.10, arm-core #85 (#84) | **Форма ЮЛ одним запросом** (макет [51-02], [79]): `POST` и `PATCH /directories/legal-entities` принимают `aliases: string[]` (1–500 символов, ≤ 100), `gsk_id`, `gsz_id` и применяют их в той же транзакции, что и поля ЮЛ (ошибка — откат всего). `POST`: алиасы и членства создаются вместе с ЮЛ. `PATCH`: не переданное поле не меняется; `aliases` — **итоговый набор** (сверка по `normalized_alias`: совпавшие не трогаются, лишние удаляются — нужно право `DIRECTORY_DELETE`, иначе 403 строкой; `aliases: null` — 422), `gsk_id`/`gsz_id` — итоговая группа (`null` — выйти, другая — сменить). Новый код 422 `WRONG_GROUP_KIND`; дубль в списке — 409 `ALIAS_EXISTS`. Ответ обоих — **`LegalEntityDetail`** (было `LegalEntityOut`; поля прежние + `memberships`, `aliases`) | «Добавить»/«Сохранить» в модалках ЮЛ — один запрос со всей формой; в `PATCH` слать `aliases` и группы целиком, как они на экране; после ответа брать карточку из него, без `GET`; удаление тега-алиаса без `DIRECTORY_DELETE` (`/me.permissions`) не предлагать — сервер ответит 403 | | 08.10, arm-core #75 (#74) | **Отмена П-1** (решение владельца 08.10.2026): «несопоставленная» строка свода — нет ЮЛ после резолва **и** нет ИНН из файла. `unmapped_total`, `GET …/osv?unmapped=true` и экспорт с этим фильтром больше не включают строки с ИНН, которого нет в справочнике (`unmapped=false` — все остальные строки). Новое поле `ConsolidatedRowOut.counterparty_entity_inn` — ИНН сопоставленного ЮЛ справочника (П-8; `counterparty_inn` — по-прежнему ИНН из файла) | «Указать» — только у строк без `counterparty_id` и без `counterparty_inn`; у строки, сопоставленной без ИНН в файле, показывать `counterparty_entity_inn` с возможностью пересопоставить (макет [32-osv-no-inn], `osv-inn-mapped-button`; повторный `POST …/counterparty` разрешён) | | 08.10, arm-core #60 (#58) | **Предупреждение автозамены** (OQ-154): если новый файл заменил прежний целиком, а части документов прежнего в нём нет (книга «ББ + ОФР» → отдельный ОФР), в `job.mapping_warnings` **нового** файла приходит одна строка на заменённый файл: «автозамена: файл «…» (версия N) заменён этим файлом целиком, но в новом файле нет документов прежнего: …; привязки сняты с версий консолидации: …» — с пометкой `CONFIRMED` у подтверждённых документов. Схемы ответов не менялись; поведение замены прежнее | показать предупреждение в карточке нового файла заметно (не только в общем списке подсказок): аналитик должен догрузить выпавшие документы; текст не разбирать | @@ -41,7 +42,7 @@ | 07.10, #46 | Ячейка чек-листа `ChecklistCellOut` стала **самоописывающей**: `document_type` и `account` своего правила | искать ячейку по `document_type`/`account`, а не по индексу | | 07.10, #46 | `VersionCard.members[]` (`MemberOut`): новые `entity_type` (`"ЮЛ"`/`"ИП"`/`null`) и `osv_accounts[]` — счета ОСВ, загруженные к периоду версии | колонки «Тип» и «ОСВ» панели «Организации» [34] | | 07.10, решения владельца П-1/П-2/П-3 | ~~«Осталось N» / «Показать ЮЛ без ИНН» = **все** несопоставленные строки (`unmapped_total`), а не только без ИНН~~ (П-1 отменён 08.10, см. arm-core #75); ГСК в версии **не хранится**; «К отчётности» доступна в любом статусе | баннер — по `unmapped_total`; колонку «ГСК» на [34] не заполнять; кнопку «К отчётности» не прятать по статусу | -| 07.10, решение владельца | Фильтра «файлы ЮЛ в контексте версии» нет и не будет | drawer [39] показывает все файлы ЮЛ; подходящесть — по `document_ids` ячеек чек-листа | +| 07.10, решение владельца | ~~Фильтра «файлы ЮЛ в контексте версии» нет и не будет~~ (пересмотрено 09.10, см. docs #90 выше) | ~~drawer [39] показывает все файлы ЮЛ~~; подходящесть документа версии — по-прежнему по `document_ids` ячеек чек-листа | | 07.10, arm-core #39 (Срез 3b) | Новые маршруты: `GET/PATCH /documents/{id}/statement`, `POST …/statement/checks/accept`, `GET/POST /documents/{id}/comments`, `GET /files/{id}/content`, `GET /users/brief`, `GET …/checklist?format=csv` | экран [41], комментарии, просмотр первички, имена пользователей, «Скачать чек-лист» | | 07.10, #39 | `POST /documents/{id}/confirm` для ББ/ОФР — новые 409 `PERIOD_REQUIRED` и `CHECKS_NOT_PASSED` (`detail.failures[]`) | обработать оба кода на «Подтвердить данные» | | 07.10, #39 | Новые поля: `comments_count` у документа (в `FileOut.documents[]` и `DocumentOut`), `documents_count` у ЮЛ | бейджи комментариев, колонка «Документы» | @@ -223,8 +224,8 @@ UI прячет элементы по `GET /me → permissions` (объедин | Область | Чтение | Изменение | Особые | |---|---|---|---| | Справочники `/directories/*` | `DIRECTORY_VIEW` | `DIRECTORY_EDIT` | удалить ГСК/ГСЗ и алиас — `DIRECTORY_DELETE`; импорт ЕЦП — `DIRECTORY_ADMIN` | -| Файлы и документы `/legal-entities/{id}/files`, `/files/*`, `/documents/*` | `DOCUMENT_VIEW` | `DOCUMENT_EDIT` (загрузка, удаление, подтверждение, правка формы, акцепт, комментарий) | — | -| Консолидации `/consolidations*`, `/company-groups/{id}/consolidations` | `CONSOLIDATION_VIEW` (в т. ч. экспорт xlsx и CSV чек-листа) | `CONSOLIDATION_EDIT` (создание, блокировка, статус, периметр, сопоставление) | перенос чужого алиаса при сопоставлении (`move_alias`) права `DIRECTORY_DELETE` не требует | +| Файлы и документы `/legal-entities/{id}/files`, `/consolidations/{cid}/legal-entities/{le}/files` (GET — `DOCUMENT_VIEW`, POST — `DOCUMENT_EDIT`), `/files/*`, `/documents/*` | `DOCUMENT_VIEW` | `DOCUMENT_EDIT` (загрузка, удаление, подтверждение, правка формы, акцепт, комментарий) | — | +| Консолидации `/consolidations*` (кроме `…/legal-entities/{le}/files`), `/company-groups/{id}/consolidations` | `CONSOLIDATION_VIEW` (в т. ч. экспорт xlsx и CSV чек-листа) | `CONSOLIDATION_EDIT` (создание, блокировка, статус, периметр, сопоставление) | перенос чужого алиаса при сопоставлении (`move_alias`) права `DIRECTORY_DELETE` не требует | | Администрирование `/admin/*` | `USERS_ADMIN` | `USERS_ADMIN` | без права — 403 до любых проверок тела | | `/me`, `/users/brief` | любой вошедший | — | — | @@ -321,8 +322,10 @@ UI прячет элементы по `GET /me → permissions` (объедин | Метод и путь | Право | Запрос | Ответ | Ошибки | |---|---|---|---|---| -| `POST /legal-entities/{legal_entity_id}/files` | EDIT | multipart, поле **`files`** повторяется на каждый файл (не `files[]`); query `consolidation_version_id?` | 201 `UploadResult {accepted[] {file_id, job_id, name}, rejected[] {name, code, message, file_id?}}`, если принят ≥ 1 файл; **422 тем же телом**, если ни одного | 404 (ЮЛ или версия), 409 `VERSION_NOT_EDITABLE`, 422 `ENTITY_NOT_IN_PERIMETER` (объектом `detail`), 503 `STORAGE_UNAVAILABLE` | +| `POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` | EDIT | multipart, поле **`files`** повторяется на каждый файл (не `files[]`) — загрузка **в консолидацию** (панель [40] внутри версии) | 201 `UploadResult {accepted[] {file_id, job_id, name}, rejected[] {name, code, message, file_id?}}`, если принят ≥ 1 файл; **422 тем же телом**, если ни одного | 404 (консолидация или ЮЛ), 409 `CONSOLIDATION_NOT_EDITABLE` (нет версии «в работе»), 422 `ENTITY_NOT_IN_PERIMETER` (объектом `detail`), 503 `STORAGE_UNAVAILABLE` | +| `POST /legal-entities/{legal_entity_id}/files` | EDIT | то же, **без контекста** (страница ЮЛ, монозаёмщики): `consolidation_id = null` | как выше | 404, 503 `STORAGE_UNAVAILABLE` | | `GET /legal-entities/{legal_entity_id}/files` | VIEW | `limit`, `offset`, `include_superseded` (по умолчанию `false`) | `Page[FileOut]`, новые первыми | — | +| `GET /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` | VIEW | `limit`, `offset`, `include_superseded` | `Page[FileOut]` — **рабочий стол консоли** для ЮЛ: файлы с этим `consolidation_id` плюс файлы, чьи документы привязаны к любой версии консолидации; новые первыми; `SUPERSEDED` — только при `include_superseded=true` (скрыт по умолчанию) | 404 | | `GET /files/{file_id}` | VIEW | — | `FileOut` | — | | `DELETE /files/{file_id}` | EDIT | — | 204 (удаляет документы, строки ОСВ, формы ББ/ОФР, комментарии, задания) | 409 `FILE_HAS_CONFIRMED_DOCUMENTS`, 409 `FILE_SUPERSEDES_OTHERS` | | `GET /files/{file_id}/content` | VIEW | `disposition` = `attachment` (по умолчанию) / `inline` | поток байт; `Content-Type` по типу файла | 404 `FILE_CONTENT_NOT_FOUND` (в БД есть, в хранилище нет), 503 `STORAGE_UNAVAILABLE` | @@ -338,10 +341,11 @@ UI прячет элементы по `GET /me → permissions` (объедин Тип и период **не спрашиваются** — их определяет парсер по содержимому. - Коды `rejected[].code`: `UNSUPPORTED_FILE` (тип не из списка, сигнатура не совпала с расширением, пустое имя), `FILE_TOO_LARGE`, `DUPLICATE_FILE` (тот же файл у ЮЛ уже есть — `file_id` указывает на него), `STORAGE_UNAVAILABLE`. -- Два разных тела 422: все файлы отклонены — `UploadResult` (смотрите `rejected[]`); ЮЛ вне периметра версии — +- Два разных тела 422: все файлы отклонены — `UploadResult` (смотрите `rejected[]`); ЮЛ вне периметра всех версий консолидации «в работе» — `{"detail": {"code": "ENTITY_NOT_IN_PERIMETER", …}}`. Различайте по наличию `detail`. -- С `consolidation_version_id` версия должна быть видима, «в работе» и содержать ЮЛ; первый принятый файл переводит версию - `INITIATED` → `DOCUMENTS_PROCESSING`. Блокировка версии для загрузки **не нужна**. +- Загрузка в консолидацию: консолидация видима, есть версия «в работе», ЮЛ в её периметре; первый принятый файл переводит + **каждую** версию консолидации «в работе» с этим ЮЛ из `INITIATED` в `DOCUMENTS_PROCESSING`. Блокировка версии для загрузки + **не нужна**. `FileOut.consolidation_id` — куда загружен файл (`null` — без контекста). - Общий размер запроса приложение не ограничивает (лимит ставит ingress), число файлов — только предел Starlette: больше 1000 в одном запросе — 400 строкой; большие пачки лучше слать по нескольку файлов. @@ -433,7 +437,7 @@ UI прячет элементы по `GET /me → permissions` (объедин 3. **Перехват**: `POST …/lock {takeover_reason: "…"}` при чужой блокировке; пустая причина или из пробелов — как отсутствующая. 4. **Ручные переходы** сейчас два: `CANCELLED` из любого «в работе» (с непустым `comment`, иначе 422 `COMMENT_REQUIRED`) и возврат в `INITIATED` из `DOCUMENTS_PROCESSING`/`DATA_CORRECTION`. Остальное — 409 `TRANSITION_NOT_ALLOWED` (маршрут - согласования — Срез 7). Автоматические переходы: загрузка с версией → `DOCUMENTS_PROCESSING`; документ `NEEDS_CHECK` по + согласования — Срез 7). Автоматические переходы: загрузка в консолидацию → `DOCUMENTS_PROCESSING`; документ `NEEDS_CHECK` по версии или подтверждение привязанного документа → `DATA_CORRECTION`. 5. **404 в консолидациях — всегда `NOT_FOUND`**; отдельных кодов «версия/строка/ГСК не найдена» нет. 6. **Ячейки чек-листа самоописывающие**: ищите по `document_type`/`account` («Бух. баланс» — `BALANCE`, «ОФР» — `INCOME`, @@ -463,7 +467,7 @@ UI прячет элементы по `GET /me → permissions` (объедин - **`CompanyGroupOut`** `{id, code, crm_id?, name, base_regional_office_id?, base_regional_office? {id, name, is_head_office}, is_archived, valid_from, legal_entities_count, created_at?, updated_at?}`. - **`LegalEntityOut`** `{id, inn, kpp?, crm_id?, entity_type?, name, company_group_id?, regional_office_id?, regional_office? {id, name, is_head_office}, close_date?, valid_from, created_at?, updated_at?, documents_count, outside_group, closed}`; **`LegalEntityDetail`** — + `memberships[] {related_group_id, code, kind, name, valid_from}`, `aliases[] {id, alias, normalized_alias, created_at?}`. - **`RelatedGroupOut`** `{id, code, company_group_id, kind, name, created_date?, members_count}`; **`RelatedGroupDetail`** — + `members[] {legal_entity_id, inn, name, valid_from}`. -- **`FileOut`** `{id, legal_entity_id, original_name?, file_type, status, version_no, size_bytes?, content_hash?, uploaded_at?, uploaded_by?, superseded_by_file_id?, job?, documents[]}`; **`JobBrief`** `{id, status, error_code?, error_message?, mapping_error?, warnings[], mapping_warnings[], finished_at?}`; **`DocumentBrief`** `{id, document_type, osv_account_code?, period_year?, period_quarter?, is_cumulative?, status, period_check?, counterparty_check?, locator?, comments_count}`. +- **`FileOut`** `{id, legal_entity_id, original_name?, file_type, status, version_no, size_bytes?, content_hash?, uploaded_at?, uploaded_by?, superseded_by_file_id?, consolidation_id: uuid | null (консолидация, в которую загружен файл; v5.18), job?, documents[]}`; **`JobBrief`** `{id, status, error_code?, error_message?, mapping_error?, warnings[], mapping_warnings[], finished_at?}`; **`DocumentBrief`** `{id, document_type, osv_account_code?, period_year?, period_quarter?, is_cumulative?, status, period_check?, counterparty_check?, locator?, comments_count}`. - **`DocumentOut`** = поля `DocumentBrief` + `{file_id, legal_entity_id, confirmed_by?, confirmed_at?, group_schema? [{type, name?, has_inn?}], warnings[]}`. - **`EntryOut`** `{id, row_number?, level?, parent_entry_id?, row_value?, group_type?, group_name?, data_type?, account_code?, account_root?, account_id?, counterparty_name?, counterparty_inn?, counterparty_id?, match_kind?, opening_debit?, opening_credit?, turnover_debit?, turnover_credit?, closing_debit?, closing_credit?}`. - **`VersionBrief`** `{id, version_no, status, scenario, approach, lock?, created_at?, members_count}`; **`LockOut`** `{locked_by, locked_by_name, locked_at, taken_over_from?, takeover_reason?}`. @@ -490,8 +494,8 @@ UI прячет элементы по `GET /me → permissions` (объедин | [33-01] «Изменить статус» / «Отмена КФО» | `POST …/versions/{id}/status {to_status, comment}` | | [36] Состав | `GET …/versions/{id}` (члены) + все ЮЛ ГК → «Сохранить» — `PUT …/members` | | [37] Чек-лист | `GET …/checklist`; «Скачать» — `GET …/checklist?format=csv` (blob, §7) | -| [39] Первичные документы ЮЛ | `GET /legal-entities/{id}/files` (+ опрос при `UPLOADED`/`PROCESSING`); «Загрузил» — `GET /users/brief?ids=…`; удалить — `DELETE /files/{id}` | -| [40] Загрузить документы | `POST /legal-entities/{id}/files?consolidation_version_id=…` → показать `accepted`/`rejected` → опрос списка | +| [39] Первичные документы ЮЛ | `GET /consolidations/{cid}/legal-entities/{id}/files` (+ опрос при `UPLOADED`/`PROCESSING`); переключатель «Все документы организации» — `GET /legal-entities/{id}/files`, подпись консолидации у файла по `consolidation_id` (год и квартал — из списка консолидаций ГК, `null` — «вне консолидации»); «Загрузил» — `GET /users/brief?ids=…`; удалить — `DELETE /files/{id}` | +| [40] Загрузить документы | `POST /consolidations/{cid}/legal-entities/{id}/files` → показать `accepted`/`rejected` → опрос списка; кнопка недоступна, если версия не «в работе» | | [41] Проверка распознавания ББ/ОФР | `GET /documents/{id}/statement`; исходник — `GET /files/{file_id}/content?disposition=inline` (blob); «Сохранить» [41-02] — `PATCH …/statement`; акцепт — `POST …/statement/checks/accept`; «Подтвердить данные» — `POST /documents/{id}/confirm`; комментарии — `GET/POST /documents/{id}/comments` | | Строки ОСВ документа | `GET /documents/{id}/entries` (только чтение) | | [30] Сводная ОСВ | `GET …/osv` с фильтрами, `GET …/osv/summary`; «Экспорт» — `GET …/osv/export` (blob) | @@ -610,10 +614,11 @@ async function download(path: string, fallbackName: string): Promise { | `PERIMETER_TOO_SMALL` | 422 | создание, `PUT …/members` | — | «Нужно минимум два ЮЛ» | | `RELATED_GROUP_MISMATCH` | 422 | создание | — | ошибка поля «ГСК» | | `ENTITY_NOT_IN_GROUP` | 422 | создание с `legal_entity_ids`, `PUT …/members` | `legal_entity_ids[]` | подсветить ЮЛ | -| `ENTITY_NOT_IN_PERIMETER` | 422 | загрузка с версией | — | «ЮЛ не входит в периметр версии» | +| `ENTITY_NOT_IN_PERIMETER` | 422 | загрузка в консолидацию | — | «ЮЛ не входит в периметр ни одной версии консолидации «в работе»» (`05-domain-rules.md` §9.5 п. 8) | | `QUERY_TOO_SHORT` | 422 | кандидаты `scope=all` | — | «Введите не менее 3 символов» | | `COMMENT_REQUIRED` | 422 | отмена версии | — | ошибка поля «Комментарий» | -| `VERSION_NOT_EDITABLE` | 409 | правки версии, загрузка с версией, взятие `CANCELLED` | — | перевести экран в просмотр | +| `VERSION_NOT_EDITABLE` | 409 | правки версии, взятие `CANCELLED` | — | перевести экран в просмотр | +| `CONSOLIDATION_NOT_EDITABLE` | 409 | загрузка в консолидацию, у которой нет версии «в работе» (`POST /consolidations/{cid}/legal-entities/{le}/files`) | — | заблокировать кнопку загрузки | | `LOCK_REQUIRED` | 409 | правки версии | — | «Возьмите версию в работу» | | `LOCKED_BY_OTHER` | 409 | взятие и правки версии | `locked_by`, `locked_at`, из `POST …/lock` ещё `locked_by_name` | «Версию держит <имя>», предложить перехват | | `NOT_LOCK_OWNER` | 409 | снятие блокировки | `locked_by`, `locked_at` | перечитать карточку | diff --git a/dev/frontend-auth-guide.md b/dev/frontend-auth-guide.md index 947dbe1..5faecc7 100644 --- a/dev/frontend-auth-guide.md +++ b/dev/frontend-auth-guide.md @@ -116,8 +116,8 @@ Dev-пользователи (`make seed-dev`), по одному на роль | Область | Чтение | Правки | Прочее | |---|---|---|---| | `/directories/regional-offices`, `/company-groups`, `/related-groups`, `/legal-entities` | `DIRECTORY_VIEW` | `DIRECTORY_EDIT` | удаление ГСК/алиаса — `DIRECTORY_DELETE`; `POST /directories/import` — `DIRECTORY_ADMIN` | -| `/legal-entities/{id}/files`, `/files/{id}`, `/documents/{id}` | `DOCUMENT_VIEW` | `DOCUMENT_EDIT` (загрузка, удаление, подтверждение) | | -| `/consolidations`, `/company-groups/{id}/consolidations`, `/consolidations/versions/{id}/*` | `CONSOLIDATION_VIEW` | `CONSOLIDATION_EDIT` (создание, блокировка, статус, периметр, сопоставление) | экспорт xlsx — `CONSOLIDATION_VIEW` | +| `/legal-entities/{id}/files`, `/consolidations/{cid}/legal-entities/{le}/files` (GET — `DOCUMENT_VIEW`, POST — `DOCUMENT_EDIT`), `/files/{id}`, `/documents/{id}` | `DOCUMENT_VIEW` | `DOCUMENT_EDIT` (загрузка, удаление, подтверждение) | | +| `/consolidations` (кроме `…/legal-entities/{le}/files`), `/company-groups/{id}/consolidations`, `/consolidations/versions/{id}/*` | `CONSOLIDATION_VIEW` | `CONSOLIDATION_EDIT` (создание, блокировка, статус, периметр, сопоставление) | экспорт xlsx — `CONSOLIDATION_VIEW` | | `/admin/users`, `/admin/roles`, `/admin/permissions` | `USERS_ADMIN` | `USERS_ADMIN` | проверка права — до разбора тела: без права 403 всегда | | `/me` | любой пользователь | — | | | `/healthcheck` (без префикса `/api/v1`) | без токена | — | живость сервера, не признак «вошёл» | diff --git a/dev/qa-guide.md b/dev/qa-guide.md index e2fee86..f437555 100644 --- a/dev/qa-guide.md +++ b/dev/qa-guide.md @@ -374,7 +374,7 @@ curl -s -X DELETE "localhost:8000/api/v1/files/$FILE" -H "$H" -w '%{http_code}\n | U14 | expert | `POST .../files` | 403 (нет `DOCUMENT_EDIT`); `GET /files/{id}` — 200 (есть `DOCUMENT_VIEW`) | | U15 | client_manager | `GET /files/{id}` файла ЮЛ другого филиала | 404, тело как у случайного uuid | | U16 | ka_go | остановить tabular-parser, загрузить новый файл | `status` остаётся `UPLOADED` (ack нет — в `PROCESSING` файл не переходит) ~2 мин (3 повтора по 30 с), затем `ERROR`, `job.error_code` заполнен; `DELETE` такого файла — 204 | -| U17 | ka_go | загрузить ОСВ ЮЛ с `consolidation_version_id=V`, где `V` в статусе «Корректировка данных» (`DATA_CORRECTION`); после `PROCESSED` (документ автоподтверждён) — `DELETE /files/{file_id}` | 204; `FILE_DELETE.after_value.unbound_version_ids` содержит `V`; статус версии `V` не меняется; `GET /files/{id}` — 404 | +| U17 | ka_go | загрузить ОСВ ЮЛ в консолидацию `C` (`POST /consolidations/$C/legal-entities/$LE/files`), у которой версия `V` в статусе «Корректировка данных» (`DATA_CORRECTION`); после `PROCESSED` (документ автоподтверждён) — `DELETE /files/{file_id}` | 204; `FILE_DELETE.after_value.unbound_version_ids` содержит `V`; статус версии `V` не меняется; `GET /files/{id}` — 404 | | U18 | ka_go | то же, но версия `V` переведена на согласование (`CONSOLIDATION_RF` и далее, см. §9.1) | 409 `FILE_HAS_CONFIRMED_DOCUMENTS`; файл и привязка на месте | Аудит (6.4 — запрос из psql): после U1 — `FILE_UPLOAD` и `DOCUMENT_CONFIRM` с пустым `user_id` (автоподтверждение); W17 — `DOCUMENT_CONFIRM` с пользователем; U10 — `FILE_REPLACE` с пустым `user_id` @@ -588,9 +588,9 @@ curl -s "localhost:8000/api/v1/documents/$DOC/statement" -H "$H" | python3 -m js ```bash curl -s -X POST localhost:8000/api/v1/consolidations -H "$H" -H 'Content-Type: application/json' \ -d '{"company_group_id":"'$G'","related_group_id":"'$S'","year":2024,"quarter":"Q3"}' -# 201: VersionCard; V = ответ.id, status = INITIATED, reporting_date = 2024-10-01 +# 201: VersionCard; V = ответ.id, C = ответ.consolidation_id, status = INITIATED, reporting_date = 2024-10-01 curl -s -X POST localhost:8000/api/v1/consolidations/versions/$V/lock -H "$H" # 201 LockOut -curl -s -X POST "localhost:8000/api/v1/legal-entities/$LE/files?consolidation_version_id=$V" -H "$H" -F 'files=@…' +curl -s -X POST "localhost:8000/api/v1/consolidations/$C/legal-entities/$LE/files" -H "$H" -F 'files=@…' curl -s localhost:8000/api/v1/consolidations/versions/$V/checklist -H "$H" curl -s "localhost:8000/api/v1/consolidations/versions/$V/osv?limit=50" -H "$H" curl -s localhost:8000/api/v1/consolidations/versions/$V/osv/summary -H "$H" @@ -606,7 +606,7 @@ curl -s -o svod.xlsx -D - "localhost:8000/api/v1/consolidations/versions/$V/osv/ | K5 | ka_rf (ГК его филиала) | `POST …/lock` на версию, занятую `ka_go` | 409 `LOCKED_BY_OTHER` (`locked_by`, `locked_at`, `locked_by_name`); с телом `{"takeover_reason":"…"}` — 201, `taken_over_from` = id `ka_go`, `ka_go` теряет блокировку; в аудите `CONSOLIDATION_LOCK_TAKEOVER` | | K6 | ka_go | после K5 `PUT …/members` | 409 `LOCKED_BY_OTHER` (чужая блокировка), без блокировки вообще — 409 `LOCK_REQUIRED` | | K7 | владелец блокировки | `PUT …/members` с составом ≤ 1 ЮЛ, с ЮЛ другой ГК, с тем же составом | 422 `PERIMETER_TOO_SMALL`; 422 `ENTITY_NOT_IN_GROUP` (`legal_entity_ids`); 200 без изменений и без события `MEMBERSHIP_CHANGE` | -| K8 | владелец блокировки | загрузить ОСВ ЮЛ периметра с `?consolidation_version_id=V` (раздел 6.6) | 201; версия `INITIATED` → `DOCUMENTS_PROCESSING`; ЮЛ вне периметра — 422 `ENTITY_NOT_IN_PERIMETER`; несуществующая версия — 404 | +| K8 | владелец блокировки | загрузить ОСВ ЮЛ периметра в консолидацию `C` (раздел 6.6) | 201; версия `INITIATED` → `DOCUMENTS_PROCESSING`; ЮЛ вне периметра — 422 `ENTITY_NOT_IN_PERIMETER`; несуществующая консолидация — 404; `GET /consolidations/$C/legal-entities/$LE/files` — загруженный файл в списке, `consolidation_id = C` | | K9 | ka_go | после `PROCESSED` и `NEEDS_CHECK` документа — `GET …/versions/{V}`; затем `POST /documents/{id}/confirm` | `NEEDS_CHECK` → версия `DATA_CORRECTION` (если файл загружал пользователь); подтверждение переводит `INITIATED`/`DOCUMENTS_PROCESSING` → `DATA_CORRECTION`; документ привязан к версии (виден в чек-листе: ячейка `CONFIRMED`) | | K10 | ka_go | `GET …/checklist` до и после подтверждения; для квартальных ОСВ — только Q3 | ячейка ОСВ: `MISSING` → `NEEDS_CHECK`/`RECOGNIZED` → `CONFIRMED`; при неполных кварталах — `missing_quarters`; ОСВ по субсчёту (58.03) не закрывает «ОСВ 58» | | K11 | ka_go | `GET …/osv`, `…/osv/summary` по данным РБ (раздел «Данные для K11») | строки свода по парам, где покрыты все кварталы `Q1..Qn`; пары без квартала — в `gaps[]` с `missing_quarters`; деньги — строки с двумя знаками; `unmapped_total` > 0 | @@ -618,6 +618,7 @@ curl -s -o svod.xlsx -D - "localhost:8000/api/v1/consolidations/versions/$V/osv/ | K17 | ka_go | `GET …/osv/export` (в т. ч. с фильтрами) | 200, `Content-Disposition: attachment; filename="svod-osv.xlsx"; filename*=UTF-8''…`; файл открывается в Excel: лист «Сводная ОСВ» (суммы — числа, формат `#,##0.00`, число строк как в API) и лист «Пробелы» (от фильтров не зависит); в аудите `EXPORT` | | K18 | владелец блокировки | `POST …/status {"to_status":"CANCELLED"}` без комментария; с комментарием | 422 `COMMENT_REQUIRED`; 200, `status = CANCELLED`, блокировка снята (`lock = null`), `history` пополнена | | K19 | ka_go | после K18: `PUT …/members`, `POST …/lock`, `POST …/status`, сопоставление | 409 `VERSION_NOT_EDITABLE`; чтение (`GET …/osv`, экспорт, чек-лист) — 200 | +| K19а | ka_go | после K18 (версия `CANCELLED`, другой версии «в работе» нет): загрузить ОСВ ЮЛ периметра в консолидацию `C` (`POST /consolidations/$C/legal-entities/$LE/files`) | 409 `CONSOLIDATION_NOT_EDITABLE` | | K20 | владелец блокировки | `POST …/status {"to_status":"INITIATED"}` из `DATA_CORRECTION`; затем `{"to_status":"DATA_CORRECTION"}` и `{"to_status":"VERIFIED"}` | 200, привязки документов сохраняются; ручные `DATA_CORRECTION`/`VERIFIED` — 409 `TRANSITION_NOT_ALLOWED` (`from_status`, `to_status`) | | K21 | expert, client_manager | `GET …/osv`, `…/checklist`; `POST …/lock`, `PUT …/members` | чтение — 200 (client_manager — только ГК своего филиала, иначе 404); правки — 403 `недостаточно прав: требуется CONSOLIDATION_EDIT` | | K22 | ka_go | заменить файл (загрузить исправленную ОСВ того же счёта и периода) после подтверждения документа | старый файл `SUPERSEDED`, привязка его документов к версии снята (`FILE_REPLACE.after_value.unbound_version_ids` содержит `V`); новый документ привязывается только после подтверждения | @@ -637,16 +638,16 @@ K17 — `EXPORT`; во всех — `consolidation_version_id`. Блокиров Для сводной ОСВ нужны все квартальные файлы двух ЮЛ. Их 22: РБ (`../fixtures/osv/rb/2024-Q{1,2,3}/`, 2 + 2 + 7) и Счастливочная (`../fixtures/osv/schastl/2024-Q{1,2,3}/`, 3 + 3 + 5). Загружать по одному через Swagger долго, поэтому цикл (`ka_go`, `$H` из 6.6; `LE_RB` и `LE_SCH` — id ЮЛ «АО "Русский бисквит"» и «ООО "Пекарня Счастливочная"», обе в одной ГСК -ГК из раздела 7; `V` — версия за 2024 Q3, созданная и взятая в работу по K1/K4 **до** загрузки — тогда переходы статусов +ГК из раздела 7; `V` — версия за 2024 Q3, созданная и взятая в работу по K1/K4 **до** загрузки; `C` — её консолидация (`consolidation_id` из ответа K1) — тогда переходы статусов видны на K8/K9): ```bash F=../fixtures/osv for f in "$F"/rb/2024-Q1/* "$F"/rb/2024-Q2/* "$F"/rb/2024-Q3/*; do - curl -s -X POST "localhost:8000/api/v1/legal-entities/$LE_RB/files?consolidation_version_id=$V" -H "$H" -F "files=@$f" | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d["accepted"][0]["name"] if d["accepted"] else d["rejected"])' + curl -s -X POST "localhost:8000/api/v1/consolidations/$C/legal-entities/$LE_RB/files" -H "$H" -F "files=@$f" | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d["accepted"][0]["name"] if d["accepted"] else d["rejected"])' done for f in "$F"/schastl/2024-Q1/* "$F"/schastl/2024-Q2/* "$F"/schastl/2024-Q3/*; do - curl -s -X POST "localhost:8000/api/v1/legal-entities/$LE_SCH/files?consolidation_version_id=$V" -H "$H" -F "files=@$f" | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d["accepted"][0]["name"] if d["accepted"] else d["rejected"])' + curl -s -X POST "localhost:8000/api/v1/consolidations/$C/legal-entities/$LE_SCH/files" -H "$H" -F "files=@$f" | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d["accepted"][0]["name"] if d["accepted"] else d["rejected"])' done # подождать ~30 с, затем убедиться, что все файлы PROCESSED, и подтвердить все документы for LE in $LE_RB $LE_SCH; do @@ -777,7 +778,7 @@ docker compose -f ../docs/dev/docker-compose.yml --profile services up -d --buil | № | Шаги | Ожидание | |---|---|---| | W16 | В панели Счастливочной клик по «ОСВ 60» | экран «Просмотр ОСВ»: крошки «Отчётность › ПЕКАРНЯ СЧАСТЛИВОЧНАЯ, ООО › Проверка документа», подзаголовок «<имя файла> · XLS · <дата>», справа статус «Загружен», кнопка «✓ Подтвердить» неактивна; надпись «Суммы в рублях; строки ОСВ только для чтения»; дерево счёт → субсчёт → контрагент, колонки «Счёт / контрагент», «ИНН» и шесть сумм. Корневая строка `60`: «—», «27 923 002,09», «107 554 149,33», «99 188 100,32», «—», «19 556 953,08» (сальдо нач. Кт + обороты Кт − обороты Дт = сальдо кон. Кт) | -| W17 | ka_go, через API (в UI не воспроизводится: загрузка только из панели ЮЛ внутри версии, см. раздел 8.7). Загрузить один файл из `../fixtures/osv/schastl/2024-Q3/` в QA-ЮЛ `$LE_QA` (id ЮЛ, заведённого для QA, не входящего в ГСК демо; без `consolidation_version_id`): `curl -s -X POST "localhost:8000/api/v1/legal-entities/$LE_QA/files" -H "$H" -F "files=@<файл>"`; дождаться `PROCESSED` (`GET /files/{file_id}`); `POST /documents/{id}/confirm` | документ `NEEDS_CHECK` («Контрагент ✗», шапка другой организации) до подтверждения; после — 200 `CONFIRMED`, `confirmed_by` заполнен; повтор — 409 `ALREADY_CONFIRMED`. Файл остаётся на стенде вне версий демо: счётчики W18 не затрагивает; `DELETE` после ручного подтверждения даёт 409 | +| W17 | ka_go, через API (в UI не воспроизводится: загрузка только из панели ЮЛ внутри версии, см. раздел 8.7). Загрузить один файл из `../fixtures/osv/schastl/2024-Q3/` в QA-ЮЛ `$LE_QA` (id ЮЛ, заведённого для QA, не входящего в ГСК демо; без консолидации — маршрутом ЮЛ): `curl -s -X POST "localhost:8000/api/v1/legal-entities/$LE_QA/files" -H "$H" -F "files=@<файл>"`; дождаться `PROCESSED` (`GET /files/{file_id}`); `POST /documents/{id}/confirm` | документ `NEEDS_CHECK` («Контрагент ✗», шапка другой организации) до подтверждения; после — 200 `CONFIRMED`, `confirmed_by` заполнен; повтор — 409 `ALREADY_CONFIRMED`. Файл остаётся на стенде вне версий демо: счётчики W18 не затрагивает; `DELETE` после ручного подтверждения даёт 409; в панели [39] версии этот файл виден только по переключателю «Все документы организации» с подписью «вне консолидации» | | W18 | Остальные файлы РБ и Счастливочной 2024 Q1–Q3 (всего 22, список — 7.1): через «Загрузить документы» у каждого ЮЛ и подтверждение каждого ОСВ, либо скриптом из каталога `arm-web`: `scripts/demo-prepare.sh ` (id — из адреса `?version=`) | скрипт печатает строку на каждый файл (пять файлов из W13 — «уже загружен», это нормально), ждёт распознавания (до 5 мин), подтверждает только неподтверждённые ОСВ (чистые уже «Загружен», автоподтверждение, OQ-157) и в конце печатает `строк: 1385 документов: 22 несопоставленных: 1260 пробелов: 7`. Статус версии — «Коррекция данных» (K9) появляется сразу после распознавания (автоподтверждение чистых документов, OQ-157), в «Организации» в колонке «ОСВ» — счета ЮЛ | `demo-prepare.sh` берёт пользователя вторым аргументом (по умолчанию `ka_go@armka.local`), адрес ядра — из `API` @@ -838,7 +839,7 @@ PDF из `fixtures/ocr` — отчётность других компаний ( может не быть до обновления. Панель «Организации» перечитывается при закрытии панели документов. - «Распознан» (старые `RECOGNIZED` и формы после правки) и «Требуется проверка» в панели документов выглядят одинаково — «Требуется проверка». - «ГСК» и «Дата верификации» в таблице консолидаций, «Утверждён в РФ» и «Верифицирован в ГО» — всегда «—». -- Панель документов показывает первые 100 файлов ЮЛ; загрузка — только из панели ЮЛ внутри версии. +- Панель документов показывает первые 100 файлов рабочего стола консоли (или ЮЛ — по переключателю); загрузка — только из панели ЮЛ внутри версии «в работе». - Шапку формы (ИНН, дату, единицу) нельзя очистить, только заменить; время принятия нарушений — в часовом поясе браузера, остальные даты — по Москве. - Ссылка `?version=` с несуществующим id «выбирает» пустую версию; при 404 на своде может прийти до трёх сообщений; diff --git a/docs/superpowers/plans/2026-10-09-console-files-docs.md b/docs/superpowers/plans/2026-10-09-console-files-docs.md new file mode 100644 index 0000000..ba414e7 --- /dev/null +++ b/docs/superpowers/plans/2026-10-09-console-files-docs.md @@ -0,0 +1,557 @@ +# Файлы в контексте консолидации — план для репозитория docs + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Поднять модель данных до v5.18 (`stored_file.consolidation_id`) и привести документы контекста, правила домена, +гайды API/QA и открытые вопросы к решениям владельца 09.10.2026 о загрузке файлов в консолидацию. + +**Architecture:** Только документы: новая версия DBML и entities копированием предыдущей с правкой, замена ссылок +`v5.17 → v5.18`, правка §9.5 и §9.1 правил, гайдов и QA-сценариев под новые маршруты +`/consolidations/{cid}/legal-entities/{le}/files`. Кода нет; код — дочерние планы arm-core и arm-web. + +**Tech Stack:** Markdown, DBML, `make check` репозитория docs (`validate-contracts`, `validate-openspec`, +`test-snapshot`), `make diagrams` (Node/npx), git, `tea`. + +**Spec:** `openspec/changes/console-files/` (`proposal.md`, `design.md`) в этом репозитории. + +## Global Constraints + +- Работать только в worktree `/Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs`, ветка `feat/90-console-files`; + git-команды — `git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs …`. +- Язык документов и коммитов — русский; идентификаторы — как в DBML (английские). +- `docs/source/**` не редактировать. +- Нумерацию версий DBML задним числом не менять: новая версия — `v5.18`, файлы v5.17 остаются как история. +- Имя колонки — ровно `consolidation_id` (nullable, FK → `consolidation.id`); индекс — `(legal_entity_id, consolidation_id)`. +- Маршруты — ровно `POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` и + `GET /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files`; query `consolidation_version_id` у + `POST /legal-entities/{id}/files` удаляется. +- Коды ошибок загрузки в консоль: 404 `NOT_FOUND`, 409 `CONSOLIDATION_NOT_EDITABLE`, 422 `ENTITY_NOT_IN_PERIMETER`. +- Каждый коммит заканчивается строкой `Co-Authored-By: Claude Fable 5.1 `. + +## Review Focus + +1. Файл, загруженный без контекста и подтверждённый, подхвачен версией — он должен быть описан как часть рабочего стола + (`BOUND`), иначе читатель §9.5 решит, что стол — только `consolidation_id`. Проверяется grep'ом в Task 3. +2. Файл `SUPERSEDED` на рабочем столе — скрыт по умолчанию, виден при `include_superseded=true`; формулировка должна быть + одинаковой в §9.5, гайде фронта и контракте. Проверяется grep'ом в Task 4. +3. Переход `INITIATED → DOCUMENTS_PROCESSING` теперь у **всех** версий консоли «в работе» с ЮЛ в периметре — таблица §9.1 + и п. 8 §9.5 не должны говорить «версия» в единственном числе. Проверяется в Task 3. +4. Решение владельца 07.10.2026 «фильтра файлов ЮЛ в контексте версии нет и не будет» (гайд фронта) пересмотрено 09.10.2026: + строка не удаляется, а зачёркивается со ссылкой. Проверяется в Task 4. +5. QA-сценарии, где `consolidation_version_id=V` использовался только ради перехода статуса (U17, K8, 7.1), должны + стать загрузкой в консоль `$C` — иначе гайд описывает удалённый параметр. Проверяется grep'ом в Task 5. + +--- + +### Task 1: DBML v5.18 + +**Files:** +- Create: `model/data-model-v5.18.dbml` (копия `model/data-model-v5.17.dbml` с правками) +- Modify: `Makefile` (переменная `DBML`) +- Modify: `model/diagrams/part04_files_jobs.{svg,png}`, `part07_consolidation.{svg,png}` — перерисовка `make diagrams` + +**Interfaces:** +- Produces: файл `model/data-model-v5.18.dbml` — его читают Task 2 (entities) и дочерний план arm-core (`make dbml-check`). + +- [ ] **Step 1: Скопировать модель** + +```bash +cd /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs +cp model/data-model-v5.17.dbml model/data-model-v5.18.dbml +``` + +- [ ] **Step 2: Шапка и changelog** + +В `model/data-model-v5.18.dbml` первую строку заменить на: + +``` +// ДКБ · Модель данных v5.18: справочники, файлы/распознавание, консолидация, КФО, аудит, ЕХФД +``` + +После строки `// страница аналитики (md); экран макета dkb_24.09.26; версия модели, в которой таблица появилась (v4 = исходная DBML аналитики).` +и пустой `//` вставить перед блоком `// v5.17 (…)`: + +``` +// v5.18 (решения владельца 09.10.2026; RSHB/arm-ka-docs#90, openspec/changes/console-files): +// 1. stored_file.consolidation_id (uuid, nullable, FK → consolidation): консолидация (ГК + год + квартал), в контексте +// которой загружен файл, — «рабочий стол консоли», общий для всех версий периода. NULL — загрузка без контекста +// (страница ЮЛ, монозаёмщики, API). Пишется один раз при загрузке (POST /consolidations/{id}/legal-entities/{le}/files); +// при автозамене новый файл без контекста наследует consolidation_id заменённого. Индекс (legal_entity_id, consolidation_id). +// Что использует конкретная версия, по-прежнему определяет consolidation_document; заморозка после VERIFIED — привязки и ex_snapshot. +// Миграция 0006 arm-core: backfill по consolidation_document (при нескольких периодах — самый ранний). +// 2. Note recognition_job.reply_deadline: срок ответа OCR — 20 мин (OQ-159, решение владельца 09.10.2026), структура не меняется. +// +``` + +- [ ] **Step 3: Колонка и индекс `stored_file`** + +В блоке `Table stored_file {` после строки ` legal_entity_id uuid [not null, ref: > legal_entity.id, note: 'организация-владелец (выбрана при загрузке)']` +добавить: + +``` + consolidation_id uuid [ref: > consolidation.id, note: 'v5.18: консолидация (период ГК), в контексте которой загружен файл — «рабочий стол консоли», общий для всех версий периода; NULL — без контекста. Пишется при загрузке; при автозамене файл без контекста наследует значение заменённого'] +``` + +В `Indexes {` того же блока после `(legal_entity_id, status)` добавить строку: + +``` + (legal_entity_id, consolidation_id) [note: 'v5.18: рабочий стол консоли для ЮЛ'] +``` + +Note таблицы `stored_file` (строка `Note: 'v5.13: физический файл одной загрузки …'`) дополнить в конце перед закрывающей +кавычкой: `. v5.18: consolidation_id — где загружен; что использует версия — consolidation_document`. + +- [ ] **Step 4: Note `consolidation` и `reply_deadline`** + +В блоке `Table consolidation {` строку `Note: 'Логическая консолидация ГК за период; версии различаются периметром (consolidation_member). Прошлые периоды — по company_group_id'` +заменить на: + +``` + Note: 'Логическая консолидация ГК за период; версии различаются периметром (consolidation_member). Прошлые периоды — по company_group_id. v5.18: файлы, загруженные в консолидацию, — stored_file.consolidation_id (рабочий стол, общий для версий периода)' +``` + +Найти в `Table recognition_job` колонку `reply_deadline` и в её note заменить `OCR — 5 мин` на `OCR — 20 мин` (OQ-159). +Проверить: `grep -n "reply_deadline" model/data-model-v5.18.dbml` — в note нет «5 мин». + +- [ ] **Step 5: Makefile и диаграммы** + +В `Makefile` строку `DBML ?= model/data-model-v5.17.dbml` заменить на `DBML ?= model/data-model-v5.18.dbml`. +Затем: + +```bash +cd /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs && make diagrams 2>&1 | tail -5 +git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs status --short model/diagrams | head +``` + +Ожидание: изменились `part04_files_jobs.*` (новая колонка и FK) и `part07_consolidation.*`; остальные могут +перерисоваться без смысловых изменений — это допустимо. Если `make diagrams` падает из-за окружения (нет сети для `npx`), +не чинить: записать в итоговый отчёт «диаграммы не перерисованы», шаг не блокирует. + +- [ ] **Step 6: Проверка** + +```bash +cd /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs +diff <(sed -n '/^Table stored_file {/,/^}/p' model/data-model-v5.17.dbml) <(sed -n '/^Table stored_file {/,/^}/p' model/data-model-v5.18.dbml) +grep -c "v5.18" model/data-model-v5.18.dbml +``` + +Ожидание: diff показывает ровно добавленную колонку, индекс и хвост Note; счётчик ≥ 5. + +- [ ] **Step 7: Коммит** + +```bash +git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs add Makefile model/data-model-v5.18.dbml model/diagrams +git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs commit -q -m "$(cat <<'EOF' +model: DBML v5.18 — stored_file.consolidation_id, рабочий стол консоли (#90) + +Co-Authored-By: Claude Fable 5.1 +EOF +)" +``` + +--- + +### Task 2: entities v5.18 и ссылки на версию модели + +**Files:** +- Create: `model/entities-v5.18.md` (копия `model/entities-v5.17.md` с правками) +- Modify: `context/04-data-model-guide.md`, `context/08-source-docs-index.md`, `context/00-workspace-layout.md`, + `context/03-architecture.md`, `context/06-test-data.md`, `context/KICKOFF-PROMPT.md`, `dev/checks.md`, + `repo-templates/arm-core.CLAUDE.md`, `/Users/andrey.epifancev/projects/awp-ca/CLAUDE.md` (вне git — workspace) + +**Interfaces:** +- Consumes: `model/data-model-v5.18.dbml` (Task 1). + +- [ ] **Step 1: Скопировать и переписать шапку entities** + +```bash +cd /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs +cp model/entities-v5.17.md model/entities-v5.18.md +``` + +В `model/entities-v5.18.md`: +- заголовок → `# АРМ КА — описание сущностей модели данных v5.18`; +- строку 3 (`Источник истины по структуре — \`data-model-v5.17.dbml\` …`) заменить на: + +``` +Источник истины по структуре — `data-model-v5.18.dbml` (полный). Диаграммы по частям — `diagrams/` (part01…09 svg/png; перерисовываются `make diagrams` из текущего DBML). Здесь — что каждая таблица значит для бизнеса, как связана с соседями и откуда взялась. +``` + +- перед разделом `## Что изменилось в v5.17 …` вставить: + +``` +## Что изменилось в v5.18 (решения владельца 09.10.2026, RSHB/arm-ka-docs#90) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `stored_file.consolidation_id` (`uuid`, nullable, FK → `consolidation`) + индекс `(legal_entity_id, consolidation_id)` | Файл загружается **в консолидацию** (период ГК) и становится частью её «рабочего стола», общего для всех версий периода; `NULL` — загрузка без контекста (страница ЮЛ, монозаёмщики, API). Панель «Первичные документы» в версии показывает рабочий стол консоли для ЮЛ плюс файлы с документами, привязанными к версиям консоли. Что использует конкретная версия — по-прежнему `consolidation_document`; заморозка после `VERIFIED` — привязки и `ex_snapshot`. Привязка файла к **версии** (`consolidation_file`) отвергнута: рабочие версии периода делят первичку, а новая версия после `VERIFIED` подхватывает подтверждённое (`openspec/changes/console-files/design.md`) | +| 2 | Note `recognition_job.reply_deadline` | Без изменения структуры: срок ответа OCR — 20 мин (OQ-159) | + +Остальные таблицы и enum — как в v5.17. Реализация — миграция `0006` в `arm-core` (backfill `consolidation_id` по `consolidation_document`). + +``` + +- [ ] **Step 2: Строки таблиц `stored_file` и `consolidation`** + +В таблице части 04 строку `| \`stored_file\` | **v5.13: только физический файл** …` заменить на: + +``` +| `stored_file` | **v5.13: только физический файл** одной загрузки: чей (ЮЛ), формат, hash, кто/когда загрузил, статус обработки (`UPLOADED → PROCESSING → PROCESSED / ERROR / SUPERSEDED`), версия при замене. **v5.18:** `consolidation_id` — консолидация, в контексте которой файл загружен («рабочий стол консоли», общий для версий периода; `NULL` — без контекста). Что внутри файла — `document`. Замена — файл целиком со всеми документами. | → `legal_entity`, `consolidation`, `s3_object`, `recognition_job`; ← `document`, `recognition_job` | ТЗ 6.2, 8.2; UC-01…05 | +``` + +Строку `| \`consolidation\` | Логическая консолидация: **ГК** + отчётный период. …` заменить на: + +``` +| `consolidation` | Логическая консолидация: **ГК** + отчётный период. ГСК фиксируется/создаётся при верификации по фактическому периметру. Уникальна по (ГК, год, квартал). **v5.18:** файлы, загруженные в консолидацию, — `stored_file.consolidation_id` (рабочий стол, общий для версий периода). | → `company_group`, `related_group`; ← `consolidation_version`, `stored_file` | ТЗ 6.10, 7.2 | +``` + +- [ ] **Step 3: Ссылки v5.17 → v5.18 в документах** + +```bash +cd /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs +for f in context/00-workspace-layout.md context/03-architecture.md context/08-source-docs-index.md context/KICKOFF-PROMPT.md dev/checks.md repo-templates/arm-core.CLAUDE.md; do + sed -i '' -e 's/data-model-v5\.17\.dbml/data-model-v5.18.dbml/g' -e 's/entities-v5\.17\.md/entities-v5.18.md/g' -e 's/DBML v5\.17/DBML v5.18/g' "$f" +done +sed -i '' -e 's/data-model-v5\.17\.dbml/data-model-v5.18.dbml/g' -e 's/entities-v5\.17\.md/entities-v5.18.md/g' -e 's/приведённая к именам v5\.17/приведённая к именам v5.18/' -e 's/(v5\.14–v5\.17 имена не меняли)/(v5.14–v5.18 имена не меняли)/' /Users/andrey.epifancev/projects/awp-ca/CLAUDE.md +grep -n "v5\.17" context/00-workspace-layout.md context/03-architecture.md context/08-source-docs-index.md context/KICKOFF-PROMPT.md dev/checks.md repo-templates/arm-core.CLAUDE.md /Users/andrey.epifancev/projects/awp-ca/CLAUDE.md +``` + +Ожидание: grep пуст, кроме исторических упоминаний (в `08-source-docs-index.md` строка про changelog `v5.1…v5.17` → вручную +заменить на `v5.1…v5.18`; в `06-test-data.md` строка «с v5.17 диаграммы перерисовываются» — история, оставить). +`08-source-docs-index.md` строка `| \`diagrams/part01…09.{svg,png}\` | диаграммы v5.17 по частям …` → `v5.18`. + +- [ ] **Step 4: `04-data-model-guide.md`** + +- Заголовок → `# 04. Как читать модель данных v5.18`. +- Строки 3–5: `data-model-v5.17.dbml` → `data-model-v5.18.dbml`, `entities-v5.17.md` → `entities-v5.18.md`, + `v5.13…v5.16` → `v5.13…v5.17` (оба упоминания), «диаграммы v5.17» → «диаграммы v5.18». +- Строку 61 (`… v5.17 — тоже (колонка \`legal_entity.kpp\`), поэтому таблица справедлива и для v5.14…v5.17.`) дополнить: + `, v5.18 — тоже (колонка \`stored_file.consolidation_id\`), поэтому таблица справедлива и для v5.14…v5.18.` (заменив + хвост `и для v5.14…v5.17.`). +- Заголовок таблицы строки 63: `| В v5.13 / v5.14 / v5.15 / v5.16 / v5.17 |` → `| В v5.13 / v5.14 / v5.15 / v5.16 / v5.17 / v5.18 |`. +- В разделе про файлы (где описан `stored_file` ↔ `document`, найти `grep -n "stored_file" context/04-data-model-guide.md`) + добавить предложение: «**v5.18:** `stored_file.consolidation_id` — в какую консолидацию (период ГК) загружен файл; это + рабочий стол консоли, общий для её версий. Что использует версия — `consolidation_document`.» + +Проверка: `grep -n "v5\.17" context/04-data-model-guide.md` — только исторические упоминания (список версий, «введён в v5.17»). + +- [ ] **Step 5: Коммит** + +```bash +git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs add model/entities-v5.18.md context dev/checks.md repo-templates +git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs commit -q -m "$(cat <<'EOF' +docs: entities v5.18 и ссылки на модель v5.18 (#90) + +Co-Authored-By: Claude Fable 5.1 +EOF +)" +``` + +--- + +### Task 3: Правила домена §9.1 и §9.5 + +**Files:** +- Modify: `context/05-domain-rules.md` (§9.1 таблица переходов, §9.5 п. 1, 3, 8; новый п. 11) + +**Interfaces:** +- Produces: формулировки, на которые ссылаются спеки arm-core (`primary-files`, `documents-api`) в дочернем плане. + +- [ ] **Step 1: §9.1 — переход при загрузке** + +Строку `| загрузка файла с \`consolidation_version_id\`, принят ≥ 1 файл | INITIATED → DOCUMENTS_PROCESSING | загрузивший |` +заменить на: + +``` +| загрузка файла в консолидацию (`POST /consolidations/{id}/legal-entities/{le}/files`), принят ≥ 1 файл | INITIATED → DOCUMENTS_PROCESSING у **каждой** версии консолидации «в работе», где ЮЛ в периметре | загрузивший | +``` + +- [ ] **Step 2: §9.5 п. 1 — два маршрута загрузки** + +В п. 1 заменить начало `1. **Загрузка** (\`POST /legal-entities/{id}/files\`, право \`DOCUMENT_EDIT\`, ЮЛ в видимости):` на: + +``` +1. **Загрузка** (`POST /legal-entities/{id}/files` — без контекста, `stored_file.consolidation_id = NULL`; в консолидацию — + п. 8; право `DOCUMENT_EDIT`, ЮЛ в видимости): +``` + +- [ ] **Step 3: §9.5 п. 3 — наследование при автозамене** + +В п. 3 после предложения `… \`version_no\` нового = максимум заменённых + 1.` добавить: + +``` +Новый файл без контекста (`consolidation_id IS NULL`) наследует `consolidation_id` первого (по `id`) заменённого файла, у +которого он задан; файл, загруженный в консолидацию, сохраняет свою (v5.18). +``` + +- [ ] **Step 4: §9.5 п. 8 — загрузка в консолидацию** + +Пункт 8 целиком заменить на: + +``` +8. **Загрузка в консолидацию** (`POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files`, + `DOCUMENT_EDIT`; v5.18, решения владельца 09.10.2026, RSHB/arm-ka-docs#90): до приёма — консолидация видима (404 + `NOT_FOUND`), ЮЛ видимо (404), есть версия консолидации «в работе» (409 `CONSOLIDATION_NOT_EDITABLE`), ЮЛ — член + периметра хотя бы одной такой версии (422 `ENTITY_NOT_IN_PERIMETER`). Принятый файл получает + `stored_file.consolidation_id`; `FILE_UPLOAD.after_value.consolidation_id`. После приёма хотя бы одного файла, отдельной + транзакцией, каждая версия консолидации «в работе» с этим ЮЛ в периметре и статусом INITIATED переходит в + DOCUMENTS_PROCESSING (§9.1; версии блокируются `FOR UPDATE` по возрастанию `id`, статус перечитывается); сбой перехода не + отменяет принятые файлы. Блокировка версии для загрузки не нужна. Query `consolidation_version_id` у + `POST /legal-entities/{id}/files` (Срез 4) удалён. +``` + +- [ ] **Step 5: §9.5 новый п. 11 — рабочий стол консоли** + +После п. 10 добавить: + +``` +11. **Рабочий стол консоли** (`GET /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files`, + `DOCUMENT_VIEW`; 404 на невидимые консолидацию или ЮЛ; `limit`, `offset`, `include_superseded`): файлы ЮЛ с + `consolidation_id` этой консолидации **плюс** файлы ЮЛ, у которых есть документ, привязанный (`consolidation_document`) + к любой версии этой консолидации (подхваченные ОСВ за `Q1` и файлы, загруженные без контекста); новые первыми + (`uploaded_at` desc, `id`); `SUPERSEDED` — только при `include_superseded=true`. Карточка файла та же, что в + `GET /legal-entities/{id}/files` (`FileOut` + `consolidation_id`). Рабочие версии одного периода видят один стол; + верифицированная версия заморожена привязками и `ex_snapshot` (Срез 7), отдельного «замороженного списка» в панели + нет. Панель [39] из карточки версии показывает стол консоли; переключатель «Все документы организации» — + `GET /legal-entities/{id}/files` с подписью консолидации у файла. +``` + +- [ ] **Step 6: Проверка** + +```bash +cd /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs +grep -n "consolidation_version_id" context/05-domain-rules.md +grep -n "каждой\* версии\|каждая версия консолидации" context/05-domain-rules.md +grep -n "привязанный (\`consolidation_document\`)\|привязанный (.consolidation_document.)" context/05-domain-rules.md +``` + +Ожидание: первый grep — только исторические упоминания (строка про Срез 4 в п. 8 «удалён» и §3/OQ, если есть) — ни одного +описания действующего параметра; второй и третий — по одной строке. + +- [ ] **Step 7: Коммит** + +```bash +git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs add context/05-domain-rules.md +git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs commit -q -m "$(cat <<'EOF' +rules: §9.1/§9.5 — загрузка в консолидацию и рабочий стол консоли (#90) + +Co-Authored-By: Claude Fable 5.1 +EOF +)" +``` + +--- + +### Task 4: Гайд фронта, контракты API, пользовательский путь + +**Files:** +- Modify: `dev/frontend-api-guide.md` (changelog, §5.5, таблица экранов), `dev/api-contract-files-directories.md`, + `dev/api-contract-slice-4.md`, `context/10-user-journey.md` (п. 2.5), `mockup/INDEX.md` (строка `39`) + +**Interfaces:** +- Consumes: формулировки §9.5 п. 8 и 11 (Task 3). + +- [ ] **Step 1: Changelog гайда фронта** + +В `dev/frontend-api-guide.md` сразу после строки `|---|---|---|` таблицы «Когда, PR | Что изменилось в API | Что сделать в клиенте» +(строка 31) вставить: + +``` +| 09.10, docs #90 (DBML v5.18; arm-core и arm-web — дочерние PR) | **Файлы в контексте консолидации.** Новые маршруты `POST /consolidations/{cid}/legal-entities/{le}/files` (загрузка в консоль: 404; 409 `CONSOLIDATION_NOT_EDITABLE` — нет версии «в работе»; 422 `ENTITY_NOT_IN_PERIMETER`) и `GET /consolidations/{cid}/legal-entities/{le}/files` (рабочий стол консоли для ЮЛ: файлы консоли + файлы с документами, привязанными к её версиям; `include_superseded`). Query `consolidation_version_id` у `POST /legal-entities/{id}/files` **удалён**; `FileOut.consolidation_id: uuid \| null`. Отменяет решение 07.10 «фильтра файлов ЮЛ в контексте версии нет» | панель [39] — стол консоли (`current_consolidation_id` из строки списка `con_id` / карточки версии `consolidation_id`), переключатель «Все документы организации» → `GET /legal-entities/{id}/files` с подписью консолидации у файла; загрузка [40] — новым маршрутом, кнопка недоступна, если версия не «в работе» | +``` + +Строку `| 07.10, решение владельца | Фильтра «файлы ЮЛ в контексте версии» нет и не будет | drawer [39] показывает все файлы ЮЛ; подходящесть — по \`document_ids\` ячеек чек-листа |` +заменить на: + +``` +| 07.10, решение владельца | ~~Фильтра «файлы ЮЛ в контексте версии» нет и не будет~~ (пересмотрено 09.10, см. docs #90 выше) | ~~drawer [39] показывает все файлы ЮЛ~~; подходящесть документа версии — по-прежнему по `document_ids` ячеек чек-листа | +``` + +- [ ] **Step 2: §5.5 гайда фронта** + +В таблице §5.5 строку `| \`POST /legal-entities/{legal_entity_id}/files\` | EDIT | multipart, поле **\`files\`** повторяется на каждый файл (не \`files[]\`); query \`consolidation_version_id?\` | … |` +заменить на две строки: + +``` +| `POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` | EDIT | multipart, поле **`files`** повторяется на каждый файл (не `files[]`) — загрузка **в консолидацию** (панель [40] внутри версии) | 201 `UploadResult {accepted[] {file_id, job_id, name}, rejected[] {name, code, message, file_id?}}`, если принят ≥ 1 файл; **422 тем же телом**, если ни одного | 404 (консолидация или ЮЛ), 409 `CONSOLIDATION_NOT_EDITABLE` (нет версии «в работе»), 422 `ENTITY_NOT_IN_PERIMETER` (объектом `detail`), 503 `STORAGE_UNAVAILABLE` | +| `POST /legal-entities/{legal_entity_id}/files` | EDIT | то же, **без контекста** (страница ЮЛ, монозаёмщики): `consolidation_id = null` | как выше | 404, 503 `STORAGE_UNAVAILABLE` | +``` + +После строки `| \`GET /legal-entities/{legal_entity_id}/files\` | VIEW | … |` добавить: + +``` +| `GET /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` | VIEW | `limit`, `offset`, `include_superseded` | `Page[FileOut]` — **рабочий стол консоли** для ЮЛ: файлы с этим `consolidation_id` плюс файлы, чьи документы привязаны к любой версии консолидации; новые первыми | 404 | +``` + +В списке «Загрузка:» пункт `- С \`consolidation_version_id\` версия должна быть видима, «в работе» и содержать ЮЛ; первый принятый файл переводит версию \`INITIATED\` → \`DOCUMENTS_PROCESSING\`. Блокировка версии для загрузки **не нужна**.` +заменить на: + +``` +- Загрузка в консолидацию: консолидация видима, есть версия «в работе», ЮЛ в её периметре; первый принятый файл переводит + **каждую** версию консолидации «в работе» с этим ЮЛ из `INITIATED` в `DOCUMENTS_PROCESSING`. Блокировка версии для загрузки + **не нужна**. `FileOut.consolidation_id` — куда загружен файл (`null` — без контекста). +``` + +В таблице экранов строки `[39]` и `[40]` заменить на: + +``` +| [39] Первичные документы ЮЛ | `GET /consolidations/{cid}/legal-entities/{id}/files` (+ опрос при `UPLOADED`/`PROCESSING`); переключатель «Все документы организации» — `GET /legal-entities/{id}/files`, подпись консолидации у файла по `consolidation_id` (год и квартал — из списка консолидаций ГК, `null` — «вне консолидации»); «Загрузил» — `GET /users/brief?ids=…`; удалить — `DELETE /files/{id}` | +| [40] Загрузить документы | `POST /consolidations/{cid}/legal-entities/{id}/files` → показать `accepted`/`rejected` → опрос списка; кнопка недоступна, если версия не «в работе» | +``` + +- [ ] **Step 3: Контракты** + +`dev/api-contract-files-directories.md`, таблица §2: строку `GET /legal-entities/{legal_entity_id}/files` оставить, после неё +добавить: + +``` +| `GET /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` | VIEW | query `limit`, `offset`, `include_superseded` | `Page[FileOut]` — рабочий стол консоли для ЮЛ (файлы с этим `consolidation_id` + файлы с документами, привязанными к версиям консолидации), новые первыми | 404 | [39] drawer внутри версии (v5.18, docs #90) | +``` + +Строку `POST /legal-entities/{legal_entity_id}/files` заменить на две: + +``` +| `POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` | EDIT | multipart, поле `files` (повторяется) — загрузка в консолидацию | 201 `UploadResult` (принят хотя бы один файл) / 422 `UploadResult` (ни один) | 404 (консолидация, ЮЛ), 409 `CONSOLIDATION_NOT_EDITABLE`, 422 `ENTITY_NOT_IN_PERIMETER`, 503 `STORAGE_UNAVAILABLE` | [40] | +| `POST /legal-entities/{legal_entity_id}/files` | EDIT | multipart, поле `files` (повторяется) — без контекста (`consolidation_id = null`); query `consolidation_version_id` удалён (v5.18) | 201 / 422 `UploadResult` | 404, 503 `STORAGE_UNAVAILABLE` | страница ЮЛ, монозаёмщики | +``` + +В описании `FileOut` этого файла (найти `grep -n "FileOut" dev/api-contract-files-directories.md`) добавить поле +`consolidation_id: uuid | null` с пояснением «консолидация, в которую загружен файл (v5.18)». + +`dev/api-contract-slice-4.md`: строку `| \`POST /legal-entities/{legal_entity_id}/files?consolidation_version_id=\` | …` заменить на: + +``` +| `POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` | `DOCUMENT_EDIT` | multipart `files` (v5.18, docs #90; прежний query `consolidation_version_id` у `POST /legal-entities/{id}/files` удалён) | как в Срезе 2c: 201/422 `{accepted[], rejected[]}` | 404 (консолидация или ЮЛ), 409 `CONSOLIDATION_NOT_EDITABLE`, 422 `ENTITY_NOT_IN_PERIMETER` | [39] / [40] | +``` + +и строку `| [39]/[40] Документы организации | \`GET /legal-entities/{id}/files\` (все файлы ЮЛ, …` на: + +``` +| [39]/[40] Документы организации | `GET /consolidations/{cid}/legal-entities/{id}/files` (рабочий стол консоли; «Все документы организации» — `GET /legal-entities/{id}/files`; опрос во время распознавания — им же или `GET /files/{id}`; схема — `dev/api-contract-files-directories.md`), `POST /consolidations/{cid}/legal-entities/{id}/files` | +``` + +- [ ] **Step 4: Пользовательский путь и макет** + +`context/10-user-journey.md` строка п. 2.5: фрагмент `**API:** загрузка — \`POST /legal-entities/{id}/files?consolidation_version_id=\`; документы и подтверждение — \`/files\`, \`/documents\` (Срез 2c); подтверждённый документ привязывается к версиям «в работе» автоматически` +заменить на: + +``` +**API:** список — `GET /consolidations/{cid}/legal-entities/{id}/files` (рабочий стол консоли: файлы, загруженные в эту консолидацию, и файлы с документами, привязанными к её версиям; переключатель «Все документы организации» — `GET /legal-entities/{id}/files`, у файла подпись консолидации); загрузка — `POST /consolidations/{cid}/legal-entities/{id}/files` (только в версии «в работе»); документы и подтверждение — `/files`, `/documents` (Срез 2c); подтверждённый документ привязывается к версиям «в работе» автоматически (v5.18, docs #90) +``` + +`mockup/INDEX.md`, строка `| \`39\` | **Панель «Первичные документы»** …`: в колонке «В модели» (`\`stored_file\`, \`document.period_check/…\`, \`comment\``) +добавить `stored_file.consolidation_id` первым элементом: `` `stored_file` (+ `consolidation_id`, v5.18: панель внутри версии показывает рабочий стол консоли) ``. + +- [ ] **Step 5: Проверка** + +```bash +cd /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs +grep -n "consolidation_version_id" dev/frontend-api-guide.md dev/api-contract-files-directories.md dev/api-contract-slice-4.md context/10-user-journey.md +grep -n "include_superseded" dev/frontend-api-guide.md dev/api-contract-files-directories.md context/05-domain-rules.md | grep -c "consolidations/{" +``` + +Ожидание: первый grep — только упоминания «удалён» и исторические (changelog, `consolidation_lock`, `osv`-маршруты); +второй — ≥ 3 (стол описан с `include_superseded` во всех трёх документах). + +- [ ] **Step 6: Коммит** + +```bash +git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs add dev/frontend-api-guide.md dev/api-contract-files-directories.md dev/api-contract-slice-4.md context/10-user-journey.md mockup/INDEX.md +git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs commit -q -m "$(cat <<'EOF' +dev: маршруты файлов консолидации в гайде фронта, контрактах и пути пользователя (#90) + +Co-Authored-By: Claude Fable 5.1 +EOF +)" +``` + +--- + +### Task 5: QA-гайд, открытые вопросы, план срезов, проверка и PR + +**Files:** +- Modify: `dev/qa-guide.md` (U17, 6.6, K8, 7.1, W17, 8.7), `context/07-open-questions.md` (OQ-159, новый OQ-160), + `context/09-delivery-plan.md` (Срез 2d), `openspec/changes/console-files/design.md` (решение R-9) + +- [ ] **Step 1: QA-гайд — curl и сценарии** + +В `dev/qa-guide.md` все вызовы вида `localhost:8000/api/v1/legal-entities/$LE…/files?consolidation_version_id=$V` заменить на +`localhost:8000/api/v1/consolidations/$C/legal-entities/$LE…/files` (три места: блок в 6.6/7, два цикла в 7.1; `$LE_RB`, +`$LE_SCH` сохраняются). Перед первым таким вызовом (блок команд раздела 7, строка с `curl -s -X POST localhost:8000/api/v1/consolidations …`) +дополнить комментарий ответа: `# 201: VersionCard; V = ответ.id, C = ответ.consolidation_id, status = INITIATED, reporting_date = 2024-10-01`. + +Строку U17: `загрузить ОСВ ЮЛ с \`consolidation_version_id=V\`, где \`V\` в статусе «Корректировка данных»` → +`загрузить ОСВ ЮЛ в консолидацию \`C\` (\`POST /consolidations/$C/legal-entities/$LE/files\`), у которой версия \`V\` в статусе «Корректировка данных»`. + +Строку K8: `загрузить ОСВ ЮЛ периметра с \`?consolidation_version_id=V\` (раздел 6.6)` → +`загрузить ОСВ ЮЛ периметра в консолидацию \`C\` (раздел 6.6)`; в ожидании `несуществующая версия — 404` → +`несуществующая консолидация — 404; консолидация без версии «в работе» (после \`CANCELLED\`) — 409 \`CONSOLIDATION_NOT_EDITABLE\`; \`GET /consolidations/$C/legal-entities/$LE/files\` — загруженный файл в списке, \`consolidation_id = C\``. + +Строку W17: `без \`consolidation_version_id\`` → `без консолидации — маршрутом ЮЛ`; в ожидании добавить в конец: +`; в панели [39] версии этот файл виден только по переключателю «Все документы организации» с подписью «вне консолидации»`. + +В 8.7 строку `- Панель документов показывает первые 100 файлов ЮЛ; загрузка — только из панели ЮЛ внутри версии.` → +`- Панель документов показывает первые 100 файлов рабочего стола консоли (или ЮЛ — по переключателю); загрузка — только из панели ЮЛ внутри версии «в работе».` + +Строку K17-примечания `… во всех — \`consolidation_version_id\`.` не трогать (речь об аудите версий). + +- [ ] **Step 2: Открытые вопросы** + +В `context/07-open-questions.md` в строке OQ-159 колонку статуса `открыт: код и §9.4 — 20 мин; note DBML правится с ближайшей версией модели` → +`**закрыт 09.10.2026:** note исправлен в DBML v5.18 (docs #90)`. + +После строки OQ-159 добавить: + +``` +| OQ-160 | Целевая модель версий консолидации (владелец, 09.10.2026): несколько рабочих версий одного периода с **разным периметром** делят первичку (рабочий стол консоли, `stored_file.consolidation_id`); после `VERIFIED` новая версия (`previous_version_id`) может менять документы при **том же** периметре. Ядро сейчас создаёт только версию 1 (`POST /consolidations`), «новая версия» и параллельные версии не реализованы. Нужны правила: создание второй рабочей версии (обязательно другой периметр?), создание версии после `VERIFIED` (копия периметра, подхват привязок), что видит панель [39] у верифицированной версии до Среза 7 (ЕХ) | brainstorming docs #90, `openspec/changes/console-files/design.md` R-6, R-7 | срез «версии консолидации» (после Среза 4), Срез 7 | открыт: модель файлов совместима; реализация — отдельный срез | +``` + +- [ ] **Step 3: План срезов** + +В `context/09-delivery-plan.md` в разделе `### Срез 2d — UI «Первичные документы»` после строки +`- Панель «Первичные документы», окно загрузки, таблица ОСВ с деревом строк и подсветкой нераспознанных контрагентов.` добавить: + +``` +- **Доработка 09.10.2026 (docs #90, DBML v5.18):** файлы в контексте консолидации — `stored_file.consolidation_id`, маршруты + `POST/GET /consolidations/{cid}/legal-entities/{le}/files`, панель [39] показывает рабочий стол консоли; спецификация — + `openspec/changes/console-files/`; дочерние PR arm-core и arm-web. Целевая модель версий — OQ-160. +``` + +- [ ] **Step 4: design.md — решение R-9** + +В `openspec/changes/console-files/design.md` в конец раздела «Decisions made without discussion» добавить: + +``` +9. 09.10.2026 — **R-9:** решение владельца 07.10.2026 «фильтра файлов ЮЛ в контексте версии нет и не будет» + (`dev/frontend-api-guide.md`) считается пересмотренным решениями 09.10.2026; строка в гайде зачёркнута со ссылкой на + docs #90, не удалена. Цена ошибки: низкая — документальная. +10. 09.10.2026 — **R-10:** note `recognition_job.reply_deadline` (OQ-159, «20 мин») исправлен в v5.18 попутно: владелец + просил править при ближайшей версии модели. Цена ошибки: нулевая. +``` + +- [ ] **Step 5: Проверка репозитория** + +```bash +cd /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs +grep -rn "consolidation_version_id" dev/qa-guide.md | grep -v "аудит\|во всех —\|versions/" +make check CHANGE=console-files 2>&1 | tail -15 +``` + +Ожидание: grep пуст или только строки про аудит; `make check` зелёный (валидация контрактов, `openspec validate console-files --strict`, снимок). + +- [ ] **Step 6: Коммит и PR** + +```bash +git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs add dev/qa-guide.md context/07-open-questions.md context/09-delivery-plan.md openspec/changes/console-files/design.md docs/superpowers/plans/2026-10-09-console-files-docs.md +git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs commit -q -m "$(cat <<'EOF' +docs: QA-гайд, OQ-160 (целевая модель версий), план срезов — файлы консолидации (#90) + +Co-Authored-By: Claude Fable 5.1 +EOF +)" +git -C /Users/andrey.epifancev/projects/awp-ca/.wt/console-files/docs push -u origin feat/90-console-files +``` + +PR — через superpowers:finishing-a-development-branch (`tea pulls create --login home --repo RSHB/arm-ka-docs …`): заголовок +«DBML v5.18: файлы в контексте консолидации — stored_file.consolidation_id, маршруты консоли (#90)», в описании — ссылка на +issue #90, список решений R-1…R-10 из design.md, вывод `make check`, пометка «дочерние PR arm-core и arm-web — следом». diff --git a/mockup/INDEX.md b/mockup/INDEX.md index 9f181da..bbae712 100644 --- a/mockup/INDEX.md +++ b/mockup/INDEX.md @@ -38,7 +38,7 @@ GSK-002), консолидации 2024 К1 (v1 Верифицирован, v2 | `36` | Модалка «Редактировать состав» | фильтры ГСК / Состав, чекбокс «Показать все ЮЛ в ГК», таблица Наименование/ИНН/Тип/ГСК с выбором, Сохранить | `consolidation_member`, `related_group_member` | 4 | | `37` | Модалка «Чек-лист загруженных документов» | фильтры Группа компаний / Документы; легенда статусов: Загружен, Обработка, Требуется проверка, Ошибка загрузки, Отсутствует; матрица: Организация × Бух. баланс, ОФР, ОСВ 01 58 60 62 66 67 76 90, Прочие документы (ОСВ №20, Прочий документ); **Скачать** (CSV) | `required_document_rule`, `document` | 3 | | `38` | Модалка «Импорт групп компаний из файла» | Файл (.xlsx, .csv), РФ, «Ожидаемые колонки: ИНН, Наименование, Тип, Базовый РФ ГК, ID ГК», «При дублировании: Пропустить существующие», Загрузить | `legal_entity`, `company_group`, `audit_event(DIRECTORY_IMPORT)` | 1 | -| `39` | **Панель «Первичные документы»** организации (drawer) | ИНН, **Загрузить документы**, Редактировать документы; таблица: Документ (имя, формат, дата; у прочих — подпись «Прочий документ» / «Бухгалтерская отчетность»), Период («2024 К1», «2025 · 9 мес.»), Проверка (✓ / ✗ / — Период, Контрагент), Статус (Загружен / Обработка / Требуется проверка / Ошибка), иконки комментарий и удалить. Строки PDF — `doc-row--previewable` (клик → `41`), строки xlsx (ОСВ, расшифровки) — `doc-row--readonly` (клик ничего не открывает) | `stored_file`, `document.period_check/counterparty_check/status`, `comment` | 2–3 | +| `39` | **Панель «Первичные документы»** организации (drawer) | ИНН, **Загрузить документы**, Редактировать документы; таблица: Документ (имя, формат, дата; у прочих — подпись «Прочий документ» / «Бухгалтерская отчетность»), Период («2024 К1», «2025 · 9 мес.»), Проверка (✓ / ✗ / — Период, Контрагент), Статус (Загружен / Обработка / Требуется проверка / Ошибка), иконки комментарий и удалить. Строки PDF — `doc-row--previewable` (клик → `41`), строки xlsx (ОСВ, расшифровки) — `doc-row--readonly` (клик ничего не открывает) | `stored_file` (+ `consolidation_id`, v5.18: файлы, загруженные в консолидацию, — основа рабочего стола консоли, который показывает панель внутри версии; полный состав — context/05-domain-rules.md §9.5 п. 11), `document.period_check/counterparty_check/status`, `comment` | 2–3 | | `39-01` | Панель документов → «Редактировать документы» | кнопка в снимке неактивна (режим не включается); иконка комментария в строке → поповер «Комментарий к документу <файл>» («Комментарий отсутствует», Закрыть) | `comment(object_type='document')` | 2–3 | | `40` | Модалка «Загрузка документов» | заголовок с организацией; зона «Перетащите файлы сюда» + **Выбрать документы**; Отмена, Загрузить (неактивна, пока нет файлов). Тип документа и период в модалке **не выбираются** — определяются при распознавании | `stored_file`, `recognition_job` | 2 | | `41` | **Проверка распознавания** (клик по PDF-документу в `39`) | крошки Отчётность › ГК › ЮЛ › Проверка документа; заголовок + имя файла, формат, дата; выпадающий список **Документ** (переход между документами организации) + бейдж статуса; **Добавить комментарий**. Слева «Исходный документ»: постраничные изображения, масштаб 100 % ±, сброс. Справа «Результат распознавания»: шапка формы (организация, ИНН, ОКПО/ОКОПФ/ОКФС, адрес, аудит), «Бухгалтерский баланс на 30 сентября 2025 г.», ОКУД, отчётная дата, ОКЕИ, единица измерения; таблица Пояснения / Наименование показателя / Код / колонки-даты, разделы и итоги; в таблице есть строки **без кода** («в том числе: Основные средства, пригодные к использованию…») и строка «Гудвилл 1105» (ФСБУ 4/2023); **Редактировать**, **Подтвердить данные**. В снимке — реальный скан ББ «Пекарни Счастливочной» из фикстур при имени файла «ОФР_2024_К1.pdf» (данные макета условны) | `document`, `financial_statement_meta/column/line/value`, `comment`, `audit_event(RECOGNIZED_DATA_EDIT, DOCUMENT_CONFIRM)` | 3 | diff --git a/model/data-model-v5.18.dbml b/model/data-model-v5.18.dbml new file mode 100644 index 0000000..7f274b5 --- /dev/null +++ b/model/data-model-v5.18.dbml @@ -0,0 +1,1373 @@ +// ДКБ · Модель данных v5.18: справочники, файлы/распознавание, консолидация, КФО, аудит, ЕХФД +// Формат: DBML (dbdiagram.io). Вставить на https://dbdiagram.io/d +// >>> Источник истины по модели данных. Аналитика в Confluence ссылается сюда. +// Над каждой таблицей — блок «ИСТОЧНИК»: пункт ТЗ АРМ КА BIQ-34 / ТЗ ЕХФД (помечено «ЕХ ТЗ») / Методика; +// страница аналитики (md); экран макета dkb_24.09.26; версия модели, в которой таблица появилась (v4 = исходная DBML аналитики). +// +// v5.18 (решения владельца 09.10.2026; RSHB/arm-ka-docs#90, openspec/changes/console-files): +// 1. stored_file.consolidation_id (uuid, nullable, FK → consolidation): консолидация (ГК + год + квартал), в контексте +// которой загружен файл, — основа «рабочего стола консоли» (полный состав стола — context/05-domain-rules.md §9.5 п. 11), общего для всех версий периода. NULL — загрузка без контекста +// (страница ЮЛ, монозаёмщики, API). Пишется один раз при загрузке (POST /consolidations/{id}/legal-entities/{le}/files); +// при автозамене новый файл без контекста наследует consolidation_id заменённого. Индекс (legal_entity_id, consolidation_id). +// Что использует конкретная версия, по-прежнему определяет consolidation_document; заморозка после VERIFIED — привязки и ex_snapshot. +// Миграция 0006 arm-core: backfill по consolidation_document (при нескольких периодах — самый ранний). +// 2. Note recognition_job.reply_deadline: срок ответа OCR — 20 мин (OQ-159, решение владельца 09.10.2026), структура не меняется. +// +// v5.17 (решение владельца 07.10.2026, пересмотр OQ-31; RSHB/arm-ka-docs#53): +// 1. legal_entity.kpp и legal_entity_history.kpp (varchar, nullable): КПП юридического лица. Формат — 9 символов: +// 4 цифры, 2 символа (цифра или латинская A–Z), 3 цифры; иначе INVALID_KPP. У ИП КПП запрещён (INVALID_KPP; +// импорт — строка пропускается). Для ЮЛ необязателен: существующие записи и импорт без колонки КПП остаются валидными. +// Уникальность — только legal_entity.inn (КПП — атрибут, сопоставление контрагентов ОСВ по ИНН не меняется). +// Изменение КПП — обычная правка со снимком в legal_entity_history. Отменяет п. 2 v5.14 («КПП не вводится»). +// +// v5.16 (Срез 4, решения владельца 06.10.2026): +// 1. Новый enum counterparty_match_kind и колонка osv_entry.counterparty_match_kind (nullable): способ, которым строке ОСВ +// сопоставлен контрагент. NULL ⇔ counterparty_id NULL; INN / NAME / ALIAS — шаг автоматического резолва при маппинге +// (ИНН; точное имя; нормализованное имя или алиас); MANUAL — ручное сопоставление аналитиком; PROPAGATED — +// автоприменение по имени в версии (DOC-221278 п. 12–13). Миграция 0004 заполняет колонку по уже смапленным строкам. +// 2. Без изменений структуры, уточнение семантики consolidation_document: строка создаётся при подтверждении документа +// (по документам ЮЛ — членов периметра версии «в работе»), при подходящем подтверждённом документе — при добавлении ЮЛ +// в периметр; удаляется при отвязке документа (замена файла, удаление, исключение ЮЛ из периметра). Для одной +// (ЮЛ, document_type, osv_account_code, период, признак нарастающего) привязан один документ — самый поздний confirmed_at. +// 3. Без изменений структуры, уточнение семантики consolidation_lock: блокировка без истечения по времени — активна, пока её не +// снял владелец, администратор или перехват другим пользователем с CONSOLIDATION_EDIT по причине (OQ-119). +// +// v5.15 (Срез 2c, решения владельца 05.10.2026): +// 1. osv_data_type: добавлено NU — строки налогового учёта из ОСВ «БУ, НУ» хранятся в osv_entry как есть (OQ-45); +// контракт результата допускал NU с v1.0. +// 2. app_user.email хранится в нижнем регистре: нормализация при записи (seed, администрирование), поиск по +// lower(email) остаётся; уникальность — существующий UNIQUE по email (OQ-59). Структура не меняется. +// 3. Без изменений структуры, уточнение семантики: stored_file.status — PROCESSING ставится inbox'ом по ack сервиса, +// PROCESSED/ERROR — циклом маппинга результата (OQ-67); маппинг выполняется отдельно от inbox (OQ-69); +// recognition_job.group_schema заполняет маппер; «Итого» в osv_entry не хранится (OQ-01). +// 4. Индексы для удаления файла и обхода дерева строк: osv_entry (parent_entry_id), osv_entry (recognition_job_id), +// stored_file (current_recognition_job_id) — проверки FK при DELETE документа/файла и очистке перед повторным +// маппингом шли seq scan'ом (OQ-117). Замер: DELETE документа 1000 строк 8,6 с → 16 мс на 200 тыс. строк. +// +// v5.14 (Срез 1, решения владельца 04.10.2026): +// 1. ЮЛ может состоять ОДНОВРЕМЕННО в одной ГСК и одной ГСЗ (но только в одной группе каждого вида) — отменяет +// ANL-030/v5.8. В related_group_member возвращена денормализованная колонка kind (= related_group.kind, +// равенство проверяет ядро при записи); частичный уникальный индекс стал (legal_entity_id, kind) WHERE is_current. +// 2. КПП у ЮЛ не вводится (OQ-31: поле есть только в макете; решение владельца — не нужен). [Отменено в v5.17 — КПП введён.] +// 3. Без изменений структуры, уточнение семантики: company_group.crm_id = «iD ГК» выгрузки ЕЦП CRM +// (docs/source/biq/ecp-export-lagal-entity.xlsx), base_regional_office — «Базовый РФ ГК в 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. +// +// v5.13 (ревью 30.09–01.10, замечания 1–7; п.2 parent_entry_id — отложен): +// 1. Права как данные: permission + role_permission (матрица доступов ТЗ сноска 29 — в БД, не в коде); +// права только через роли, прямых назначений пользователю нет. Отменяет решение v5.4. +// 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_s3_object_id; сырой ответ ИС Сканирование в БД не регистрируется +// (scan-gateway пишет его в S3 по конвенции raw/{job_id}.json для разбора инцидентов). +// 6. stored_file.fields_modified убран — COUNT(is_modified) по данным документа. +// 6-бис. Разделение «файл / документ»: stored_file = физический файл (один на загрузку), document = логическая +// единица (ББ, ОФР, ОСВ по счёту, расшифровка) — один файл содержит N документов (PDF с ББ+ОФР, xlsx с +// несколькими ОСВ листами). Чек-лист, проверки периода/контрагента, подтверждение, FK разобранных данных, +// consolidation_document и ex_snapshot_item — на document. Замена (superseded_by) остаётся на файле целиком. +// 7. file_type: убран P7. +// +// Изменения относительно v4 (кратко): +// FIX — исправлены дефекты v4 (PK у meta, типы, копипаст-note про «контрагента», row_type, parent_entry_id) +// ADD — audit_event, status_history, company_group_assignment, elimination_rule, statement_check_result, +// consolidated_osv_line, chess_cell, vgo_pair_match (агрегат по ТЗ 6.4), ofr_vgo_match, kfo_result_*, +// client_kfo_line, comment, export_log/preset, ex_snapshot*, correction_version +// CHG — role → 7 ролей ТЗ + сегмент (ЗО); consolidation_version — сценарий/подход/гудвилл/решение УОБ; +// stored_file — счёт ОСВ, версии/замена; osv_entry — признак контрагента + override; legal_entity — монозаёмщик +// +// Ключи: у ВСЕХ сущностей суррогатный PK (uuid). Натуральные ключи — UNIQUE-колонки. +// (Для высокообъёмных таблиц — osv_entry, audit_event — uuid можно заменить на bigint identity.) +// ЮЛ и ГК — SCD Type 4 (живая таблица + *_history). Членство в ГСК/ГСЗ — SCD Type 2 (is_current). +// Первичка не мутируется: правки распознанных значений = поле + *_original_value + is_modified + audit_event. +// Производные объекты версии (сводная ОСВ, шахматка, КФО) — snapshot на consolidation_version: +// пересчитываются пока версия «в работе», замораживаются при VERIFIED (ТЗ 6.10, 7.2). +// +// DECISION-1 (ТЗ 6.4 vs v4): первичная модель сопоставления ВГО — АГРЕГАТ по паре компаний × правилу пары счетов +// (vgo_pair_match), как в ТЗ/Прил. 3. Строковое 1:1 сопоставление (vgo_match) сохранено как детализация агрегата. +// DECISION-2 (ЕХФД): ЕХ — те же таблицы + ex_snapshot как «замороженный указатель» на состав верифицированной версии. +// Отдельная физическая БД для ЕХ не предполагается; архив в data lake — выгрузка снапшота. +// DECISION-3: ГО — обычная запись regional_office с is_head_office=true (вместо NULL). +// DECISION-5 (производные консолидации): сводная ОСВ, пары ВГО и шахматка НЕ хранятся, пока версия в работе — +// считаются запросом из osv_entry (актуальные прогоны) × consolidation_member × elimination_rule. +// Признак контрагента ГСК/ГК/Внешний — функция от osv_entry.counterparty_id и периметра версии; ручное действие +// аналитика = сопоставление контрагента с ЮЛ (counterparty_id + алиас), комментарий — в audit_event. +// v5.16: способ сопоставления контрагента (osv_entry.counterparty_match_kind) ХРАНИТСЯ; пробелы квартальной склейки и сам свод — нет. +// При VERIFIED производные замораживаются один раз: структурированный снапшот — jsonb в ex_snapshot_item.payload +// (просмотр в ЛК ЕХ), подготовленный xlsx для выгрузки — s3_object. Отдельная NoSQL-база не вводится. +// Корректировки и гудвилл ссылаются на пару ВГО по естественному ключу (правило, компания 1, компания 2). +// DECISION-4 (распознавание): два отдельных stateless-сервиса без доступа к БД — +// tabular-parser (XLS/XLSX/XLSB) и scan-gateway (адаптер ИС Сканирование для PDF/PNG/JPG). +// Запрос: NATS recognition.{tabular|image}.request + файл в S3 uploads/. Результат: JSON в S3 results/ + +// NATS recognition.result. Единственный писатель в БД — arm-core; корреляция по recognition_job.id; +// идемпотентность по msg_id; outbox при создании recognition_job; DLQ. +// v5.13: результат одного файла содержит N документов; ядро создаёт document при маппинге результата. +// +// v5.1 (по макету dkb_24.09.26): consolidation_lock (взятие в работу), participant_disclosure (расшифровки участника), +// legal_entity_history.event_type, statement_meta реквизиты (ОКПО/ОКОПФ/ОКФС/адрес), kfo_result_line: vgo/share, +// client_kfo_line: ВГО клиента/банка + причина, audit_event: section/result/user_agent, статусы REJECTED/ARCHIVED, +// document_check (проверка периода/контрагента файла), company_group.crm_id nullable (ANL-037). +// v5.2 (DECISION-4): recognition_job: subject/msg_id/deadline/external_task_id/result_schema_version/error_code, статусы timeout/dlq. +// v5.12 (ревью part09): ЕХ самодостаточен — срез не ссылается FK на консолидацию и её результаты (их удаляют по сроку, +// срез хранится «вечно»): ex_snapshot.source_version_id/no без FK; ex_snapshot_item = stored_file_id (исходники храним) +// ИЛИ payload jsonb (сводная ОСВ, ВГО, шахматка, КФО, гудвилл, корректировки); убраны object_type/object_id, +// prepared_s3_object_id (готовые файлы не храним), ex_snapshot.source_system, audit_event.s3_object_id; +// добавлен ex_snapshot.superseded_by_snapshot_id (архивный → актуальный). +// v5.11: consolidation_version.elimination_rules_version — какая версия правил Прил. 3/4 применялась (объяснимость и +// воспроизводимость расчёта замороженной версии). +// v5.10 (DECISION-5, ревью part08): убраны consolidated_osv_line, consolidated_osv_source, vgo_pair_match, chess_sheet, chess_cell, +// osv_entry.counterparty_type (выводится); correction ссылается на пару по (rule_id, company_1_id, company_2_id); +// ex_snapshot_item получил payload jsonb. +// v5.9 (ревью part06/07): statement_check_result убрана — проверки считаются на лету, акцепт КА — на документе +// (financial_statement_meta.checks_accepted_*); reviewed_by/at перенесены в meta (документ ревьюится целиком); +// financial_statement_line: убраны ordinal/row_kind/section (порядок и группирующие строки — шаблон формы на фронте); +// consolidation: company_group_id NOT NULL, related_group_id nullable — ГСК фиксируется при VERIFIED (ANL-029); +// consolidation_version: убран package_uuid (документы в ИС Сканирование по одному, ключ = recognition_job.id), добавлен updated_by. +// v5.8 (ревью part01): убраны company_group_assignment (видимость ТЗ 8.1 выводится из company_group.base_regional_office_id, +// legal_entity.regional_office_id, app_user.regional_office_id; история перезакрепления — company_group_history), +// legal_entity.is_mono_borrower (монозаёмщик = company_group_id IS NULL), related_group_member.kind и индекс related_group(id, kind) +// (после ANL-030 partial-unique по legal_entity_id без условия на kind). +// v5.7: метрики качества распознавания — recognition_job.fields_total/low_confidence/unrecognized (что пришло от сервиса). +// (stored_file.fields_modified/fields_reviewed и financial_statement_value.needs_review — убраны в v5.9/v5.13 как производные.) +// v5.6 (ревизия 28.09, решения по каждой убранной таблице): +// account_code_alias — нормализация кодов в коде; редкие субсчета клиентов добавляются в chart_of_account +// vat_rate — константы; средневзвешенная ставка хранится в vgo_pair_match +// crm_import_batch — импорт справочников только на старте (миграция); след в audit_event(DIRECTORY_IMPORT) + файл в s3_object +// osv_entry_edit — audit_event(RECOGNIZED_DATA_EDIT) + osv_entry.original_values/modified_by/modified_at +// document_check — колонки period_check/counterparty_check (с v5.13 — на document); сверка с периодом версии вычисляется +// export_preset — выбор колонок в момент экспорта, без сохранения наборов +// export_log — audit_event(EXPORT, after_value={report_code, format, columns, rows}); файлы выгрузок не хранятся (S3 lifecycle exports/ = 7 дней) +// v5.5: убрана correction_version — история корректировок (сноска 22) ведётся audit_event(before/after), version_no в correction. +// v5.4: возвращён s3_object (реестр объектов S3 — lifecycle/ретеншн ТЗ 8.4, sha256, единая точка ключей); permission/role_permission — матрица в коде (ОТМЕНЕНО в v5.13). +// v5.3 РЕВИЗИЯ по правилу «в базе только то, что есть в ТЗ или на экране макета». Удалено 14 таблиц: +// - account_code_alias: нормализация кодов (60.1=60.01) — алгоритм в коде (ARCH-004), не справочник +// - vat_rate: ставки 0/10/20/22 — константы; средневзвешенная ставка хранится в vgo_pair_match.vat_weighted_rate +// - disclosure_item: расшифровки участника: в UI скрыты, в ТЗ только тип документа «Расшифровка» → вне scope до решения ANL-040 +// - participant_disclosure: см. disclosure_item +// - crm_import_batch: журнал импортов не в ТЗ/UI; факт импорта и ошибки — audit_event(GROUP_CHANGE/IMPORT) +// - permission / role_permission: ВОЗВРАЩЕНЫ в v5.13 (замечание 1) +// - integration_message: РЕШЕНИЕ — обмен с NATS завязан только на recognition_job (status queued→published = outbox; result_msg_id UNIQUE = inbox/дедуп); отдельная таблица появится, только если возникнет второй тип сообщений +// - osv_entry_edit: история правок сумм ОСВ — audit_event(RECOGNIZED_DATA_EDIT, before/after) +// - document_check: результат проверки периода/контрагента — две колонки (с v5.13 — в document) +// - vgo_match: строковое сопоставление 1:1 — только в аналитике, нет ни в ТЗ (агрегат по Прил. 3), ни в UI; детализация агрегата — consolidated_osv_source +// - export_preset: сохранённые пресеты колонок не в ТЗ/UI (выбор столбцов — при экспорте, без сохранения) +// - export_log: история выгрузок (ТЗ 8.2, ЕХ 6.6) — audit_event(EXPORT) с составом в after_value + +// ============ ENUMS ============ +Enum entity_type { + "ЮЛ" + "ИП" +} + +Enum related_group_kind { + gsk + gsz +} + +Enum account_kind { + A [note: 'актив'] + P [note: 'пассив'] + AP [note: 'активно-пассивный'] +} + +Enum account_row_type { + account [note: 'синтетический счёт, напр. 60'] + subaccount [note: 'субсчёт, напр. 60.01'] + analytic [note: 'подсубсчёт, напр. 60.01.1 / 76.07.1'] +} + +Enum business_segment { + DKB [note: 'крупный бизнес'] + DMSB [note: 'малый и средний'] + DM [note: 'микробизнес'] +} + +Enum document_type { + BALANCE [note: 'Бухгалтерский баланс (0710001)'] + INCOME [note: 'ОФР (0710002)'] + OSV [note: 'ОСВ по счёту — osv_account_code обязателен'] + DISCLOSURE [note: 'Расшифровка'] + OTHER [note: 'Прочий документ'] +} + +Enum file_type { + XLS + XLSX + XLSB + PDF + PNG + JPG +} + +Enum recognition_pipeline { + tabular [note: 'XLS/XLSX/XLSB — tabular-parser'] + image [note: 'PDF/PNG/JPG — ИС Сканирование через scan-gateway'] +} + +Enum period_quarter { + Q1 + Q2 + Q3 + Q4 +} + +// v5.13: статус ФАЙЛА — только техническая обработка. Проверка и подтверждение аналитиком — на document. +Enum file_status { + UPLOADED [note: 'Загружен: поступил, в очереди'] + PROCESSING [note: 'Обработка: распознавание/парсинг'] + PROCESSED [note: 'Результат получен, документы созданы'] + ERROR [note: 'Ошибка: нечитаемый файл, требуется повторная загрузка'] + SUPERSEDED [note: 'Заменён новой версией файла (ТЗ 8.2) — вместе со всеми документами в нём'] +} + +// v5.13: статус ДОКУМЕНТА (ANL-003: «Требуется проверка» при confidence < 100% или непройденных проверках; +// «Подтвержден» — финальный после проверки аналитиком) +Enum document_status { + RECOGNIZED [note: 'данные получены, ручная проверка не требуется по порогу'] + NEEDS_CHECK [note: 'Требуется ручная проверка: confidence < 100% / контрольные суммы / период или контрагент не совпали'] + CONFIRMED [note: 'Подтвержден аналитиком: данные зафиксированы, доступны консолидации'] + ERROR [note: 'документ не разобран (напр. не найдена шапка ОСВ)'] +} + +Enum s3_bucket_role { + uploads + results + exports + archive +} + +Enum recognition_status { + queued [note: 'создано, запрос в outbox'] + published [note: 'запрос опубликован в NATS'] + processing [note: 'сервис подтвердил взятие в работу (опц. heartbeat)'] + done + needs_check [note: 'результат получен, но confidence < 100% / контрольные суммы не сошлись'] + error + timeout [note: 'reply_deadline истёк без результата'] + dlq [note: 'сообщение ушло в DLQ после max_deliver'] +} + +Enum statement_type { + balance + income +} + +Enum osv_group_type { + ACCOUNT + DEPARTMENT + COUNTERPARTY + CONTRACT + OTHER +} + +Enum counterparty_match_kind { + INN [note: 'автоматический резолв при маппинге: ИНН строки совпал с legal_entity.inn'] + NAME [note: 'автоматический резолв: точное совпадение имени строки с legal_entity.name'] + ALIAS [note: 'автоматический резолв: нормализованное имя ЮЛ или алиас (legal_entity_alias)'] + MANUAL [note: 'ручное сопоставление аналитиком'] + PROPAGATED [note: 'автоприменение ручного сопоставления к строкам с тем же именем в версии (DOC-221278 п. 12–13)'] +} + +Enum osv_data_type { + BU + NU [note: 'v5.15: налоговый учёт из ОСВ «БУ, НУ» (OQ-45)'] + CURRENCY + QUANTITY + SUM +} + +Enum consolidation_scenario { + CREATE [note: '«Создание КФО» при отсутствии клиентской консолидации'] + CONFIRM [note: '«Подтверждение КФО клиента»'] +} + +Enum consolidation_approach { + BASIC [note: 'базовый: ББ, ОФР, ОСВ 01,58,60,62,66,67,76,90'] + EXTENDED [note: 'опция к базовому: + ОСВ 02,03,08,44,26,91,51,52,59,80,83,86,97,001,008,009,011'] +} + +// Статусы КФО по ТЗ 6.10 + «Корректировка» при расхождении ≥5% (ТЗ 6.4) +Enum consolidation_status { + INITIATED + DOCUMENTS_PROCESSING + DATA_CORRECTION [note: 'корректировка распознанных данных'] + CONSOLIDATION_RF + ADJUSTMENT [note: '«Корректировка» ВГО (расхождение ≥ 5%); подстатус этапа консолидации'] + VERIFICATION_RF + CONSOLIDATION_GO + VERIFICATION_GO [note: 'РКА ГО, если КФО инициировано КА ГО (ТЗ 6.10 маршрут)'] + VERIFIED + CANCELLED + // v5.1 по макету — ANL-035: подтвердить с заказчиком + REJECTED [note: 'Отклонено: возвращено на доработку (макет). Альтернатива — событие в status_history без отдельного статуса'] + ARCHIVED [note: 'Архив: скрыто из основного списка (макет); применимо к VERIFIED/CANCELLED'] +} + +Enum correction_status { + DRAFT + APPLIED + REVOKED +} + +Enum document_check_result { + MATCH + MISMATCH + UNKNOWN +} + +Enum le_history_event { + CREATE + UPDATE + MOVE_TO_GROUP [note: '«Переход в другую ГК»'] + CLOSE [note: '«Закрытие» / вывод из состава ГК'] + REMOVE_FROM_GROUP +} + +Enum ex_snapshot_status { + ACTUAL + ARCHIVE +} + +// v5.13: объекты и действия матрицы прав (ТЗ Прил. 2 «Роль: перечень полномочий», сноска 29) +Enum permission_object { + DIRECTORY [note: 'справочники ГК/ГСК/ГСЗ/ЮЛ'] + DOCUMENT [note: 'первичные документы и распознанные данные'] + CONSOLIDATION [note: 'консолидация, версии, корректировки, гудвилл'] + KFO [note: 'результат КФО: утверждение, верификация'] + EX [note: 'Единое хранилище'] + EXPORT + USERS [note: 'пользователи и роли'] + AUDIT [note: 'журнал действий'] +} + +Enum permission_action { + VIEW + EDIT + APPROVE [note: 'утверждение в РФ'] + VERIFY [note: 'верификация в ГО'] + DELETE + EXPORT + ADMIN +} + +Enum audit_section { + DIRECTORIES + CONSOLIDATION + PRIMARY_DOCUMENTS + RECOGNITION + KFO + EXPORT + ACCESS + SESSION + USERS +} + +Enum audit_action { + LOGIN + LOGIN_FAILED + LOGOUT + CONSOLIDATION_CREATE + CONSOLIDATION_STATUS_CHANGE + CONSOLIDATION_APPROVE + CONSOLIDATION_VERIFY + GROUP_CHANGE + LEGAL_ENTITY_CHANGE [note: 'v5.14: ЮЛ создано/изменено/закрыто/переведено, алиасы'] + MEMBERSHIP_CHANGE + ASSIGNMENT_CHANGE + FILE_UPLOAD + FILE_DELETE + FILE_REPLACE + DOCUMENT_CONFIRM + CONSOLIDATION_LOCK + CONSOLIDATION_LOCK_TAKEOVER + DISCLOSURE_EDIT + RECOGNIZED_DATA_EDIT + OSV_COUNTERPARTY_MAP + OSV_TYPE_OVERRIDE + CORRECTION_CREATE + CORRECTION_EDIT + CORRECTION_REVOKE + GOODWILL_EDIT + EXPORT + DIRECTORY_IMPORT + USER_ROLE_CHANGE + USER_CHANGE [note: 'v5.14: пользователь создан/изменён/деактивирован администратором'] + ROLE_PERMISSION_CHANGE +} + +// ============ СПРАВОЧНИКИ ============ + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.1 (пользователь ГО / пользователь РФ; закрепление ГК за РФ); Прил. 2 «Пользователь: привязка к РФ/ГО» +// Аналитика: md «Модель данных» (regional_office) +// Макет: справочник ГК: колонка «Основной РФ»; пользователи: «Ростовский РФ» и т.п. +// В модели: v4 +// Зачем: справочник филиалов; DECISION-3 — ГО явной записью +Table regional_office { + id uuid [pk] + name varchar [not null, unique] + is_head_office boolean [not null, default: false, note: 'DECISION-3: ГО — явная запись'] + is_active boolean [not null, default: true] + Note: 'Филиал: ГО и региональные филиалы (РФ)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.1 (справочник ГК, импорт из ЕЦП CRM, создание вручную Куратором); Прил. 2 «ГК: id, наименование, id ЕЦП CRM» +// Аналитика: md «Модель данных» (company_group, SCD4) +// Макет: «Группы компаний»: Создать ГК, CRM ID «генерируется автоматически», Архив +// В модели: v4 + v5.1 (code, crm_id nullable — ANL-037) +Table company_group { + id uuid [pk] + code varchar [not null, unique, note: 'v5.1 ANL-037: внутренний код ГК, генерируется системой'] + crm_id varchar [unique, note: 'ключ из ЕЦП CRM; NULL для ГК, созданной вручную Куратором до сверки с CRM'] + name varchar [not null] + base_regional_office_id uuid [ref: > regional_office.id, note: 'закрепление ГК за филиалом (ТЗ 6.1/8.1); перезакрепление Куратором = UPDATE + снимок в history'] + is_archived boolean [not null, default: false, note: 'v5.1 макет: «Архив» ГК'] + valid_from timestamptz [not null] + updated_by uuid [ref: > app_user.id] + created_at timestamptz + updated_at timestamptz + Note: 'ГК — ТЕКУЩЕЕ состояние (SCD4). История — company_group_history' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.10 «фиксация изменений ГК/ГСК в истории изменений»; 8.2 «изменение состава ГСК, ГК» +// Аналитика: md «Модель данных» (company_group_history) +// Макет: «История изменений» в карточке ГК +// В модели: v4 +// Зачем: прошлые снимки ГК (SCD4) +Table company_group_history { + id uuid [pk] + company_group_id uuid [not null, ref: > company_group.id] + crm_id varchar + name varchar + base_regional_office_id uuid + valid_from timestamptz + valid_to timestamptz + changed_by uuid [ref: > app_user.id] + Indexes { (company_group_id, valid_from) [unique] } +} + + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.1 (ЮЛ/ИП, только одна ГСК/ГСЗ); Прил. 2 «ЮЛ: id, наименование, ИНН, id CRM, ГК, признак»; ЕХ ТЗ 6.1 «Монозаёмщик» +// Аналитика: md «Модель данных» (legal_entity, SCD4) +// Макет: «Юридические лица», «ЮЛ вне ГК» / «Монозаемщики» +// В модели: v4 + v5 (crm_id, is_mono_borrower, ГК nullable); v5.17 — kpp +Table legal_entity { + id uuid [pk] + inn varchar [not null, unique] + crm_id varchar [unique, note: 'ADD: id ЮЛ в ЕЦП CRM (Прил. 2 ТЗ)'] + entity_type entity_type + name varchar [not null] + company_group_id uuid [ref: > company_group.id, note: 'nullable: NULL = монозаёмщик (ЕХ 6.1), признак выводится'] + regional_office_id uuid [ref: > regional_office.id] + close_date date [note: 'NULL = активна'] + valid_from timestamptz [not null] + updated_by uuid [ref: > app_user.id] + created_at timestamptz + updated_at timestamptz + kpp varchar [note: 'v5.17 (добавлена последней колонкой, как её добавляет миграция 0005): КПП, nullable; 9 символов (4 цифры, 2 цифры/латинские A–Z, 3 цифры); у ИП запрещён — проверяет ядро (INVALID_KPP). Источник: макет [50][51-02][61][79], решение владельца 07.10.2026 (OQ-31)'] + Note: 'Организация (ЮЛ/ИП) — ТЕКУЩЕЕ состояние (SCD4). История — legal_entity_history' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.2 (журнал изменений справочников); 6.10 (история изменений состава) +// Аналитика: md «Модель данных» (legal_entity_history) +// Макет: Архив ГК: события «Переход в другую ГК», «Закрытие», дата перевода +// В модели: v4 + v5.1 (event_type, effective_date — ANL-046); v5.17 — kpp +Table legal_entity_history { + id uuid [pk] + legal_entity_id uuid [not null, ref: > legal_entity.id] + inn varchar + crm_id varchar + entity_type entity_type + name varchar + company_group_id uuid + regional_office_id uuid + close_date date + event_type le_history_event [note: 'v5.1 макет: «Переход в другую ГК» / «Закрытие» — для архива ГК'] + effective_date date [note: 'дата перевода/закрытия (не раньше текущей — ANL-046)'] + valid_from timestamptz + valid_to timestamptz + changed_by uuid [ref: > app_user.id] + kpp varchar [note: 'v5.17 (последней колонкой, миграция 0005): снимок КПП на момент события'] + Indexes { (legal_entity_id, valid_from) [unique] } +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.5 «ведение справочника соответствий наименований контрагентов», «использование ранее подтверждённых сопоставлений» +// Аналитика: md «Сводная ОСВ» (алгоритм ручного сопоставления, legal_entity_alias); FNR01 п.9 +// Макет: карточка ЮЛ: «Альтернативные наименования», «Связь с компанией из ОСВ (алиас)» +// В модели: v4 +Table legal_entity_alias { + id uuid [pk] + legal_entity_id uuid [not null, ref: > legal_entity.id] + alias varchar [not null, note: 'как в ОСВ'] + normalized_alias varchar [not null, note: 'lower/trim/без пунктуации/без ОПФ'] + created_by uuid [ref: > app_user.id] + created_at timestamptz + Indexes { + (legal_entity_id, normalized_alias) [unique] + normalized_alias [note: 'ANL-028: конфликт «один алиас у разных ЮЛ» решается при ручном сопоставлении'] + } + Note: 'Словарь соответствий наименований контрагентов (ТЗ 6.5)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.1 (ГСК/ГСЗ в составе ГК; иерархия не поддерживается; в одной ГК несколько ГСК — сноска 11); Прил. 2 «ГСК/ГСЗ» +// Аналитика: md «Модель данных» (related_group, kind) +// Макет: вкладки «ГСК», «ГСЗ», коды GSK-001/GSZ-001 +// В модели: v4 +Table related_group { + id uuid [pk] + code varchar [not null, unique] + company_group_id uuid [not null, ref: > company_group.id] + kind related_group_kind [not null] + name varchar [not null] + created_date date + Note: 'ГСК/ГСЗ — current-state. Иерархия групп не поддерживается (ТЗ 6.1)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.1 «КА может редактировать состав ГСК»; «одно ЮЛ только в одной ГСК или ГСЗ» +// Аналитика: md «Модель данных» (related_group_member, SCD2) +// Макет: «Состав», «Входит в ГСК / ГСЗ», «Редактировать состав» +// В модели: v4 (индекс уточнён — ANL-030); v5.14 — kind и индекс (legal_entity_id, kind) +Table related_group_member { + id uuid [pk] + related_group_id uuid [not null, ref: > related_group.id] + legal_entity_id uuid [not null, ref: > legal_entity.id] + kind related_group_kind [not null, note: 'v5.14: денормализация related_group.kind ради частичного уникального индекса; равенство с группой проверяет ядро при записи'] + valid_from timestamptz [not null] + valid_to timestamptz + is_current boolean [not null] + created_by uuid [ref: > app_user.id] + Indexes { + (related_group_id, legal_entity_id) [note: 'partial UNIQUE WHERE is_current'] + (legal_entity_id, kind) [note: 'partial UNIQUE WHERE is_current — v5.14: ЮЛ в одной ГСК И одной ГСЗ одновременно (решение владельца 04.10.2026, отменяет ANL-030); ГК у ЮЛ и у группы должна совпадать (проверяет ядро)'] + } + Note: 'Состав ГСК/ГСЗ — SCD2. Исключение = закрытие версии (valid_to, is_current=false)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2/6.5 (счета и субсчета 01…90, нормализация 60.1=60.01); Прил. 3/4 (пары счетов); сноска 32 +// Аналитика: md «Модель данных» (chart_of_account); md «Шахматка» (kind A/P/AP, row_type) +// Макет: фильтр «Дебетовые/Кредитовые счета», полный план счетов 1С в бандле +// В модели: v4 + v5 (row_type, is_off_balance) +Table chart_of_account { + id uuid [pk] + code varchar [not null, unique, note: 'нормализованный код: 60.01, 76.07.1'] + parent_id uuid [ref: > chart_of_account.id] + name varchar [not null] + kind account_kind + row_type account_row_type [not null, note: 'FIX: используется в «Шахматке»'] + is_off_balance boolean [not null, default: false, note: '001, 008, 009, 011'] + Note: 'План счетов' +} + + +// ADD ТЗ Прил. 3/4 как данные (сноски 32/35: пары могут корректироваться на этапе реализации) +// ИСТОЧНИК: +// ТЗ: ТЗ 6.4 + Прил. 3 «Перечень статей ББ» и Прил. 4 «Модель расчётов ОФР»; сноски 32/35 «могут быть скорректированы на этапе реализации» +// Аналитика: md «Автоматическое сопоставление ВГО» (таблица «Правила встречных счетов» — хардкод) +// Макет: — +// В модели: v5 новая +// Зачем: правила элиминации как данные, версионируемые +Table elimination_rule { + id uuid [pk] + statement_type statement_type [not null, note: 'balance = Прил. 3, income = Прил. 4'] + rule_no smallint [not null, note: '№ строки приложения'] + article_1_line varchar [not null, note: 'статья Компании 1, код строки ББ/ОФР, напр. 1230'] + article_2_line varchar [not null, note: 'статья Компании 2, напр. 1520'] + side_1 varchar [not null, note: 'D | K — сторона Компании 1'] + measure varchar [not null, note: 'closing | turnover — сальдо на конец (ББ) или оборот (ОФР)'] + elimination_mode varchar [not null, note: 'min | full | manual — min(A,B) / вся сумма ВГО (ОФР выручка) / вручную'] + vat_handling varchar [note: 'none | check_90_01_vs_2110 | weighted_rate — только для ОФР'] + excise_adjust boolean [not null, default: false] + comment text + version_no int [not null] + valid_from date [not null] + valid_to date + Indexes { (statement_type, rule_no, version_no) [unique] } + Note: 'Правило элиминации. Состав счетов — в elimination_rule_account' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ Прил. 3/4 (столбцы «Пары счетов Компании 1 и Компании 2», исключения «76 кроме 76.7.1») +// Аналитика: md «Автоматическое сопоставление ВГО» +// Макет: — +// В модели: v5 новая +// Зачем: состав счетов сторон правила +Table elimination_rule_account { + id uuid [pk] + rule_id uuid [not null, ref: > elimination_rule.id] + company_no smallint [not null, note: '1 или 2'] + account_id uuid [not null, ref: > chart_of_account.id] + is_excluded boolean [not null, default: false, note: 'true = исключение внутри маски (напр. «76 кроме 76.7.1»)'] + Note: 'Счета/субсчета сторон правила' +} + + +// ============ ПОЛЬЗОВАТЕЛИ И ДОСТУП ============ + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.1 (роли, ГО/РФ, ЗО); Прил. 2 «Пользователь: ФИО, роль, привязка к РФ/ГО, ЗО, табельный номер»; РАИСА +// Аналитика: md «Аутентификация и авторизация» (users: email, role, JWT РАИСА); md «Модель данных» (app_user) +// Макет: «Пользователи»: Логин, Роль, Активен, Деактивация +// В модели: v4 + v5 (employee_no, business_segment) +Table app_user { + id uuid [pk] + external_id varchar [unique, note: 'субъект РАИСА/SSO; аутентификация внешняя (ANL-018)'] + email varchar [unique, note: 'v5.15: хранится в нижнем регистре (нормализация при записи), сравнение по lower(email) (OQ-59)'] + full_name varchar [not null] + employee_no varchar [note: 'ADD: табельный номер (Прил. 2 ТЗ)'] + regional_office_id uuid [not null, ref: > regional_office.id, note: 'ГО или РФ (DECISION-3)'] + business_segment business_segment [note: 'ADD: зона ответственности ДКБ/ДМСБ/ДМ (ТЗ 8.1)'] + is_active boolean [not null, default: true] + created_at timestamptz + updated_at timestamptz +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.1 таблица ролей (КА РФ, КА ГО, РКА, КМ, Куратор, Эксперт, Администратор); Прил. 2 «Роль» +// Аналитика: md «Ролевая модель»; md «Модель данных» (role.code) +// Макет: «Изменение ролей и прав доступа» (Аналитик ДКБ, Наблюдатель… — расходится с ТЗ, ANL-034) +// В модели: v4 (коды → ТЗ в v5); v5.13 — матрица полномочий в role_permission +Table role { + id uuid [pk] + code varchar [not null, unique, note: 'ТЗ 8.1: KA_RF | KA_GO | RKA | CLIENT_MANAGER | CURATOR_GO | EXPERT | ADMIN. Seed; администратор может заводить дополнительные роли (замена исключениям по пользователю)'] + name varchar [not null] + description text + is_system boolean [not null, default: false, note: 'роли ТЗ 8.1 — не удаляются'] + is_active boolean [not null, default: true] + Note: 'Роль. Полномочия — через role_permission (v5.13); код роли в бизнес-логике больше не проверяется' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ Прил. 2 «Роль: перечень полномочий»; сноска 29 «матрица доступов будет разработана» +// Аналитика: md «Ролевая модель» +// Макет: карточка роли: «Права: Просмотр, редактирование КФО / справочники / консолидации» +// В модели: v5 новая, убрана в v5.3/v5.4, ВОЗВРАЩЕНА в v5.13 (замечание 1) +// Зачем: атомарное право «объект × действие»; seed, администратор новые коды не создаёт +Table permission { + id uuid [pk] + code varchar [not null, unique, note: 'напр. CONSOLIDATION_EDIT, KFO_VERIFY, DIRECTORY_EDIT, USERS_ADMIN'] + object_type permission_object [not null] + action permission_action [not null] + name varchar [not null] + description text + Indexes { (object_type, action) [unique] } + Note: 'Справочник прав (seed, привязан к коду). Ограничения по ЗО/филиалу и статусу КФО — отдельный policy-слой поверх прав: право говорит «может», policy — «эту ли и сейчас ли»' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ Прил. 2 «Роль: перечень полномочий» +// Аналитика: нет +// Макет: «Права» в карточке роли, «Изменение ролей и прав доступа» +// В модели: v5.13 (возвращена) +// Зачем: матрица доступов как данные; редактируется администратором, история — audit_event(ROLE_PERMISSION_CHANGE) +Table role_permission { + role_id uuid [not null, ref: > role.id] + permission_id uuid [not null, ref: > permission.id] + granted_by uuid [ref: > app_user.id] + granted_at timestamptz + Indexes { (role_id, permission_id) [pk] } + Note: 'effective_permissions(user) = ∪ permissions(roles(user)). Прямых прав у пользователя нет (решение 30.09): исключения — через заведение роли' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.1 (роль пользователя); Прил. 2 «Пользователь — Роль: многие-к-одному» +// Аналитика: md «Модель данных» v4 (user_role M:N) +// Макет: «Роль» пользователя +// В модели: v4 +// Зачем: M:N оставлен (пользователь может быть КА и Куратором); если ТЗ строго 1:1 — заменить на app_user.role_id +Table user_role { + id uuid [pk] + user_id uuid [not null, ref: > app_user.id] + role_id uuid [not null, ref: > role.id] + Indexes { (user_id, role_id) [unique] } + Note: 'M:N current-state. История назначений — audit_event(USER_ROLE_CHANGE)' +} + +// ============ ФАЙЛЫ / РАСПОЗНАВАНИЕ ============ + +// ИСТОЧНИК: +// ТЗ: 6.2 (файлы с префиксом UUID, исходные документы хранятся); 8.4 «хранение 5 лет с даты прекращения отношений, архивирование/удаление»; ЕХ 6.2 «ссылка на исходные документы с возможностью скачать», ЕХ 8.3 «архивирование в data lake» +// Аналитика: md «Модель данных» v4 (s3_key, raw_result_s3_key — строки) +// Макет: — +// В модели: v5.2 новая (DECISION-4), подтверждена в v5.4 +// Зачем: единая точка ключей всех бакетов для lifecycle/ретеншна и дедупа; без реестра ключи размазаны по 4 таблицам +Table s3_object { + id uuid [pk] + bucket_role s3_bucket_role [not null] + bucket varchar [not null] + key varchar [not null, note: 'схема ключа: {file_id}/{job_id}/{name}'] + content_type varchar + size_bytes bigint + sha256 varchar + version_id varchar [note: 'S3 object version при включённом versioning'] + created_by varchar [note: 'arm-core | tabular-parser | scan-gateway | user:{id}'] + created_at timestamptz [not null] + expires_at timestamptz [note: 'lifecycle: NULL = весь срок (ТЗ 8.4); exports/ = 7 дней; results/ — решение ARCH-010'] + archived_to varchar [note: 'ссылка в data lake после архивирования (ЕХ 8.3)'] + Indexes { (bucket, key) [unique] } + Note: 'Реестр объектов S3. stored_file / recognition_job ссылаются сюда FK' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 (типы и форматы документов, UUID); 8.2 «первичные документы не редактируются, только замена с версионностью» +// Аналитика: md «Загрузка документов» (панель, UC-01…05); FNR01 «Маппинг stored_file» +// Макет: «Первичные документы», «Загрузка документов», «Замена документа», «Удаление документа» +// В модели: v4 + v5 (version_no, superseded_by) + v5.2 (s3_object_id); v5.13 — только ФИЗИЧЕСКИЙ файл, документы → document; v5.18 — consolidation_id +Table stored_file { + id uuid [pk] + legal_entity_id uuid [not null, ref: > legal_entity.id, note: 'организация-владелец (выбрана при загрузке)'] + file_type file_type [not null] + original_name varchar + s3_object_id uuid [not null, ref: > s3_object.id, note: 'исходный файл в uploads'] + content_hash varchar [note: 'sha256, дедуп (UC-01 A2)'] + size_bytes bigint + status file_status [not null] + version_no int [not null, default: 1, note: 'ADD ТЗ 8.2: версия при замене'] + superseded_by_file_id uuid [ref: > stored_file.id, note: 'ссылка на файл-замену; статус SUPERSEDED. Замена — файл ЦЕЛИКОМ со всеми документами в нём (решение 01.10)'] + current_recognition_job_id uuid [ref: > recognition_job.id, note: 'авторитетный прогон; его результат породил актуальные document'] + uploaded_by uuid [ref: > app_user.id] + uploaded_at timestamptz + updated_at timestamptz + consolidation_id uuid [ref: > consolidation.id, note: 'v5.18: консолидация (период ГК), в контексте которой загружен файл — основа «рабочего стола консоли» (полный состав — context/05-domain-rules.md §9.5 п. 11), общего для версий периода; NULL — без контекста. Пишется при загрузке; при автозамене файл без контекста наследует значение заменённого'] + Indexes { + (legal_entity_id, status) + (legal_entity_id, consolidation_id) [note: 'v5.18: основа рабочего стола консоли для ЮЛ (§9.5 п. 11)'] + current_recognition_job_id [note: 'v5.15: проверка FK при удалении recognition_job / файла (OQ-117)'] + } + Note: 'v5.13: физический файл одной загрузки (S3 — только ссылка). Что внутри — document (1:N). Первичка не редактируется. Удаление физическое запрещено после INITIATED — только SUPERSEDED (ANL-020). v5.18: consolidation_id — где загружен; что использует версия — consolidation_document' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 (типы документов: ББ, ОФР, ОСВ по счетам; обязательный набор); 5.1 (подход); 8.2; Прил. 2 «Документ ФО» +// Аналитика: md «Загрузка документов» (чек-лист, UC-01 A1 период, «Подтвердить данные»); md «Парсинг ОСВ» (счёт/период из шапки) +// Макет: «Чек-лист загруженных документов», «Соответствует период», «Не соответствует контрагент», «Подтвердить данные» +// В модели: v5.13 новая (замечание 6-бис) +// Зачем: один файл = N документов (PDF ББ+ОФР; xlsx с несколькими ОСВ). Чек-лист, проверки, подтверждение и +// FK разобранных данных — здесь; без этого «ОСВ 58 загружена?» по файлу не ответить +Table document { + id uuid [pk] + stored_file_id uuid [not null, ref: > stored_file.id] + legal_entity_id uuid [not null, ref: > legal_entity.id, note: 'денормализация stored_file.legal_entity_id — выборки чек-листа по ЮЛ+период'] + document_type document_type [not null] + osv_account_code varchar [note: 'для OSV — счёт по шапке (01, 60, 62…), нормализованный; NULL для прочих типов'] + period_year smallint [note: 'из шапки/формы'] + period_quarter period_quarter + is_cumulative boolean [note: 'true = нарастающий итог с начала года'] + reporting_date date [note: 'отчётная дата, если определена'] + locator varchar [note: 'где в файле: лист xlsx / диапазон страниц PDF (напр. «sheet:ОСВ 58», «pages:1-2»)'] + status document_status [not null] + period_check document_check_result [note: 'UC-01 A1 / макет «Соответствует период»'] + counterparty_check document_check_result [note: 'макет «Не соответствует контрагент» — ИНН/наименование из шапки vs legal_entity'] + confirmed_by uuid [ref: > app_user.id, note: 'кто нажал «Подтвердить данные» → audit_event(DOCUMENT_CONFIRM)'] + confirmed_at timestamptz + created_at timestamptz + updated_at timestamptz + Indexes { + (stored_file_id, document_type, osv_account_code) [unique, note: 'NULLS NOT DISTINCT — один ББ / один ОФР / одна ОСВ по счёту на файл'] + (legal_entity_id, document_type, osv_account_code, period_year, period_quarter, status) [note: 'чек-лист required_document_rule'] + } + Note: 'Логический документ внутри файла. Создаётся ядром при маппинге результата recognition_job. Разобранные данные: OSV → osv_entry; BALANCE/INCOME → financial_statement_meta. Счётчики «исправлено полей» — COUNT(is_modified) по данным, не хранятся' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 (статусы процесса распознавания, ИС Сканирование); 6.10 «повторная обработка»; 7.1 +// Аналитика: FNR01 «Маппинг recognition_job»; md «Загрузка документов» UC-02; md «Парсинг ОСВ» (схема группировок шапки) +// Макет: «Система распознавания: получен ответ», «Обработка», «Ошибка» +// В модели: v4, переписана в v5.2 под контракт NATS (DECISION-4); v5.13 — group_schema jsonb, убран raw_result +Table recognition_job { + id uuid [pk, note: 'job_id в контракте NATS'] + file_id uuid [not null, ref: > stored_file.id] + pipeline recognition_pipeline [not null, note: 'tabular → tabular-parser; image → scan-gateway'] + request_subject varchar [not null, note: 'recognition.tabular.request | recognition.image.request'] + request_msg_id varchar [unique, note: 'Nats-Msg-Id запроса (outbox)'] + result_msg_id varchar [unique, note: 'Nats-Msg-Id результата (inbox, дедуп)'] + schema_version varchar [not null, note: 'версия контракта сообщений'] + service_name varchar [note: 'tabular-parser | scan-gateway — из результата'] + service_version varchar + status recognition_status [not null] + attempt smallint [not null, default: 1] + max_attempts smallint [not null, default: 1, note: 'OCR: 1 (0 повторов), парсер: 3'] + requested_at timestamptz + published_at timestamptz + started_at timestamptz + finished_at timestamptz + reply_deadline timestamptz [note: 'таймаут ожидания (OCR — 20 мин)'] + external_task_id varchar [note: 'id задания в ИС Сканирование (только image)'] + error_code varchar [note: 'UNSUPPORTED_FORMAT | UNREADABLE | TIMEOUT | SCHEMA_MISMATCH | …'] + error_message text + result_s3_object_id uuid [ref: > s3_object.id, note: 'нормализованный JSON в results — единственная ссылка на результат (v5.13). Сырой ответ ИС Сканирование — S3 raw/{job_id}.json по конвенции, в БД не регистрируется'] + result_schema_version varchar [note: 'версия схемы JSON результата (ARCH-008/009)'] + group_schema jsonb [note: 'v5.13 (вместо osv_group_schema): схема группировок шапки ОСВ по листам — [{\"locator\":\"sheet:ОСВ 58\",\"groups\":[{\"type\":\"ACCOUNT\"},{\"type\":\"COUNTERPARTY\",\"has_inn\":true},{\"type\":\"CONTRACT\"}]}]; только для tabular'] + metrics jsonb [note: 'duration_ms, pages, rows, warnings'] + fields_total int [note: 'v5.7: всего извлечённых значений (ячейки формы / строки ОСВ)'] + fields_low_confidence int [note: 'v5.7: значений с confidence < 100 (ТЗ 6.2) — заполняется при маппинге результата'] + fields_unrecognized int [note: 'v5.7: значений с is_recognized = false'] + Indexes { (file_id, attempt) [unique] } + Note: 'РЕШЕНИЕ v5.4: единственная точка обмена с NATS. Запись = outbox: создаётся в queued в одной транзакции с stored_file, фоновый публикатор переводит в published (при сбое публикации — повтор по queued старше N сек); результат приходит по NATS, дедуп по result_msg_id UNIQUE; redelivery после max_deliver → status=dlq, replay = сброс в queued с attempt+1. Ядро маппит JSON из S3 в document → osv_entry / financial_statement_*' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 (Osv_report_lines); 6.5 (строка ОСВ: компания, счёт, контрагент, признак, 6 сумм); Прил. 2 «Строка сводной ОСВ», «Распознанный показатель» +// Аналитика: FNR01 «Маппинг osv_entry»; md «Парсинг ОСВ» п.11 (row_number, level, group_type, data_type) +// Макет: «Оборотно-сальдовая ведомость»: Дебет/Кредит начало/оборот/конец, «Тип контрагента» +// В модели: v4 + v5 (owner_legal_entity_id — FIX, parent_entry_id, is_modified, is_excisable); v5.13 — document_id, group_type вместо group_schema_id; v5.16 — counterparty_match_kind +Table osv_entry { + id uuid [pk] + recognition_job_id uuid [not null, ref: > recognition_job.id] + document_id uuid [not null, ref: > document.id, note: 'v5.13: ОСВ-документ (счёт + период); файл — через document.stored_file_id'] + owner_legal_entity_id uuid [not null, ref: > legal_entity.id, note: 'FIX: организация-ВЛАДЕЛЕЦ ОСВ (денормализация). В v4 называлось legal_entity_id с ошибочной note'] + parent_entry_id uuid [ref: > osv_entry.id, note: 'родитель в дереве строк ОСВ (ТЗ 6.5). ОТКРЫТО (замечание 2, отложено 01.10): хранить ли только контрагентские строки и нужно ли дерево'] + group_type osv_group_type [note: 'v5.13: тип строки по схеме шапки (recognition_job.group_schema) — ACCOUNT / DEPARTMENT / COUNTERPARTY / CONTRACT / OTHER'] + group_name varchar [note: 'исходное название группировки из шапки («Контрагенты», «Договоры»)'] + row_number int + level smallint + row_value varchar [note: 'исходный текст строки'] + data_type osv_data_type + account_code varchar [note: 'сырой код счёта'] + account_id uuid [ref: > chart_of_account.id] + account_root varchar + counterparty_name varchar [note: 'сырое имя из ОСВ, не изменяется'] + counterparty_inn varchar [note: 'сырой ИНН, не изменяется'] + counterparty_id uuid [ref: > legal_entity.id, note: 'резолв по ИНН/имени/алиасу или ручное сопоставление; NULL = не сопоставлен. Признак ГСК/ГК/Внешний выводится из периметра версии (DECISION-5)'] + opening_debit numeric(18,2) + opening_credit numeric(18,2) + turnover_debit numeric(18,2) + turnover_credit numeric(18,2) + closing_debit numeric(18,2) + closing_credit numeric(18,2) + is_modified boolean [not null, default: false, note: 'значения правились аналитиком'] + original_values jsonb [note: 'v5.6: суммы до первой ручной правки {opening_debit,…}; NULL = не правилось'] + modified_by uuid [ref: > app_user.id] + modified_at timestamptz + is_excisable boolean [note: 'ADD ТЗ 6.4: признак «подакцизный товар» для ОСВ 90'] + counterparty_match_kind counterparty_match_kind [note: 'v5.16: NULL ⇔ counterparty_id NULL; INN/NAME/ALIAS — шаг автоматического резолва при маппинге; MANUAL — ручное сопоставление; PROPAGATED — автоприменение по имени в версии (DOC-221278 п. 12–13)'] + Indexes { + (document_id, row_number) [unique] + (owner_legal_entity_id, account_id, counterparty_id) + parent_entry_id [note: 'v5.15: обход дерева строк и проверка self-FK при удалении документа (OQ-117)'] + recognition_job_id [note: 'v5.15: проверка FK при удалении recognition_job / файла (OQ-117)'] + } + Note: 'Строка ОСВ. Правки сумм аналитиком — original_values/modified_by + audit_event(RECOGNIZED_DATA_EDIT, before/after)' +} + + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 «извлекаются значения по кодам форм Приказа 157н»; 6.3 проверки единиц измерения/даты +// Аналитика: FNR01 «fin_report_header» (report_type, org, unit, currency, reporting_date); md «Модель данных» v4 +// Макет: «Реквизиты организации» (ОКПО, ОКОПФ, ОКФС, ОКУД, ОКЕИ, адрес), «Единица измерения», «Отчетная дата» +// В модели: v4 (FIX pk) + v5.1 (реквизиты — ANL-044); v5.13 — document_id +Table financial_statement_meta { + id uuid [pk, note: 'FIX: был без PK'] + document_id uuid [not null, unique, ref: > document.id, note: 'v5.13: документ BALANCE/INCOME (1:1); файл — через document'] + recognition_job_id uuid [not null, ref: > recognition_job.id] + statement_type statement_type [not null] + inn varchar + inn_original_value varchar + inn_confidence numeric(5,2) + inn_verified boolean + inn_modified boolean + organization_name varchar [note: 'как в документе; сверяется с legal_entity/alias, при несовпадении — document.counterparty_check'] + report_date date + report_date_original_value varchar [note: 'FIX: строка как распознано (был `date []`)'] + report_date_verified boolean + report_date_modified boolean + units varchar [note: 'тыс. руб. / руб.'] + units_original_value varchar + units_verified boolean + units_modified boolean + currency varchar [default: 'RUB'] + okpo varchar [note: 'v5.1 ANL-044: реквизиты по макету экрана распознавания'] + okopf varchar + okfs varchar + okud varchar [note: '0710001 / 0710002'] + okei varchar + legal_form varchar [note: 'ОПФ'] + ownership_form varchar + address varchar + subject_to_audit boolean + reviewed_by uuid [ref: > app_user.id, note: 'v5.9: кто проверил данные формы; подтверждение документа — document.confirmed_by'] + reviewed_at timestamptz + checks_accepted_by uuid [ref: > app_user.id, note: 'v5.9 ТЗ 6.3 п.7: акцепт КА непройденных проверок (напр. 1370/2400) с комментарием'] + checks_accepted_at timestamptz + checks_accepted_comment text + Note: 'Шапка распознанной формы ББ/ОФР. Логические проверки (ТЗ 6.3 Таблица 1) не хранятся — считаются на лету по value' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.3 п.7 «стр. 1370 на конец и на начало года»; 6.7 «УК ДО на отчётную дату» +// Аналитика: FNR01 «rows[0].cells[2..4] — наименования колонок периода»; md «Логические проверки» («по каждой отчётной дате (колонке)») +// Макет: колонки «На 30 сентября 2025 г.», «На 31 декабря 2024 г.», «Выбор периодов» +// В модели: v5 новая (FIX-14) +// Зачем: дата колонки — без неё межформенная проверка и гудвилл не считаются +Table financial_statement_column { + id uuid [pk] + meta_id uuid [not null, ref: > financial_statement_meta.id] + column_index smallint [not null] + label varchar [note: 'как в форме: «На 30 сентября 2025 г.», «За 9 месяцев 2025 г.»'] + period_date date [note: 'ADD FIX-14: дата колонки — нужна для 1370(нач.года), гудвилла «на отчётную дату», сравнения версий'] + is_current boolean [note: 'колонка отчётного периода'] + Indexes { (meta_id, column_index) [unique] } + Note: 'Колонки-периоды формы (2–3 сравнительные даты)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 (коды статей по формам 157н); 6.3 (Таблица 1 — коды строк 1100…2400) +// Аналитика: FNR01 «fin_report_rows» (row_name, row_code, section, is_total_row); md «Модель данных» v4 +// Макет: «Результат распознавания»: Наименование показателя, Код, разделы +// В модели: v4 (FIX: убраны копипаст-поля) +Table financial_statement_line { + id uuid [pk] + meta_id uuid [not null, ref: > financial_statement_meta.id] + line_code varchar [note: 'код строки ОКУД, напр. 1230'] + line_code_original_value varchar + line_code_confidence numeric(5,2) + line_code_modified boolean + line_name varchar + line_name_original_value varchar + line_name_confidence numeric(5,2) + line_name_modified boolean [note: 'FIX: был numeric'] + Indexes { (meta_id, line_code) [unique] } + Note: 'Только строки с кодом; порядок, разделы и итоговые строки — шаблон формы ФСБУ 4/2023 на фронте' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 «уверенность распознавания ниже 100% → Требуется ручная проверка»; Прил. 2 «Распознанный показатель: значение, уверенность, признак ручной корректировки» +// Аналитика: FNR01 (value_current/prior/prior2, confidence, скобки → отрицательное); md «Логические проверки» (правка ячеек) +// Макет: подсветка ячеек, «не распознано», «Редактировать/Сохранить», «Подтвердить данные» +// В модели: v4 + v5 (column_id, confidence, original_amount, modified_by); v5.13 — убран needs_review +Table financial_statement_value { + id uuid [pk] + line_id uuid [not null, ref: > financial_statement_line.id] + column_id uuid [not null, ref: > financial_statement_column.id] + amount numeric(18,2) + string_value varchar [note: 'как распознано, если не число'] + confidence numeric(5,2) [note: 'уверенность распознавания (Прил. 2 ТЗ). Подсветка «требует проверки» = confidence < порог OR NOT is_recognized — считается, не хранится (v5.13)'] + is_recognized boolean + is_modified boolean [not null, default: false] + original_amount numeric(18,2) [note: 'значение до ручной правки'] + modified_by uuid [ref: > app_user.id] + modified_at timestamptz + Indexes { (line_id, column_id) [unique] } +} + + +// ============ КОНСОЛИДАЦИЯ ============ + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.10 (процесс формирования КФО за отчётный период); 7.2 «формируется по требованию»; Прил. 2 «Консолидация: ГСК, отчётный период» +// Аналитика: md «Сводная ОСВ» / «Шахматка» (consolidation.year/quarter); md «Модель данных» v4 +// Макет: «Создание консолидации: ГК → ГСК → Квартал» +// В модели: v4 + v5 (reporting_date) +// Зачем: логическая консолидация ГСК+период; версии — ниже +Table consolidation { + id uuid [pk] + company_group_id uuid [not null, ref: > company_group.id, note: 'v5.9: консолидация создаётся от ГК'] + related_group_id uuid [ref: > related_group.id, note: 'v5.9: ГСК — NULL до утверждения; фиксируется/создаётся при VERIFIED по составу consolidation_member (ANL-029)'] + year smallint [not null] + quarter period_quarter [not null] + reporting_date date [not null, note: 'ADD: 01.04 / 01.07 / 01.10 / 01.01 след. года — для ЕХ и сравнений'] + created_at timestamptz + Indexes { (company_group_id, year, quarter) [unique] } + Note: 'Логическая консолидация ГК за период; версии различаются периметром (consolidation_member). Прошлые периоды — по company_group_id. v5.18: файлы, загруженные в консолидацию, — stored_file.consolidation_id (основа рабочего стола; полный состав — context/05-domain-rules.md §9.5 п. 11)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 5.1 (сценарий, подход, чекбокс Гудвилл); 6.10 (статусная модель, маршрут по инициатору, № и дата решения УОБ, «новая версия КФО» после Верифицирован); 6.4 (причины отклонений от Методики) +// Аналитика: md «Статусная модель»; md «Модель данных» v4 (consolidation_version: version_no, status varchar, initiator, approved_in_rf, verified_in_go, kfo_confirmation_enabled) +// Макет: «Версия», «Дата верификации», «Включить/Отключить подтверждение КФО», «Смена статуса отчёта», «Ответственный / Утвердил / Верифицировал» +// В модели: v4 + v5 (enum статусов, scenario, approach, goodwill_enabled, initiator_office_id, uob_*, methodology_deviations, previous_version_id) +Table consolidation_version { + id uuid [pk] + consolidation_id uuid [not null, ref: > consolidation.id] + version_no int [not null] + status consolidation_status [not null, note: 'CHG: enum вместо varchar'] + scenario consolidation_scenario [not null, note: 'ADD ТЗ 5.1'] + approach consolidation_approach [not null, default: 'BASIC', note: 'ADD ТЗ 5.1'] + goodwill_enabled boolean [not null, default: false, note: 'ADD ТЗ 5.1/6.7: чек-бокс расчёта Гудвилла'] + initiator_id uuid [not null, ref: > app_user.id] + initiator_office_id uuid [not null, ref: > regional_office.id, note: 'ГО/РФ инициатора — определяет маршрут (ТЗ 6.10)'] + approved_in_rf_by uuid [ref: > app_user.id] + approved_in_rf_at timestamptz + verified_in_go_by uuid [ref: > app_user.id] + verified_in_go_at timestamptz + uob_decision_no varchar [note: 'ADD ТЗ 6.10: № решения УОБ'] + uob_decision_date date [note: 'ADD ТЗ 6.10'] + methodology_deviations text [note: 'ADD ТЗ 6.4: причины отклонений от Методики'] + elimination_rules_version int [note: 'v5.11: версия elimination_rule (version_no), по которой считалась версия; фиксируется при первом расчёте, замораживается при VERIFIED'] + is_in_work boolean [not null, default: true] + previous_version_id uuid [ref: > consolidation_version.id, note: '«новая версия КФО» после VERIFIED'] + cancelled_reason text + created_at timestamptz + created_by uuid [ref: > app_user.id] + updated_by uuid [ref: > app_user.id] + updated_at timestamptz + Indexes { (consolidation_id, version_no) [unique] } + Note: 'Версия консолидации. После VERIFIED — read-only; изменения только через новую версию' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.10 (переходы, возврат на доработку); 8.2 «изменение статусов; согласование и утверждение КФО» +// Аналитика: md «Статусная модель» (переходы) +// Макет: «Смена статуса отчёта», «Комментарий (обязательно)», «Отклонено: отчёт возвращён на доработку» +// В модели: v5 новая +Table consolidation_status_history { + id uuid [pk] + version_id uuid [not null, ref: > consolidation_version.id] + from_status consolidation_status + to_status consolidation_status [not null] + changed_by uuid [not null, ref: > app_user.id] + changed_at timestamptz [not null] + comment text [note: 'обязателен при возврате на доработку'] + Note: 'ADD ТЗ 6.10/8.2: маршрут согласования' +} + +// ADD v5.1 (макет, ANL-036): «взять в работу» / перехват прав редактирования +// ИСТОЧНИК: +// ТЗ: нет в ТЗ (ANL-036) +// Аналитика: нет +// Макет: «Взятие КФО в работу (перехват прав редактирования)», «Причина: Отпуск», «В данный момент консолидация редактируется пользователем …» +// В модели: v5.1 новая (макет) +// Зачем: блокировка версии; подтвердить с заказчиком +// v5.16: без истечения по времени (OQ-119): активна, пока не снята владельцем/администратором или не перехвачена по причине +Table consolidation_lock { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + locked_by uuid [not null, ref: > app_user.id] + locked_at timestamptz [not null] + released_at timestamptz [note: 'NULL = активная блокировка'] + taken_over_from uuid [ref: > app_user.id, note: 'при перехвате — предыдущий владелец'] + takeover_reason text [note: 'обязателен при перехвате (макет: «Причина: Отпуск»)'] + Indexes { consolidation_version_id [note: 'partial UNIQUE WHERE released_at IS NULL'] } + Note: 'Ответственный за версию сейчас = активная блокировка' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.1 «КА в рамках консолидации редактирует состав ГСК»; 6.10 «выбор состава ГСК»; ЕХ 6.3 «Периметр на дату» +// Аналитика: md «Сводная ОСВ» п.2 («зафиксированный периметр; изменение related_group_member не меняет версию»); md «Модель данных» v4 +// Макет: «Состав ЮЛ», «Минимум для консолидации: 2», «Редактировать состав» +// В модели: v4 + v5 (is_head_entity для Гудвилла) +Table consolidation_member { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + legal_entity_id uuid [not null, ref: > legal_entity.id] + is_head_entity boolean [not null, default: false, note: 'ADD: головная организация (для Гудвилла)'] + Indexes { (consolidation_version_id, legal_entity_id) [unique] } + Note: 'Зафиксированный периметр версии. Изменение related_group_member не меняет существующие версии' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ Прил. 2 «Документ ФО: ссылка на Консолидацию и ЮЛ»; 6.4/6.5 «использование данных предыдущих периодов»; ЕХ 7 «повторное использование первички без дублей» +// Аналитика: md «Сводная ОСВ» п.3 (файлы выбираются по ЮЛ+год+квартал — без явной связи) +// Макет: «Первичные документы консолидации» +// В модели: v5 новая; v5.13 — document_id вместо stored_file_id +// Зачем: явная привязка документа к версии (переиспользование, монозаёмщики из ЕХ) +// v5.16: точки привязки/отвязки — подтверждение документа, добавление ЮЛ в периметр; замена/удаление файла, исключение ЮЛ (Срез 4, D-5) +Table consolidation_document { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + document_id uuid [not null, ref: > document.id, note: 'v5.13: логический документ (ОСВ по счёту / ББ / ОФР), не файл'] + Indexes { (consolidation_version_id, document_id) [unique] } + Note: 'ADD Прил. 2 ТЗ «Документ ФО → Консолидация»: явная привязка документов к версии (переиспользование в нескольких версиях/периодах, монозаёмщики из ЕХ). Чек-лист = required_document_rule × периметр − consolidation_document' +} + + + + +// ---- Корректировки ---- +// ИСТОЧНИК: +// ТЗ: ТЗ 6.8 (форма корректировок: колонки ДБ/КР/Комментарий, >5%, импорт/экспорт, отдельная форма ОФР); 6.4 (доп. корректировки ОФР); Прил. 2 «Расхождение/корректировка»; Прил. 9 +// Аналитика: md «Корректировка данных» (correction: parent_correction_id, debit/credit_line_code, adjustment); md «Модель данных» v4 +// Макет: «Таблица корректировки»: Актив/Пассив/Контроль/Корректировка, «Строка баланса Дт/Кт», «Комментарий» +// В модели: v4 + v5 (statement_type, pair_match_id, reason_code, comment NOT NULL, status, version_no) +Table correction { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + statement_type statement_type [not null, note: 'ADD: ББ или ОФР (ТЗ 6.8 — отдельные формы)'] + rule_id uuid [ref: > elimination_rule.id, note: 'v5.10: пара ВГО по естественному ключу (правило + компании); NULL = ручная без пары'] + parent_correction_id uuid [ref: > correction.id, note: 'дочерняя строка распределения по нескольким статьям; глубина 1'] + legal_entity_id uuid [ref: > legal_entity.id, note: 'компания 1 (чья первичка корректируется)'] + counterparty_id uuid [ref: > legal_entity.id, note: 'компания 2'] + debit_line_code varchar + debit_account_id uuid [ref: > chart_of_account.id] + debit_adjustment numeric(18,2) + credit_line_code varchar + credit_account_id uuid [ref: > chart_of_account.id] + credit_adjustment numeric(18,2) + deviation_amount numeric(18,2) + deviation_percent numeric(9,4) + reason_code varchar [note: 'ADD ТЗ 6.4: unrealized_revenue | excise | agency_commission | intra_lease_depreciation | intra_loan_interest | equity_income | other'] + comment text [not null, note: 'ТЗ 6.8: комментарий обязателен'] + status correction_status [not null, default: 'DRAFT'] + version_no int [not null, default: 1, note: 'сноска 22 ТЗ: история версий корректировки — audit_event(CORRECTION_EDIT, before/after); инкремент при каждом изменении'] + created_by uuid [ref: > app_user.id] + created_at timestamptz + updated_by uuid [ref: > app_user.id] + updated_at timestamptz + Indexes { + consolidation_version_id + (consolidation_version_id, rule_id, legal_entity_id, counterparty_id) + } + Note: 'Overlay поверх первички: итог = свод первички − Σ применённых корректировок. Расхождение пары (deviation_*) — справочно, пересчитывается' +} + + +// ---- Гудвилл (ТЗ 6.7, Прил. 3 п.2, Прил. 8) ---- +// ИСТОЧНИК: +// ТЗ: ТЗ 6.7 (Гудвилл: ФВ ГО − доля в капитале ДО, комментарий обязателен, отдельная строка ББ); Прил. 3 п.2 (58.01 против 80/83, стр. 1310/1350/1370); Прил. 8 +// Аналитика: md «Гудвилл» (шаги 1–10: 58.01, стр. 1310, доля владения, include_in_kfo); md «Модель данных» v4 (goodwill) +// Макет: «Таблица гудвилла», «Доля владения», «Учет в отчете», «Изменение долей владения (гудвилл)» +// В модели: v4 + v5 (additional_capital_amount, comment) +Table goodwill { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + head_legal_entity_id uuid [not null, ref: > legal_entity.id] + subsidiary_legal_entity_id uuid [not null, ref: > legal_entity.id] + ownership_share numeric(7,4) [note: 'ручной ввод, %'] + financial_investment_amount numeric(18,2) [note: 'ΣДБ 58.01 ГО по ДО'] + charter_capital_amount numeric(18,2) [note: 'стр. 1310 ББ ДО на отчётную дату'] + additional_capital_amount numeric(18,2) [note: 'ADD ТЗ Прил. 3: стр. 1350 ББ ДО'] + participation_amount numeric(18,2) [note: '(1310 + 1350) * share / 100 — DECISION: уточнить у методологов, входит ли 1350 в базу'] + goodwill_amount numeric(18,2) + include_in_kfo boolean [not null, default: false] + comment text [note: 'ТЗ 6.7: обязателен при include_in_kfo'] + created_by uuid [ref: > app_user.id] + created_at timestamptz + updated_by uuid [ref: > app_user.id] + updated_at timestamptz + Indexes { (consolidation_version_id, head_legal_entity_id, subsidiary_legal_entity_id) [unique] } +} + +// ---- Результат КФО (ТЗ 6.4, Прил. 10) ---- +// ИСТОЧНИК: +// ТЗ: ТЗ 6.4 (итоговая форма КФО: подход, Гудвилл, расхождения, корректировки, верификация); 6.8 (контрольные пороги 10% ЧА / 1% ВБ, 10% прибыли / 1% выручки); Прил. 10 +// Аналитика: нет (пробел — ANL-008/009) +// Макет: «КФО», «Сводка расхождений КФО», «Есть несохранённые изменения в КФО» +// В модели: v5 новая +// Зачем: шапка результата + пороги +Table kfo_result { + id uuid [pk] + consolidation_version_id uuid [not null, unique, ref: > consolidation_version.id] + computed_at timestamptz [not null] + is_frozen boolean [not null, default: false, note: 'true после VERIFIED'] + net_assets numeric(18,2) [note: 'ЧА ГСК'] + balance_total numeric(18,2) [note: 'ВБ ГСК'] + revenue numeric(18,2) + sales_profit numeric(18,2) + total_deviation_bb numeric(18,2) [note: 'Σ расхождений по статьям ББ'] + total_deviation_ofr numeric(18,2) + threshold_bb_exceeded boolean [note: 'ТЗ 6.8: > 10% ЧА (или > 1% ВБ, если ЧА < 10% ВБ)'] + threshold_ofr_exceeded boolean [note: '> 10% прибыли от продаж (или > 1% выручки при рентабельности < 10%)'] + threshold_justification text [note: 'обоснование принятия риска при превышении'] + goodwill_total numeric(18,2) + has_deviations boolean + Note: 'Шапка результата КФО + контрольные пороги' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.4 «шаг 1 — сложение по кодам строк; шаг 2 — элиминация; неэлиминируемая часть остаётся»; Прил. 2 «Результат КФО: статья, значение»; Прил. 10; ФСБУ 4/2023 +// Аналитика: нет (пробел — ANL-008) +// Макет: колонки «в т.ч. взаимные обязательства», «Очищенные показатели», «Расхождения», «источник: Баланс/ОСВ», «в т.ч. Гудвилл» +// В модели: v5 новая + v5.1 (vgo_amount, source_kind, deviation_amount, comment) +Table kfo_result_line { + id uuid [pk] + kfo_result_id uuid [not null, ref: > kfo_result.id] + statement_type statement_type [not null] + line_code varchar [not null, note: 'код строки ФСБУ 4/2023; GOODWILL — отдельная строка раздела I'] + line_name varchar + sum_amount numeric(18,2) [note: 'шаг 1: простое сложение по компаниям'] + eliminated_auto numeric(18,2) [note: 'шаг 2: авто-элиминация по правилам'] + eliminated_manual numeric(18,2) [note: 'Σ применённых корректировок'] + goodwill_adjust numeric(18,2) + consolidated_amount numeric(18,2) [not null, note: 'итог («Очищенные показатели» в макете)'] + vgo_amount numeric(18,2) [note: 'v5.1 макет: «в т.ч. взаимные обязательства/обороты» = eliminated_auto + eliminated_manual'] + source_kind varchar [note: 'v5.1 макет: balance | osv — источник значения статьи'] + deviation_amount numeric(18,2) [note: 'v5.1 макет «Расхождения» по статье'] + comment text [note: 'v5.1 макет: пояснение к строке КФО'] + ordinal int + Indexes { (kfo_result_id, statement_type, line_code) [unique] } +} + +// ИСТОЧНИК: +// ТЗ: ЕХ ТЗ 6.6 «выгрузка расшифровки статей отчётности в разрезе контрагентов»; ТЗ 6.9 (настраиваемые поля экспорта) +// Аналитика: нет +// Макет: «Доля в показателе по группе, %», расшифровка по компаниям +// В модели: v5 новая + v5.1 (share_percent) +Table kfo_result_line_company { + kfo_result_line_id uuid [not null, ref: > kfo_result_line.id] + legal_entity_id uuid [not null, ref: > legal_entity.id] + amount numeric(18,2) + share_percent numeric(7,4) [note: 'v5.1 макет: «Доля в показателе по группе, %»'] + Indexes { (kfo_result_line_id, legal_entity_id) [pk] } + Note: 'Разрез по компаниям для расшифровки статьи (ЕХ 6.6 «расшифровка в разрезе контрагентов»)' +} + +// ---- Сценарий «Подтверждение КФО клиента» (ТЗ 5.1, 6.10) ---- +// ИСТОЧНИК: +// ТЗ: ТЗ 5.1 «подтверждение консолидированной отчётности, предоставленной Клиентом»; 6.10 «ручной ввод данных КФО клиента; сравнение и анализ расхождений»; Методика сноска 14 (процент к наименьшему) +// Аналитика: md «Статусная модель» (kfo_confirmation_enabled — только флаг; ANL-006) +// Макет: «Подтверждение КФО клиента»: Версия клиента / Версия банка, ВГО клиента / ВГО Банка, Расхождение, Причина +// В модели: v5 новая + v5.1 (client_vgo/bank_vgo/deviation_reason) +Table client_kfo_line { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + statement_type statement_type [not null] + line_code varchar [not null] + client_amount numeric(18,2) [note: 'ручной ввод из КФО клиента («Консолидированная отчетность от клиента»)'] + client_vgo_amount numeric(18,2) [note: 'v5.1 макет: «ВГО клиента» — ручной ввод'] + bank_amount numeric(18,2) [note: 'из kfo_result_line на момент сравнения'] + bank_vgo_amount numeric(18,2) [note: 'v5.1 макет: «ВГО Банка» из kfo_result_line.vgo_amount'] + deviation_amount numeric(18,2) + deviation_percent numeric(9,4) [note: 'относительно наименьшего (Методика сноска 14)'] + deviation_reason text [note: 'v5.1 макет: «Причина расхождения»'] + comment text + entered_by uuid [ref: > app_user.id] + entered_at timestamptz + Indexes { (consolidation_version_id, statement_type, line_code) [unique] } +} + + + +// ============ КОММЕНТАРИИ, АУДИТ, ЭКСПОРТ ============ + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.3 «возможность добавления комментария к отчётности»; 6.4 «комментарии» в форме КФО; 6.5/6.8 «обязательный комментарий» +// Аналитика: md «Загрузка документов» («Комментарий к документу») +// Макет: «Новый комментарий», «Комментарий к документу», комментарии в ячейках шахматки, «Редактировать комментарии» в КФО +// В модели: v5 новая +// Зачем: полиморфные комментарии к объектам +Table comment { + id uuid [pk] + object_type varchar [not null, note: 'document | consolidation_version | chess_cell(ключ пары) | kfo_result_line | …'] + object_id uuid [not null] + text text [not null] + author_id uuid [not null, ref: > app_user.id] + created_at timestamptz [not null] + Indexes { (object_type, object_id) } + Note: 'ADD ТЗ 6.3/6.4: комментарии к отчётности и КФО (в т.ч. обязательные при расхождениях)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.2 (журнал изменений, логирование всех действий, перечень событий: вход/выход, создание консолидации, ГСК/ГК, загрузка, распознанные данные, корректировки, статусы, утверждение, выгрузка; хранение ≥ срока данных); Прил. 2 «Запись журнала изменений» +// Аналитика: нет (пробел — ANL-019); md «Ролевая модель» ссылается на «журнал аудита» +// Макет: «Журнал действий»: разделы, Объект/Событие/Что изменилось/Результат/Идентификатор, IP/браузер, «неверный пароль» +// В модели: v5 новая + v5.1 (section, result, changed_field, user_agent) +Table audit_event { + id uuid [pk] + occurred_at timestamptz [not null] + user_id uuid [ref: > app_user.id, note: 'NULL для системных событий'] + action audit_action [not null] + section audit_section [not null, note: 'v5.1 макет: раздел журнала'] + object_type varchar [not null] + object_id uuid + consolidation_version_id uuid [ref: > consolidation_version.id, note: 'для выборки «история изменений КФО»'] + description text + before_value jsonb + after_value jsonb + comment text + result varchar [note: 'v5.1 макет: Успешно / Отклонено / Ошибка'] + changed_field varchar [note: 'v5.1 макет: фильтр «Изменяемое поле»'] + ip_address varchar + user_agent varchar [note: 'v5.1 макет: браузер в событиях сеанса'] + Indexes { + (consolidation_version_id, occurred_at) + (object_type, object_id) + (user_id, occurred_at) + } + Note: 'ТЗ 8.2: журнал изменений/аудит. Append-only. Хранение ≥ срока хранения консолидации. Также покрывает: историю правок распознанных данных, факт/состав выгрузок (ЕХ 6.6), импорт справочников, изменения матрицы прав' +} + + + +// ============ ЕХФД (ТЗ BIQ-34649) ============ +// DECISION-2: срез = замороженный указатель на верифицированную версию + список её артефактов. + +// ИСТОЧНИК: +// ТЗ: ЕХ ТЗ 6.2 «запись срезами: одна ГСК на одну отчётную дату, после Верифицирован»; 6.4 «версии среза Актуальная/Архив, неизменяемость»; 5.3 «архивирование в data lake» +// Аналитика: нет (пробел — ANL-032) +// Макет: «Единое хранилище», «Сформированная отчетность», «Верифицирована» +// В модели: v5 новая (DECISION-2) +// Зачем: замороженный указатель на верифицированную версию +Table ex_snapshot { + id uuid [pk] + related_group_id uuid [not null, ref: > related_group.id] + company_group_id uuid [not null, ref: > company_group.id, note: 'денормализация на момент верификации'] + reporting_date date [not null] + year smallint [not null] + quarter period_quarter [not null] + version_no int [not null] + status ex_snapshot_status [not null, note: 'ACTUAL — последняя верифицированная; предыдущие → ARCHIVE'] + source_version_id uuid [not null, unique, note: 'v5.12: id consolidation_version-источника БЕЗ FK — источник удаляется по сроку, срез нет'] + source_version_no int [not null] + superseded_by_snapshot_id uuid [ref: > ex_snapshot.id, note: 'v5.12: для ARCHIVE — ссылка на актуальный срез, который его заместил'] + verified_at timestamptz [not null] + written_at timestamptz [not null] + written_by uuid [ref: > app_user.id, note: 'инициатор записи'] + archived_at timestamptz + data_lake_ref varchar [note: 'ссылка на архив в data lake'] + Indexes { + (related_group_id, year, quarter, version_no) [unique] + (related_group_id, year, quarter, status) + } + Note: 'Срез данных ЕХ: одна ГСК на одну отчётную дату (ЕХ 6.2). Неизменяем и самодостаточен (не зависит от таблиц консолидации). Состав — ex_snapshot_item + ex_snapshot_member' +} + +// ИСТОЧНИК: +// ТЗ: ЕХ ТЗ 6.3 «Периметр на дату: состав ЮЛ/ИП группы на отчётную дату» +// Аналитика: нет +// Макет: дерево «ГК → ГСК → Организации» с ИНН +// В модели: v5 новая +// Зачем: денормализация периметра на дату среза +Table ex_snapshot_member { + ex_snapshot_id uuid [not null, ref: > ex_snapshot.id] + legal_entity_id uuid [not null, ref: > legal_entity.id] + inn varchar [not null, note: 'денормализация на дату среза'] + name varchar [not null] + Indexes { (ex_snapshot_id, legal_entity_id) [pk] } + Note: 'Периметр на дату (ЕХ 6.3)' +} + +// ИСТОЧНИК: +// ТЗ: ЕХ ТЗ 5.2 «Состав хранимых данных» (КФО, первичные, сводная ОСВ, шахматка, гудвилл, корректировки, иные отчёты); 6.7.3/6.7.4 «Сформированная отчётность / Первичные документы», «Просмотр / Выгрузка» +// Аналитика: нет +// Макет: область документов ЕХ, «Скачать», «Связать» документ с организацией +// В модели: v5 новая; v5.13 — document_id вместо stored_file_id +// Зачем: состав среза со ссылками на объекты +Table ex_snapshot_item { + id uuid [pk] + ex_snapshot_id uuid [not null, ref: > ex_snapshot.id] + item_type varchar [not null, note: 'primary_document | summary_osv | vgo_pairs | chess | kfo_balance | kfo_income | goodwill | corrections | other_report'] + legal_entity_id uuid [ref: > legal_entity.id, note: 'для первички — организация'] + document_id uuid [ref: > document.id, note: 'v5.13: только для primary_document — исходники хранятся весь срок, FK допустим; «Скачать» — через document.stored_file → s3_object'] + payload jsonb [note: 'v5.12 DECISION-5: замороженные данные для всех остальных типов; самодостаточно, без ссылок на таблицы консолидации'] + payload_schema_version varchar + Indexes { (ex_snapshot_id, item_type) } + Note: 'Состав среза: primary_document → document; всё остальное → payload. «Просмотр» в ЛК ЕХ рендерит payload по схеме; «Выгрузка» собирает xlsx из payload на лету' +} + +// ============ ПРОЧЕЕ ============ + +// ИСТОЧНИК: +// ТЗ: ТЗ 5.1 (базовый / расширенный пакет ОСВ); 6.2 «набор обязательных ОСВ»; сноска 12 «процесс останавливается, если отсутствует документ» +// Аналитика: md «Загрузка документов» (чек-лист: «ОСВ по обязательным счетам», «прочие документы») +// Макет: «Чек-лист загруженных документов», «Условные обозначения статусов» +// В модели: v5 новая +// Зачем: конфигурация чек-листа по подходу; с v5.13 сверяется с document (тип + счёт + период), а не с файлом +Table required_document_rule { + id uuid [pk] + approach consolidation_approach [not null] + document_type document_type [not null] + account_id uuid [ref: > chart_of_account.id, note: 'для OSV'] + is_mandatory boolean [not null, note: 'обязателен → блокирует переход к консолидации (сноска 12 ТЗ)'] + Indexes { (approach, document_type, account_id) [unique] } + Note: 'ADD ТЗ 5.1/6.2: состав обязательных документов по подходу — основа чек-листа. Выполнено = есть document CONFIRMED с таким типом/счётом за период у каждого ЮЛ периметра' +} diff --git a/model/diagrams/part04_files_jobs.png b/model/diagrams/part04_files_jobs.png index 5eb4559..29b8b89 100644 Binary files a/model/diagrams/part04_files_jobs.png and b/model/diagrams/part04_files_jobs.png differ diff --git a/model/diagrams/part04_files_jobs.svg b/model/diagrams/part04_files_jobs.svg index 3054f9d..ee162ea 100644 --- a/model/diagrams/part04_files_jobs.svg +++ b/model/diagrams/part04_files_jobs.svg @@ -4,213 +4,213 @@ - - + + dbml document_type - - + + + +       document_type        + + +    BALANCE     + -       document_type        +    INCOME     -    BALANCE     +    OSV     -    INCOME     +    DISCLOSURE     -    OSV     - - -    DISCLOSURE     - - -    OTHER     - +    OTHER     + file_type - - - -       file_type        - - -    XLS     - - -    XLSX     - - -    XLSB     - - -    PDF     - - -    PNG     - - -    JPG     - + + + +       file_type        + + +    XLS     + + +    XLSX     + + +    XLSB     + + +    PDF     + + +    PNG     + + +    JPG     + recognition_pipeline - - - -       recognition_pipeline        - - -    tabular     - - -    image     - + + + +       recognition_pipeline        + + +    tabular     + + +    image     + period_quarter - - + + + +       period_quarter        + + +    Q1     + -       period_quarter        +    Q2     -    Q1     +    Q3     -    Q2     - - -    Q3     - - -    Q4     - +    Q4     + file_status - - - -       file_status        - - -    UPLOADED     - - -    PROCESSING     - - -    PROCESSED     - - -    ERROR     - - -    SUPERSEDED     - + + + +       file_status        + + +    UPLOADED     + + +    PROCESSING     + + +    PROCESSED     + + +    ERROR     + + +    SUPERSEDED     + document_status - - + + + +       document_status        + + +    RECOGNIZED     + -       document_status        +    NEEDS_CHECK     -    RECOGNIZED     +    CONFIRMED     -    NEEDS_CHECK     - - -    CONFIRMED     - - -    ERROR     - +    ERROR     + s3_bucket_role - - - -       s3_bucket_role        - - -    uploads     - - -    results     - - -    exports     - - -    archive     - + + + +       s3_bucket_role        + + +    uploads     + + +    results     + + +    exports     + + +    archive     + recognition_status - - - -       recognition_status        - - -    queued     - - -    published     - - -    processing     - - -    done     - - -    needs_check     - - -    error     - - -    timeout     - - -    dlq     - + + + +       recognition_status        + + +    queued     + + +    published     + + +    processing     + + +    done     + + +    needs_check     + + +    error     + + +    timeout     + + +    dlq     + document_check_result - - + + + +       document_check_result        + + +    MATCH     + -       document_check_result        +    MISMATCH     -    MATCH     - - -    MISMATCH     - - -    UNKNOWN     - +    UNKNOWN     + @@ -229,20 +229,37 @@ + + +consolidation + + + + +       consolidation        + + +id +     +uuid + + + + legal_entity - - - -       legal_entity        - - -id -     -uuid - + + + +       legal_entity        + + +id +     +uuid + @@ -250,333 +267,346 @@ s3_object - - - -       s3_object        - - -id -     -uuid - - -bucket_role     -s3_bucket_role - -(!) - - -bucket     -varchar - -(!) - - -key     -varchar - -(!) - - -content_type     -varchar - - -size_bytes     -bigint - - -sha256     -varchar - - -version_id     -varchar - - -created_by     -varchar - - -created_at     -timestamptz - -(!) - - -expires_at     -timestamptz - - -archived_to     -varchar - + + + +       s3_object        + + +id +     +uuid + + +bucket_role     +s3_bucket_role + +(!) + + +bucket     +varchar + +(!) + + +key     +varchar + +(!) + + +content_type     +varchar + + +size_bytes     +bigint + + +sha256     +varchar + + +version_id     +varchar + + +created_by     +varchar + + +created_at     +timestamptz + +(!) + + +expires_at     +timestamptz + + +archived_to     +varchar + - + s3_object:e->s3_bucket_role:w - + stored_file - - - - -       stored_file        - - -id -     -uuid - - -legal_entity_id     -uuid - -(!) - - -file_type     -file_type - -(!) - - -original_name     -varchar - - -s3_object_id     -uuid - -(!) - - -content_hash     -varchar - - -size_bytes     -bigint - - -status     -file_status - -(!) - - -version_no     -int - -(!) - - -superseded_by_file_id     -uuid - - -current_recognition_job_id     -uuid - - -uploaded_by     -uuid - - -uploaded_at     -timestamptz - - -updated_at     -timestamptz - + + + + +       stored_file        + + +id +     +uuid + + +legal_entity_id     +uuid + +(!) + + +file_type     +file_type + +(!) + + +original_name     +varchar + + +s3_object_id     +uuid + +(!) + + +content_hash     +varchar + + +size_bytes     +bigint + + +status     +file_status + +(!) + + +version_no     +int + +(!) + + +superseded_by_file_id     +uuid + + +current_recognition_job_id     +uuid + + +uploaded_by     +uuid + + +uploaded_at     +timestamptz + + +updated_at     +timestamptz + + +consolidation_id     +uuid + - + stored_file:e->file_type:w - + - + stored_file:e->file_status:w - + stored_file:e->app_user:w - - + + 1 -* +* + + + + +stored_file:e->consolidation:w + + +1 +* stored_file:e->legal_entity:w - - -1 -* + + +1 +* stored_file:e->s3_object:w - - -1 -* + + +1 +* stored_file:e->stored_file:w - - -1 -* + + +1 +* recognition_job - - - -       recognition_job        - - -id -     -uuid - - -file_id     -uuid - -(!) - - -pipeline     -recognition_pipeline - -(!) - - -request_subject     -varchar - -(!) - - -request_msg_id     -varchar - - -result_msg_id     -varchar - - -schema_version     -varchar - -(!) - - -service_name     -varchar - - -service_version     -varchar - - -status     -recognition_status - -(!) - - -attempt     -smallint - -(!) - - -max_attempts     -smallint - -(!) - - -requested_at     -timestamptz - - -published_at     -timestamptz - - -started_at     -timestamptz - - -finished_at     -timestamptz - - -reply_deadline     -timestamptz - - -external_task_id     -varchar - - -error_code     -varchar - - -error_message     -text - - -result_s3_object_id     -uuid - - -result_schema_version     -varchar - - -group_schema     -jsonb - - -metrics     -jsonb - - -fields_total     -int - - -fields_low_confidence     -int - - -fields_unrecognized     -int - + + + +       recognition_job        + + +id +     +uuid + + +file_id     +uuid + +(!) + + +pipeline     +recognition_pipeline + +(!) + + +request_subject     +varchar + +(!) + + +request_msg_id     +varchar + + +result_msg_id     +varchar + + +schema_version     +varchar + +(!) + + +service_name     +varchar + + +service_version     +varchar + + +status     +recognition_status + +(!) + + +attempt     +smallint + +(!) + + +max_attempts     +smallint + +(!) + + +requested_at     +timestamptz + + +published_at     +timestamptz + + +started_at     +timestamptz + + +finished_at     +timestamptz + + +reply_deadline     +timestamptz + + +external_task_id     +varchar + + +error_code     +varchar + + +error_message     +text + + +result_s3_object_id     +uuid + + +result_schema_version     +varchar + + +group_schema     +jsonb + + +metrics     +jsonb + + +fields_total     +int + + +fields_low_confidence     +int + + +fields_unrecognized     +int + @@ -584,179 +614,179 @@ stored_file:e->recognition_job:w - - -1 -* + + +1 +* document - - - -       document        - - -id -     -uuid - - -stored_file_id     -uuid - -(!) - - -legal_entity_id     -uuid - -(!) - - -document_type     -document_type - -(!) - - -osv_account_code     -varchar - - -period_year     -smallint - - -period_quarter     -period_quarter - - -is_cumulative     -boolean - - -reporting_date     -date - - -locator     -varchar - - -status     -document_status - -(!) - - -period_check     -document_check_result - - -counterparty_check     -document_check_result - - -confirmed_by     -uuid - - -confirmed_at     -timestamptz - - -created_at     -timestamptz - - -updated_at     -timestamptz - + + + +       document        + + +id +     +uuid + + +stored_file_id     +uuid + +(!) + + +legal_entity_id     +uuid + +(!) + + +document_type     +document_type + +(!) + + +osv_account_code     +varchar + + +period_year     +smallint + + +period_quarter     +period_quarter + + +is_cumulative     +boolean + + +reporting_date     +date + + +locator     +varchar + + +status     +document_status + +(!) + + +period_check     +document_check_result + + +counterparty_check     +document_check_result + + +confirmed_by     +uuid + + +confirmed_at     +timestamptz + + +created_at     +timestamptz + + +updated_at     +timestamptz + - + document:e->document_type:w - + - + document:e->period_quarter:w - + - -document:e->document_status:w - - - - -document:e->document_check_result:w - - - +document:e->document_status:w + + + + document:e->document_check_result:w - + + + + +document:e->document_check_result:w + - + document:e->app_user:w - - + + 1 -* +* - + document:e->legal_entity:w - - -1 -* + + +1 +* - + document:e->stored_file:w - - -1 -* + + +1 +* - + recognition_job:e->recognition_pipeline:w - + - + recognition_job:e->recognition_status:w - + - + recognition_job:e->s3_object:w - - -1 -* + + +1 +* - + recognition_job:e->stored_file:w - - -1 -* + + +1 +* diff --git a/model/diagrams/part07_consolidation.svg b/model/diagrams/part07_consolidation.svg index 2ad775c..f70a66d 100644 --- a/model/diagrams/part07_consolidation.svg +++ b/model/diagrams/part07_consolidation.svg @@ -209,7 +209,7 @@ consolidation - + diff --git a/model/entities-v5.18.md b/model/entities-v5.18.md new file mode 100644 index 0000000..5d27d2f --- /dev/null +++ b/model/entities-v5.18.md @@ -0,0 +1,182 @@ +# АРМ КА — описание сущностей модели данных v5.18 + +Источник истины по структуре — `data-model-v5.18.dbml` (полный). Диаграммы по частям — `diagrams/` (part01…09 svg/png; перерисовываются `make diagrams` из текущего DBML). Здесь — что каждая таблица значит для бизнеса, как связана с соседями и откуда взялась. + +## Что изменилось в v5.18 (решения владельца 09.10.2026, RSHB/arm-ka-docs#90) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `stored_file.consolidation_id` (`uuid`, nullable, FK → `consolidation`) + индекс `(legal_entity_id, consolidation_id)` | Файл загружается **в консолидацию** (период ГК) и становится основой её «рабочего стола» (полный состав стола — `context/05-domain-rules.md` §9.5 п. 11), общего для всех версий периода; `NULL` — загрузка без контекста (страница ЮЛ, монозаёмщики, API). Панель «Первичные документы» в версии показывает рабочий стол консоли для ЮЛ плюс файлы с документами, привязанными к версиям консоли. Что использует конкретная версия — по-прежнему `consolidation_document`; заморозка после `VERIFIED` — привязки и `ex_snapshot`. Привязка файла к **версии** (`consolidation_file`) отвергнута: рабочие версии периода делят первичку, а новая версия после `VERIFIED` подхватывает подтверждённое (`openspec/changes/console-files/design.md`) | +| 2 | Note `recognition_job.reply_deadline` | Без изменения структуры: срок ответа OCR — 20 мин (OQ-159) | + +Остальные таблицы и enum — как в v5.17. Реализация — миграция `0006` в `arm-core` (backfill `consolidation_id` по `consolidation_document`). + +## Что изменилось в v5.17 (решение владельца 07.10.2026, пересмотр OQ-31, RSHB/arm-ka-docs#53) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `legal_entity.kpp` и `legal_entity_history.kpp` (`varchar`, nullable) | КПП юридического лица нужен (макет: колонка «КПП» на экранах `50`, `61`, поле форм `51-02`, `79`; фронтенд: «в ручке получения ЮЛ не приходит КПП»). Формат — 9 символов: 4 цифры, 2 символа (цифра или латинская A–Z), 3 цифры, иначе `INVALID_KPP`. У ИП (`entity_type = ИП`) КПП запрещён. Для ЮЛ необязателен: существующие записи и импорт без колонки остаются валидными. Уникален по-прежнему только `inn` (КПП — атрибут; сопоставление контрагентов ОСВ по ИНН не меняется). Изменение КПП — обычная правка со снимком в `legal_entity_history`. Отменяет п. 2 v5.14 («КПП не вводится») | + +Остальные таблицы и enum — как в v5.16. Реализация — миграция `0005` в `arm-core`. + +## Что изменилось в v5.16 (Срез 4, решения владельца 06.10.2026) + +| № | Изменение | Решение | +|---|---|---| +| 1 | Enum `counterparty_match_kind` (`INN`, `NAME`, `ALIAS`, `MANUAL`, `PROPAGATED`) и колонка `osv_entry.counterparty_match_kind` (nullable) | Способ сопоставления контрагента строки ОСВ хранится. `NULL` ⇔ `counterparty_id` `NULL`; `INN` / `NAME` / `ALIAS` — шаг автоматического резолва при маппинге (ИНН; точное имя; нормализованное имя ЮЛ или алиас); `MANUAL` — ручное сопоставление аналитиком; `PROPAGATED` — автоприменение по имени в версии (DOC-221278 п. 12–13). Вычислять на чтение нельзя: после ручного связывания появляется алиас, и ручное сопоставление неотличимо от `ALIAS`. Миграция `0004` заполняет колонку по уже смапленным строкам (ИНН без пробелов = `legal_entity.inn` → `INN`; `trim(name) = legal_entity.name` → `NAME`; остальные с `counterparty_id` → `ALIAS` — приближение: ручных сопоставлений до Среза 4 не было) | +| 2 | Семантика `consolidation_document` | Без изменения структуры: строка создаётся при подтверждении документа (по документам ЮЛ — членов периметра версий «в работе») и при добавлении ЮЛ в периметр (подтверждённые подходящие документы подхватываются); удаляется при замене/удалении файла и исключении ЮЛ из периметра. Для одной (ЮЛ, `document_type`, `osv_account_code`, период, признак нарастающего) привязан один документ — самый поздний `confirmed_at` (D-5 Среза 4) | +| 3 | Семантика `consolidation_lock` | Без изменения структуры: блокировка без истечения по времени — активна, пока её не снял владелец, администратор или не перехватил другой пользователь с `CONSOLIDATION_EDIT` по причине (OQ-119) | + +Пробелы квартальной склейки и сама сводная ОСВ по-прежнему не хранятся (DECISION-5). Остальные таблицы и enum — как в v5.15 (43 таблицы; enum — 28). Реализация — миграция `0004` в `arm-core`. + +## Что изменилось в v5.15 (Срез 2c, решения владельца 05.10.2026) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `osv_data_type` + `NU` (после `BU`) | Строки налогового учёта из ОСВ «БУ, НУ» хранятся в `osv_entry` как есть (OQ-45); контракт результата допускал `NU` с v1.0, ядро раньше такой строки принять не могло | +| 2 | `app_user.email` — в нижнем регистре | Без изменения структуры: нормализация при записи (seed, администрирование), поиск по `lower(email)` сохраняется, уникальность — существующий UNIQUE по `email` (OQ-59). Expression-индекс `lower(email)` не вводится: сверка ORM↔DBML не умеет expression-индексы. Миграция `0003` приводит существующие email к нижнему регистру (при дублях по регистру — падает, слияние решает человек) | +| 3 | Семантика `stored_file.status`, `recognition_job.group_schema` | Без изменения структуры: `PROCESSING` ставит inbox по ack сервиса, `PROCESSED`/`ERROR` — цикл маппинга результата (OQ-67); маппинг выполняется отдельно от inbox (OQ-69); `group_schema` заполняет маппер; «Итого» в `osv_entry` не хранится (OQ-01) | +| 4 | Индексы `osv_entry (parent_entry_id)`, `osv_entry (recognition_job_id)`, `stored_file (current_recognition_job_id)` | Индексы для удаления файла и обхода дерева строк: без них проверки FK (в том числе self-FK `parent_entry_id`) при `DELETE /files/{id}` и очистке файла перед повторным маппингом шли seq scan'ом. Замер: DELETE документа 1000 строк 8,6 с → 16 мс на 200 тыс. строк. Обычные (не unique) индексы; закрывает OQ-117 (I-1 финального ревью Среза 2c) | + +Остальные таблицы и enum — как в v5.14 (43 таблицы, 27 enum). Реализация — миграция `0003` в `arm-core`. + +## Что изменилось в 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 | КПП | **Не вводится** *(отменено в v5.17: КПП введён)*. Поле есть только в макете; у `legal_entity` его нет, импорт его не читает (закрывал OQ-31 до v5.17) | +| 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` | Юридическое лицо / ИП. Натуральный ключ — ИНН; **v5.17:** `kpp` — КПП (9 символов, nullable, только у ЮЛ, не ключ). `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`), версия при замене. **v5.18:** `consolidation_id` — консолидация, в контексте которой файл загружен (основа «рабочего стола консоли», общего для версий периода; полный состав — `context/05-domain-rules.md` §9.5 п. 11; `NULL` — без контекста). Что внутри файла — `document`. Замена — файл целиком со всеми документами. | → `legal_entity`, `consolidation`, `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**), код счёта и резолв в план счетов, сырое имя/ИНН контрагента, резолв в ЮЛ и способ сопоставления (`counterparty_match_kind`, v5.16), шесть сумм, тип данных (БУ/валюта/кол-во), «подакцизный» для 90-го. Ручные правки: `is_modified`, `original_values`, `modified_by`. Признак ГСК/ГК/Внешний не хранится — выводится из периметра версии; способ сопоставления контрагента хранится (`NULL` ⇔ контрагент не сопоставлен). | → `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` | Логическая консолидация: **ГК** + отчётный период. ГСК фиксируется/создаётся при верификации по фактическому периметру. Уникальна по (ГК, год, квартал). **v5.18:** файлы, загруженные в консолидацию, — `stored_file.consolidation_id` (основа рабочего стола, общего для версий периода; полный состав — `context/05-domain-rules.md` §9.5 п. 11). | → `company_group`, `related_group`; ← `consolidation_version`, `stored_file` | ТЗ 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` | «Взять в работу»: кто редактирует версию, перехват с причиной (v5.16: без истечения по времени, OQ-119). Из макета — 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/console-files/.openspec.yaml b/openspec/changes/console-files/.openspec.yaml new file mode 100644 index 0000000..ea7acf6 --- /dev/null +++ b/openspec/changes/console-files/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +created: 2026-10-09 +skip_specs: true diff --git a/openspec/changes/console-files/design.md b/openspec/changes/console-files/design.md new file mode 100644 index 0000000..4b97373 --- /dev/null +++ b/openspec/changes/console-files/design.md @@ -0,0 +1,173 @@ +## Context + +Панель «Первичные документы» [39] открывается из строки ЮЛ в карточке версии консолидации и показывает +`GET /legal-entities/{id}/files` — все файлы ЮЛ без фильтра. У одной ГК несколько консолидаций за разные периоды, их +файлы лежат в одном списке ЮЛ. Хранится только привязка подтверждённых документов к версии (`consolidation_document`, +§3 п. 1); факт «файл загружен в эту консоль» не хранится: `consolidation_version_id` при загрузке используется для +проверки версии и перехода `INITIATED → DOCUMENTS_PROCESSING` и забывается. + +Целевая картина (владелец, 09.10.2026): + +- рабочие версии одного периода делят первичку; различаются периметром; +- верифицированная версия заморожена, первичка зафиксирована в ЕХ (`ex_snapshot_item.stored_file_id`, Срез 7); +- новая версия после `VERIFIED` может менять документы, периметр сохраняется; +- загрузка в консоль — маршрутом от консолидации. + +Текущее состояние ядра: одна консолидация — одна версия (`POST /consolidations` создаёт версию 1; «новая версия» и +параллельные версии не реализованы). Единственная точка создания `stored_file` — `services/primary_files._store` +(загрузка); горячие папки scan-gateway файлов в ядре не создают. Автозамена (`services/result_mapping`) создаёт +`SUPERSEDED` у старых файлов по ключу документа (тип, счёт, год, квартал), не по консоли. + +## Goals / Non-Goals + +Goals: панель ЮЛ в версии показывает рабочий стол консоли этого ЮЛ; файл помнит консоль загрузки; маршруты загрузки и +списка в контексте консоли — от консолидации; модель совместима с целевой картиной версий без копирования связей. +Non-goals: см. proposal. + +## Модель + +`stored_file.consolidation_id uuid [ref: > consolidation.id]`, nullable. Семантика: консолидация (ГК + год + квартал), +в контексте которой файл загружен; `NULL` — без контекста. Пишется один раз при загрузке. Индекс +`(legal_entity_id, consolidation_id)` — выборка рабочего стола. + +Почему период, а не версия: + +| Вариант | Плюс | Минус | +|---|---|---| +| `consolidation_file(version_id, file_id)` | буквально «загружено в версию» | при второй рабочей версии и новой версии после `VERIFIED` связи надо копировать; вторая связь рядом с `consolidation_document`; верифицированная версия и так заморожена привязками и ЕХ | +| `stored_file.uploaded_in_version_id` | дешевле таблицы | те же проблемы разделения между рабочими версиями | +| **`stored_file.consolidation_id`** | рабочие версии периода делят стол автоматически; новая версия после `VERIFIED` видит стол и подхватывает подтверждённое | нет истории «в какой версии загружено» — она в `FILE_UPLOAD.after_value` | + +Заморозка остаётся на `consolidation_document` (после `VERIFIED` привязки не снимаются) и на `ex_snapshot` (Срез 7). + +## Алгоритмы (§9.5) + +**Загрузка в консоль.** `POST /consolidations/{cid}/legal-entities/{le}/files` (`DOCUMENT_EDIT`): + +``` +консолидация cid видима (policy по ГК)? нет → 404 NOT_FOUND («консолидация не найдена») +ЮЛ le видимо? нет → 404 NOT_FOUND +есть версия cid в работе (IN_WORK_STATUSES)? нет → 409 CONSOLIDATION_NOT_EDITABLE +le в периметре хотя бы одной такой версии? нет → 422 ENTITY_NOT_IN_PERIMETER +приём файлов как в POST /legal-entities/{le}/files; file.consolidation_id = cid +после приёма ≥ 1 файла, отдельной транзакцией: + для каждой версии cid в работе с le в периметре и статусом INITIATED: + FOR UPDATE → перечитать статус → transition(INITIATED → DOCUMENTS_PROCESSING, changed_by = загрузивший) +``` + +Аудит `FILE_UPLOAD.after_value` дополняется `consolidation_id`; версии, переведённые в `DOCUMENTS_PROCESSING`, пишут +`CONSOLIDATION_STATUS_CHANGE` как сейчас. + +**Загрузка без контекста.** `POST /legal-entities/{le}/files` — как сейчас, без query; `consolidation_id = NULL`. + +**Рабочий стол.** `GET /consolidations/{cid}/legal-entities/{le}/files` (`DOCUMENT_VIEW`; 404 на невидимую +консолидацию или ЮЛ; `limit`, `offset`, `include_superseded`): + +``` +DESK = файлы le с consolidation_id = cid +BOUND = файлы le, у которых есть документ с consolidation_document к любой версии cid +результат = DESK ∪ BOUND, новые первыми (uploaded_at desc, id) +SUPERSEDED — только при include_superseded = true (и для DESK, и для BOUND) +``` + +`BOUND` нужен для файлов, загруженных без контекста или в другой период и подхваченных версией при подтверждении +(ОСВ за `Q1`, подходящая версиям `Q2`–`Q4`, R-14 Среза 4, §3 п. 4). Файл отдаётся той же карточкой `FileOut`, что и список ЮЛ. + +**Автозамена.** Новый файл после маппинга: если `consolidation_id IS NULL`, берётся `consolidation_id` первого +заменённого файла (по порядку `id`), у которого он не `NULL`. При загрузке в контексте своё значение сохраняется. + +**Удаление, подтверждение, привязки** — без изменений. + +**Backfill (миграция 0006):** `stored_file.consolidation_id` = консолидация версии, к которой привязан любой документ +файла; при привязках к версиям разных консолидаций — самый ранний период (год, затем квартал). Файлы без привязок — +`NULL`. Откат — удаление колонки. + +## API + +| Маршрут | Право | Вход | Ответ | Ошибки | +|---|---|---|---|---| +| `POST /consolidations/{cid}/legal-entities/{le}/files` | `DOCUMENT_EDIT` | multipart `files` (повторяется) | 201 `UploadResult`; 422 `UploadResult`, если не принят ни один | 404; 409 `CONSOLIDATION_NOT_EDITABLE`; 422 `ENTITY_NOT_IN_PERIMETER`; 503 `STORAGE_UNAVAILABLE` | +| `GET /consolidations/{cid}/legal-entities/{le}/files` | `DOCUMENT_VIEW` | `limit`, `offset`, `include_superseded` | `Page[FileOut]` | 404 | +| `POST /legal-entities/{le}/files` | `DOCUMENT_EDIT` | как сейчас, **без** `consolidation_version_id` | как сейчас | как сейчас | +| `GET /legal-entities/{le}/files`, `GET /files/{id}` | `DOCUMENT_VIEW` | как сейчас | `FileOut` + `consolidation_id: uuid \| null` | как сейчас | + +## Фронт (arm-web) + +- Стор `consolidations`: `current_consolidation_id` рядом с `current_version_id`; ставится там же (строка списка — + `con_id`, карточка версии — `consolidation_id`). +- `services/api/files.ts`: `uploadFiles(legalEntityId, files, consolidationId?)` и + `listLegalEntityFiles(legalEntityId, consolidationId?)` — с консолидацией маршруты `/consolidations/{cid}/…`, без неё + маршруты ЮЛ. +- Панель [39]: список рабочего стола; переключатель «Все документы организации» внизу переключает на список ЮЛ; у файла + с `consolidation_id`, отличным от текущего, подпись «2025 К1» (год и квартал из списка консолидаций ГК в сторе), у + файла без консолидации — «вне консолидации». Опрос раз в 5 с — как сейчас, по активному списку. +- Кнопка «Загрузить документы» недоступна, если открытая версия не «в работе» (статус из строки списка). +- Экран [60] «Монозаёмщики» (когда появится) — панель без консолидации. + +## Документы + +`model/data-model-v5.18.dbml` (+ changelog в шапке), `model/entities-v5.18.md`, `04-data-model-guide.md` (v5.18 имена +не меняет), `05-domain-rules.md` §9.5 п. 1, 3 и новый пункт «Рабочий стол консоли», `10-user-journey.md` п. 2.5, +`dev/frontend-api-guide.md`, `dev/api-contract-files-directories.md`, `dev/api-contract-slice-4.md`, `dev/qa-guide.md`, +`07-open-questions.md` (новый вопрос о целевой модели версий), ссылки на v5.17 в `CLAUDE.md` workspace и +`repo-templates/*.CLAUDE.md`. + +## Risks / Trade-offs + +- Файл с неподходящим периодом, загруженный в консоль, остаётся в её столе без пометки: «не подходит версии» видно + только по чек-листу. Приемлемо: владелец выбрал семантику «загружено сюда». +- Два списка (стол и все файлы ЮЛ) с одинаковыми карточками — пользователь может не заметить переключатель. Подпись + консолидации у файла и заголовок панели это смягчают. +- Backfill по «самому раннему периоду» для файла, привязанного к версиям разных периодов, — эвристика; таких файлов + на стенде единицы (ОСВ `Q1`, подхваченные версией `Q2`). +- Переход `INITIATED → DOCUMENTS_PROCESSING` у нескольких версий сразу — сейчас версия одна; порядок блокировок (по + возрастанию `id`, R-2 Среза 4) сохраняется. + +## Decisions made without discussion + +1. 09.10.2026 — **R-1:** `consolidation_file` (связь файл–версия) не вводится; поле `stored_file.consolidation_id` на + уровне периода. Обсуждено с владельцем на brainstorming; здесь зафиксировано как итог. Цена ошибки: средняя — + при переходе к «файлы по версиям» потребуется таблица связей и миграция данных. +2. 09.10.2026 — **R-2:** коды ошибок загрузки в консоль: 404 `NOT_FOUND` (консолидация/ЮЛ вне видимости, как везде), + 409 `CONSOLIDATION_NOT_EDITABLE` (нет версии «в работе»), 422 `ENTITY_NOT_IN_PERIMETER` (существующий код). Цена + ошибки: низкая — коды в README и гайде фронта. +3. 09.10.2026 — **R-3:** backfill — самый ранний период среди привязок; файлы без привязок остаются `NULL`. Цена ошибки: + низкая — стенд разработки, данные тестовые. +4. 09.10.2026 — **R-4:** при автозамене новый файл без контекста наследует консоль заменённого; с контекстом — своё. + Цена ошибки: низкая — влияет на то, в каком столе виден файл; привязки документов не затрагиваются. +5. 09.10.2026 — **R-5:** рабочий стол = `DESK ∪ BOUND` (файлы консоли плюс файлы с документами, привязанными к любой + версии консоли). Без `BOUND` файл, загруженный без контекста и подхваченный версией, отсутствовал бы в панели при + заполненной ячейке чек-листа. Цена ошибки: низкая — лишние строки в панели. +6. 09.10.2026 — **R-6:** целевые правила версий («вторая рабочая версия = другой периметр», «новая версия после + `VERIFIED` с тем же периметром и правом менять документы») в этом change не реализуются и не специфицируются — + фиксируются вопросом в `07-open-questions.md` для среза «версии консолидации». Цена ошибки: низкая — модель файлов + с ними совместима. +7. 09.10.2026 — **R-7:** замороженный список первички верифицированной версии в панели не строится (ЕХ, Срез 7); + панель из карточки версии не «в работе» показывает стол консоли без кнопки загрузки. Цена ошибки: средняя — если + Срез 7 задержится, у верифицированной версии не будет экрана «что именно использовано»; чек-лист версии частично + закрывает (`document_ids` в ячейках). +8. 09.10.2026 — **R-8:** query `consolidation_version_id` у `POST /legal-entities/{id}/files` удаляется, а не + сохраняется для совместимости: единственный клиент — arm-web, меняется в том же выпуске. Цена ошибки: низкая. +9. 09.10.2026 — **R-9:** решение владельца 07.10.2026 «фильтра файлов ЮЛ в контексте версии нет и не будет» + (`dev/frontend-api-guide.md`) считается пересмотренным решениями 09.10.2026; строка в гайде зачёркнута со ссылкой на + docs #90, не удалена. Цена ошибки: низкая — документальная. +10. 09.10.2026 — **R-10:** note `recognition_job.reply_deadline` (OQ-159, «20 мин») исправлен в v5.18 попутно: владелец + просил править при ближайшей версии модели. Цена ошибки: нулевая. +11. 09.10.2026 — **R-11:** колонка `stored_file.consolidation_id` ставится последней в блоке `Table stored_file` DBML + (вопреки порядку в плане) — `check_dbml_vs_orm` в arm-core сверяет порядок колонок с физическим, а `ALTER TABLE ADD + COLUMN` в миграции 0006 добавляет колонку в конец (как `kpp` в v5.17). Цена ошибки: падение `make dbml-check` в + arm-core; правится перестановкой. +12. 09.10.2026 — **R-12:** ссылки на текущую версию модели v5.17 → v5.18 в `context/05-domain-rules.md` (заголовок) и + скобки об истории версий в `context/08-source-docs-index.md`. Выполнено в коммите 64ccfe3 (docs #90); в задаче 5 + правок не потребовалось, исторические упоминания v5.13…v5.17 не трогаются. Цена ошибки: лишняя правка исторической + ссылки. +13. 09.10.2026 — **R-13:** `openspec/config.yaml` (контекст схемы): ссылки на текущую модель переведены на v5.18 + (`data-model-v5.18.dbml`, `entities-v5.18.md`); исторические упоминания не трогались. Цена ошибки: низкая — + текст контекста для агента, не исполняемый. +14. 09.10.2026 — **R-14 / R-15:** формулировка про `SUPERSEDED` одна по смыслу в `dev/frontend-api-guide.md`, + `dev/api-contract-files-directories.md` и `context/05-domain-rules.md` §9.5 п. 11: «`SUPERSEDED` — только при + `include_superseded=true` (скрыт по умолчанию)». Цена ошибки: разночтение, правится текстом. +15. 09.10.2026 — **R-16:** push ветки и создание PR выполняет контроллер, а не исполнитель Task 5 (шаг 6 плана). Цена ошибки: + нулевая. +16. 09.10.2026 — **R-17:** трейлер коммита Task 5 исправлен amend'ом локального непушнутого коммита на + `Co-Authored-By: Claude Fable 5.1` по явному требованию задачи. Цена ошибки: переписана одна локальная запись. diff --git a/openspec/changes/console-files/proposal.md b/openspec/changes/console-files/proposal.md new file mode 100644 index 0000000..3477481 --- /dev/null +++ b/openspec/changes/console-files/proposal.md @@ -0,0 +1,74 @@ +## Why + +Панель «Первичные документы» ЮЛ [39], открытая из версии консолидации, показывает все файлы ЮЛ, в том числе +загруженные в другие консолидации той же ГК. Причина в модели: файл принадлежит ЮЛ (`stored_file.legal_entity_id`), +контекста консолидации у него нет, `GET /legal-entities/{id}/files` ничем не фильтруется. Связь с версией есть только у +подтверждённых документов (`consolidation_document`), поэтому «что версия использует» и «что аналитик загрузил в эту +консоль» сегодня разные множества, и второе нигде не хранится (`consolidation_version_id` при загрузке используется лишь +для перехода статуса версии). + +Решения владельца 09.10.2026 (RSHB/arm-ka-docs#90), целевая картина: + +- файл загружается **в консолидацию** (ГК + год + квартал) и становится частью её «рабочего стола», общего для всех + рабочих версий периода; рабочие версии одного периода различаются периметром; +- верифицированная версия заморожена: её первичка фиксируется в ЕХ (`ex_snapshot_item.stored_file_id`, Срез 7); + новая версия после `VERIFIED` может менять документы при том же периметре; +- что использует конкретная версия, по-прежнему определяют `consolidation_document` и чек-лист версии; +- загрузка и список файлов в контексте консоли идут по маршрутам от консолидации, а не query-параметром у ЮЛ. + +Срез: вне плана `09-delivery-plan.md` — доработка Среза 2d/4 (UI первичных документов в карточке версии). + +## What Changes + +- **Модель, DBML v5.18:** `stored_file.consolidation_id uuid` (nullable, FK → `consolidation`) — консолидация, в + контексте которой загружен файл; `NULL` — загрузка без контекста (страница ЮЛ, монозаёмщики, API). Индекс + `(legal_entity_id, consolidation_id)`. Пишется один раз при загрузке; при автозамене наследуется заменённым файлом, + если у нового файла контекста нет. +- **API ядра:** новые маршруты `POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` + (загрузка в консоль) и `GET /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` (рабочий стол + консоли для ЮЛ). У `POST /legal-entities/{id}/files` query `consolidation_version_id` убирается — маршрут остаётся + для загрузки без контекста. `FileOut.consolidation_id` в списках и карточке файла. Переход `INITIATED → + DOCUMENTS_PROCESSING` после загрузки — у всех версий консоли «в работе», где ЮЛ в периметре. +- **Правила домена, §9.5:** загрузка в консоль (проверки, коды ошибок, переход версий), состав рабочего стола, + наследование при автозамене. +- **Фронт arm-web:** панель [39] и загрузка [40] ходят на маршруты консолидации; переключатель «Все документы + организации» с подписью консолидации у файла; загрузка недоступна, если открытая версия не «в работе». +- **Документы:** `model/data-model-v5.18.dbml`, `model/entities-v5.18.md`, `04-data-model-guide.md`, + `05-domain-rules.md` §9.5, `10-user-journey.md` п. 2.5, `dev/frontend-api-guide.md`, + `dev/api-contract-files-directories.md`, `dev/api-contract-slice-4.md`, `dev/qa-guide.md`, `07-open-questions.md`. + +Дочерние change'и `console-files`: RSHB/arm-core (миграция `0006_dbml_v5_18`, сервис, маршруты, спеки `primary-files` +и `documents-api`), RSHB/arm-web (спека `primary-files-ui`). Issue этого репозитория — RSHB/arm-ka-docs#90. +Контракт обмена с сервисами распознавания не меняется, `schema_version` прежняя. + +## Non-goals + +- Несколько рабочих версий одного периода с разным периметром и «новая версия после `VERIFIED`» ядро пока не создаёт + (`POST /consolidations` создаёт только версию 1); эти правила — отдельный срез (OQ, design «Decisions» №6). Этот + change лишь делает модель файлов совместимой с ними. +- Замороженный список первички верифицированной версии в панели не строится: заморозка — `ex_snapshot` (Срез 7). + Панель из карточки версии не «в работе» показывает рабочий стол консоли как есть, без кнопки загрузки. +- Признак «документ подходит / привязан к версии» в списке файлов не вводится: это чек-лист версии (OQ-120). +- Привязка файла к версии (`consolidation_file`) не вводится: отвергнута на brainstorming 09.10.2026 (design, №1). +- `consolidation_document`, правило `fits`, подхват при создании версии и добавлении ЮЛ в периметр — без изменений. +- Горячие папки scan-gateway файлов в ядре не создают; их контекст не рассматривается. +- Backfill касается только файлов с документами в `consolidation_document`; прочие остаются `NULL`. + +## Capabilities + +### New Capabilities + +### Modified Capabilities +- Спецификации этого репозитория (`dev-environment`, `quality-checks`, `recognition-contract`) не меняются; требования + к API и UI — в дочерних change'ах arm-core (`primary-files`, `documents-api`) и arm-web (`primary-files-ui`). + +## Impact + +- `arm-core`: миграция с backfill; `services/primary_files.py` (загрузка, `page_files`, автозамена в + `services/result_mapping.py`), `repositories/files.py`, `api/endpoints/files/upload.py`, `read.py`, новый роутер + под `/consolidations/{id}/legal-entities/{le}/files`, `schemas/files.py`; тесты; README «Ошибки API». +- `arm-web`: `services/api/files.ts`, стор `consolidations` (`current_consolidation_id`), `widgets/FilesDrawer/*`, + типы `FileOut`. +- Совместимость: query `consolidation_version_id` у `POST /legal-entities/{id}/files` исчезает — фронт меняется + одновременно с ядром (стенд обновляется после слияния обоих PR). Файлы, загруженные до миграции без привязок, + остаются «вне консолидации» и видны только по переключателю «Все документы организации». diff --git a/openspec/changes/console-files/tasks.md b/openspec/changes/console-files/tasks.md new file mode 100644 index 0000000..8a0de86 --- /dev/null +++ b/openspec/changes/console-files/tasks.md @@ -0,0 +1,19 @@ +## Порядок + +Подробный план с шагами и тестами — superpowers writing-plans (ledger SDD); здесь — крупные задачи по репозиториям +в порядке зависимостей. + +- [x] 1. **docs** (этот change): `model/data-model-v5.18.dbml` + changelog, `model/entities-v5.18.md`, `04-data-model-guide.md`, + `05-domain-rules.md` §9.5, `10-user-journey.md` п. 2.5, `dev/frontend-api-guide.md`, + `dev/api-contract-files-directories.md`, `dev/api-contract-slice-4.md`, `dev/qa-guide.md`, `07-open-questions.md` + (целевая модель версий), ссылки на версию DBML в `CLAUDE.md` workspace и `repo-templates/*.CLAUDE.md`. +- [ ] 2. **arm-core** (change `console-files`, спеки `primary-files`, `documents-api`): модель `StoredFile.consolidation_id`, + миграция `0006_dbml_v5_18` с backfill и откатом, `make dbml-check`; `repositories/files.py` (рабочий стол); + `services/primary_files.py` (загрузка в консоль, проверки, переход версий, `page_desk`); `services/result_mapping.py` + (наследование при автозамене); маршруты `/consolidations/{cid}/legal-entities/{le}/files`; удаление query у + `POST /legal-entities/{id}/files`; `schemas/files.py`; README «Ошибки API»; тесты; `make check-full`. +- [ ] 3. **arm-web** (change `console-files`, спека `primary-files-ui`): стор `current_consolidation_id`, `services/api/files.ts`, + `widgets/FilesDrawer/*` (стол, переключатель, подпись консолидации, недоступная загрузка вне «в работе»), типы; + `make check`; ручной прогон сценария из design на стенде в Playwright со сверкой с макетом [39]/[40]. +- [ ] 4. **Закрытие:** обновить `03-architecture.md` (если затронуты схемы), `07-open-questions.md`, `09-delivery-plan.md` + (строка про доработку 2d/4); архив change'ей в трёх репозиториях. diff --git a/openspec/config.yaml b/openspec/config.yaml index 092407e..96ea222 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -1,12 +1,12 @@ schema: spec-driven context: | - Репозиторий arm-ka-docs — контекст, модель данных (DBML v5.17), правила домена, контракты между сервисами + Репозиторий arm-ka-docs — контекст, модель данных (DBML v5.18), правила домена, контракты между сервисами (docs/contracts/*.schema.json), макет UI и экспорт ТЗ системы АРМ КА (ДКБ, Россельхозбанк). Здесь оформляются СКВОЗНЫЕ изменения: контракт + окружение разработки (docs/dev) + проверки качества (make check) + изменения документов контекста. Код сервисов живёт в соседних репозиториях (../arm-core, ../tabular-parser, ../scan-gateway), у каждого свой openspec/ и свой change; сквозной change ссылается на них. - Источники истины по приоритету: docs/model/data-model-v5.17.dbml → docs/model/entities-v5.17.md → + Источники истины по приоритету: docs/model/data-model-v5.18.dbml → docs/model/entities-v5.18.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 4721134..1887ac5 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.17.dbml`; `scripts/check_dbml_vs_orm.py` — обязательная проверка CI. +- ORM 1:1 с `../docs/model/data-model-v5.18.dbml`; `scripts/check_dbml_vs_orm.py` — обязательная проверка CI. - Алгоритмы — только из `../docs/context/05-domain-rules.md`; номер § — в docstring. - Вычисляемые объекты консолидации — `backend/app/domain/`, без ORM-таблиц. - Фронт: пользователь слаб во фронте — для TS/React изменений делай ревью сам и объясняй решения в PR.