diff --git a/context/03-architecture.md b/context/03-architecture.md index 54c8d63..f779d63 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -1,4 +1,4 @@ -# 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.15) +# 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.16) ## Компоненты (= репозитории, см. `00-workspace-layout.md`) @@ -113,6 +113,37 @@ `pg_advisory_xact_lock` по ЮЛ (иначе два параллельно смапленных файла с одним документом не увидят друг друга), загрузка — advisory-lock по `(ЮЛ, sha256)` (двойной клик не проскакивает дедуп). +### Консолидации и сводная ОСВ (Срез 4) + +Версия консолидации — единица работы аналитика: периметр (`consolidation_member`), комплект документов +(`consolidation_document`, явная привязка), статус, блокировка «взять в работу». Сводная ОСВ, чек-лист, признак контрагента и +пробелы — **чистые функции** от версии (DECISION-5, ничего не кэшируется в БД и в процессе). + +- **Чистый домен свода (D-1).** `repositories/consolidations.py:load_osv_rows` одним запросом отдаёт строки привязанных + ОСВ-документов; `domain/consolidated_osv.py:build` и `domain/consolidation_periods.py:select_sources` (без БД и сессий, + mypy strict) выбирают источники, склеивают кварталы, находят пробелы, резолвят контрагента, применяют фильтры. + `services/consolidated_osv.py:_build` — единственная сборка, которую делят страница (`GET …/osv`), сводка, кандидаты, + ручное сопоставление (ответ) и экспорт xlsx (`services/osv_export.py`). Весь свод версии строится в памяти на запрос + (замер синтетического теста `test_large_version_is_fast_without_n_plus_one`: 5,4 тыс. исходных строк → 1,8 тыс. строк свода — + 0,27 с, 8 запросов; на фикстурах РБ — 1385 строк свода, 22 документа; порог для кэша — OQ-121). +- **Пакет консолидаций.** `api/endpoints/consolidations/` (periods, versions, locks, status, members, checklist, osv, export, + mapping); сервисы `consolidations` (создание, список, карточка, периметр), `consolidation_guard` (общий пролог правок: + версия `FOR UPDATE` → видимость → статус «в работе» → своя блокировка), `consolidation_locks`, `consolidation_status`, + `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` блокирует надмножество — член ИЛИ та же ГК — и + только потом отбирает версии, где ЮЛ член, иначе confirm ∥ `PUT …/members` теряет привязку: R-2); создание версии берёт + `pg_advisory_xact_lock` по ГК; правка версии начинается с `consolidation_version FOR UPDATE`. Документы блокируются + `FOR NO KEY UPDATE` (R-3): вставка `consolidation_document` берёт `FOR KEY SHARE` на документ и конфликтовала бы с + `FOR UPDATE`. Ручное сопоставление: версия → advisory-lock по нормализованному алиасу (его берёт и `aliases.add` + справочника) → алиасы → строки `osv_entry` с тем же именем. +- **Нагрузка и транзакции.** Переход версии после загрузки выполняется в отдельной короткой транзакции + (`session_transaction`), сбой не отменяет принятые файлы (R-10). Чтение (`GET …/osv`, сводка, экспорт) блокировок не берёт. + ## Схема ключей S3 **Один бакет** (`OPENBAO__MINIO__MINIO_BUCKET` шаблона); роль объекта — первый сегмент ключа: @@ -152,7 +183,7 @@ api/src/ # машины статусов, policy — pydantic-модели на входе и выходе services/ # use-cases: оркестрация domain + repositories + outbox + audit_event repositories/ # SQLAlchemy-запросы, в т. ч. запросы-вычисления (сводная ОСВ, шахматка) - models/ # ORM 1:1 с DBML v5.15; имена таблиц/колонок/enum — как в DBML + models/ # ORM 1:1 с DBML v5.16; имена таблиц/колонок/enum — как в DBML schemas/ # pydantic DTO API /api/v1 core/ # config, clients (S3; NATS outbox-publisher и result-consumer), auth (JWT РАИСА / dev-эмитент), # logger, exception_handlers @@ -237,7 +268,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.15. +- PK везде `uuid` через `UUIDPrimaryMixin`; имена таблиц, колонок и enum'ов — ровно как в DBML v5.16. ### Инфраструктурные клиенты diff --git a/context/04-data-model-guide.md b/context/04-data-model-guide.md index 9559cd6..d06d23e 100644 --- a/context/04-data-model-guide.md +++ b/context/04-data-model-guide.md @@ -1,9 +1,9 @@ -# 04. Как читать модель данных v5.15 +# 04. Как читать модель данных v5.16 -Файлы: `docs/model/data-model-v5.15.dbml` (источник истины), `docs/model/entities-v5.15.md` (смысл таблиц; предыдущие -версии `data-model-v5.13.dbml` / `data-model-v5.14.dbml` и `entities-v5.13.md` / `entities-v5.14.md` остаются как история), +Файлы: `docs/model/data-model-v5.16.dbml` (источник истины), `docs/model/entities-v5.16.md` (смысл таблиц; предыдущие +версии `data-model-v5.13…v5.15.dbml` и `entities-v5.13…v5.15.md` остаются как история), `docs/model/diagrams/part01…09.svg` (диаграммы v5.12 — на одну версию старше: нет `permission`/`role_permission`, -нет `document`, есть `osv_group_schema`, нет `related_group_member.kind`; читать с поправкой на changelog v5.13, v5.14 и v5.15 в шапке DBML). +нет `document`, есть `osv_group_schema`, нет `related_group_member.kind`; читать с поправкой на changelog v5.13…v5.16 в шапке DBML). ## Девять частей модели @@ -38,6 +38,8 @@ ## Что намеренно НЕ хранится (DECISION-5) и как это вычислять +Способ сопоставления контрагента хранится (`osv_entry.counterparty_match_kind`, v5.16); пробелы квартальной склейки и сам свод — нет. + | Объект | Вычисление | |---|---| | Сводная ОСВ | `osv_entry` актуальных `document` (OSV, CONFIRMED, период версии) всех `consolidation_member` версии; только строки `group_type = COUNTERPARTY` и `data_type IN (BU, SUM)`; строки не агрегируются | @@ -55,9 +57,9 @@ ## v4 → v5.13: таблица переименований (обязательна при чтении ТЗ) v5.14 имена таблиц и полей не менял (добавлены `related_group_member.kind` и два значения `audit_action`), v5.15 — тоже -(добавлено значение `osv_data_type.NU`), поэтому таблица справедлива и для v5.14, и для v5.15. +(добавлено значение `osv_data_type.NU`), v5.16 — тоже (колонка `osv_entry.counterparty_match_kind`), поэтому таблица справедлива и для v5.14, v5.15 и v5.16. -| В страницах ТЗ (модель v4 и старше) | В v5.13 / v5.14 / v5.15 | +| В страницах ТЗ (модель v4 и старше) | В v5.13 / v5.14 / v5.15 / v5.16 | |---|---| | `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 4821565..03c3618 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -235,18 +235,35 @@ openpyxl, после чтения — только диапазоны, кото --- -## §2. Приведение периодов (DOC-259006) +## §2. Приведение периодов (DOC-259006; Срез 4, решения владельца 06.10.2026: OQ-03, OQ-19) -Комплект (ББ + ОФР + ОСВ) за один период нарастающим итогом. Если ОСВ квартальные (`is_cumulative = false`), перед -консолидацией строится **расчётное представление** (не хранится): +Комплект (ББ + ОФР + ОСВ) за один период нарастающим итогом. Если ОСВ квартальные (`is_cumulative = false`), при построении +сводной ОСВ (§3) строится **расчётное представление** (не хранится, DECISION-5). Реализация — +`domain/consolidation_periods.py:select_sources` (выбор документов) и `domain/consolidated_osv.py:build` (склейка). + +**Выбор источников на пару (ЮЛ, счёт документа)** среди документов, привязанных к версии (§3 п. 1); `n` — квартал версии: + +1. есть привязанный документ с `is_cumulative = true` и `period_quarter = n` — он один, строки берутся как есть; +2. иначе нужны привязанные квартальные документы за **каждый** квартал `Q1..Qn`; любого нет — **пробел** + (`Gap(missing_quarters)`), строк пары в своде нет, пробел показывается в сводке (`gaps[]`) и в листе «Пробелы» экспорта; +3. нарастающий и квартальные одновременно — берётся нарастающий (он полнее по определению); +4. **Q1-эквивалентность ОСВ (R-14, design №81; OQ-124):** документ ОСВ за `Q1` равнозначен и нарастающему, и квартальному + (парсер помечает ОСВ за Q1 как нарастающую: период с начала года = Q1) — в версиях Q2+ он служит квартальным источником + Q1. ББ/ОФР за Q1 версиям Q2–Q4 не подходят (R-14a: баланс на конец Q1). + +**Склейка квартальных строк** — ключ `(account_code строки, counterparty_name.strip(), normalize_inn(counterparty_inn))`; +договоры (`group_type = CONTRACT`) в свод не входят — строка COUNTERPARTY уже содержит их сумму: ``` -opening_* = opening_* первой ОСВ года (Q1) +opening_* = opening_* строки Q1 turnover_* = Σ turnover_* по кварталам Q1..Qn -closing_* = closing_* последней ОСВ (Qn) +closing_* = closing_* строки Qn ``` -Ключ соединения строк между кварталами — (счёт, контрагент, [договор]). **[открыто]**: поведение при отсутствии -промежуточного квартала; агрегировать на уровне контрагента или договора. + +Отсутствующая в квартале строка ключа = нули; `None` в ячейке = 0; две строки с одним ключом внутри квартала +суммируются (design №1); `entry_ids` строки свода перечисляет все источники. Нарастающий документ отдаётся строка в +строку (OQ-125: различие с квартальным Q1 версии Q1 — только на дублях ключа и `None`). Строки свода сортируются: +имя ЮЛ, счёт (натуральный порядок: `9 < 60 < 60.2 < 60.10`), номер строки. --- @@ -254,8 +271,14 @@ closing_* = closing_* последней ОСВ (Qn) Вход: `consolidation_version_id`. Периметр — `consolidation_member` версии (не текущий `related_group_member`!). -1. Для каждой компании периметра: `document` типа OSV со статусом CONFIRMED за `year/quarter` версии, входящие в - `consolidation_document` версии (если версия ещё не зафиксировала документы — последние подтверждённые). +1. Для каждой компании периметра: `document` типа OSV, входящие в `consolidation_document` версии (**явная привязка**, а не + «последние подтверждённые»). Документ **подходит** версии, если ЮЛ — член периметра, `period_year` = год версии, и: + нарастающий (ББ/ОФР — всегда) — `period_quarter` = квартал версии; квартальный — `period_quarter` ≤ квартал версии; + ОСВ за `Q1` подходит версии любого квартала года (§2 п. 4, OQ-124). Привязываются только `CONFIRMED` документы + незаменённых файлов. Точки привязки: подтверждение документа (ко всем версиям «в работе»), создание версии и добавление + ЮЛ в периметр (подхват: на ключ «тип, счёт, квартал, признак нарастающего» — документ с самым поздним `confirmed_at`); + при привязке документ с тем же ключом у того же ЮЛ заменяется. Точки отвязки: автозамена файла (§9.5 п. 3), удаление ЮЛ + из периметра. Версии `CANCELLED` привязок не меняют. Выбор источников и склейка кварталов — §2. 2. Строки `osv_entry` этих документов с `group_type = COUNTERPARTY`; `data_type IN (BU, SUM)`. 3. Строки **не агрегируются**: сто строк по 60.01 у компании — сто строк в своде. `counterparty_name/inn` показываются как в файле. @@ -269,13 +292,35 @@ closing_* = closing_* последней ОСВ (Qn) проваливается к имени); каждый шаг считает кандидатов отдельно: единственное точное совпадение имени побеждает неоднозначность нормализованного; имя из одних ОПФ (пусто после `normalize`) не ищется. 5. Признак контрагента: ГСК / ГК / Внешний (см. `04-data-model-guide.md`). -6. Количество строк свода = количеству отобранных `osv_entry`. +6. Количество строк свода = количеству отобранных `osv_entry` для нарастающей ОСВ; для квартальных документов строки + склеиваются по ключу (§2), `entry_ids` строки свода перечисляет источники. +7. **Дорезолв на чтение.** Строка без `counterparty_id` резолвится на лету тем же `CounterpartyIndex`; результат **не + сохраняется** (производное, DECISION-5): в ответе `match_stored = false`, `match_kind` — шаг резолва. Сохранённое значение + `osv_entry.counterparty_id` приоритетнее; при расхождении между кварталами берётся значение самого позднего квартала + (расхождение — в `warnings` строки). +8. **Способ сопоставления** `osv_entry.counterparty_match_kind` (DBML v5.16): `INN`/`NAME`/`ALIAS` — шаг, на котором при + маппинге нашёлся единственный кандидат; `MANUAL` — ручное сопоставление; `PROPAGATED` — автоприменение; `NULL` ⇔ + `counterparty_id IS NULL`. Отдельное поле нужно, потому что после ручного сопоставления появляется алиас и ручное решение + становится неотличимым от `ALIAS`. -**Ручное сопоставление** (алгоритм в DOC-221278): список кандидатов = компании ГСК и ГК; поиск по ИНН/имени/алиасам; -выбор → проверка дубля `normalized_alias` у другой компании (предложить перенести или отменить) → создать -`legal_entity_alias` → `osv_entry.counterparty_id = выбранное`; `counterparty_name/inn` не трогать → -`audit_event(OSV_COUNTERPARTY_MAP)` → автоприменить к другим строкам версии с тем же именем, если `counterparty_id` -пуст, алиас однозначен и ИНН строки не противоречит. +**Ручное сопоставление** (алгоритм в DOC-221278; Срез 4, `POST …/osv/entries/{entry_id}/counterparty`): правка версии — +право `CONSOLIDATION_EDIT`, статус «в работе» и своя блокировка (§10). + +- **Кандидаты:** `scope=group` (по умолчанию) — ЮЛ периметра и ЮЛ той же ГК, `q` необязателен; `scope=all` — весь справочник + без policy видимости, `q` не короче 3 символов (иначе 422 `QUERY_TOO_SHORT`); поиск — ИНН по префиксу, имя и алиасы — + подстрока без регистра; не больше 100; `recommended` — нормализованное имя/алиас совпало с `normalize_alias` имени + строки `entry_id` (точное совпадение, OQ-127); рекомендуемые первыми. +- **Строка** — строка свода версии: `group_type = COUNTERPARTY`, `data_type ∈ {BU, SUM}` привязанного ОСВ-документа; строки + `NU`/`ACCOUNT` — 404 и в автоприменение не входят (R-11, §3 п. 2; OQ-126). +- **Шаги в одной транзакции:** выбор → проверка дубля `normalized_alias` у другой компании (`409 ALIAS_TAKEN` со списком + владельцев; с `move_alias = true` алиасы владельцев удаляются и алиас переносится — право `DIRECTORY_DELETE` для переноса + не требуется, решение владельца 06.10.2026) → создать `legal_entity_alias` (`LEGAL_ENTITY_CHANGE`) → + `osv_entry.counterparty_id = выбранное`, `counterparty_match_kind = MANUAL`; `counterparty_name/inn` не трогать → + автоприменить к строкам **всех** ОСВ-документов версии с тем же `counterparty_name.strip()`, пустым `counterparty_id` и + ИНН пустым или равным ИНН выбранного (`PROPAGATED`; ранее связанные строки не перевязываются) → + `audit_event(OSV_COUNTERPARTY_MAP)` с `before/after` и `propagated_entry_ids`. +- Строка `osv_entry` общая у всех версий, куда входит документ: сопоставление в одной версии меняет её для всех (OQ-123). +- Пустое имя (после нормализации) — алиас не создаётся и автоприменения нет. Неоднозначность дорезолва при тёзке — OQ-128. Наблюдение по фикстурам: у «Счастливочной» контрагент «Русский бисквит» идёт **с ИНН 3528052718** в колонке E, а у «Русского бисквита» ОСВ без колонки ИНН — контрагент «Пекарня Счастливочная ООО» только текстом → резолв по алиасу. @@ -436,6 +481,21 @@ NEEDS_CHECK`; пройдено/акцептовано + нажато «Подт Каждый переход → `consolidation_status_history` + `audit_event(CONSOLIDATION_STATUS_CHANGE)`. +**Срез 4 реализует только переходы, не требующие маршрута согласования** (`consolidation_status.transition` — единственное +место смены статуса; остальные переходы таблицы — Срез 7, вручную дают 409 `TRANSITION_NOT_ALLOWED`): + +| Событие | Переход | Кто (`changed_by`) | +|---|---|---| +| загрузка файла с `consolidation_version_id`, принят ≥ 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 | пользователь с блокировкой | +| `POST …/status {INITIATED}` | DOCUMENTS_PROCESSING / DATA_CORRECTION → INITIATED (привязки сохраняются) | пользователь с блокировкой | + +«В работе» = INITIATED, DOCUMENTS_PROCESSING, DATA_CORRECTION. `CANCELLED` снимает активную блокировку и `is_in_work`. +Загрузка и подтверждение блокировкой не ограничиваются; запрет загрузки/подтверждения после DATA_CORRECTION (ТЗ 03_9 A1) — +Срез 7 (OQ-118). Условие «все обязательные документы CONFIRMED» для DATA_CORRECTION → CONSOLIDATION_* — Срез 7. + ### 9.2 Файл (`file_status`): UPLOADED → PROCESSING → PROCESSED | ERROR; любой → SUPERSEDED (замена файлом целиком, `superseded_by_file_id`, `version_no + 1`). Статус меняют только по **текущему** заданию файла (`current_recognition_job_id`): `PROCESSING` — ack сервиса (или результат `done`/`needs_check`, если ack потерян); @@ -450,7 +510,7 @@ INITIATED; в Срезе 2c консолидаций ещё нет, поэтом (OQ-05 закрыт: `RECOGNIZED` нужен); «Подтвердить данные» → `CONFIRMED`; `ERROR` — зарезервирован (в Срезе 2c ядро документ в `ERROR` не создаёт: файл без разбираемой ОСВ получает `ERROR` на уровне файла). `counterparty_check`: ИНН шапки равен ИНН владельца файла → `MATCH`; ИНН нет — нормализованное имя шапки совпало с именем или алиасом владельца → `MATCH`; -иначе `MISMATCH`; пустая шапка → `UNKNOWN`. `period_check` — `UNKNOWN` до Среза 4. +иначе `MISMATCH`; пустая шапка → `UNKNOWN`. `period_check` — `UNKNOWN`: подходящесть документа версии — функция (документ, версия), а один документ входит в несколько версий (§3 п. 1, Срез 4; OQ-120). ### 9.4 Задание (`recognition_status`): queued → published → processing → done | needs_check | error | timeout → dlq; replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `reply_deadline` OCR 5 мин. @@ -478,18 +538,27 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; ` заменённых файлов сохраняются, но по умолчанию не показываются. Если новый файл содержит лишь часть документов заменяемого, заменяемый уходит в `SUPERSEDED` целиком (OQ-06: замена файлом целиком; частный случай — OQ-115). Аудит: системное `FILE_REPLACE` (`user_id = NULL`, `object_id` — заменённый файл, `after_value` — новый, в `description` - перечень документов и `confirmed=true`). + перечень документов и `confirmed=true`). **Срез 4:** автозамена снимает привязки (`consolidation_document`) **всех** + документов заменённого файла из всех версий «в работе» (`CANCELLED` не трогаются); версии, где привязка снята, — + `FILE_REPLACE.after_value.unbound_version_ids`; новый документ привязывается только после подтверждения (§3 п. 1). 4. **Подтверждение** (`POST /documents/{id}/confirm`, `DOCUMENT_EDIT`): `RECOGNIZED`/`NEEDS_CHECK` → `CONFIRMED` (`confirmed_by/at`, `audit_event(DOCUMENT_CONFIRM)`); повтор — 409 `ALREADY_CONFIRMED`; документ заменённого файла — 409 `FILE_SUPERSEDED`; прочие статусы — 409 `DOCUMENT_NOT_CONFIRMABLE`. Правка строк и корректировки — подсрез 2d/Срез 3. 5. **Удаление** (`DELETE /files/{id}`, `DOCUMENT_EDIT`): запрещено при `CONFIRMED` документе (`FILE_HAS_CONFIRMED_DOCUMENTS`) и если файл заменил другие (`FILE_SUPERSEDES_OTHERS`); `SUPERSEDED` файл удалить можно. Удаляются строки, документы, задания и `stored_file`; объектам S3 ставится `expires_at = now()` (физически не - стираются — OQ-111); аудит `FILE_DELETE` с карточкой файла в `before_value`. + стираются — OQ-111); аудит `FILE_DELETE` с карточкой файла в `before_value`. **OQ-110 закрыт для Среза 4:** к версии + привязываются только `CONFIRMED` документы, а файл с таким документом удалить нельзя — удалить файл с документом в + версии невозможно; расширять удаление не требуется. 6. **Аудит.** `FILE_UPLOAD`, `FILE_REPLACE`, `FILE_DELETE`, `DOCUMENT_CONFIRM` (раздел `PRIMARY_DOCUMENTS`); смены статусов задания/файла в обмене и в цикле маппинга `audit_event` не пишут (OQ-55). -7. **Порядок блокировок** (против взаимоблокировок): задание → файл → документы; автозамена дополнительно берёт - advisory-lock по ЮЛ. Подробности — `03-architecture.md`. +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); сбой этого перехода не отменяет принятые файлы. Блокировка версии для загрузки не + нужна. --- @@ -507,6 +576,15 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; ` Policy-слой (не права): KA_RF/RKA/CLIENT_MANAGER видят только ГК своего `regional_office`; редактирование версии — только в допустимом статусе и при отсутствии чужой активной `consolidation_lock`; ЗО (`business_segment`) — фильтр видимости. + +**Срез 4 (`domain/policy.py:ensure_version_editable`, решения владельца 06.10.2026, OQ-19):** правка версии (замена +периметра, ручной статус, ручное сопоставление контрагента) разрешена, когда версия «в работе» (INITIATED, +DOCUMENTS_PROCESSING, DATA_CORRECTION; иначе 409 `VERSION_NOT_EDITABLE`) **и** у пользователя своя активная +`consolidation_lock` (нет — 409 `LOCK_REQUIRED`, чужая — 409 `LOCKED_BY_OTHER`). Блокировка закрывает все правки версии, но не +загрузку и подтверждение документов. Перехват чужой блокировки — любому с `CONSOLIDATION_EDIT`, с обязательной причиной; +блокировка не истекает по времени (OQ-119). Чтение (свод, чек-лист, экспорт, кандидаты) требует `CONSOLIDATION_VIEW`, +блокировки не нужно, версия любого статуса читается. Видимость версии — видимость её ГК; чужая версия — 404. +Порядок проверок: право → видимость → статус/блокировка → операция. Матрица в ТЗ помечена «будет разработана» (сноска 29) — seed считать черновиком, редактируется администратором в UI. ### 10.1 Видимость справочников (Срез 1, без проверки кода роли) diff --git a/context/07-open-questions.md b/context/07-open-questions.md index 502cf75..efda567 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -8,7 +8,7 @@ |---|---|---|---|---| | OQ-01 | Хранить ли все строки ОСВ или только контрагентские; нужен ли `parent_entry_id` (замечание 2 ревью v5.13) | DBML, «Парсинг ОСВ» п. 11 | срез 2 (парсер, `osv_entry`) | **закрыт решением владельца 05.10.2026:** хранить **все** строки ОСВ с `level` и `parent_entry_id`; строку «Итого» не хранить, использовать только для сверки (парсер отдаёт её как `group_type = TOTAL`, маппер Среза 2c отбрасывает) | | OQ-02 | ОСВ «за 2/3/4 квартал» — квартальная или нарастающая? Как определять автоматически | FNR01 маппинг периода | срез 2, §2 правил | **закрыт решением владельца 05.10.2026:** ОСВ «за N квартал» — **квартальная**; признак сверяется по данным: если есть ОСВ Q1 того же года и `opening` совпадает — нарастающая (парсер ставит `is_cumulative` по тексту, ядро переопределяет при маппинге/в консолидации); **допущение парсера (Срез 2a):** `period_quarter` и `is_cumulative` вычисляются только по тексту периода (`rules/periods.yaml`; Q2–Q4 «N квартал» → `false`), сверка по данным — в ядре, подсрез 2c (`ASSUMPTION(OQ-02)`) | -| OQ-03 | Приведение квартальных ОСВ к нарастающим: ключ соединения (контрагент vs договор), пропуск промежуточного квартала | DOC-259006 | срез 4 | допущение: ключ (счёт, контрагент); пропуск квартала → блокирующая ошибка чек-листа | +| OQ-03 | Приведение квартальных ОСВ к нарастающим: ключ соединения (контрагент vs договор), пропуск промежуточного квартала | DOC-259006 | срез 4 | **закрыт решением владельца 06.10.2026 (Срез 4):** ключ склейки — (счёт, контрагент по сырому имени и ИНН), договоры внутри контрагента (строка COUNTERPARTY уже содержит их сумму); пропуск квартала — **пробел** по паре (ЮЛ, счёт), а не блокирующая ошибка: строк пары в своде нет, пробел отдаётся в `gaps[]` сводки и в листе «Пробелы» экспорта; квартал без строки ключа = нули; дубль ключа внутри квартала суммируется. Правило — `05-domain-rules.md` §2; код — `domain/consolidation_periods.select_sources`, `domain/consolidated_osv.build` | | OQ-04 | ОСВ по субсчёту (58.03 вместо 58) закрывает ли обязательный документ «ОСВ 58» | фикстуры | чек-лист | **закрыт решением владельца 05.10.2026:** **нет**, ОСВ по субсчёту (58.03) — только субсчёт, обязательный документ «ОСВ 58» закрывает только полная ОСВ по счёту 58; чек-лист считает документ выполненным при `osv_account_code = код правила` | | OQ-05 | Нужен ли статус `document.RECOGNIZED` или все идут через `NEEDS_CHECK → CONFIRMED` | DBML | срез 2–3 | **закрыт решением владельца 05.10.2026:** статус `RECOGNIZED` **нужен**: документ после успешного распознавания — `RECOGNIZED`, при нарушениях — `NEEDS_CHECK`, после подтверждения аналитиком — `CONFIRMED` | | OQ-06 | Замена одного документа внутри многодокументного файла | DBML | срез 3 | решено: замена файлом целиком | @@ -24,7 +24,7 @@ | OQ-16 | Матрица прав (ТЗ сноска 29 «будет разработана»); роли макета (Аналитик ДКБ, Наблюдатель) ≠ ТЗ (ANL-034) | ТЗ 8.1, макет | срез 1 | допущение: seed по §10 правил | | OQ-17 | `user_role` M:N или одна роль на пользователя | DBML | срез 1 | **закрыт реализацией Среза 1 (05.10.2026): M:N** — `user_role`, `PUT /admin/users/{id}/roles` заменяет набор ролей | | OQ-18 | Статусы REJECTED / ARCHIVED из макета (ANL-035); `VERIFICATION_GO` (РКА ГО) — есть ли в маршруте | DBML, DOC-037734 | срез 7 | допущение: REJECTED не вводить (возврат = переход + комментарий); ARCHIVED — флаг фильтра; VERIFICATION_GO — есть | -| OQ-19 | `consolidation_lock` (взять в работу / перехват, ANL-036) — подтвердить с заказчиком | макет | срез 7 | реализовать как в DBML | +| OQ-19 | `consolidation_lock` (взять в работу / перехват, ANL-036) — подтвердить с заказчиком | макет | срез 7 | **закрыт решением владельца 06.10.2026 (Срез 4):** блокировка «взять в работу» закрывает **все** правки версии (периметр, статус, ручное сопоставление; загрузка и подтверждение документов блокировкой не ограничиваются); перехват — любому с `CONSOLIDATION_EDIT`, с обязательной причиной (`takeover_reason`), событие `CONSOLIDATION_LOCK_TAKEOVER`; истечения нет (OQ-119). `05-domain-rules.md` §10 | | OQ-20 | `company_group.crm_id` обязателен (модалка) или nullable (UC-02, DBML) | DOC-221280 | срез 1 | решено DBML: nullable + `code` | | OQ-21 | «Дата перевода» ЮЛ в другую ГК — помечена «??» | DOC-221280 | срез 1 | **закрыт решением владельца 05.10.2026:** дата перевода — `effective_date` ≥ сегодня, пишется в `legal_entity_history`; перевод применяется сразу; граница суток — по МСК (OQ-96, follow-up arm-core#8) | | OQ-22 | Ретеншн `results/` в S3 (ARCH-010) | DBML | срез 2 | допущение: как `uploads/` | @@ -45,7 +45,7 @@ | OQ-37 | Строка «Гудвилл 1105» (ФСБУ 4/2023) в распознанной форме (`41`) vs строка `kfo_result_line.line_code = 'GOODWILL'` в разделе I КФО: использовать код 1105 или служебный GOODWILL? Влияет на шаблон строк на фронте и на формулы §7 (1100 = … + 1105?) | макет [41], ФСБУ 4/2023 | срез 3, 6 | допущение: хранить код 1105 как пришёл; строка КФО «GOODWILL» — отдельная, уточнить у методологов | | OQ-38 | ЕХ в макете показывает и **неверифицированные** консолидации («Консолидация 2025 К4», без даты верификации, `62` [83-01]); ЕХ ТЗ 6.2 — срез пишется только после «Верифицирован». Показывать ли в ЛК ЕХ незавершённые консолидации (как «в работе») или только `ex_snapshot`? | макет [62] [83-01] vs ЕХ ТЗ 6.2 | срез 7 | допущение: только срезы `ex_snapshot`; незавершённые — в разделе «Отчётность» | | OQ-39 | Состав «сформированной отчётности» в ЕХ (`83-01`): КФО .xlsx, Шахматка .xlsx и «Консолидированная отчетность .pdf» с датой «Сформирован» и подписями Утвердил/Верифицировал. Нужен ли PDF-экспорт КФО и какие ещё артефакты (сводная ОСВ, корректировки, гудвилл — ЕХ ТЗ 5.2) показывать? | макет [83-01] vs ЕХ ТЗ 5.2 | срез 7 | допущение: xlsx из `payload` на лету для всех `item_type`; PDF — не делать до решения | -| OQ-40 | «Квартальная дата» в журнале для консолидации 2024 К1 показана как 01.01.2024 (`43-02`); по правилам банка 1 квартал 2024 = отчётная дата 01.04.2024 (`consolidation.reporting_date`). Что выводить? | макет [43-02] | срез 4, 7 | допущение: `reporting_date` по правилам (01.04.2024); макет неточен | +| OQ-40 | «Квартальная дата» в журнале для консолидации 2024 К1 показана как 01.01.2024 (`43-02`); по правилам банка 1 квартал 2024 = отчётная дата 01.04.2024 (`consolidation.reporting_date`). Что выводить? | макет [43-02] | срез 4, 7 | **допущение принято и реализовано в Срезе 4 (06.10.2026):** `consolidation.reporting_date` по правилу банка (Q1 → 01.04.Y, Q2 → 01.07.Y, Q3 → 01.10.Y, Q4 → 01.01.Y+1), `domain/consolidation_periods.reporting_date`; макет неточен | | OQ-41 | Экран «ЮЛ вне ГК» (`61`, заголовок «Юридические лица») показывает все ЮЛ с заполненной колонкой ГК, а не только монозаёмщиков. Это сводный реестр ЮЛ или фильтр `company_group_id IS NULL`? | макет [61] | срез 1 | допущение: сводный реестр всех ЮЛ с фильтром «вне ГК», по умолчанию выключенным | | OQ-42 | «Проверка распознавания» для ОСВ (xlsx) в макете отсутствует (строки `doc-row--readonly`, `39`). Нужен ли отдельный экран проверки ОСВ (дерево строк, подсветка контрагентов) или проверка ОСВ только через сводную ОСВ (`30`)? | макет [39] | срез 2 (UI) | **закрыт решением владельца 05.10.2026:** экран проверки ОСВ **нужен** (дерево строк, суммы, нераспознанные контрагенты); в макете его нет — проектируется в подсрезе 2d «UI первичных документов» | | OQ-43 | Кнопка «К отчетности» неактивна у консолидаций в статусах «Инициация» и «Верификация РФ», активна у «Консолидация РФ» и «Верифицирован» (`34`). Каково правило доступности отчёта по статусу / роли? | макет [34] | срез 4, 7 (policy) | допущение: отчёт доступен с `CONSOLIDATION_RF` и далее для всех ролей с `CONSOLIDATION_VIEW`; «Инициация» — только карточка | @@ -105,7 +105,7 @@ | OQ-97 | Право `DIRECTORY_DELETE` по матрице §10 только у ADMIN: удалять алиасы и пустые ГСК/ГСЗ могут только администраторы, а не КА. Расширить право на KA_RF/KA_GO/CURATOR_GO? | §10; arm-core O-6 | срез 1 (seed) | **закрыт решением владельца 05.10.2026: право `DIRECTORY_DELETE` на КА не расширяется**, удаляют алиасы и пустые ГСК/ГСЗ только ADMIN; матрица §10 без изменений | | OQ-98 | Единый список кодов валют для строк `data_type = CURRENCY` («USD», «EUR» под контрагентом): достаточно ли ISO 4217-списка из `rules/data_types.yaml` (`currency_codes`: RUB, USD, EUR, CNY, KZT, BYN, GBP, CHF, JPY, TRY, AED)? Нужны ли нестандартные написания и коды, которые используют конфигурации 1С | tabular-parser: design D-4, R-6, R-22 | срез 2c (маппинг CURRENCY-строк), словарь `data_types.yaml` | допущение принято: ISO-перечень, дополняется строкой в YAML вместе с синтетикой; коды не входят в тест покрытия словарей (R-22) | | OQ-99 | Несколько ОСВ **одного счёта** в одной книге (разные листы: два периода или две организации): парсер выдаёт по одному `document` на лист (`locator = sheet:<имя>`) и ничего не дедуплицирует. Как ядро выбирает/сопоставляет такие документы с периодом и организацией? | tabular-parser: spec `osv-parsing` («Листы ОСВ находятся по содержимому») | срез 2c (маппинг, `document.period_check`) | допущение принято: каждый лист — отдельный документ, выбор — за ядром и аналитиком при проверке | -| OQ-100 | ИНН внутри **текста** строки контрагента («Полиграф, 3528315710», 20 строк в ОСВ 76 Счастливочной) и склеенная ячейка «ИНН/КПП»: извлекать ли ИНН в парсере или в ядре при резолве контрагента? Сейчас `counterparty_inn` берётся только из колонки ИНН (§1.3), `counterparty_name` = весь текст строки | tabular-parser: R-15, R-17 (design №25, №27) | срез 2c (резолв `counterparty_id` по ИНН/имени) | допущение принято: парсер не извлекает; ядро разбирает имя; при отрицательном решении — правило в парсере одной функцией | +| OQ-100 | ИНН внутри **текста** строки контрагента («Полиграф, 3528315710», 20 строк в ОСВ 76 Счастливочной) и склеенная ячейка «ИНН/КПП»: извлекать ли ИНН в парсере или в ядре при резолве контрагента? Сейчас `counterparty_inn` берётся только из колонки ИНН (§1.3), `counterparty_name` = весь текст строки | tabular-parser: R-15, R-17 (design №25, №27) | срез 2c (резолв `counterparty_id` по ИНН/имени) | **решено 07.10.2026 (владелец):** извлекать в парсере (ИНН → `counterparty_inn`, имя без ИНН) — RSHB/arm-tabular-parser#9; до реализации — как раньше | | OQ-101 | Числа с ведущим «+», «.5», «5.» парсер отвергает как нечисловой текст (`null` + предупреждение с номером строки). Встречаются ли такие записи в выгрузках других систем; принимать ли их | tabular-parser: T7.3 | срез 2a+, словарь/правила `numbers.py` | допущение принято: отвергаются; в реальных файлах не встречаются | | OQ-102 | Нужен **реальный xlsb из Excel**: единственный `fixtures/osv/synthetic/s13_no_outline_indent.xlsb` собран тестовым писателем BIFF12 и не доказывает чтение файлов, сохранённых Excel (стили, другая раскладка записей). Получить xlsb у аналитиков или пересохранить `s02` в Excel и заменить `s13` по инструкции в README fixtures | tabular-parser: R-9, O-7 | срез 2a (достоверность xlsb) | допущение принято: читаем `pyxlsb` без outline level и форматирования; проверка — на первом настоящем xlsb | | OQ-103 | Счета с буквой в **первом** сегменте кода (забалансовые «МЦ.04», «ГТД», «НЕ.01.1») не распознаются как ACCOUNT: регулярка требует цифр в первом сегменте, строка станет контрагентом/группировкой по позиции. Нужны ли такие ОСВ, как их кодировать в `chart_of_account` | tabular-parser: R-16 (design №26), `classify.py` | срез 2c (план счетов), парсер | допущение принято: не распознаются; в 66 реальных файлах не встречаются | @@ -115,7 +115,7 @@ | OQ-107 | При `hierarchy=sums` (дерево восстановлено по равенству «сумма детей = родитель») сверка «сумма детей = родитель» тавтологична — она не может не сойтись. Считать ли такой разбор `needs_check` (или отдельное предупреждение достаточно)? | tabular-parser: финальное ревью Среза 2a, M-5 | срез 2a+ (статус результата), срез 2c (UI проверки) | допущение принято: статус — по контрольным суммам, стратегия видна в `metrics.warnings` (`hierarchy=sums`); на реальных файлах стратегия не выбирается (все — `outline`/`trivial`) | | OQ-108 | Защита фазы чтения xlsx от XML-бомб (entity expansion) — `defusedxml` как новая зависимость сервиса — и иные лимиты памяти чтения. Остаточные риски после R-23/R-25: (1) бомба по объёму самих ячеек в пределах лимита распакованного zip — xlsx в 5 МБ с 2 млн обычных ячеек читается openpyxl 13 с и занимает 1,5 ГБ (замер ревьюера); (2) объём `sharedStrings` и стилей в тех же пределах; (3) entity expansion (от экспоненты «billion laughs» защищает только сам expat ≥ 2.4); (4) **известный обход предсканирования (R-26):** лист в части без `.xml` с ≥ 64 байтами ведущих пробелов и без XML-декларации не попадает в скан (файл 1,8 КБ → 27 с / 1,4 ГБ в openpyxl) — первый follow-up: убрать «sniff» и отдавать expat каждую некаталожную запись; (5) скан сам не ограничен размером чанка: «мусорная» XML-часть с атрибутом в 400 МБ → 1,3 ГБ / 1,2 с, 400 МБ тегов → 8,5 с CPU; (6) xls с очень многими крупными merged-диапазонами (> 10 000 ячеек каждый) — линейный просмотр индекса `Sheet` (≈ 4,4 мс на обращение), останавливает только `PARSE_TIMEOUT_SECONDS`: потолок на число таких диапазонов. Предскан — эшелон защиты, не граница безопасности; настоящая граница — лимит памяти контейнера / подпроцесс с RLIMIT. Рекомендация: жёсткий лимит памяти контейнера сервиса; и/или предпроверка объявленного `` и потоковый счётчик ячеек ``/строк `sharedStrings` (тем же expat-сканом, что объединения) с лимитом; и/или разбор книги в подпроцессе с `RLIMIT_AS`/таймаутом, который можно убить. Решение платформы | tabular-parser: финальное ревью Среза 2a, I-1 (хвост), R-23, R-25 | tabular-parser (readers), платформа (зависимости и лимиты образа/пода) | допущение принято: действуют лимиты R-23/R-25 — распакованный zip ≤ 10 × `PARSE_MAX_BYTES`, площадь объединённых ячеек книги ≤ 200 000 (потоковый expat-скан всех XML-частей до openpyxl); объём ячеек, `sharedStrings`/стили и entity expansion не ограничены (README «Известные ограничения») | | OQ-109 | Воркер tabular-parser скачивает объект S3 целиком и только потом проверяет `PARSE_MAX_BYTES`; проверять размер по `stat` до скачивания | tabular-parser: финальное ревью Среза 2a, M-7 | tabular-parser (`core/worker.py`) — вне Среза 2a | **закрыт реализацией Среза 2c (06.10.2026, `arm-core/openspec/changes/slice-2c-osv-ingest`):** ядро ограничивает размер файла на загрузке (`UPLOAD_MAX_BYTES` = 50 МиБ, как `PARSE_MAX_BYTES` парсера), поэтому в воркер большие объекты из ядра не попадают; проверка по `stat` до скачивания в `tabular-parser` остаётся как есть (дефенсивная, не требуется) | -| OQ-110 | Удаление файла после консолидаций: у `document` появятся FK из `financial_statement_meta`, `consolidation_document`, `ex_snapshot_item`, а `DELETE /files/{id}` Среза 2c их не чистит (в 2c строк в этих таблицах нет). ANL-020 запрещает физическое удаление после INITIATED. Что делать: запретить удаление файла, документ которого входит в версию консолидации, и/или каскадно чистить служебные таблицы (`financial_statement_meta` — в Срезе 3) | arm-core Срез 2c: Task 10 (R-23, design №36, №48) | Срезы 3, 4 (расширить удаление или запрет) | открыт: в 2c сделана только проверка `CONFIRMED`/`superseded_by`; без расширения удаление файла с документом в версии даст IntegrityError (500) | +| OQ-110 | Удаление файла после консолидаций: у `document` появятся FK из `financial_statement_meta`, `consolidation_document`, `ex_snapshot_item`, а `DELETE /files/{id}` Среза 2c их не чистит (в 2c строк в этих таблицах нет). ANL-020 запрещает физическое удаление после INITIATED. Что делать: запретить удаление файла, документ которого входит в версию консолидации, и/или каскадно чистить служебные таблицы (`financial_statement_meta` — в Срезе 3) | arm-core Срез 2c: Task 10 (R-23, design №36, №48) | Срезы 3, 4 (расширить удаление или запрет) | **закрыт для Среза 4 (06.10.2026):** привязываются к версии только `CONFIRMED` документы незаменённых файлов, а файл с `CONFIRMED` документом удалить нельзя (`FILE_HAS_CONFIRMED_DOCUMENTS`) — удаление файла с документом в версии невозможно, расширять `DELETE /files/{id}` не нужно; автозамена снимает привязки документов заменённого файла (`FILE_REPLACE.unbound_version_ids`). Остаток: FK из `financial_statement_meta` и `ex_snapshot_item` — Срезы 3 и 7 | | OQ-111 | Ретеншн S3-объектов при удалении файла: сейчас `DELETE` лишь ставит `s3_object.expires_at = now()`, объекты `uploads`/`results` физически не стираются (дополняет OQ-22). Кто и когда их удаляет — lifecycle-правило бакета по метаданным, фоновая задача ядра, ручная очистка? Объект, записанный в S3 до сбоя БД, остаётся сиротой | arm-core Срез 2c: design D-10, №36 | платформа (lifecycle), Срез 2 → эксплуатация | допущение принято: бакет растёт; очистка — вне среза | | OQ-112 | Остаточные объекты e2e-прогона: каждый запуск `make e2e` оставляет в MinIO dev-стенда ~2 объекта (`uploads/…`, `results/…` — следствие OQ-111) | arm-core Срез 2c: Task 11 (O-5) | dev-стенд | допущение принято: чистить бакет `arm-ka` вручную (раздел «Сброс стенда» `dev/qa-guide.md`) | | OQ-113 | Полнота плана счетов seed: 19 нормализованных кодов строк-счетов golden ОСВ отсутствуют в `chart_of_account` (`01.01`, `66.01`, `76.01`, `60.АВ`, `76.ОТ` и др.), поэтому у их строк `account_id = NULL`. Достаточно ли «администратор добавит» или seed должен включать полный набор субсчетов (включая буквенные) до Среза ВГО (§4–§5) | arm-core Срез 2c: Task 8 (O-3) | Срезы 4–5 (пары счетов элиминации) | открыт: допущение — `account_id = NULL` легален (§1.3), `account_code`/`account_root` заполнены | @@ -123,6 +123,19 @@ | OQ-115 | Автозамена: если новый файл содержит лишь часть документов прежнего (прежний — ОСВ 60 и 51, новый — только 60), прежний уходит в `SUPERSEDED` **целиком**, документ 51 пропадает из списка файлов; в `FILE_REPLACE.description` о нём ничего нет. Подтвердить «замена файлом целиком» (OQ-06) для этого случая или заменять по документам | arm-core Срез 2c: Task 8 (T8.4) | Срез 2d (список файлов), Срез 4 (комплект документов) | допущение принято: замена файлом целиком, документы заменённого файла сохраняются в БД | | OQ-116 | ОСВ Q2–Q4, признанная нарастающей по данным Q1 (сальдо на начало корня равно сальдо Q1, §1.5), получает предупреждение маппинга и поэтому остаётся `NEEDS_CHECK`, а не `RECOGNIZED` (буквально D-7/D-9 спеки: `RECOGNIZED` только без предупреждений). Верно ли, что аналитик подтверждает такие документы вручную, или нарастающая по данным — штатный случай без предупреждения (OQ-02, OQ-05) | arm-core Срез 2c: Task 7 (R-17, design №30) | Срез 2d (нагрузка на аналитика), §9.3 | допущение принято: `NEEDS_CHECK`; снять предупреждение — одна правка в `result_mapping.py` и правило статуса | | OQ-117 | Индексы `osv_entry(parent_entry_id)`, `osv_entry(recognition_job_id)`, `stored_file(current_recognition_job_id)`: `DELETE /files/{id}` (и очистка файла при повторном маппинге) делает seq scan на проверке self-FK — на временной таблице 200 тыс. строк удаление документа в 1000 строк 8,6 с без индекса и 16 мс с индексом (замер в psql на временной таблице, final review); при ~3 млн строк DELETE ≈ 2 мин → HTTP-таймаут и долгое удержание блокировок. Требуется DBML v5.16 + миграция 0004. Решает владелец (источник истины — DBML) | arm-core Срез 2c: final review (I-1, R-24, design №51) | `DELETE /files/{id}`, повторный маппинг; подсрез 2d (UI удаления), прод | **закрыт в v5.15 (06.10.2026):** индексы добавлены в DBML v5.15 (п. 4 changelog) и миграцию `0003_dbml_v5_15` arm-core — `ix_osv_entry_parent_entry_id`, `ix_osv_entry_recognition_job_id`, `ix_stored_file_current_recognition_job_id`; v5.16 и миграция 0004 не понадобились (v5.15 ещё не слит) | +| OQ-118 | Запрет загрузки и подтверждения документов после перехода версии в `DATA_CORRECTION` (ТЗ 03_9 A1: «после перехода к исправлению данных загрузка и подтверждение заблокированы»). В Срезе 4 загрузка и подтверждение блокировкой не ограничиваются и в `DATA_CORRECTION` разрешены | решение владельца 06.10.2026; arm-core Срез 4 design D-6 | Срез 7 (маршрут) | допущение принято: в Срезе 4 запрета нет (только статус «в работе»); запрет вводится вместе с маршрутом | +| OQ-119 | Истечение блокировки «взять в работу»: DBML не задаёт срока, перехват — только вручную с причиной. Нужен ли авто-сброс брошенной блокировки (таймаут, «сессия умерла») | arm-core Срез 4 design D-7 | Срез 4, 7 (эксплуатация) | допущение принято: блокировка не истекает; освободить — `DELETE …/lock` владельцем или перехват | +| OQ-120 | `document.period_check` для документов, входящих в версии консолидации: подходящесть документа версии — функция (документ, версия), а один документ может входить в несколько версий (разные периметры и кварталы), поэтому записывать её в `document.period_check` нельзя. Показывать ли «подходит версиям: …» на карточке файла/документа | arm-core Срез 4 design D-12 | карточка файла (подсрез 2d), §9.3 | допущение принято: `period_check` остаётся `UNKNOWN`; подходящесть видна в чек-листе версии | +| OQ-121 | Порог размера сводной ОСВ, после которого нужен кэш процесса: свод строится целиком в памяти на каждый запрос (страница, сводка, экспорт); синтетический тест (5,4 тыс. исходных строк → 1,8 тыс. строк свода) — 0,27 с / 8 запросов; на фикстурах РБ — 1385 строк свода (22 документа). Замер на реальной группе не делался; при десятках тысяч строк может потребоваться кэш (DECISION-5 запрещает кэш в БД) | arm-core Срез 4 design D-1, Risks | эксплуатация, Срез 5 (шахматка поверх свода) | допущение принято: без кэша; замер на реальной группе — до Среза 5 | +| OQ-122 | Ячейка чек-листа `PROCESSING` по всем недостающим столбцам ЮЛ, пока файл не смаплен: до маппинга счёт файла неизвестен, поэтому «в обработке» показывается у всех незакрытых правил ЮЛ. Устраивает ли такая неточность или нужна эвристика по имени файла | arm-core Срез 4 design D-13, №6 | чек-лист, экраны `34`–`37` | допущение принято: временный шум в чек-листе | +| OQ-123 | Общие строки `osv_entry` у нескольких версий: документ может входить в несколько версий одной ГК, ручное сопоставление контрагента хранится на строке `osv_entry` (DECISION-5) и поэтому меняет её для всех версий. Допустимо ли, или сопоставление должно быть «по версии» | arm-core Срез 4 design Risks, D-10 | Срез 4, 7 (замороженные версии не меняются — они в `ex_snapshot`) | допущение принято: сопоставление общее; `before_value` аудита хранит прежнее значение | +| OQ-124 | Q1-эквивалентность ОСВ (R-14, design №81): парсер помечает ОСВ за Q1 как нарастающую (период с начала года = Q1), поэтому документ ОСВ за `Q1` в Срезе 4 равнозначен и нарастающему, и квартальному и подходит версиям любого квартала года (в версиях Q2+ он — квартальный источник Q1). ББ/ОФР за Q1 версиям Q2–Q4 **не** подходят (R-14a: баланс на конец Q1). Подтвердить; если ББ/ОФР за Q1 тоже должны подхватываться — снять условие в `consolidation_periods.effective_cumulative` | arm-core Срез 4: Task 13, R-14/R-14a, design №81 | `fits` (привязка), `select_sources` (свод), чек-лист | **закрыт 07.10.2026 (владелец):** ОСВ за Q1 используется и как нарастающая, и как квартальная (первое звено цепочки для версий Q2–Q4); ББ/ОФР за Q1 — только версии Q1 (R-14a) | +| OQ-125 | Версия за Q1 с квартальным ОСВ Q1: spec говорит «квартальный Q1 равнозначен нарастающему», но по design №50 квартальный Q1 идёт путём склейки (дубли ключа суммируются, `None` → 0, `is_cumulative = false`), а нарастающий Q1 отдаётся строка в строку (`None` сохраняется, дубли остаются раздельными). Различие видно только на дублях ключа и пустых ячейках. Приводить ли обе формы к одной | arm-core Срез 4: Task 9 (T9.b), design №50 | `consolidated_osv.build`, версии за Q1 | допущение принято: два пути; расхождение — только на дубле ключа внутри квартала и `None` в ячейках | +| OQ-126 | Область ручного сопоставления контрагента (R-11, design №62/№63): «строка версии» сужена до строк свода — `group_type = COUNTERPARTY`, `data_type` BU/SUM привязанных ОСВ-документов; строки `NU`/`ACCOUNT` дают 404 `NOT_FOUND` («строка ОСВ не найдена в версии») и в автоприменение не входят (§3 п. 2, R-3 Среза 2c: НУ не участвует). Цена: NU-строки того же контрагента остаются без `counterparty_id`. Буквальное чтение spec шире. Подтвердить | arm-core Срез 4: Task 11, R-11 | `counterparty_mapping`, автоприменение | допущение принято и реализовано | +| OQ-127 | Рекомендация кандидата (`recommended`) — только точное совпадение `normalize_alias` имени строки с нормализованным именем/алиасом ЮЛ. На реальных данных «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» (так записан контрагент в ОСВ РБ) рекомендации не даёт: аналитик находит ЮЛ поиском по `q`. Нужна ли нечёткая рекомендация (токены, порядок слов, префикс «Юг-Финанс /») или достаточно ручного выбора и накопления алиасов | arm-core Срез 4: Task 13 (O-3), design D-11 | `GET …/counterparty-candidates` | допущение принято: рекомендация точная; после первого ручного сопоставления алиас закрепляет имя | +| OQ-128 | Неоднозначность дорезолва на лету при тёзке по имени: после ручного сопоставления создаётся алиас, но `CounterpartyIndex.resolve` на шаге «нормализованное имя/алиас» возвращает контрагента только при единственном кандидате — если ЮЛ-тёзка (по нормализованному `name`) есть рядом с владельцем нового алиаса, кандидатов двое, резолв неоднозначен и строки без сохранённого `counterparty_id` остаются несопоставленными. ТЗ DOC-221278: «алиас однозначен» — решает ли это, или алиас должен иметь приоритет над именем | arm-core Срез 4: Task 11 (T11.9) | дорезолв свода (D-3), `CounterpartyIndex` | открыт: допущение — резолв по Срезу 2c (неоднозначность → `NULL`); спасает автоприменение `PROPAGATED` (сохраняет `counterparty_id`) | +| OQ-129 | Формулировка сценария «сопоставление и автоприменение» (spec consolidated-osv): «`unmapped_total` падает на 1 + propagated» неверна на реальных данных. `propagated` считает записи `osv_entry` (включая пары-пробелы и строки, не дающие строк свода), а свод склеивает кварталы, поэтому `unmapped_total` падает на число **склеенных** строк свода (R-15, design №80). Уточнить текст spec при архивации | arm-core Срез 4: Task 13 (T13.M1, R-15) | spec `consolidated-osv` при архивации change | допущение принято: тест сверяет обе величины отдельно; текст spec уточнён в change, `openspec/specs/` — при архивации | +| OQ-130 | ОСВ произвольного формата: ИНН в ячейке **на строку выше** названия контрагента (строка только с ИНН, без сумм). Сейчас такая строка уходит в OTHER, ИНН для резолва теряется. Решение владельца 07.10.2026: склеивать со следующей строкой контрагента; синтетические фикстуры на этот случай, на ИНН в тексте и на «ИНН/КПП» в одной ячейке | владелец, 07.10.2026 | tabular-parser §1.0 п. 4, §1.3; резолв контрагента в ядре | **решено:** RSHB/arm-tabular-parser#9 (bounded), ждёт реализации | Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения; `DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4. diff --git a/context/09-delivery-plan.md b/context/09-delivery-plan.md index 2120f00..72f318a 100644 --- a/context/09-delivery-plan.md +++ b/context/09-delivery-plan.md @@ -170,13 +170,41 @@ Change'и: `arm-core/openspec/changes/slice-2c-osv-ingest` (issue arm-core#14, ## Срез 4 — Консолидация: карточка, версии, периметр, периоды, сводная ОСВ -- `consolidation` / `consolidation_version` / `consolidation_member` / `consolidation_document`; статусы INITIATED → - DOCUMENTS_PROCESSING → DATA_CORRECTION; `consolidation_lock`. -- Приведение квартальных ОСВ к нарастающим (§2) как вычисляемое представление. -- Сводная ОСВ (§3) как запрос; признак контрагента; ручное сопоставление с алиасами и автоприменением. -- UI: список консолидаций, карточка, состав, сводная ОСВ с фильтрами и экспортом xlsx. -- Тест: сводная ОСВ группы РБ за 9 мес. 2024: строки «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» → ГСК после одного ручного - сопоставления. +**Статус: реализован в arm-core (бэкенд + API + экспорт xlsx), октябрь 2026; UI — отдельный исполнитель.** Объём по решению +владельца 06.10.2026: бэкенд, API, экспорт xlsx. Issue arm-core#18, docs#19; спецификация — +`arm-core/openspec/changes/slice-4-consolidation/` (design D-1…D-16, «Decisions made without discussion» №1–№85), +документный change — `docs/openspec/changes/slice-4-consolidation/`. + +**Сделано (по факту):** +- DBML v5.16: `osv_entry.counterparty_match_kind` + enum `counterparty_match_kind` (миграция `0004_dbml_v5_16`, backfill). +- Версии консолидации (создание, список по ГК, карточка), периметр (`PUT …/members`), блокировка «взять в работу» + (перехват с причиной, без истечения), статусы: автоматические по событиям документов (INITIATED → DOCUMENTS_PROCESSING → + DATA_CORRECTION) и ручные (`CANCELLED`, возврат в `INITIATED`); остальные — Срез 7. +- Явная привязка подтверждённых документов к версии (`consolidation_document`): подтверждение, подхват при создании версии и + добавлении ЮЛ, отвязка при автозамене файла и удалении ЮЛ; Q1-эквивалентность ОСВ (R-14, OQ-124). +- **Чек-лист** комплекта документов (перенесён из Среза 3: `required_document_rule × периметр`, статусы ячеек) — API; CSV не делали. +- Сводная ОСВ как чистая функция от версии: выбор источников и склейка кварталов (§2), пробелы, дорезолв контрагента, признак + `GSK/GK/EXTERNAL`, способ сопоставления; страница, сводка, фильтры. +- Кандидаты и ручное сопоставление контрагента с алиасом, переносом алиаса и автоприменением (`PROPAGATED`). +- Экспорт xlsx (лист «Сводная ОСВ» и лист «Пробелы»). +- Сквозной тест на фикстурах РБ: ОСВ 60/62 РБ и 60/62/76 Счастливочной за Q1–Q3 2024 → версия Q3 → свод 1385 строк, 22 + документа привязано; пары с одним лишь Q3 (РБ: 01, 58.03, 66, 67, 76; Счастливочная: 01, 02) — в `gaps` с пропуском Q1–Q2; + одно ручное сопоставление «Пекарня Счастливочная ООО» → ГСК. +- Контракт API для фронта — `dev/api-contract-slice-4.md`; QA — `dev/qa-guide.md` §7. + +**Закрыты OQ:** 03, 19, 40 (допущение реализовано), 110 (для Среза 4). **Открыты новые:** OQ-118…OQ-129 (запрет загрузки после +DATA_CORRECTION, истечение блокировки, `period_check`, порог кэша свода, `PROCESSING` в чек-листе, общие строки `osv_entry` +у версий, Q1-эквивалентность, склейка Q1 версии Q1, область ручного сопоставления, рекомендация кандидата, неоднозначность +дорезолва, формулировка сценария «1 + propagated»). + +**Остаётся (UI и последующие срезы):** экраны `34`–`37`, `42`, `30`, `32-*`, `31-00` (другой разработчик); CSV чек-листа; +«Добавить версию», `CONFIRM`, `EXTENDED`, гудвилл, маршрут согласования (Срезы 6–7); ББ/ОФР в своде и чек-лист ББ/ОФР по +реальным документам — Срез 3; шахматка и ВГО — Срез 5. + +**Исходный план среза (для справки):** `consolidation` / `consolidation_version` / `consolidation_member` / +`consolidation_document`; приведение квартальных ОСВ (§2); сводная ОСВ (§3) как запрос; признак контрагента; ручное +сопоставление с алиасами и автоприменением; UI со списком, карточкой, составом, сводной ОСВ с фильтрами и экспортом; +тест: строки «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» → ГСК после одного ручного сопоставления. ## Срез 5 — ВГО: автоматическое сопоставление, шахматка, корректировки diff --git a/context/10-user-journey.md b/context/10-user-journey.md index ea19e60..baedf10 100644 --- a/context/10-user-journey.md +++ b/context/10-user-journey.md @@ -72,12 +72,12 @@ | Шаг | Экран | Действие | БД / статус | |---|---|---|---| -| 2.1 | Сайдбар → **Новая консолидация** [42] | ГК → ГСК (активна после ГК) → Год → Квартал → **Создать**. `scenario = CREATE`, `approach = BASIC`, `goodwill_enabled = false` по умолчанию (OQ-27) | `consolidation`, `consolidation_version(INITIATED)`, `consolidation_member` из текущего состава ГСК | -| 2.2 | Отчётность → Группы компаний [03] → **Консолидации →** [34] | [03]: таблица ГК (Группа компаний, CRM ID, Основной РФ, Организаций), поиск по ГК, CRM ID или РФ. [34]: таблица консолидаций ГК: Год, Квартал, Версия (дерево версий под периодом), Статус (бейдж), ГСК, Дата создания, Дата верификации, Организаций; кнопки в строке **Взять в работу**, **К отчетности** (неактивна у статусов «Инициация» и «Верификация РФ» — по снимку, OQ-43), меню «…»; чекбокс «Только в работе». Выбор строки открывает панель **Организации** (Ответственный / Утверждён в РФ / Верифицирован в ГО; таблица Наименование, Документы, ОФР, Бух. баланс, ОСВ — перечень счетов, ИНН, Тип) | `consolidation_version`, `consolidation_lock`, `consolidation_member`, `document` | -| 2.3 | [34] → **Взять в работу** [35] | «Консолидация … будет закреплена за вами для редактирования» | `consolidation_lock`, `audit_event(CONSOLIDATION_LOCK)` | -| 2.4 | [34] панель Организации → **Редактировать состав** [36] | фильтр по ГСК, «Показать все ЮЛ в ГК», чекбоксы, Сохранить | `consolidation_member` | -| 2.5 | строка организации → **Документы** → drawer «Первичные документы» [39] | список: документ (имя, формат, дата; подпись типа у прочих — «Прочий документ», «Бухгалтерская отчетность»), Период («2024 К1», «2025 · 9 мес.»), Проверка (✓ / ✗ / — Период, Контрагент), Статус (Загружен / Обработка / Требуется проверка / Ошибка); иконки: комментарий (поповер «Комментарий к документу …», [39-01]) и удалить; **Редактировать документы** — в снимке неактивна; **Загрузить документы** [40]: модалка с зоной «Перетащите файлы сюда» / **Выбрать документы**, Загрузить; **тип документа и период в модалке не выбираются** — определяются при распознавании | `stored_file(UPLOADED)`, `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 | `required_document_rule × consolidation_member − document(CONFIRMED)` | +| 2.1 | Сайдбар → **Новая консолидация** [42] | ГК → ГСК (активна после ГК) → Год → Квартал → **Создать**. `scenario = CREATE`, `approach = BASIC`, `goodwill_enabled = false` по умолчанию (OQ-27). **API (Срез 4):** `POST /consolidations` (`{company_group_id, related_group_id, year, quarter}`; ≥ 2 члена ГСК), ГСК для выбора — справочники | `consolidation`, `consolidation_version(INITIATED)`, `consolidation_member` из текущего состава ГСК | +| 2.2 | Отчётность → Группы компаний [03] → **Консолидации →** [34] | [03]: таблица ГК (Группа компаний, CRM ID, Основной РФ, Организаций), поиск по ГК, CRM ID или РФ. [34]: таблица консолидаций ГК: Год, Квартал, Версия (дерево версий под периодом), Статус (бейдж), ГСК, Дата создания, Дата верификации, Организаций; кнопки в строке **Взять в работу**, **К отчетности** (неактивна у статусов «Инициация» и «Верификация РФ» — по снимку, OQ-43), меню «…»; чекбокс «Только в работе». Выбор строки открывает панель **Организации** (Ответственный / Утверждён в РФ / Верифицирован в ГО; таблица Наименование, Документы, ОФР, Бух. баланс, ОСВ — перечень счетов, ИНН, Тип). **API:** `GET /company-groups/{id}/consolidations?in_work_only=` (периоды и версии, блокировка), `GET /consolidations/versions/{id}` (карточка: члены с ячейками чек-листа, история) | `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.6 | [34] → **Скачать чек-лист** [37] | матрица Организация × (Бух. баланс, ОФР, ОСВ 01 58 60 62 66 67 76 90, Прочие); легенда: Загружен / Обработка / Требуется проверка / Ошибка загрузки / Отсутствует; Скачать CSV. **API:** `GET …/versions/{id}/checklist` (`rules[]`, `rows[].cells[]`: `status`, `missing_quarters`, `document_ids`); CSV в Срезе 4 нет | `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` | Монозаёмщики [60] [84]: тот же drawer документов без консолидации — первичка в ЕХ напрямую. @@ -90,7 +90,7 @@ | Шаг | Вкладка | Действие | БД | |---|---|---|---| -| 3.1 | **ОСВ** [30] | сводная ОСВ: Компания ГК, Счёт, Период, Контрагенты, ИНН, 6 сумм, Тип контрагента. Баннер «Требуется указать соответствия… Осталось: N» → **Показать ЮЛ без ИНН** [32-osv-no-inn] → **Указать** → модалка «Выбор организации для связи с …» [32-osv-map-counterparty]: ИНН, Организация, ГК, ГСК, ГСЗ, блок «Рекомендуемые», **Связать**. При уходе с вкладки с несопоставленными — «Остались неуказанные ИНН… Остаться / Перейти» [31-00] | вычисляется; `osv_entry.counterparty_id`, `legal_entity_alias`, `audit_event(OSV_COUNTERPARTY_MAP)` | +| 3.1 | **ОСВ** [30] | сводная ОСВ: Компания ГК, Счёт, Период, Контрагенты, ИНН, 6 сумм, Тип контрагента. Баннер «Требуется указать соответствия… Осталось: N» → **Показать ЮЛ без ИНН** [32-osv-no-inn] → **Указать** → модалка «Выбор организации для связи с …» [32-osv-map-counterparty]: ИНН, Организация, ГК, ГСК, ГСЗ, блок «Рекомендуемые», **Связать**. При уходе с вкладки с несопоставленными — «Остались неуказанные ИНН… Остаться / Перейти» [31-00]. **API:** `GET …/versions/{id}/osv` (фильтры, `limit` ≤ 2000), `…/osv/summary` (баннер «Осталось: N» — `unmapped_total`, пробелы — `gaps[]`), `…/counterparty-candidates?scope=&q=&entry_id=` (блок «Рекомендуемые» — `recommended`), `POST …/osv/entries/{entry_id}/counterparty` (**Связать**), `GET …/osv/export` (**Экспорт**, xlsx) | вычисляется; `osv_entry.counterparty_id`, `legal_entity_alias`, `audit_event(OSV_COUNTERPARTY_MAP)` | | 3.2 | **Шахматка** [31-01] | фильтры Год, Период, Компании, Дт-счета, Кт-счета; матрица компания × компания: сумма A / сумма B / (расхождение) красным; Итого; ниже результирующая таблица «Статья × компания» | вычисляется (§5–6 правил) | | 3.3 | **Таблица корректировки** [31-02] [31-02-02] | фильтр «Отклонение» (Все); пары «Актив баланса ЮЛ-1 / Пассив баланса ЮЛ-2»: Наименование, Строка баланса, Счёт, Сумма; **Контроль**: % отклонения, Сумма отклонения; **Корректировка**: Строка баланса / Дт, Строка баланса / Кт, Комментарий; строка пары раскрывается в детализацию по субсчетам; Итого. В макете % отклонения считается к сумме стороны ЮЛ-1, а не к меньшей (OQ-35). **Редактировать** [31-02-01] → баннер «Режим редактирования включён…», ячейки пары и корректировки становятся полями ввода, **Отменить изменения** / **Сохранить** | `correction` (+ дочерние строки `parent_correction_id`), `audit_event(CORRECTION_*)`; ≥ 5 % → `ADJUSTMENT` | | 3.4 | **Гудвилл** [31-03] | здесь же включается/выключается расчёт гудвилла для консолидации (`goodwill_enabled`; переключателя в снимках нет — решение OQ-27); фильтр «Гудвилл: Все значения»; по головной организации (с «Выделить все» и строкой «Всего»): Учёт в отчёте ☐, Наименование дочерней организации, Доля владения, Фондовые вложения головной организации, Доля участия головной организации в уставном капитале дочерней организации, Уставный капитал, Гудвилл (+/−), Комментарий; **Редактировать** [31-03-01] → «Доступны для редактирования столбцы „Доля владения“ и „Комментарий“» | `consolidation_version.goodwill_enabled`, `goodwill(ownership_share, include_in_kfo, comment)` | diff --git a/dev/api-contract-slice-4.md b/dev/api-contract-slice-4.md new file mode 100644 index 0000000..f466a21 --- /dev/null +++ b/dev/api-contract-slice-4.md @@ -0,0 +1,150 @@ +# Контракт API Среза 4 (консолидация) для разработчика фронта + +Источник — реальная схема `GET /openapi.json` arm-core (ветка `feat/18-slice-4-consolidation`, Swagger — `/docs`); коды ошибок и +нюансы сверены с кодом и спекой `arm-core/openspec/changes/slice-4-consolidation/` (design D-1…D-16, решения №1–№81+). +Правила предметной области — `context/05-domain-rules.md` §2, §3, §9.1, §10; экраны — `context/10-user-journey.md` +(пути 2 и 3), номера в `[…]` — снимки макета (`mockup/INDEX.md`). Здесь — только то, что нужно, чтобы писать клиент. + +## 1. Общее + +- Префикс `/api/v1`, заголовок `Authorization: Bearer ` (в dev — `POST /dev/token`). Права: `CONSOLIDATION_VIEW` (чтение), + `CONSOLIDATION_EDIT` (правки); загрузка файлов — `DOCUMENT_EDIT`. Нет права — 403 строкой + `{"detail": "недостаточно прав: требуется <КОД>"}`; нет/плохой токен — 401. Скрывать кнопки по праву можно по `GET /me`. +- Доменные ошибки: `{"detail": {"code": "...", "message": "на русском", ...доп. поля}}`; по `code` работает клиент. 422 валидации + тела/параметров — массив pydantic (`detail: [{loc, msg, type}]`). Каталог кодов — `arm-core/README.md`, «Ошибки API». +- Видимость: ГК и версии вне области видимости пользователя (`ka_rf`/`rka`/`client_manager` — только свой филиал) неотличимы от + несуществующих: **404 с кодом `NOT_FOUND`**. Различие — только по тексту `message` (см. §4); клиент не должен ветвиться по тексту. +- **Деньги — строки JSON с двумя знаками** (`"100.00"`, `"0.00"`), могут быть `null` (нарастающий документ отдаётся «как в файле»). + Не парсить во `float`: суммы `numeric(18,2)`. +- Идентификаторы — uuid-строки; даты — ISO; `quarter` — `Q1`…`Q4`. +- Страницы — `{items, total, limit, offset}`; `limit ≤ 2000` (по умолчанию 500), `offset ≥ 0`; больше 2000 — 422. +- Перечисления: `ConsolidationStatus` — `INITIATED`, `DOCUMENTS_PROCESSING`, `DATA_CORRECTION`, `CONSOLIDATION_RF`, `ADJUSTMENT`, + `VERIFICATION_RF`, `CONSOLIDATION_GO`, `VERIFICATION_GO`, `VERIFIED`, `CANCELLED`, `REJECTED`, `ARCHIVED` (в Срезе 4 достижимы + первые три и `CANCELLED`); `CounterpartyType` — `GSK`, `GK`, `EXTERNAL`; `CounterpartyMatchKind` — `INN`, `NAME`, `ALIAS`, + `MANUAL`, `PROPAGATED`; `CellStatus` — `CONFIRMED`, `NEEDS_CHECK`, `RECOGNIZED`, `PROCESSING`, `ERROR`, `MISSING`; + `ConsolidationScenario` — `CREATE`, `CONFIRM`; `ConsolidationApproach` — `BASIC`, `EXTENDED`. +- «В работе» — `INITIATED`, `DOCUMENTS_PROCESSING`, `DATA_CORRECTION` (`VersionCard.is_in_work`). + +## 2. Эндпоинты + +Порядок проверок на каждом маршруте: право (403) → видимость (404) → для правок статус «в работе» и своя блокировка (409) → операция. +В таблице «Ошибки» — доменные коды и HTTP; 401/403 везде и ниже не повторяются. + +| Метод и путь | Право | Запрос | Ответ | Ошибки | Экран | +|---|---|---|---|---|---| +| `POST /consolidations` | EDIT | `ConsolidationCreate {company_group_id, related_group_id, year (2000–2100), quarter}` | 201 `VersionCard` (версия №1, `INITIATED`, `lock = null`) | 404 `NOT_FOUND` (ГК/ГСК не видна), 409 `CONSOLIDATION_EXISTS`, 422 `RELATED_GROUP_MISMATCH`, 422 `PERIMETER_TOO_SMALL` | [42] | +| `GET /company-groups/{group_id}/consolidations` | VIEW | query `in_work_only` (bool, по умолчанию `false`) | `ConsolidationPeriodOut[]` (период + `versions[]` — `VersionBrief`) | 404 `NOT_FOUND` | [34] | +| `GET /consolidations/versions/{version_id}` | VIEW | — | `VersionCard` | 404 `NOT_FOUND` | [34] панель «Организации», [36], [37] | +| `POST /consolidations/versions/{version_id}/lock` | EDIT | тело необязательно: `LockIn {takeover_reason?}` (≤ 1000) | 201 `LockOut` (новая блокировка или перехват) / 200 `LockOut` (уже ваша — идемпотентно) | 404 `NOT_FOUND`, 409 `VERSION_NOT_EDITABLE` (версия `CANCELLED`), 409 `LOCKED_BY_OTHER`, 422 (pydantic-массив) при `takeover_reason` длиннее 1000 символов | [35] | +| `DELETE /consolidations/versions/{version_id}/lock` | EDIT | — | 204 (в т. ч. если активной блокировки нет) | 404 `NOT_FOUND`, 409 `NOT_LOCK_OWNER` | [34] | +| `POST /consolidations/versions/{version_id}/status` | EDIT | `StatusIn {to_status, comment?}` | 200 `VersionCard` | 404, 409 `VERSION_NOT_EDITABLE` / `LOCK_REQUIRED` / `LOCKED_BY_OTHER` / `TRANSITION_NOT_ALLOWED`, 422 `COMMENT_REQUIRED` | [33-01], меню «…» [34] | +| `PUT /consolidations/versions/{version_id}/members` | EDIT | `MembersIn {legal_entity_ids[]}` | 200 `VersionCard` | 404, 409 `VERSION_NOT_EDITABLE` / `LOCK_REQUIRED` / `LOCKED_BY_OTHER`, 422 `ENTITY_NOT_IN_GROUP` (`legal_entity_ids`), 422 `PERIMETER_TOO_SMALL` | [36] | +| `GET /consolidations/versions/{version_id}/checklist` | VIEW | — | `ChecklistOut {rules[], rows[]}` | 404 | [37] | +| `GET /consolidations/versions/{version_id}/osv` | VIEW | query: `limit`, `offset`, `legal_entity_id`, `account_root`, `counterparty_type`, `unmapped` (bool), `match_kind` (повторяемый), `q` | 200 `Page[ConsolidatedRowOut]` | 404 | [30] | +| `GET /consolidations/versions/{version_id}/osv/summary` | VIEW | — | `OsvSummaryOut {rows_total, unmapped_total, auto_matched_total, documents_total, gaps[]}` | 404 | [30], баннер «Осталось: N», [31-00] | +| `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 /documents/{id}/confirm` (Срез 2c) привязывает подтверждённый документ ко всем +версиям «в работе» своей ГК и может перевести версию в `DATA_CORRECTION`; `DELETE /files/{id}` и автозамена — снимают привязки +(см. `context/05-domain-rules.md` §9.5). + +## 3. Схемы + +`Ref {id, name}`. Поля без `?` присутствуют всегда. + +**`ConsolidationPeriodOut`** `{consolidation_id, year, quarter, reporting_date, versions[]}`. +**`VersionBrief`** `{id, version_no, status, scenario, approach, lock (LockOut | null), created_at, members_count}`. +**`LockOut`** `{locked_by, locked_by_name, locked_at, taken_over_from (uuid | null), takeover_reason (string | null)}`. +**`VersionCard`** = `VersionBrief` + `{consolidation_id, year, quarter, reporting_date, company_group (Ref), initiator (Ref), +initiator_office (Ref), goodwill_enabled, is_in_work, previous_version_id, members[], history[]}`: +- `members[]` — `{id, name, inn, is_head_entity, checklist[]}`, где `checklist[]` — ячейки по правилам комплекта (тот же порядок, + что `ChecklistOut.rules`); +- `history[]` — `{from_status (null у первой записи), to_status, changed_by, changed_at, comment}`. + +**`ChecklistOut`** `{rules[], rows[]}`; `rules[]` — `{document_type (BALANCE/INCOME/OSV…), account (string | null — счёт ОСВ), +is_mandatory}` в порядке: ББ, ОФР, ОСВ по коду счёта (натурально: `58 < 58.03 < 60`); `rows[]` — `{legal_entity_id, +legal_entity_name, legal_entity_inn, cells[]}`, порядок `cells` = порядок `rules`. **`ChecklistCellOut`** `{status (CellStatus), +missing_quarters[], document_ids[]}`: +`CONFIRMED` — пара полностью покрыта привязанными подтверждёнными документами; `NEEDS_CHECK` / `RECOGNIZED` — есть +неподтверждённый подходящий документ; `PROCESSING` — у ЮЛ есть файл в обработке (по **всем** недостающим столбцам ЮЛ, пока файл +не смаплен — это известная неточность, OQ-122); `ERROR` — последний файл ЮЛ в `ERROR`; `MISSING` — нет ничего. +`missing_quarters` и `document_ids` — при частичном покрытии квартальными ОСВ (`document_ids` — только привязанные документы). +ОСВ по субсчёту (58.03) закрывает только своё правило, не «ОСВ 58». + +**`ConsolidatedRowOut`** — строка свода: +`{entry_ids[], legal_entity_id, legal_entity_name, legal_entity_inn, account_code, account_root, period ("<год> Q"), +is_cumulative, row_number, counterparty_name, counterparty_inn, counterparty_id, counterparty_entity_name, counterparty_type, +match_kind, match_stored, opening_debit, opening_credit, turnover_debit, turnover_credit, closing_debit, closing_credit, warnings[]}`. +`counterparty_name/inn` — как в файле ОСВ; `counterparty_id`/`counterparty_entity_name` — сопоставленное ЮЛ справочника или `null`; +`match_stored = true` — сопоставление хранится в БД, `false` — вычислено на лету (в БД не пишется); `match_kind` — способ +(`null`, если ЮЛ не найдено). Для склеенных строк `entry_ids` — записи по кварталам (порядок — от раннего к позднему). +**`OsvSummaryOut`** `{rows_total, unmapped_total, auto_matched_total, documents_total, gaps[]}`: `rows_total` — число строк свода (без фильтров); `unmapped_total` — число строк свода с пустым `counterparty_id` **после** дорезолва на лету (то же, что `GET …/osv?unmapped=true` даёт в `total`; `unmapped=false` — только сопоставленные); `auto_matched_total` — число строк с `match_kind` `NAME` или `ALIAS` независимо от `match_stored` (`INN`, `MANUAL`, `PROPAGATED` не входят); `documents_total` — число ОСВ-документов, привязанных к версии; **`gaps[]`** — +`{legal_entity_id, legal_entity_name, account_code, missing_quarters[]}` (пара ЮЛ × счёт, у которой нет ОСВ за какие-то кварталы; +строк такой пары в своде нет). **`CandidateOut`** `{id, inn, name, company_group (Ref | null), gsk (Ref | null), gsz (Ref | null), +counterparty_type, recommended}`. **`MapCounterpartyOut`** `{row (ConsolidatedRowOut | null), propagated, alias_created, +alias_moved_from (uuid | null)}`. + +## 4. Нюансы, которые легко пропустить + +1. **Блокировка — не «мягкая».** Любая правка версии (`PUT …/members`, `POST …/status`, ручное сопоставление) требует статуса «в + работе» **и** вашей активной блокировки; нет блокировки — 409 `LOCK_REQUIRED`, чужая — 409 `LOCKED_BY_OTHER`. Загрузка файлов и + подтверждение документов блокировкой не ограничиваются. Блокировка **не истекает по времени**; `CANCELLED` её снимает. +2. **`LOCKED_BY_OTHER` приходит в двух формах.** Из `POST …/lock` — с `locked_by`, `locked_by_name`, `locked_at`. Из правок + (`PUT …/members`, `POST …/status`, сопоставление, т. е. из `ensure_version_editable`) — только `locked_by`, `locked_at`, **без + `locked_by_name`** (доменная функция имени не знает; имя — из `lock.locked_by_name` карточки версии). `NOT_LOCK_OWNER` отдаёт + `locked_by` и `locked_at` владельца. +3. **Перехват.** `POST …/lock` с непустым `takeover_reason` при чужой блокировке снимает её и создаёт вашу (201, + `taken_over_from`, `takeover_reason`). Пустая причина или из одних пробелов считается отсутствующей (→ 409 `LOCKED_BY_OTHER`); + при свободной версии причина игнорируется. Повторный `POST` по своей блокировке — 200, а не 201. +4. **404 — всегда код `NOT_FOUND`**; различие по тексту `message`: «версия консолидации не найдена», «ГК не найдена», + «ГСК/ГСЗ не найдена», «строка ОСВ не найдена в версии», «ЮЛ не найдено». Отдельных кодов `CONSOLIDATION_NOT_FOUND` / + `VERSION_NOT_FOUND` / `ENTRY_NOT_FOUND` **нет** (design №17, №62) — это обозначения в спеке. ГСК из чужой ГК или с видом `gsz` — + 422 `RELATED_GROUP_MISMATCH`, а невидимая/несуществующая ГСК — 404. +5. **`MapCounterpartyOut.row` может быть `null`:** строка привязанного документа, которая в свод не попадает (пара с пробелом, + квартальный документ при нарастающем), сопоставляется, но строки свода для ответа нет (design №69). Если `row == null`, перечитайте + `GET …/osv` и `…/summary`; обязательно проверяйте на `null`. +6. **`match_kind` склеенной строки — от последнего квартала** (D-3, design №8). Строка свода из нескольких кварталов несёт + `entry_ids[]`; ручное сопоставление пишется на записи, чью `entry_id` вы отправили. Чтобы в ответе увидеть `MANUAL`, отправляйте + **последнюю** `entry_id` строки (`entry_ids[entry_ids.length - 1]`); для записи более раннего квартала в строке будет `PROPAGATED` + (в БД при этом одна запись `MANUAL`). Если в записях разных кварталов разные `counterparty_id`, расхождение — в `warnings`. +7. **Ручное сопоставление — только строки свода** (R-11, design №62/№63): `group_type = COUNTERPARTY`, `data_type` БУ/СУММ привязанного + ОСВ-документа версии. Строки `NU`/`ACCOUNT`, чужие и несуществующие — 404 `NOT_FOUND`. Автоприменение (`PROPAGATED`) затрагивает + строки с тем же `counterparty_name.strip()`, пустым `counterparty_id` и ИНН контрагента пустым или равным ИНН выбранного ЮЛ — только среди строк + **свода** (`COUNTERPARTY`, БУ/СУММ) ОСВ-документов, привязанных именно к ЭТОЙ версии; ранее сопоставленные строки не перевязываются; + `propagated` — число записей `osv_entry`, а не строк + свода: `unmapped_total` падает на число **склеенных** строк (R-15). +8. **`ALIAS_TAKEN`** (`owners[]`): алиас закреплён за другим ЮЛ; повторите с `move_alias: true` (право `DIRECTORY_DELETE` не нужно), + в ответе `alias_moved_from` — прежний владелец. +9. **Экспорт.** Заголовок `Content-Disposition: attachment; filename="svod-osv.xlsx"; filename*=UTF-8''svod-osv_<ГК>_<год>-_v.xlsx` + (имя percent-кодировано целиком). Забирайте файл как blob с `Authorization` (прямая ссылка `` не подойдёт). Фильтры — те + же, что у `GET …/osv`, пагинации нет, порядок строк тот же. **Лист «Пробелы» строится по своду целиком и от фильтров не зависит** + (при `unmapped=true` без совпадений лист «Сводная ОСВ» — только заголовок, «Пробелы» заполнен). Суммы — числовые ячейки + `#,##0.00`, текст не вычисляется как формула. +10. **`warnings` — не контракт.** Тексты диагностики строки (расхождение `counterparty_id` между кварталами и т. п.) могут меняться; + показывать как подсказку, не разбирать. +11. **Q1.** ОСВ за `Q1` (парсер помечает её нарастающей) подходит версии любого квартала года, в версиях Q2+ служит квартальным + источником Q1; ББ/ОФР за Q1 версиям Q2–Q4 не подходят (R-14/R-14a, OQ-124). В `ConsolidatedRowOut.is_cumulative` — + признак строки свода: `true` только для строк нарастающего документа за квартал версии. +12. **Чтение не требует блокировки** и доступно для версии любого статуса, включая `CANCELLED`. +13. **Все деньги — строки с двумя знаками**; склеенные квартальные строки не содержат `null` (пустое = `"0.00"`), строки нарастающего + документа — как в файле (могут быть `null`). +14. **`period_check` документа остаётся `UNKNOWN`**: подходящесть документа версии — свойство пары (документ, версия), показывайте её + через чек-лист версии, а не через поле документа (OQ-120). +15. **Создание не ставит блокировку.** После `POST /consolidations` версия свободна; «Взять в работу» — отдельное действие [35]. + ГСК в запросе не сохраняется (`related_group_id` в версии пока пуст); члены берутся из текущего состава ГСК на момент создания. + +## 5. Сквозные сценарии экранов + +| Экран | Вызовы | +|---|---| +| [42] Новая консолидация | справочники (ГК → её ГСК) → `POST /consolidations` | +| [34] Консолидации ГК | `GET /company-groups/{id}/consolidations` (+ `in_work_only`), клик по строке — `GET …/versions/{id}`; «Взять в работу» — `POST …/lock`; меню «…» → «Отмена КФО» — `POST …/status {CANCELLED, comment}` | +| [36] Состав | `GET …/versions/{id}` (члены), все ЮЛ ГК — справочники, «Сохранить» — `PUT …/members` | +| [39]/[40] Документы организации | `GET /legal-entities/{id}/files`, `POST /legal-entities/{id}/files?consolidation_version_id=` | +| [37] Чек-лист | `GET …/checklist` (CSV в Срезе 4 нет) | +| [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` | +| [31-00] Предупреждение при уходе с вкладки | `GET …/osv/summary` (`unmapped_total`) | diff --git a/dev/qa-guide.md b/dev/qa-guide.md index 29226c9..0f4fcbb 100644 --- a/dev/qa-guide.md +++ b/dev/qa-guide.md @@ -1,4 +1,4 @@ -# Гайд тестировщика: локальный стенд АРМ КА (Срезы 0–2c) +# Гайд тестировщика: локальный стенд АРМ КА (Срезы 0–2c, 4) Документ для ручного тестирования текущего `main`. Путь проверен 05.10.2026 на macOS (Docker 28, compose 2.34, Node 24, uv 0.10): от пустого каталога до экрана администратора — около 15 минут, из них 10 — скачивание образов. @@ -18,11 +18,14 @@ Node 24, uv 0.10): от пустого каталога до экрана адм - Загрузка файлов ОСВ через API (`POST /legal-entities/{id}/files`), распознавание реальным `tabular-parser`, разбор результата в документы и строки ОСВ, статусы файла и документа, автозамена версий, подтверждение документа, удаление файла (раздел 6.6). Всё — через Swagger/curl, UI «Первичные документы» — подсрез 2d. +- Консолидация через API (Срез 4): версии за период, периметр, блокировка «взять в работу», статусы, привязка подтверждённых + документов, чек-лист, сводная ОСВ, ручное сопоставление контрагента, экспорт xlsx (раздел 7). UI консолидации — другой + исполнитель. **Нет (это не баги):** - Экранов справочников ГК/ЮЛ/ГСК — проверяются только через Swagger (`/docs`) или curl. -- Экрана загрузки и таблицы ОСВ (подсрез 2d), ББ/ОФР, распознавания PDF, консолидации, КФО — следующие срезы. +- Экрана загрузки и таблицы ОСВ (подсрез 2d), экранов консолидации (API есть, раздел 7), ББ/ОФР, распознавания PDF, шахматки, КФО — следующие срезы. - Настоящего входа через SSO банка. Вход — dev-форма по email без пароля. ## 2. Что установить и проверить из ПО @@ -208,7 +211,7 @@ curl -s localhost:8000/api/v1/me -H "Authorization: Bearer $TOKEN" ### 6.3 Импорт из ЕЦП CRM (пользователь `admin`, curl или Swagger) Файл: `fixtures/directories/ecp-export-russkiy-biskvit.xlsx` (21 строка). Выполняйте на свежем стенде или после -сброса (раздел 8), иначе счётчики будут другими. +сброса (раздел 9), иначе счётчики будут другими. | № | Шаги | Ожидание | |---|---|---| @@ -254,7 +257,7 @@ docker compose -f ../docs/dev/docker-compose.yml exec postgres \ ### 6.6 Загрузка ОСВ и просмотр строк через API (Срез 2c) Предусловия: раздел 4.2 (ядро с `NATS_ENABLED=true`, tabular-parser на `:8001`), файлы из `../fixtures/osv/`. Выполняйте на -свежем стенде (раздел 8) или с другим ЮЛ: дедуп и автозамена зависят от уже загруженного. Ожидаемое время от загрузки до +свежем стенде (раздел 9) или с другим ЮЛ: дедуп и автозамена зависят от уже загруженного. Ожидаемое время от загрузки до `PROCESSED` — до 15 секунд (опрос публикатора и цикла маппинга по 5 с, парсер — доли секунды). Подготовка: пользователь `ka_go` (права `DOCUMENT_EDIT`, `DOCUMENT_VIEW`, видит все ЮЛ). ЮЛ должно называться как в шапке ОСВ — @@ -303,7 +306,67 @@ curl -s -X DELETE "localhost:8000/api/v1/files/$FILE" -H "$H" -w '%{http_code}\n (системное событие) и `description`, где перечислены документы заменённого файла; U12 — `FILE_DELETE` с карточкой файла в `before_value`. Смены статусов обмена (`PROCESSING`, `PROCESSED`, `ERROR`) аудитом не фиксируются — так задумано. -## 7. Известные ограничения (не заводить как баги) +## 7. Консолидация через API (Срез 4) + +Предусловия: ядро запущено (раздел 4.2); для сценариев с файлами — ещё `NATS_ENABLED=true` и tabular-parser на `:8001` +(раздел 6.6); данные — в `../fixtures/osv/`. Токены: `ka_go` (`CONSOLIDATION_VIEW/EDIT`, видит всё), `ka_rf` +(`CONSOLIDATION_VIEW/EDIT`, только ГК Ростовского РФ), `expert` (только `CONSOLIDATION_VIEW`), `client_manager` +(`CONSOLIDATION_VIEW`, Краснодарский РФ), `admin` (прав на консолидации нет). Подробные схемы запросов и ответов, коды ошибок и +нюансы для фронта — `dev/api-contract-slice-4.md`; Swagger — `/docs`. + +**Подготовка.** Нужна ГК с ГСК не менее чем из двух ЮЛ (ГК — в Ростовском РФ, чтобы её видел `ka_rf`): создайте через API +справочников (раздел 6.2; ГСК — `POST /directories/company-groups/{id}/related-groups`, участники — `POST +/directories/related-groups/{id}/members`) либо импортом ЕЦП (I2). Для данных РБ: ЮЛ «АО "Русский бисквит"» (ИНН 3528052718) и +«ООО "Пекарня Счастливочная"» (в ГСК обе). Ниже `V` — id версии, `G` — id ГК, `S` — id ГСК. + +```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 +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 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" +curl -s -o svod.xlsx -D - "localhost:8000/api/v1/consolidations/versions/$V/osv/export" -H "$H" # Content-Disposition +``` + +| № | Пользователь | Шаги | Ожидание | +|---|---|---|---| +| K1 | ka_go | создать консолидацию ГК/ГСК за 2024 Q3 (команда выше) | 201, `status = INITIATED`, `version_no = 1`, `scenario = CREATE`, `approach = BASIC`, `goodwill_enabled = false`, `members` = текущие члены ГСК, `lock = null`; повтор — 409 `CONSOLIDATION_EXISTS` | +| K2 | ka_go | то же с `year = 1999`; с ГСК другой ГК; с ГСК из одного ЮЛ; с чужим/несуществующим id ГК | 422 (валидация года); 422 `RELATED_GROUP_MISMATCH`; 422 `PERIMETER_TOO_SMALL`; 404 `NOT_FOUND` | +| K3 | ka_go | `GET /company-groups/{G}/consolidations`, затем с `?in_work_only=true`; `GET …/versions/{V}` | период с версией; карточка с `members[]` (у каждого `checklist[]`), `history[0].from_status = null`; `ka_rf` для ГК вне своего филиала — 404 | +| K4 | ka_go | `POST …/lock`; повтор | 201 `LockOut` (`locked_by`, `locked_by_name`); повтор — 200, блокировка та же | +| 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 | +| 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 | +| K12 | ka_go | фильтры: `legal_entity_id`, `account_root=60`, `counterparty_type=EXTERNAL`, `unmapped=true`, `match_kind=MANUAL&match_kind=PROPAGATED`, `q=бисквит`, `limit=2001` | отфильтрованные страницы с `total`; `limit=2001` — 422 | +| K13 | ka_go | `GET …/counterparty-candidates?q=счастл&entry_id=`; `scope=all&q=ab` | список ЮЛ с `counterparty_type`; `recommended` — только при точном совпадении нормализованного имени; `scope=all` с `q` короче 3 — 422 `QUERY_TOO_SHORT` | +| K14 | владелец блокировки | `POST …/osv/entries/{entry_id}/counterparty {legal_entity_id}` для строки «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» | 200: `row.match_kind = MANUAL` (слать **последнюю** `entry_id` строки свода), `propagated` ≥ 0, `alias_created = true`; в сводке `unmapped_total` уменьшился, у других строк с тем же именем — `PROPAGATED`; в аудите `OSV_COUNTERPARTY_MAP` и `LEGAL_ENTITY_CHANGE` | +| K15 | владелец блокировки | сопоставить другое имя с алиасом, уже закреплённым за другим ЮЛ; затем с `"move_alias": true` | 409 `ALIAS_TAKEN` (`owners[]`); с `move_alias` — 200, `alias_moved_from` = прежний владелец | +| K16 | ka_go | `entry_id` строки `NU`/`ACCOUNT` или чужой версии | 404 `NOT_FOUND`; без блокировки — 409 `LOCK_REQUIRED` | +| 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 | +| 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`); новый документ привязывается только после подтверждения | + +Данные для K11 (РБ 2024, как в сквозном тесте `test_consolidation_fixtures`): ОСВ 60 и 62 РБ и Счастливочной, ОСВ 76 +Счастливочной за Q1, Q2 и Q3 2024 (`../fixtures/osv/…`, подтвердить документы), плюс по одному документу за Q3 (РБ: 01, 58.03, +66, 67, 76; Счастливочная: 01, 02). Для версии Q3 пары с одним Q3 попадают в `gaps` (пропущены Q1–Q2); ОСВ за Q1 (нарастающая +по признаку парсера) подходит версии любого квартала (R-14). + +Аудит (6.4): K1 — `CONSOLIDATION_CREATE`; K4 — `CONSOLIDATION_LOCK`; K5 — `CONSOLIDATION_LOCK_TAKEOVER`; K7 — `MEMBERSHIP_CHANGE` +(только при изменении состава); переходы статуса — `CONSOLIDATION_STATUS_CHANGE`; K14 — `OSV_COUNTERPARTY_MAP` и `LEGAL_ENTITY_CHANGE`; +K17 — `EXPORT`; во всех — `consolidation_version_id`. Блокировка не истекает по времени — освободить её можно только `DELETE …/lock`, +перехватом или аннулированием версии (OQ-119). + +## 8. Известные ограничения (не заводить как баги) - UI только для администрирования; справочники — через API. - Вход без пароля, открытый `/docs`, CORS `*` — dev-режим. @@ -315,10 +378,15 @@ curl -s -X DELETE "localhost:8000/api/v1/files/$FILE" -H "$H" -w '%{http_code}\n - В консоли браузера сообщение `Store does not have a valid reducer` — каркас, не влияет. - Таблица пользователей показывает первые 200. - При импорте ИНН, записанный в Excel числом с ведущим нулём, теряет ноль и даёт `INVALID_INN`. +- Консолидация (Срез 4): только API; маршрут согласования (`CONSOLIDATION_RF` и далее), «Добавить версию», гудвилл, шахматка — следующие + срезы; ББ/ОФР в своде не участвуют; блокировка версии не истекает по времени; `period_check` документов остаётся `UNKNOWN`; + `PROCESSING` в чек-листе ставится по всем недостающим столбцам ЮЛ, пока файл не смаплен (OQ-119, OQ-120, OQ-122). +- Ручное сопоставление контрагента меняет строку ОСВ для всех версий, куда входит документ (OQ-123); рекомендация кандидата — только + при точном совпадении нормализованного имени (OQ-127). Полный список отложенных находок — Gitea `RSHB/arm-core`, issue «Follow-ups from #5». -## 8. Сброс стенда +## 9. Сброс стенда ```bash docker compose -f ../docs/dev/docker-compose.yml down -v # БД и бакет с нуля @@ -331,7 +399,7 @@ make migrate && make seed-dev Файлы, загруженные в 6.6, удаляются из БД командой `DELETE /files/{id}`, но объекты в MinIO остаются (им ставится только `expires_at`) — `down -v` стирает и их. -## 9. Как заводить баги +## 10. Как заводить баги Gitea, репозиторий `RSHB/arm-core` (для документации — `RSHB/arm-ka-docs`). Один дефект — одна issue. @@ -351,7 +419,7 @@ Gitea, репозиторий `RSHB/arm-core` (для документации Вопросы по ожидаемому поведению (не ясно, баг ли) — тоже issue, с пометкой «вопрос» в заголовке. -## 10. Типичные проблемы при запуске +## 11. Типичные проблемы при запуске | Симптом | Причина и решение | |---|---| diff --git a/model/data-model-v5.16.dbml b/model/data-model-v5.16.dbml new file mode 100644 index 0000000..b0a9f7d --- /dev/null +++ b/model/data-model-v5.16.dbml @@ -0,0 +1,1353 @@ +// ДКБ · Модель данных v5.16: справочники, файлы/распознавание, консолидация, КФО, аудит, ЕХФД +// Формат: DBML (dbdiagram.io). Вставить на https://dbdiagram.io/d +// >>> Источник истины по модели данных. Аналитика в Confluence ссылается сюда. +// Над каждой таблицей — блок «ИСТОЧНИК»: пункт ТЗ АРМ КА BIQ-34 / ТЗ ЕХФД (помечено «ЕХ ТЗ») / Методика; +// страница аналитики (md); экран макета dkb_24.09.26; версия модели, в которой таблица появилась (v4 = исходная DBML аналитики). +// +// 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: поле есть только в макете; решение владельца — не нужен). +// 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) +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 + Note: 'Организация (ЮЛ/ИП) — ТЕКУЩЕЕ состояние (SCD4). История — legal_entity_history' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.2 (журнал изменений справочников); 6.10 (история изменений состава) +// Аналитика: md «Модель данных» (legal_entity_history) +// Макет: Архив ГК: события «Переход в другую ГК», «Закрытие», дата перевода +// В модели: v4 + v5.1 (event_type, effective_date — ANL-046) +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] + 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 +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 + Indexes { + (legal_entity_id, status) + current_recognition_job_id [note: 'v5.15: проверка FK при удалении recognition_job / файла (OQ-117)'] + } + Note: 'v5.13: физический файл одной загрузки (S3 — только ссылка). Что внутри — document (1:N). Первичка не редактируется. Удаление физическое запрещено после INITIATED — только SUPERSEDED (ANL-020)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 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 — 5 мин)'] + 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' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 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/entities-v5.16.md b/model/entities-v5.16.md new file mode 100644 index 0000000..eff925b --- /dev/null +++ b/model/entities-v5.16.md @@ -0,0 +1,165 @@ +# АРМ КА — описание сущностей модели данных v5.16 + +Источник истины по структуре — `data-model-v5.16.dbml` (полный). Диаграммы по частям — `diagrams/` (part01…09 svg/png; отрисованы по v5.13: v5.14 меняет только `related_group_member` — колонку `kind` и индекс, v5.15 — одно значение enum и три индекса, v5.16 — один enum и одна колонка, — диаграммы не перерисовывались). Здесь — что каждая таблица значит для бизнеса, как связана с соседями и откуда взялась. + +## Что изменилось в 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 | КПП | **Не вводится.** Поле есть только в макете; у `legal_entity` его нет, импорт его не читает (закрывает OQ-31) | +| 3 | Семантика `company_group.crm_id`, `base_regional_office_id` | Без изменения структуры: `crm_id` = «iD ГК» выгрузки ЕЦП CRM, `base_regional_office_id` = «Базовый РФ ГК в CRM»; флаги «входит в ГСК/ГСЗ» выгрузки → членство в одной неявной ГСК/ГСЗ на ГК (правила — `05-domain-rules.md` §13) | +| 4 | `audit_action` + `LEGAL_ENTITY_CHANGE`, `USER_CHANGE` | Журнал изменений ЮЛ (создание/изменение/закрытие/перевод, алиасы) и пользователей (создание/деактивация администратором) — ТЗ 8.2; в v5.13 для них не было действия (были `GROUP_CHANGE`, `MEMBERSHIP_CHANGE`, `USER_ROLE_CHANGE`) | + +Остальные таблицы и enum — как в v5.13 (43 таблицы). Реализация — миграция `0002` в `arm-core`. + +## Что изменилось в v5.13 (ревью 30.09–01.10) + +| № | Замечание | Решение | +|---|---|---| +| 1 | Вместо RBAC «в коде» — роли **и** права | `permission` + `role_permission` возвращены; права только через роли, прямых назначений пользователю нет. Исключения — заведением роли | +| 2 | `osv_entry.parent_entry_id` не нужен — храним только строки с контрагентами | **Отложено.** Открыт вопрос: хранить ли строки уровня ACCOUNT для сверки «Σ контрагентов = итог счёта»; аналитика «Парсинг ОСВ» п.11 пока говорит «все строки» | +| 3 | `osv_group_schema` не особо нужна | Убрана. Схема шапки — `recognition_job.group_schema jsonb`; тип строки — `osv_entry.group_type` | +| 4 | `financial_statement_value.needs_review` непонятен | Убран: производное от `confidence` / `is_recognized`, порог может меняться | +| 5 | В `recognition_job` не нужны и `result`, и `raw_result` | Оставлен только `result_s3_object_id`. Сырой ответ ИС Сканирование — S3 `raw/{job_id}.json` по конвенции, в БД не регистрируется | +| 6 | `stored_file.fields_modified` — что это | Счётчик «исправлено полей» с карточки документа (v5.7). Убран — `COUNT(is_modified)` по данным | +| 6-бис | `document_type` не предусматривает несколько ОСВ или ББ+ОФР в одном файле | **Разделение файл / документ**: `stored_file` — физический файл, `document` — логическая единица (1:N). Чек-лист, проверки, подтверждение, FK разобранных данных, `consolidation_document`, `ex_snapshot_item` — на `document`. Замена (`superseded_by`) — файл целиком | +| 7 | `file_type`: убрать Р7 | Убран. `xls/xlsx/xlsb → tabular-parser`, `pdf/png/jpg → scan-gateway` | + +**Принципы модели (коротко):** +- Первичка не редактируется: файлы и распознанные значения хранятся как пришли; правки аналитика — рядом (`is_modified`, `original_*`) и в аудите. +- Производные консолидации в работе (сводная ОСВ, пары ВГО, шахматка) **не хранятся** — считаются запросом. Хранятся только ручные решения (корректировки, гудвилл, подтверждение КФО клиента). +- При верификации всё замораживается в Единое хранилище (ЕХ) как самодостаточный срез, который не зависит от таблиц консолидации. +- Единственный писатель в БД — ядро `arm-core`; сервисы распознавания видят только NATS и S3. +- Права — данные (`role_permission`), ограничения по ЗО/филиалу/статусу — policy-слой поверх прав. + +--- + +## Часть 01 — Группы компаний, организации, филиалы + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `regional_office` | Филиал банка: головной офис (ГО, `is_head_office`) и региональные филиалы (РФ). Определяет зону видимости пользователя и закрепление ГК. | ← `company_group.base_regional_office_id`, `legal_entity.regional_office_id`, `app_user.regional_office_id` | ТЗ 8.1 | +| `company_group` | Группа компаний (ГК) — верхний уровень справочника клиентов. Текущее состояние (SCD4). `crm_id` = «iD ГК» из ЕЦП CRM (NULL, если создана вручную Куратором; v5.14 — семантика по выгрузке), внутренний `code`, закрепление за филиалом (`base_regional_office_id` = «Базовый РФ ГК в CRM»), флаг архива. | → `regional_office`; ← `legal_entity`, `related_group`, `consolidation` | ТЗ 6.1, Прил. 2; макет | +| `company_group_history` | Прошлые снимки ГК: каждое изменение — новая строка с `valid_from/valid_to`. | → `company_group` | ТЗ 6.10, 8.2 | +| `legal_entity` | Юридическое лицо / ИП. Натуральный ключ — ИНН. `company_group_id` = текущая ГК; **NULL = монозаёмщик**. `close_date` — закрыто/архивировано. | → `company_group`, `regional_office`; ← `related_group_member`, `stored_file`, `document`, `osv_entry.counterparty_id`, `consolidation_member` | ТЗ 6.1, Прил. 2; ЕХ 6.1 | +| `legal_entity_history` | Прошлые снимки ЮЛ + вид события (создание, изменение, переход в другую ГК, закрытие, вывод из ГК) и дата вступления в силу. | → `legal_entity` | ТЗ 8.2; макет | +| `legal_entity_alias` | Словарь написаний контрагента: как имя встречается в ОСВ и нормализованный ключ для матчинга. Пополняется при ручном сопоставлении. | → `legal_entity` | ТЗ 6.5 | +| `related_group` | ГСК или ГСЗ внутри ГК; `kind` различает. Для ГСК формируется КФО. Иерархия не поддерживается. | → `company_group`; ← `related_group_member`, `consolidation.related_group_id` | ТЗ 6.1, Прил. 2 | +| `related_group_member` | Состав ГСК/ГСЗ — история членства (SCD2). **v5.14:** ЮЛ может одновременно входить в **одну ГСК и одну ГСЗ** (не более одной группы каждого вида): колонка `kind` = `related_group.kind` (денормализация) и частичный уникальный индекс `(legal_entity_id, kind) WHERE is_current`. Решение владельца 04.10.2026, отменяет ANL-030. | → `related_group`, `legal_entity` | ТЗ 6.1; макет [79] | + +## Часть 02 — Пользователи, роли, права + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `app_user` | Пользователь АРМ. Аутентификация внешняя (РАИСА/SSO, `external_id`). Филиал (ГО/РФ), зона ответственности (ДКБ/ДМСБ/ДМ), табельный номер. Видимость данных = права роли + филиал + ЗО. | → `regional_office`; ← `user_role`, все `*_by` поля | ТЗ 8.1, Прил. 2 | +| `role` | Роли: seed из ТЗ 8.1 (КА РФ, КА ГО, РКА, КМ, Куратор ГО, Эксперт, Администратор; `is_system`) + роли, заведённые администратором для исключений. **v5.13:** код роли в бизнес-логике не проверяется — только права. | ← `user_role`, `role_permission` | ТЗ 8.1 | +| `permission` | **v5.13.** Атомарное право «объект × действие»: `DIRECTORY/DOCUMENT/CONSOLIDATION/KFO/EX/EXPORT/USERS/AUDIT` × `VIEW/EDIT/APPROVE/VERIFY/DELETE/EXPORT/ADMIN`. Seed, привязан к коду — администратор новые коды не создаёт. Право говорит «может»; policy-слой (ЗО, филиал, статус КФО) — «эту ли и сейчас ли». | ← `role_permission` | ТЗ Прил. 2, сноска 29 | +| `role_permission` | **v5.13.** Матрица доступов как данные: `effective_permissions(user) = ∪ permissions(roles(user))`. Редактируется администратором на экране «Изменение ролей и прав доступа»; история — `audit_event(ROLE_PERMISSION_CHANGE)`. Прямых прав у пользователя нет. | → `role`, `permission` | ТЗ Прил. 2; макет | +| `user_role` | Назначение ролей пользователю (M:N). История — в аудите. | → `app_user`, `role` | ТЗ 8.1 | + +## Часть 03 — Бухгалтерские справочники и правила + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `chart_of_account` | План счетов с иерархией, тип A/P/AP, признак забалансового. Seed — полный план 1С; нестандартные субсчета клиентов добавляет администратор. | ← `osv_entry.account_id`, `elimination_rule_account`, `required_document_rule` | ТЗ 6.2/6.5, Прил. 3/4 | +| `elimination_rule` | Правило элиминации из Прил. 3 (ББ) / Прил. 4 (ОФР) как данные: пара статей, сторона, мера, режим, НДС, акцизы. Версионируется. | ← `elimination_rule_account`, `correction.rule_id`; версия фиксируется в `consolidation_version` | ТЗ 6.4, Прил. 3/4 | +| `elimination_rule_account` | Счета/субсчета каждой стороны правила, включая исключения («76 кроме 76.7.1»). | → `elimination_rule`, `chart_of_account` | ТЗ Прил. 3/4 | +| `required_document_rule` | Какие документы обязательны по подходу. **v5.13:** сверяется с `document` (тип + счёт + период), а не с файлом: выполнено = есть `document CONFIRMED` у каждого ЮЛ периметра. | → `chart_of_account` | ТЗ 5.1, 6.2, сноска 12 | + +## Часть 04 — Файлы, документы, S3, задания распознавания + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `s3_object` | Реестр всех объектов в S3: бакет-роль, ключ, размер, sha256, срок жизни, ссылка на архив в data lake. Единая точка ретеншна (5 лет по ТЗ 8.4) и дедупа. | ← `stored_file`, `recognition_job` | ТЗ 6.2, 8.4; ЕХ 8.3 | +| `stored_file` | **v5.13: только физический файл** одной загрузки: чей (ЮЛ), формат, hash, кто/когда загрузил, статус обработки (`UPLOADED → PROCESSING → PROCESSED / ERROR / SUPERSEDED`), версия при замене. Что внутри файла — `document`. Замена — файл целиком со всеми документами. | → `legal_entity`, `s3_object`, `recognition_job`; ← `document`, `recognition_job` | ТЗ 6.2, 8.2; UC-01…05 | +| `document` | **v5.13, новая.** Логический документ внутри файла: ББ, ОФР, ОСВ по счёту (`osv_account_code`), расшифровка, прочее. Период, отчётная дата, где в файле (`locator`: лист / страницы), статус проверки (`RECOGNIZED / NEEDS_CHECK / CONFIRMED / ERROR`), результаты проверок периода и контрагента, кто подтвердил. Один файл → N документов (PDF с ББ+ОФР; xlsx с несколькими ОСВ). Создаёт ядро при маппинге результата `recognition_job`. На документ ссылаются разобранные данные, чек-лист, консолидация и ЕХ. | → `stored_file`, `legal_entity`, `app_user`; ← `osv_entry`, `financial_statement_meta`, `consolidation_document`, `ex_snapshot_item` | ТЗ 6.2, 5.1, 8.2; макет (чек-лист, «Подтвердить данные») | +| `recognition_job` | Одна попытка распознать/распарсить файл. **Единственная точка обмена с NATS** (outbox/inbox, дедуп по `result_msg_id`, DLQ). Хранит сервис и версию, **одну** ссылку на нормализованный JSON-результат в S3 (v5.13), метрики, и **схему группировок шапки ОСВ по листам** (`group_schema jsonb`, v5.13 — вместо `osv_group_schema`). | → `stored_file`, `s3_object`; ← `osv_entry`, `financial_statement_meta` | ТЗ 6.2, 6.10; DECISION-4 | + +## Часть 05 — Разобранные ОСВ + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `osv_entry` | Строка ОСВ: документ (счёт + период), владелец, тип строки по схеме шапки (`group_type`, v5.13), уровень и родитель (`parent_entry_id` — **открыт вопрос, замечание 2**), код счёта и резолв в план счетов, сырое имя/ИНН контрагента, резолв в ЮЛ и способ сопоставления (`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` | Логическая консолидация: **ГК** + отчётный период. ГСК фиксируется/создаётся при верификации по фактическому периметру. Уникальна по (ГК, год, квартал). | → `company_group`, `related_group`; ← `consolidation_version` | ТЗ 6.10, 7.2 | +| `consolidation_version` | Версия консолидации: номер, статус, сценарий, подход, гудвилл, инициатор и его офис (маршрут), кто утвердил / верифицировал, решение УОБ, отклонения от Методики, версия правил элиминации, ссылка на предыдущую. После VERIFIED — read-only. | → `consolidation`, `app_user`, `regional_office`, self; ← всё в частях 07–08 | ТЗ 5.1, 6.4, 6.10; макет | +| `consolidation_status_history` | Каждый переход статуса: из → в, кто, когда, комментарий (обязателен при возврате). | → `consolidation_version`, `app_user` | ТЗ 6.10, 8.2 | +| `consolidation_lock` | «Взять в работу»: кто редактирует версию, перехват с причиной (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/slice-4-consolidation/.openspec.yaml b/openspec/changes/slice-4-consolidation/.openspec.yaml new file mode 100644 index 0000000..d4c9260 --- /dev/null +++ b/openspec/changes/slice-4-consolidation/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +created: 2026-10-06 +skip_specs: true diff --git a/openspec/changes/slice-4-consolidation/design.md b/openspec/changes/slice-4-consolidation/design.md new file mode 100644 index 0000000..75ca12a --- /dev/null +++ b/openspec/changes/slice-4-consolidation/design.md @@ -0,0 +1,31 @@ +# Design: Срез 4 (сквозное) — документы + +## Context + +Дочерний change в arm-core (`slice-4-consolidation`) держит технические решения D-1…D-16. Здесь — что меняется в +документах контекста и модели и почему. + +## Decisions + +### D-1. Новая версия DBML ради одной колонки +`osv_entry.counterparty_match_kind` — ручное решение аналитика (DECISION-5 разрешает хранить ручные решения), а +`INN/NAME/ALIAS` — зафиксированный шаг автоматического резолва, нужный для подсветки строк к проверке. Вычислять на +чтение нельзя (алиас после ручного связывания маскирует ручное). Версия 5.16, v5.15 остаётся в репозитории. + +### D-2. §2 переписывается из «[открыто]» в правило +Формула остаётся; добавляются ключ, выбор источников, пробел и нули. OQ-03 закрывается решением владельца 06.10.2026. + +### D-3. §3 п. 1 меняет «последние подтверждённые» на явную привязку +Подхват при создании версии и расширении периметра заменяет fallback «если версия ещё не зафиксировала документы». +Фраза убирается, чтобы код и правило не расходились. + +### D-4. Контракт API для фронта — отдельный документ в `docs/dev/` +Разработчик фронта не читает OpenSpec arm-core. `api-contract-slice-4.md` — выжимка: маршруты, схемы, фильтры, коды +ошибок, соответствие экранам макета. Источник истины — спеки arm-core; документ помечен как производный. + +## Decisions made without discussion + +| # | Дата | Решение | Обоснование | Цена ошибки | +|---|---|---|---|---| +| 1 | 2026-10-06 | OQ-40 закрывается допущением `reporting_date` по правилу банка, макет помечен неточным | владелец не возражал на двух срезах; правило банка — в DBML note | одно поле, правится миграцией данных | +| 2 | 2026-10-06 | Контракт для фронта выносится в `docs/dev/api-contract-slice-4.md`, а не в `docs/contracts/` | `docs/contracts/` — JSON Schema между сервисами с версией; API фронта описывает OpenAPI | дублирование OpenAPI текстом; помечено «производный» | diff --git a/openspec/changes/slice-4-consolidation/proposal.md b/openspec/changes/slice-4-consolidation/proposal.md new file mode 100644 index 0000000..c915b0c --- /dev/null +++ b/openspec/changes/slice-4-consolidation/proposal.md @@ -0,0 +1,52 @@ +# Proposal: Срез 4 (сквозное) — DBML v5.16, правила консолидации и сводной ОСВ по факту, OQ + +**Срез:** 4 (консолидация: версии, периметр, блокировка, привязка документов, сводная ОСВ, экспорт). **Репозиторий:** +arm-ka-docs. **Issue:** RSHB/arm-ka-docs#19. **Дочерний change:** `arm-core/openspec/changes/slice-4-consolidation` +(RSHB/arm-core#18). Объём по решению владельца 06.10.2026: бэкенд + API + экспорт xlsx; UI — другой разработчик. + +## Why + +Решения владельца 06.10.2026 закрывают OQ-03 (пропуск квартала → пробел; ключ (счёт, контрагент)), OQ-19 +(блокировка закрывает все правки версии, перехват по `CONSOLIDATION_EDIT` с причиной), фиксируют автоматику статусов +Среза 4, явную привязку документов к версии при подтверждении и требование показывать в API способ сопоставления +контрагента. Последнее требует новой версии модели: `osv_entry` хранит только `counterparty_id`, и после ручного +сопоставления появляется алиас, по которому ручное неотличимо от автоматического. + +## What Changes + +- **DBML v5.16** (`model/data-model-v5.16.dbml`, v5.15 остаётся): enum `counterparty_match_kind` (`INN`, `NAME`, + `ALIAS`, `MANUAL`, `PROPAGATED`), колонка `osv_entry.counterparty_match_kind` (NULL ⇔ `counterparty_id` NULL); + changelog в шапке; уточнение note `consolidation_document` (точки привязки/отвязки) и `consolidation_lock` + (без истечения). `entities-v5.16.md`, `04-data-model-guide.md` (DECISION-5: «что не хранится» дополнено способом + сопоставления и пробелами), ссылки в `CLAUDE.md`/`repo-templates`/`arm-core/openspec/config.yaml` — на v5.16. +- **`05-domain-rules.md`**: §2 — ключ склейки (счёт, контрагент по сырому имени и ИНН), договоры внутри контрагента, + пропуск квартала → пробел по паре (ЮЛ, счёт), правило выбора источников (нарастающий > квартальные), нули для + отсутствующего квартала, дубль ключа внутри квартала; §3 — явная привязка `consolidation_document` (вместо «последние + подтверждённые»), дорезолв на чтение не сохраняется, `counterparty_match_kind`, кандидаты (`group`/`all`), перенос + алиаса без `DIRECTORY_DELETE`, автоприменение по всем ОСВ версии; §9.1 — таблица переходов Среза 4 (кто/чем), остальные + переходы — Срез 7; §9.3 — `period_check` остаётся `UNKNOWN` (OQ-120); §9.5 — отвязка при автозамене, удаление файла + с документом в версии (OQ-110 закрыт для Среза 4); §10 — policy «в работе + своя блокировка». +- **`03-architecture.md`**: пакет консолидаций, чистый домен свода (D-1), порядок блокировок «файл → документ → версия». +- **`07-open-questions.md`**: закрыты OQ-03, OQ-19, OQ-110 (Срез 4), OQ-40 (допущение принято); новые OQ-118 (запрет + загрузки/подтверждения после `DATA_CORRECTION`, ТЗ 03_9 A1 — Срез 7), OQ-119 (истечение блокировки), OQ-120 + (`period_check` документа по версиям), OQ-121 (порог размера свода для кэша), OQ-122 (ячейка `PROCESSING` чек-листа до + маппинга), OQ-123 (общие строки `osv_entry` у нескольких версий — ручное сопоставление в одной меняет все). +- **`09-delivery-plan.md`**: Срез 4 по факту (чек-лист перенесён из Среза 3; UI — отдельно); **`10-user-journey.md`**: + шаги 2.1–2.6, 3.1 — ссылки на эндпоинты; **`dev/qa-guide.md`**: §7 «Консолидация через API» (создание, блокировка, + загрузка с версией, подтверждение, чек-лист, свод, сопоставление, экспорт). +- **`dev/api-contract-slice-4.md`** (новый): перечень эндпоинтов, схем и кодов ошибок для разработчика фронта (экраны + `34`–`37`, `42`, `30`, `32-*`, `31-00`). + +## Capabilities + +Change документный (`skip_specs: true`); поведение описывают спеки дочернего change'а. + +## Impact + +- Таблицы: `osv_entry` (колонка), `consolidation_document`/`consolidation_lock` (семантика). Правила: §2, §3, §9, §10. + Потребители: arm-core (миграция 0004, `DBML_VERSION`), фронт (контракт API); tabular-parser — без изменений. + +## Non-goals + +- Маршрут согласования, УОБ, ЕХ, «Добавить версию», `CONFIRM`, `EXTENDED` (Срезы 6–7); шахматка и ВГО (Срез 5); + ББ/ОФР (2b), PDF (3); UI (другой разработчик); CSV чек-листа. diff --git a/openspec/changes/slice-4-consolidation/tasks.md b/openspec/changes/slice-4-consolidation/tasks.md new file mode 100644 index 0000000..414b083 --- /dev/null +++ b/openspec/changes/slice-4-consolidation/tasks.md @@ -0,0 +1,9 @@ +# Tasks: Срез 4 — arm-ka-docs + +- [x] 1 **DBML v5.16.** `model/data-model-v5.16.dbml` (copy v5.15 + changelog v5.16 + enum + колонка + notes), + `model/entities-v5.16.md`, `04-data-model-guide.md` (DECISION-5, переименования без изменений), ссылки на v5.16 в + `CLAUDE.md`, `repo-templates/*`, `arm-core/openspec/config.yaml` (в дочернем change). +- [x] 2 **Правила.** `05-domain-rules.md` §2, §3, §9.1, §9.3, §9.5, §10 по proposal; `03-architecture.md`. +- [x] 3 **Вопросы и план.** `07-open-questions.md` (закрытия и OQ-118…123), `09-delivery-plan.md`, `10-user-journey.md`. +- [x] 4 **Контракт и QA.** `dev/api-contract-slice-4.md`, `dev/qa-guide.md` §7 — после того как эндпоинты arm-core зафиксированы + (последняя задача, синхронно с 5.2 arm-core). diff --git a/repo-templates/arm-core.CLAUDE.md b/repo-templates/arm-core.CLAUDE.md index 8d46ee1..f8a421d 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.15.dbml`; `scripts/check_dbml_vs_orm.py` — обязательная проверка CI. +- ORM 1:1 с `../docs/model/data-model-v5.16.dbml`; `scripts/check_dbml_vs_orm.py` — обязательная проверка CI. - Алгоритмы — только из `../docs/context/05-domain-rules.md`; номер § — в docstring. - Вычисляемые объекты консолидации — `backend/app/domain/`, без ORM-таблиц. - Фронт: пользователь слаб во фронте — для TS/React изменений делай ревью сам и объясняй решения в PR.