docs(slice-4): DBML v5.16, правила консолидации по факту, OQ, контракт API #20

Merged
aep merged 6 commits from feat/19-slice-4-consolidation into main 2026-10-06 09:38:30 +03:00
15 changed files with 2040 additions and 57 deletions

View File

@ -1,4 +1,4 @@
# 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.15) # 03. Архитектура (целевая, по решениям DECISION-1…5 и ARCH-* в DBML v5.16)
## Компоненты (= репозитории, см. `00-workspace-layout.md`) ## Компоненты (= репозитории, см. `00-workspace-layout.md`)
@ -113,6 +113,37 @@
`pg_advisory_xact_lock` по ЮЛ (иначе два параллельно смапленных файла с одним документом не увидят друг друга), загрузка — `pg_advisory_xact_lock` по ЮЛ (иначе два параллельно смапленных файла с одним документом не увидят друг друга), загрузка —
advisory-lock по `(ЮЛ, sha256)` (двойной клик не проскакивает дедуп). 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 ## Схема ключей S3
**Один бакет** (`OPENBAO__MINIO__MINIO_BUCKET` шаблона); роль объекта — первый сегмент ключа: **Один бакет** (`OPENBAO__MINIO__MINIO_BUCKET` шаблона); роль объекта — первый сегмент ключа:
@ -152,7 +183,7 @@ api/src/
# машины статусов, policy — pydantic-модели на входе и выходе # машины статусов, policy — pydantic-модели на входе и выходе
services/ # use-cases: оркестрация domain + repositories + outbox + audit_event services/ # use-cases: оркестрация domain + repositories + outbox + audit_event
repositories/ # SQLAlchemy-запросы, в т. ч. запросы-вычисления (сводная ОСВ, шахматка) 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 schemas/ # pydantic DTO API /api/v1
core/ # config, clients (S3; NATS outbox-publisher и result-consumer), auth (JWT РАИСА / dev-эмитент), core/ # config, clients (S3; NATS outbox-publisher и result-consumer), auth (JWT РАИСА / dev-эмитент),
# logger, exception_handlers # logger, exception_handlers
@ -237,7 +268,7 @@ FastAPI» платформы РАИСА/aurora): дерево импортиро
`version_table_schema` и `include_name` Alembic). Следуем шаблону, не `public`. `version_table_schema` и `include_name` Alembic). Следуем шаблону, не `public`.
- Alembic берёт URL из настроек, не из `alembic.ini`; ревизии — `api/alembic/versions/`, имя файла стандартное - Alembic берёт URL из настроек, не из `alembic.ini`; ревизии — `api/alembic/versions/`, имя файла стандартное
`<rev>_<slug>.py`. Автогенерация — только черновик: итоговую ревизию сверяет с DBML `api/scripts/check_dbml_vs_orm.py`. `<rev>_<slug>.py`. Автогенерация — только черновик: итоговую ревизию сверяет с DBML `api/scripts/check_dbml_vs_orm.py`.
- PK везде `uuid` через `UUIDPrimaryMixin`; имена таблиц, колонок и enum'ов — ровно как в DBML v5.15. - PK везде `uuid` через `UUIDPrimaryMixin`; имена таблиц, колонок и enum'ов — ровно как в DBML v5.16.
### Инфраструктурные клиенты ### Инфраструктурные клиенты

View File

@ -1,9 +1,9 @@
# 04. Как читать модель данных v5.15 # 04. Как читать модель данных v5.16
Файлы: `docs/model/data-model-v5.15.dbml` (источник истины), `docs/model/entities-v5.15.md` (смысл таблиц; предыдущие Файлы: `docs/model/data-model-v5.16.dbml` (источник истины), `docs/model/entities-v5.16.md` (смысл таблиц; предыдущие
версии `data-model-v5.13.dbml` / `data-model-v5.14.dbml` и `entities-v5.13.md` / `entities-v5.14.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`, `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) и как это вычислять ## Что намеренно НЕ хранится (DECISION-5) и как это вычислять
Способ сопоставления контрагента хранится (`osv_entry.counterparty_match_kind`, v5.16); пробелы квартальной склейки и сам свод — нет.
| Объект | Вычисление | | Объект | Вычисление |
|---|---| |---|---|
| Сводная ОСВ | `osv_entry` актуальных `document` (OSV, CONFIRMED, период версии) всех `consolidation_member` версии; только строки `group_type = COUNTERPARTY` и `data_type IN (BU, SUM)`; строки не агрегируются | | Сводная ОСВ | `osv_entry` актуальных `document` (OSV, CONFIRMED, период версии) всех `consolidation_member` версии; только строки `group_type = COUNTERPARTY` и `data_type IN (BU, SUM)`; строки не агрегируются |
@ -55,9 +57,9 @@
## v4 → v5.13: таблица переименований (обязательна при чтении ТЗ) ## v4 → v5.13: таблица переименований (обязательна при чтении ТЗ)
v5.14 имена таблиц и полей не менял (добавлены `related_group_member.kind` и два значения `audit_action`), v5.15 — тоже 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.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 | | `stored_file.status` «Загружен / Обработка / Требуется проверка / Ошибка» | техника: `stored_file.status` UPLOADED/PROCESSING/PROCESSED/ERROR/SUPERSEDED; проверка: `document.status` RECOGNIZED/NEEDS_CHECK/CONFIRMED/ERROR |

View File

@ -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 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`!). Вход: `consolidation_version_id`. Периметр — `consolidation_member` версии (не текущий `related_group_member`!).
1. Для каждой компании периметра: `document` типа OSV со статусом CONFIRMED за `year/quarter` версии, входящие в 1. Для каждой компании периметра: `document` типа OSV, входящие в `consolidation_document` версии (**явная привязка**, а не
`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)`. 2. Строки `osv_entry` этих документов с `group_type = COUNTERPARTY`; `data_type IN (BU, SUM)`.
3. Строки **не агрегируются**: сто строк по 60.01 у компании — сто строк в своде. `counterparty_name/inn` показываются 3. Строки **не агрегируются**: сто строк по 60.01 у компании — сто строк в своде. `counterparty_name/inn` показываются
как в файле. как в файле.
@ -269,13 +292,35 @@ closing_* = closing_* последней ОСВ (Qn)
проваливается к имени); каждый шаг считает кандидатов отдельно: единственное точное совпадение имени побеждает проваливается к имени); каждый шаг считает кандидатов отдельно: единственное точное совпадение имени побеждает
неоднозначность нормализованного; имя из одних ОПФ (пусто после `normalize`) не ищется. неоднозначность нормализованного; имя из одних ОПФ (пусто после `normalize`) не ищется.
5. Признак контрагента: ГСК / ГК / Внешний (см. `04-data-model-guide.md`). 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): список кандидатов = компании ГСК и ГК; поиск по ИНН/имени/алиасам; **Ручное сопоставление** (алгоритм в DOC-221278; Срез 4, `POST …/osv/entries/{entry_id}/counterparty`): правка версии —
выбор → проверка дубля `normalized_alias` у другой компании (предложить перенести или отменить) → создать право `CONSOLIDATION_EDIT`, статус «в работе» и своя блокировка (§10).
`legal_entity_alias` → `osv_entry.counterparty_id = выбранное`; `counterparty_name/inn` не трогать →
`audit_event(OSV_COUNTERPARTY_MAP)` → автоприменить к другим строкам версии с тем же именем, если `counterparty_id` - **Кандидаты:** `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, а у Наблюдение по фикстурам: у «Счастливочной» контрагент «Русский бисквит» идёт **с ИНН 3528052718** в колонке E, а у
«Русского бисквита» ОСВ без колонки ИНН — контрагент «Пекарня Счастливочная ООО» только текстом → резолв по алиасу. «Русского бисквита» ОСВ без колонки ИНН — контрагент «Пекарня Счастливочная ООО» только текстом → резолв по алиасу.
@ -436,6 +481,21 @@ NEEDS_CHECK`; пройдено/акцептовано + нажато «Подт
Каждый переход → `consolidation_status_history` + `audit_event(CONSOLIDATION_STATUS_CHANGE)`. Каждый переход → `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 (замена файлом целиком, ### 9.2 Файл (`file_status`): UPLOADED → PROCESSING → PROCESSED | ERROR; любой → SUPERSEDED (замена файлом целиком,
`superseded_by_file_id`, `version_no + 1`). Статус меняют только по **текущему** заданию файла `superseded_by_file_id`, `version_no + 1`). Статус меняют только по **текущему** заданию файла
(`current_recognition_job_id`): `PROCESSING` — ack сервиса (или результат `done`/`needs_check`, если ack потерян); (`current_recognition_job_id`): `PROCESSING` — ack сервиса (или результат `done`/`needs_check`, если ack потерян);
@ -450,7 +510,7 @@ INITIATED; в Срезе 2c консолидаций ещё нет, поэтом
(OQ-05 закрыт: `RECOGNIZED` нужен); «Подтвердить данные» → `CONFIRMED`; `ERROR` — зарезервирован (в Срезе 2c ядро (OQ-05 закрыт: `RECOGNIZED` нужен); «Подтвердить данные» → `CONFIRMED`; `ERROR` — зарезервирован (в Срезе 2c ядро
документ в `ERROR` не создаёт: файл без разбираемой ОСВ получает `ERROR` на уровне файла). `counterparty_check`: ИНН шапки документ в `ERROR` не создаёт: файл без разбираемой ОСВ получает `ERROR` на уровне файла). `counterparty_check`: ИНН шапки
равен ИНН владельца файла → `MATCH`; ИНН нет — нормализованное имя шапки совпало с именем или алиасом владельца → `MATCH`; равен ИНН владельца файла → `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; ### 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 мин. 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). заменяемого, заменяемый уходит в `SUPERSEDED` целиком (OQ-06: замена файлом целиком; частный случай — OQ-115).
Аудит: системное `FILE_REPLACE` (`user_id = NULL`, `object_id` — заменённый файл, `after_value` — новый, в `description` Аудит: системное `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` 4. **Подтверждение** (`POST /documents/{id}/confirm`, `DOCUMENT_EDIT`): `RECOGNIZED`/`NEEDS_CHECK` → `CONFIRMED`
(`confirmed_by/at`, `audit_event(DOCUMENT_CONFIRM)`); повтор — 409 `ALREADY_CONFIRMED`; документ заменённого файла — (`confirmed_by/at`, `audit_event(DOCUMENT_CONFIRM)`); повтор — 409 `ALREADY_CONFIRMED`; документ заменённого файла —
409 `FILE_SUPERSEDED`; прочие статусы — 409 `DOCUMENT_NOT_CONFIRMABLE`. Правка строк и корректировки — подсрез 2d/Срез 3. 409 `FILE_SUPERSEDED`; прочие статусы — 409 `DOCUMENT_NOT_CONFIRMABLE`. Правка строк и корректировки — подсрез 2d/Срез 3.
5. **Удаление** (`DELETE /files/{id}`, `DOCUMENT_EDIT`): запрещено при `CONFIRMED` документе 5. **Удаление** (`DELETE /files/{id}`, `DOCUMENT_EDIT`): запрещено при `CONFIRMED` документе
(`FILE_HAS_CONFIRMED_DOCUMENTS`) и если файл заменил другие (`FILE_SUPERSEDES_OTHERS`); `SUPERSEDED` файл удалить (`FILE_HAS_CONFIRMED_DOCUMENTS`) и если файл заменил другие (`FILE_SUPERSEDES_OTHERS`); `SUPERSEDED` файл удалить
можно. Удаляются строки, документы, задания и `stored_file`; объектам S3 ставится `expires_at = now()` (физически не можно. Удаляются строки, документы, задания и `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`); смены статусов 6. **Аудит.** `FILE_UPLOAD`, `FILE_REPLACE`, `FILE_DELETE`, `DOCUMENT_CONFIRM` (раздел `PRIMARY_DOCUMENTS`); смены статусов
задания/файла в обмене и в цикле маппинга `audit_event` не пишут (OQ-55). задания/файла в обмене и в цикле маппинга `audit_event` не пишут (OQ-55).
7. **Порядок блокировок** (против взаимоблокировок): задание → файл → документы; автозамена дополнительно берёт 7. **Порядок блокировок** (против взаимоблокировок): задание → файл → документы (`FOR NO KEY UPDATE`) → advisory-lock ГК →
advisory-lock по ЮЛ. Подробности — `03-architecture.md`. версии консолидации (`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`; редактирование версии — только в Policy-слой (не права): KA_RF/RKA/CLIENT_MANAGER видят только ГК своего `regional_office`; редактирование версии — только в
допустимом статусе и при отсутствии чужой активной `consolidation_lock`; ЗО (`business_segment`) — фильтр видимости. допустимом статусе и при отсутствии чужой активной `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. Матрица в ТЗ помечена «будет разработана» (сноска 29) — seed считать черновиком, редактируется администратором в UI.
### 10.1 Видимость справочников (Срез 1, без проверки кода роли) ### 10.1 Видимость справочников (Срез 1, без проверки кода роли)

View File

@ -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-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-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-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-05 | Нужен ли статус `document.RECOGNIZED` или все идут через `NEEDS_CHECK → CONFIRMED` | DBML | срез 2–3 | **закрыт решением владельца 05.10.2026:** статус `RECOGNIZED` **нужен**: документ после успешного распознавания — `RECOGNIZED`, при нарушениях — `NEEDS_CHECK`, после подтверждения аналитиком — `CONFIRMED` |
| OQ-06 | Замена одного документа внутри многодокументного файла | DBML | срез 3 | решено: замена файлом целиком | | OQ-06 | Замена одного документа внутри многодокументного файла | DBML | срез 3 | решено: замена файлом целиком |
@ -24,7 +24,7 @@
| OQ-16 | Матрица прав (ТЗ сноска 29 «будет разработана»); роли макета (Аналитик ДКБ, Наблюдатель) ≠ ТЗ (ANL-034) | ТЗ 8.1, макет | срез 1 | допущение: seed по §10 правил | | 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-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-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-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-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/` | | 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-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-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-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-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-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`; «Инициация» — только карточка | | 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-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-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-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-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-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 реальных файлах не встречаются | | 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-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. Рекомендация: жёсткий лимит памяти контейнера сервиса; и/или предпроверка объявленного `<dimension>` и потоковый счётчик ячеек `<c>`/строк `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-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. Рекомендация: жёсткий лимит памяти контейнера сервиса; и/или предпроверка объявленного `<dimension>` и потоковый счётчик ячеек `<c>`/строк `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-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-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-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` заполнены | | 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-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-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-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` — архитектурные решения; Служебные идентификаторы в DBML/аналитике: `ANL-nnn` — замечания аналитика к макету; `ARCH-nnn` — архитектурные решения;
`DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4. `DECISION-n` — решения по модели; `FIX-nn` — исправления дефектов v4.

View File

@ -170,13 +170,41 @@ Change'и: `arm-core/openspec/changes/slice-2c-osv-ingest` (issue arm-core#14,
## Срез 4 — Консолидация: карточка, версии, периметр, периоды, сводная ОСВ ## Срез 4 — Консолидация: карточка, версии, периметр, периоды, сводная ОСВ
- `consolidation` / `consolidation_version` / `consolidation_member` / `consolidation_document`; статусы INITIATED → **Статус: реализован в arm-core (бэкенд + API + экспорт xlsx), октябрь 2026; UI — отдельный исполнитель.** Объём по решению
DOCUMENTS_PROCESSING → DATA_CORRECTION; `consolidation_lock`. владельца 06.10.2026: бэкенд, API, экспорт xlsx. Issue arm-core#18, docs#19; спецификация —
- Приведение квартальных ОСВ к нарастающим (§2) как вычисляемое представление. `arm-core/openspec/changes/slice-4-consolidation/` (design D-1…D-16, «Decisions made without discussion» №1–№85),
- Сводная ОСВ (§3) как запрос; признак контрагента; ручное сопоставление с алиасами и автоприменением. документный change — `docs/openspec/changes/slice-4-consolidation/`.
- UI: список консолидаций, карточка, состав, сводная ОСВ с фильтрами и экспортом xlsx.
- Тест: сводная ОСВ группы РБ за 9 мес. 2024: строки «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» → ГСК после одного ручного **Сделано (по факту):**
сопоставления. - 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 — ВГО: автоматическое сопоставление, шахматка, корректировки ## Срез 5 — ВГО: автоматическое сопоставление, шахматка, корректировки

View File

@ -72,12 +72,12 @@
| Шаг | Экран | Действие | БД / статус | | Шаг | Экран | Действие | БД / статус |
|---|---|---|---| |---|---|---|---|
| 2.1 | Сайдбар → **Новая консолидация** [42] | ГК → ГСК (активна после ГК) → Год → Квартал → **Создать**. `scenario = CREATE`, `approach = BASIC`, `goodwill_enabled = false` по умолчанию (OQ-27) | `consolidation`, `consolidation_version(INITIATED)`, `consolidation_member` из текущего состава ГСК | | 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), меню «…»; чекбокс «Только в работе». Выбор строки открывает панель **Организации** (Ответственный / Утверждён в РФ / Верифицирован в ГО; таблица Наименование, Документы, ОФР, Бух. баланс, ОСВ — перечень счетов, ИНН, Тип) | `consolidation_version`, `consolidation_lock`, `consolidation_member`, `document` | | 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] | «Консолидация … будет закреплена за вами для редактирования» | `consolidation_lock`, `audit_event(CONSOLIDATION_LOCK)` | | 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] | фильтр по ГСК, «Показать все ЮЛ в ГК», чекбоксы, Сохранить | `consolidation_member` | | 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]: модалка с зоной «Перетащите файлы сюда» / **Выбрать документы**, Загрузить; **тип документа и период в модалке не выбираются** — определяются при распознавании | `stored_file(UPLOADED)`, `s3_object`, `recognition_job(queued)`, `document.period_check/counterparty_check` → `DOCUMENTS_PROCESSING` | | 2.5 | строка организации → **Документы** → drawer «Первичные документы» [39] | список: документ (имя, формат, дата; подпись типа у прочих — «Прочий документ», «Бухгалтерская отчетность»), Период («2024 К1», «2025 · 9 мес.»), Проверка (✓ / ✗ / — Период, Контрагент), Статус (Загружен / Обработка / Требуется проверка / Ошибка); иконки: комментарий (поповер «Комментарий к документу …», [39-01]) и удалить; **Редактировать документы** — в снимке неактивна; **Загрузить документы** [40]: модалка с зоной «Перетащите файлы сюда» / **Выбрать документы**, Загрузить; **тип документа и период в модалке не выбираются** — определяются при распознавании. **API:** загрузка — `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 | `required_document_rule × consolidation_member − document(CONFIRMED)` | | 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` | | 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 документов без консолидации — первичка в ЕХ напрямую. Монозаёмщики [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.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.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)` | | 3.4 | **Гудвилл** [31-03] | здесь же включается/выключается расчёт гудвилла для консолидации (`goodwill_enabled`; переключателя в снимках нет — решение OQ-27); фильтр «Гудвилл: Все значения»; по головной организации (с «Выделить все» и строкой «Всего»): Учёт в отчёте ☐, Наименование дочерней организации, Доля владения, Фондовые вложения головной организации, Доля участия головной организации в уставном капитале дочерней организации, Уставный капитал, Гудвилл (+/−), Комментарий; **Редактировать** [31-03-01] → «Доступны для редактирования столбцы „Доля владения“ и „Комментарий“» | `consolidation_version.goodwill_enabled`, `goodwill(ownership_share, include_in_kfo, comment)` |

150
dev/api-contract-slice-4.md Normal file
View File

@ -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 <JWT>` (в 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<n>"),
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_<ГК>_<год>-<Qn>_v<n>.xlsx`
(имя percent-кодировано целиком). Забирайте файл как blob с `Authorization` (прямая ссылка `<a href>` не подойдёт). Фильтры — те
же, что у `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`) |

View File

@ -1,4 +1,4 @@
# Гайд тестировщика: локальный стенд АРМ КА (Срезы 0–2c) # Гайд тестировщика: локальный стенд АРМ КА (Срезы 0–2c, 4)
Документ для ручного тестирования текущего `main`. Путь проверен 05.10.2026 на macOS (Docker 28, compose 2.34, Документ для ручного тестирования текущего `main`. Путь проверен 05.10.2026 на macOS (Docker 28, compose 2.34,
Node 24, uv 0.10): от пустого каталога до экрана администратора — около 15 минут, из них 10 — скачивание образов. Node 24, uv 0.10): от пустого каталога до экрана администратора — около 15 минут, из них 10 — скачивание образов.
@ -18,11 +18,14 @@ Node 24, uv 0.10): от пустого каталога до экрана адм
- Загрузка файлов ОСВ через API (`POST /legal-entities/{id}/files`), распознавание реальным `tabular-parser`, разбор - Загрузка файлов ОСВ через API (`POST /legal-entities/{id}/files`), распознавание реальным `tabular-parser`, разбор
результата в документы и строки ОСВ, статусы файла и документа, автозамена версий, подтверждение документа, удаление результата в документы и строки ОСВ, статусы файла и документа, автозамена версий, подтверждение документа, удаление
файла (раздел 6.6). Всё — через Swagger/curl, UI «Первичные документы» — подсрез 2d. файла (раздел 6.6). Всё — через Swagger/curl, UI «Первичные документы» — подсрез 2d.
- Консолидация через API (Срез 4): версии за период, периметр, блокировка «взять в работу», статусы, привязка подтверждённых
документов, чек-лист, сводная ОСВ, ручное сопоставление контрагента, экспорт xlsx (раздел 7). UI консолидации — другой
исполнитель.
**Нет (это не баги):** **Нет (это не баги):**
- Экранов справочников ГК/ЮЛ/ГСК — проверяются только через Swagger (`/docs`) или curl. - Экранов справочников ГК/ЮЛ/ГСК — проверяются только через Swagger (`/docs`) или curl.
- Экрана загрузки и таблицы ОСВ (подсрез 2d), ББ/ОФР, распознавания PDF, консолидации, КФО — следующие срезы. - Экрана загрузки и таблицы ОСВ (подсрез 2d), экранов консолидации (API есть, раздел 7), ББ/ОФР, распознавания PDF, шахматки, КФО — следующие срезы.
- Настоящего входа через SSO банка. Вход — dev-форма по email без пароля. - Настоящего входа через SSO банка. Вход — dev-форма по email без пароля.
## 2. Что установить и проверить из ПО ## 2. Что установить и проверить из ПО
@ -208,7 +211,7 @@ curl -s localhost:8000/api/v1/me -H "Authorization: Bearer $TOKEN"
### 6.3 Импорт из ЕЦП CRM (пользователь `admin`, curl или Swagger) ### 6.3 Импорт из ЕЦП CRM (пользователь `admin`, curl или Swagger)
Файл: `fixtures/directories/ecp-export-russkiy-biskvit.xlsx` (21 строка). Выполняйте на свежем стенде или после Файл: `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) ### 6.6 Загрузка ОСВ и просмотр строк через API (Срез 2c)
Предусловия: раздел 4.2 (ядро с `NATS_ENABLED=true`, tabular-parser на `:8001`), файлы из `../fixtures/osv/`. Выполняйте на Предусловия: раздел 4.2 (ядро с `NATS_ENABLED=true`, tabular-parser на `:8001`), файлы из `../fixtures/osv/`. Выполняйте на
свежем стенде (раздел 8) или с другим ЮЛ: дедуп и автозамена зависят от уже загруженного. Ожидаемое время от загрузки до свежем стенде (раздел 9) или с другим ЮЛ: дедуп и автозамена зависят от уже загруженного. Ожидаемое время от загрузки до
`PROCESSED` — до 15 секунд (опрос публикатора и цикла маппинга по 5 с, парсер — доли секунды). `PROCESSED` — до 15 секунд (опрос публикатора и цикла маппинга по 5 с, парсер — доли секунды).
Подготовка: пользователь `ka_go` (права `DOCUMENT_EDIT`, `DOCUMENT_VIEW`, видит все ЮЛ). ЮЛ должно называться как в шапке ОСВ — Подготовка: пользователь `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` с карточкой файла (системное событие) и `description`, где перечислены документы заменённого файла; U12 — `FILE_DELETE` с карточкой файла
в `before_value`. Смены статусов обмена (`PROCESSING`, `PROCESSED`, `ERROR`) аудитом не фиксируются — так задумано. в `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=<entry_ids[0]>`; `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. - UI только для администрирования; справочники — через API.
- Вход без пароля, открытый `/docs`, CORS `*` — dev-режим. - Вход без пароля, открытый `/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` — каркас, не влияет. - В консоли браузера сообщение `Store does not have a valid reducer` — каркас, не влияет.
- Таблица пользователей показывает первые 200. - Таблица пользователей показывает первые 200.
- При импорте ИНН, записанный в Excel числом с ведущим нулём, теряет ноль и даёт `INVALID_INN`. - При импорте ИНН, записанный в 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». Полный список отложенных находок — Gitea `RSHB/arm-core`, issue «Follow-ups from #5».
## 8. Сброс стенда ## 9. Сброс стенда
```bash ```bash
docker compose -f ../docs/dev/docker-compose.yml down -v # БД и бакет с нуля 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 остаются (им ставится только Файлы, загруженные в 6.6, удаляются из БД командой `DELETE /files/{id}`, но объекты в MinIO остаются (им ставится только
`expires_at`) — `down -v` стирает и их. `expires_at`) — `down -v` стирает и их.
## 9. Как заводить баги ## 10. Как заводить баги
Gitea, репозиторий `RSHB/arm-core` (для документации — `RSHB/arm-ka-docs`). Один дефект — одна issue. Gitea, репозиторий `RSHB/arm-core` (для документации — `RSHB/arm-ka-docs`). Один дефект — одна issue.
@ -351,7 +419,7 @@ Gitea, репозиторий `RSHB/arm-core` (для документации
Вопросы по ожидаемому поведению (не ясно, баг ли) — тоже issue, с пометкой «вопрос» в заголовке. Вопросы по ожидаемому поведению (не ясно, баг ли) — тоже issue, с пометкой «вопрос» в заголовке.
## 10. Типичные проблемы при запуске ## 11. Типичные проблемы при запуске
| Симптом | Причина и решение | | Симптом | Причина и решение |
|---|---| |---|---|

1353
model/data-model-v5.16.dbml Normal file

File diff suppressed because it is too large Load Diff

165
model/entities-v5.16.md Normal file
View File

@ -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`.

View File

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

View File

@ -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 текстом; помечено «производный» |

View File

@ -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 чек-листа.

View File

@ -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).

View File

@ -16,7 +16,7 @@
- NATS subjects и схема ключей S3 — `../docs/context/03-architecture.md`. - 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. - Алгоритмы — только из `../docs/context/05-domain-rules.md`; номер § — в docstring.
- Вычисляемые объекты консолидации — `backend/app/domain/`, без ORM-таблиц. - Вычисляемые объекты консолидации — `backend/app/domain/`, без ORM-таблиц.
- Фронт: пользователь слаб во фронте — для TS/React изменений делай ревью сам и объясняй решения в PR. - Фронт: пользователь слаб во фронте — для TS/React изменений делай ревью сам и объясняй решения в PR.