diff --git a/Makefile b/Makefile index 0cd520e..98b7495 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ PY := $(if $(wildcard $(VENV)/bin/python),$(CURDIR)/$(VENV)/bin/python,python3) CHANGE ?= # Текущая версия модели данных — по ней рисуются диаграммы (make diagrams) -DBML ?= model/data-model-v5.26.dbml +DBML ?= model/data-model-v5.27.dbml # Checkout библиотеки arm_statements (RSHB/pylib_ai_arm_statements) рядом с docs — для statements-orders-check STATEMENTS_DIR ?= ../pylib_ai_arm_statements diff --git a/analyst/eliminations-vgo.md b/analyst/eliminations-vgo.md index d50c3cc..1901711 100644 --- a/analyst/eliminations-vgo.md +++ b/analyst/eliminations-vgo.md @@ -1,7 +1,7 @@ # Элиминация ВГО: правила, расчёт, корректировки Документ для аналитика, 19.10.2026 (RSHB/arm-ka-docs#132; сопоставление — по решениям OQ-07 и OQ-173, #136). Основа — модель данных -[`model/data-model-v5.26.dbml`](../model/data-model-v5.26.dbml) и правила предметной области +[`model/data-model-v5.27.dbml`](../model/data-model-v5.27.dbml) и правила предметной области [`context/05-domain-rules.md`](../context/05-domain-rules.md) §3–§6, §8. Источник истины — они: при расхождении прав DBML и `05-domain-rules.md`, а этот документ поправь следом. Где написано наше прочтение ТЗ, а не прямая цитата, это помечено; такие места собраны в раздел 6 вопросами. diff --git a/context/00-workspace-layout.md b/context/00-workspace-layout.md index b2954cf..fc3a4cb 100644 --- a/context/00-workspace-layout.md +++ b/context/00-workspace-layout.md @@ -76,7 +76,7 @@ git remote add origin && git push -u origin main | Слой | Где | Статус | Правило для агента | |---|---|---|---| -| Модель данных | `docs/model/data-model-v5.26.dbml` | стабильный, версионируется | единственный источник имён и структур | +| Модель данных | `docs/model/data-model-v5.27.dbml` | стабильный, версионируется | единственный источник имён и структур | | Правила домена | `docs/context/05-domain-rules.md` | курируемый — пишет человек/агент по итогам ревью | **единственный вход алгоритмов в код**; из ТЗ напрямую не кодируем | | Открытые вопросы | `docs/context/07-open-questions.md` | живой реестр | каждое допущение в коде — `ASSUMPTION(OQ-nn)` | | Экспорт ТЗ | `docs/source/tz/` | снимок на дату из `docs/source/tz/_export.md` | читать для контекста; расхождение с §05 → вопрос, не код | diff --git a/context/04-data-model-guide.md b/context/04-data-model-guide.md index 8969763..4ea9834 100644 --- a/context/04-data-model-guide.md +++ b/context/04-data-model-guide.md @@ -1,8 +1,8 @@ -# 04. Как читать модель данных v5.26 +# 04. Как читать модель данных v5.27 -Файлы: `docs/model/data-model-v5.26.dbml` (источник истины), `docs/model/entities-v5.26.md` (смысл таблиц; предыдущие +Файлы: `docs/model/data-model-v5.27.dbml` (источник истины), `docs/model/entities-v5.27.md` (смысл таблиц; предыдущие версии `data-model-v5.13…v5.20.dbml` и `entities-v5.13…v5.20.md` остаются как история), -`docs/model/diagrams/part01…10.{svg,png}` (диаграммы v5.26 по частям из таблицы ниже; перерисовываются из DBML командой +`docs/model/diagrams/part01…10.{svg,png}` (диаграммы v5.27 по частям из таблицы ниже; перерисовываются из DBML командой `make diagrams` в корне репозитория docs — скрипт `scripts/gen_model_diagrams.py`, нужен npx; при новой версии DBML обновить `DBML` в Makefile, а новую таблицу — добавить в раскладку частей скрипта и в эту таблицу). @@ -21,6 +21,8 @@ | 09 Комментарии, аудит, ЕХ | `comment`, `audit_event`, `ex_snapshot`, `ex_snapshot_member`, `ex_snapshot_item` | полиморфные комментарии, append-only журнал, замороженные срезы | | 10 Метрики качества первички | `quality_intake_fact`, `quality_outcome_fact`, `quality_failure_fact` | факты распознавания и исхода документа, технические отказы — append-only, без FK | +**v5.27:** КФО: ОФР и хранение (arm-core PR #251, миграция `0014_dbml_v5_27`): `vgo_assignment.side` (`D`/`K` для ОФР — независимые решения по позициям «строка ОСВ + сторона», `NULL` для ББ; §5 п. 2b); незамороженный результат КФО не хранится — до «Верифицирован» в `kfo_result`/`kfo_result_line` лишь ручные поля (комментарии строк, обоснование порога), полный снимок пишет заморозка (§8 «КФО» п. 12, OQ-192); `correction.ordinal` не вводится. + **v5.25:** три таблицы фактов качества распознавания (`quality_intake_fact`/`_outcome_fact`/`_failure_fact`), `s3_bucket_role.raw`, `stored_file.is_scan`, `recognition_job.raw_s3_object_id`, `permission_object.QUALITY_METRICS` (AURORA-1759). diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index aa22a96..9a35ebb 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -667,15 +667,38 @@ closing_* = closing_* строки Qn Правило 5 % для пар: суммы равны → маппим; расхождение ≤ 5 % (к меньшей) → берём `min`; > 5 % → подсветка, авто не маппится, ждёт корректировки (`ADJUSTMENT`). -ОФР (`statement_type = income`, `measure = turnover`), из FNR03 — **[открыто]**: формализовать с методологами: +ОФР (`statement_type = income`, `measure = turnover`, `valid_from = 2024-01-01`; версия 1 — seed +`arm-core/api/seed/core/elimination_rules_income_v1.yaml`; источники — DOC-274230 «Правила пар счетов», раздел ОФР, и +DOC-277424 «Консолидированный ОФР», 09.11.2026; AURORA-1999). Суммы — обороты `turnover_debit` / `turnover_credit`, а не +сальдо. Позиция расчёта — «строка ОСВ + сторона D/K» (§5 п. 2b); у правила сторона 1 берётся по направлению, сторона 2 — +всегда по Кт: -| Статья | Сторона А | Сторона Б | mode / НДС | -|---|---|---|---| -| 2110 Выручка | Кт 90.01 (с НДС, приоритет) либо Дт 62.01/62.21/62.31 по контрагенту | Кт 60.01/60.21/60.31 + 76 (кроме 76.07.*, 76.03.*, плюс 76.07.2) | `full` — берётся сторона А; без НДС = с НДС / (1 + средневзвешенная ставка/100) | -| 2120 Себестоимость | — | — | элиминация = ВГО по 2110 (зеркально, без НДС) | -| 2310 Доходы от участия | Дт 76.03 | Кт 76.03 | сторона А | -| 2320 Проценты к получению | Дт 76.03/76.05/76.08/76.09 | Кт 66.02/.04/.06/.22/.24/.26, 67.02/.04/.06/.22/.24/.26 | min | -| 2330 Проценты к уплате | Кт чётных субсчетов 66/67 + 76.07.5/76.08/76.28/76.38 | Дт 76.03/.05/.08/.09 | min | +| № | Статья 1 / статья 2 | Сторона 1 (компания 1) | Сторона 2 (компания 2) | mode | Что исключается | +|---|---|---|---|---|---| +| 1 | 2110 / 2120 | Кт 90.01 | Кт 60.01, 60.21, 60.31, 76 за вычетом 76.03 и 76.07.1, 76.07.5, 76.07.9 (сверка; 62.01/62.21/62.31 компании 1 — тоже только сверка, в seed не заведены) | `full` | сумма Кт 90.01 компании 1 по компании 2 **без НДС**; 2110 компании 1 и 2120 компании 2; `min` не применяется, сверка со счетами 60/62/76 информационная | +| 2 | 2120 / 2110 | Кт 90.01 | — (счетов нет) | `full` | вторая сторона операции по правилу 1: та же сумма, собственной автосуммы нет | +| 3 | 2310 / 2310 (заглушка) | Дт 76.03 | Кт 76.03 (сверка) | `full` | Дт 76.03 компании 1 по компании 2 из 2310 получателя; у компании 2 автоисключения нет, `min` не применяется | +| 4 | 2320 / 2330 | Дт 76.03, 76.05, 76.08, 76.09 | Кт 66.02/.04/.06/.22/.24/.26, 67.02/.04/.06/.22/.24/.26, 76.07.5, 76.08, 76.28, 76.38 | `min` | `min(ΣA, ΣB)` **обычный, со знаком**, если есть позиции обеих сторон (сторона с нулём — тоже данные); 2320 кредитора и 2330 заёмщика; без встречной стороны автоисключения нет — ручной разбор | +| 5 | 2330 / 2320 | Кт тех же счетов 66/67/76 | Дт 76.03/.05/.08/.09 | `min` | зеркало правила 4 (сторона заёмщика той же операции), собственной автосуммы нет | +| 6 | 2340 | — | — | — | **только вручную** (КА при необходимости); в seed не заведено | +| 7 | 2350 | — | — | — | **только вручную**; в seed не заведено | + +Агрегаты ОФР строятся по основному правилу: 1 и 2 → правило 1, 3 → 3, 4 и 5 → 4; автосумму дают только 1, 3 и 4. Ключ +агрегата — версия × правило × компания 1 × компания 2, направленный (А → Б и Б → А — разные агрегаты); `side` в ключ не +входит. + +**Выручка и НДС (правило 1, DOC-277424 §4).** Общие по компании 1 за период: `V_TOTAL` — Кт 90.01, `VAT_TOTAL` — Дт 90.03, +`R_2110` — статья 2110 формы. `V_TOTAL` сравнивается с 2110 с точностью представления формы (форма в тыс. руб. — после +округления `V_TOTAL` до тысяч; в расчёте остаётся полный оборот). Равны → оборот без НДС, сумма агрегата как есть. `V_TOTAL` +больше 2110 и `V_TOTAL ≠ 0` → `K = VAT_TOTAL / V_TOTAL` (эффективная доля, не ставка), сумма `S = V_AB × (1 − K)`, где +`V_AB` — Кт 90.01 по покупателю из периметра. Иначе автоисключения нет. Акцизы не учитываются; сумма по 2120 покупателя не +ограничивается самой статьёй. В коде — `domain/kfo.revenue_exclusion`; `vat_handling = check_90_01_vs_2110` у правила — только +метка (код поле не читает). + +**Ограничение seed (правило 1, компания 2).** Субсчета 76.07 вычитаются раскрытием масок (включённые минус исключённые), +поэтому в исключениях стоят `76.03` и `76.07.1`, `76.07.5`, `76.07.9`; 76.07.2 остаётся в счетах правила. Голый `76.07` +(без субсчёта) остаётся выбранным через маску `76` — на практике такие строки в ОСВ редки; это ограничение стороны сверки, +на автосумму правила 1 (она берётся с компании 1) не влияет. Дополнительные ручные корректировки ОФР (ТЗ 6.4, `correction.reason_code`): нереализованная выручка, акцизы (`osv_entry.is_excisable` для 90), агентское вознаграждение, внутригрупповая амортизация по аренде, проценты по @@ -686,8 +709,8 @@ closing_* = closing_* строки Qn ## §5. Автоматическое сопоставление ВГО и ручное распределение строк (DOC-223044, DOC-272613; OQ-07, OQ-173 — решения владельца 19.10, 21.10 и 23.10.2026; AURORA-1751/1540/1541 — реализовано, слито в main) Границы: только `closing_debit` / `closing_credit`; только компании периметра версии; правила и счета сторон — из §4 -(версия правил — `consolidation_version.elimination_rules_version`), только правила ББ (`statement_type = balance`, -`measure = closing`; правило с другим показателем — ошибка `INVALID_RULE`); строки `group_type = COUNTERPARTY`, +(версия правил — `consolidation_version.elimination_rules_version`), в пп. 1–2a — только правила ББ (`statement_type = balance`, +`measure = closing`; правило с другим показателем — ошибка `INVALID_RULE`); ОФР — п. 2b; строки `group_type = COUNTERPARTY`, `data_type ∈ (BU, SUM)`. Единица расчёта — **тройка** (правило r, компания A, компания B). Правила считаются для каждой упорядоченной пары компаний — и (A, B), и (B, A); поэтому зеркальные правила № 9–16 Прил. 3 в seed не нужны (№ 14 для (A, B) — это № 6 для (B, A)). @@ -774,8 +797,29 @@ AURORA-1751; эндпоинт `GET …/vgo-assignments/distribute`, домен ` - «не учитывать в КФО» (`is_exclude`) — **только у нераспределённой строки** (строка агрегата так не отмечается: её сначала убирают из правила — `UNASSIGNED`, — и уже нераспределённую отмечают); CHECK в БД запрещает `is_exclude = true` одновременно с `rule_id`; - - поле `side` (черновик `dev/api-contract-corrections.md` предполагал его для строки с одновременно дебетовым и - кредитовым сальдо) в реализации нет — решение одно на обе позиции строки. + - у **ББ** `side = NULL`: решение одно на обе позиции строки (сальдо). У **ОФР** (DBML v5.27) решение хранится по + позиции «строка + `side`» — п. 2b. + +**2b. Разнесение ОФР по позициям D/K (DOC-277424 §3; `statement_type = income`; `GET …/vgo-assignments/distribute? +statement_type=income`; домен `api/src/domain/vgo.py`, `VgoService`).** Из каждой контрагентской строки свода +(`group_type = COUNTERPARTY`, БУ/СВОД, счёт определён, владелец и контрагент — разные компании периметра) независимо +получаются две **позиции**: `(osv_entry_id, D)` с суммой `turnover_debit` и `(osv_entry_id, K)` с суммой `turnover_credit`; +`NULL` — нет значения (причина `MISSING_AMOUNT`), 0 и отрицательные числа сохраняются. Назначение одной позиции другую не +затрагивает; одна позиция входит не более чем в один агрегат (`DUPLICATE_VGO_POSITION` — внутренняя ошибка расчёта). +1. ручное решение (`vgo_assignment` с `side`) важнее автоматики; `ASSIGN` проверяется на допустимость правила для счёта, + направления и компаний (`INVALID_VGO_POSITION`), иначе позиция идёт на проверку (`MANUAL_REVIEW_REQUIRED`); +2. без ручного решения — правило определяется по счёту и направлению (роль компании 1/2 по §4); единственное + подходящее правило назначает позицию даже без встречной; несколько — нераспределённая (`MULTIPLE_RULES`), ни одного — + `NO_RULE`; `UNASSIGNED` — снята вручную; исключённая вручную в расчёте не участвует; +3. агрегаты — по основному правилу (§4: 1 и 2 → 1, 4 и 5 → 4, 3 → 3); сторона 1 — позиции компании 1 по её направлению + (правило 1 — K, остальные — D), сторона 2 — K компании 2; +4. решения аналитика: `ASSIGN` (`rule_id`, `side`), `UNASSIGNED`, `is_exclude` — как у ББ (п. 2a), но все операции + принимают **обязательный `side`**; запрос без `side` у ОФР — 422 `INVALID_VGO_SIDE`, у ББ `side` указывать нельзя. + Смена разнесения ОФР **не переводит корректировки в `DRAFT`** (решение R-4 OpenSpec `kfo-calculation`, DOC-277424 §3.5 + п. 5: применённые ручные корректировки сохраняют приоритет); у ББ поведение прежнее. Исключение уже назначенной позиции + — `UNASSIGNED` + `is_exclude`; «Сбросить» (`reset`) удаляет решение. Распределить сумму одного оборота по нескольким + статьям можно только корректировкой стороны агрегата (`correction`), не дроблением позиции. +Знаки и точность (§3.7 DOC-277424): суммы со знаком, `min` — обычный (`min(−100; −80) = −100`), автосумма — до двух знаков. Сумма стороны агрегата здесь — Σ всех её строк (п. 1 + п. 2 + ручные), включая односторонние (п. 3) и перенесённые вручную; агрегат целиком уходит в КФО, лишнюю строку убирают из правила, а не из агрегата (ответ Лысенко п. 4). Аудит @@ -813,7 +857,7 @@ min = 106 311 116,46 (до 10.11.2026, с 76.АВ: ΣA = 110 429 113,15, 3,87 %) расхождение); при ручном выборе забалансовые счета допускаются. Дополнительно пресетом может быть **правило ББ из §4** (`statement_type = balance`): дебетовые и кредитовые счета берутся из сторон правила (с исключениями `is_excluded`), показатель — `measure` правила (у всех правил ББ seed v1 — `closing`); каскада §5 нет — это просмотр сумм по счетам правила, не сумма элиминации (решение владельца -21.10.2026, arm-core #141). Правила ОФР пресетом не являются (ОФР в §4 открыт). +21.10.2026, arm-core #141). Правила ОФР пресетом не являются (они описаны в §4 и считаются по оборотам в агрегатах §5 п. 2b). Шахматка — суммы первички: корректировки (§8) в ячейках не учитываются, список «строк без пары» шахматка не даёт — строки ОСВ ячейки — AURORA-1653 (`osv_entry_ids_*` ячейки), строки без пары — упаковка §5 п. 2 (AURORA-1536). @@ -976,8 +1020,10 @@ OQ-145); тем же правилом сервисы разрешают ничь - **Автосумма агрегата** (`eliminated_amount`) — по режиму правила, **одной функцией домена с КФО** (блок «КФО», п. 4): `min` — меньшая сторона в допуске, иначе 0; `manual` — 0; `min_or_full` (правило 2 после правки seed в AURORA-1546) — меньшая сторона при обеих > 0, `amount_1` при пустой стороне 2, порога нет: `exceeds_threshold = false`, статус - `AUTO` при обеих сторонах, `ONE_SIDED` при пустой стороне 2 (`eliminated_amount = amount_1`). Сейчас - `services/correction_source.py` знает только `manual` и «остальное как `min`» — правится вместе с seed. + `AUTO` при обеих сторонах, `ONE_SIDED` при пустой стороне 2 (`eliminated_amount = amount_1`). Функция — + `domain/corrections.automatic_elimination`, общая для таблицы корректировок и КФО (ББ; у ОФР таблица автосумму не + считает, см. блок «КФО», п. 4, I-2). Для ОФР режимы `full` и `min` и + отсутствие автосуммы у правил 2 и 5 — блок «КФО», п. 4. - **Порог отклонения агрегата** (`exceeds_threshold`, не путать с контролем выше): `deviation_percent = |ΣA − ΣB| / min(ΣA, ΣB) × 100` сторон агрегата (§5, п. 1/2a); реализовано **строго больше 5 %** (R-35, `domain/corrections.py`, `arm-core#215`/Task 8) — ровно 5,00 % в допуске. ТЗ/DOC-272613 читались как «от 5 % включительно»; аналитик на это не @@ -1012,10 +1058,10 @@ OQ-145); тем же правилом сервисы разрешают ничь - **КФО** — блок «КФО» ниже, п. 8: ветка гудвилла идёт по паре этой таблицы независимо от агрегатов корректировок; у пары с `use_goodwill` — ГО `1170` на ФВ, ДО `1310` на P, строка `GOODWILL` на G, агрегат правила 2 той же пары не применяется; при `G ≤ 0` или выключенном `goodwill_enabled` — обычный агрегат правила 2. Изменение ввода, - переключателя или источников делает результат КФО устаревшим. + переключателя или источников видно в КФО сразу: пока версия не заморожена, КФО считается на каждом чтении (п. 12). -**КФО** (ТЗ 6.4, 6.8, Прил. 10; AURORA-1546 — расчёт, AURORA-1547 — экран, AURORA-1417 — пересчёт; ещё не реализовано; -контракт — `dev/api-contract-kfo.md`). Источники, в порядке приоритета: DOC-274230 «Правила пар счетов» (Лысенко, +**КФО** (ТЗ 6.4, 6.8, Прил. 10; AURORA-1546 — расчёт, AURORA-1547 — экран, AURORA-1417 — заморозка; ОФР — AURORA-1999; +реализовано в arm-core PR #251, OpenSpec `kfo-calculation`; контракт — `dev/api-contract-kfo.md`). Источники, в порядке приоритета: DOC-274230 «Правила пар счетов» (Лысенко, 27.10.2026), DOC-259025 «Гудвилл» (03.11), DOC-272613 «Корректировка данных агрегатом» (04.11); черновик команды DOC-274636 «Расчёт КФО» и DOC-274647 «Экран: КФО» — там, где страниц Лысенко нет (решение владельца 28.10.2026: страницы аналитика важнее черновика). Развилки, которые решает заказчик, закрыты вариантом черновика как стартовым и помечены @@ -1055,7 +1101,11 @@ OQ — менять по ответу. | `min` (ББ 3–8) | `min(amount_1, amount_2)`, если обе стороны > 0 и отклонение в допуске (порог — блок «Корректировка», OQ-178); иначе 0 | `article_1_line` компании 1, `article_2_line` компании 2 | | `min_or_full` (ББ 2) | `amount_1 ≤ 0` → 0; иначе `amount_2 > 0` → `min(amount_1, amount_2)`, `amount_2 ≤ 0` (пусто или встречная позиция) → `amount_1`; порога нет (DOC-274230, правило 2) | `article_1_line` компании 1; компании 2 — каскадом по `article_2_line` | | `manual` (ББ 1) | 0 — только корректировка | — | - | режимы ОФР | по правилам ОФР (DOC-274230, AURORA-1999, §5); до неё агрегатов ОФР нет — `INCOME_RULES_MISSING` | по правилу | + | `full` (ОФР 1, 3) | **правило 1** — сумма Кт 90.01 компании 1 по компании 2 **без НДС** (§4: сравнение Кт 90.01 с 2110, при превышении — `× (1 − Дт 90.03 / Кт 90.01)`; иначе автоисключения нет), `min` не применяется; **правило 3** — вся сторона 1 (Дт 76.03 компании 1), встречная сторона и расхождение на сумму не влияют | правило 1: 2110 компании 1 и 2120 компании 2; правило 3: 2310 компании 1, у компании 2 автоисключения нет | + | `min` (ОФР 4) | `min(amount_1, amount_2)` — **обычный, со знаком**, без порога 5 %, если присутствуют позиции обеих сторон (сумма 0 — данные, `min` с ней считается); нет встречных позиций — автосуммы нет, ручной разбор | 2320 компании 1 (кредитор), 2330 компании 2 (заёмщик) | + | ОФР 2, 5 | собственной автосуммы **нет**: правило 2 — вторая сторона операции правила 1, правило 5 — зеркало правила 4; агрегаты строятся по основному правилу (§5 п. 2b) | — | + | ОФР 6, 7 | не заведены — только ручная корректировка (§4) | — | + | нет правил ОФР у версии (база до seed) | агрегатов ОФР нет; ББ считается как прежде, ОФР — без исключений, `INCOME_RULES_MISSING` | — | **Каскад** (составная статья «a/b/c»): сумма снимается со строки `a` компании в пределах её доступного остатка («по данным клиента» строки в рублях минус уже снятое с неё другими агрегатами, не ниже 0), остаток — со строки `b`, @@ -1069,14 +1119,20 @@ OQ — менять по ответу. `article_2_line: "1310/1350/1370"`. **Правило 1 остаётся `manual`**: по DOC-274230 обе его суммы — у компании 2 (Кт 76.7.1 − Дт 76.7.9 с каскадом 1150 → 1450 → 1550), а сторона агрегата устроена «компания 1 — сторона 1, компания 2 — сторона 2»; автосумма правила 1 — после переработки seed (OQ-193), до того — корректировкой. + Правила ОФР 1–5 версии 1 — отдельный seed `elimination_rules_income_v1.yaml` (§4, идемпотентный: повторный seed + ничего не дублирует и правила ББ не меняет). - Автосумма — **одна функция домена** для таблицы корректировок и КФО (блок «Корректировка», `eliminated_amount`). + Автосумма — **одна функция домена** для таблицы корректировок и КФО (блок «Корректировка», `eliminated_amount`) — + **только для ББ**. Для ОФР (I-2, отложено): в таблице корректировок `eliminated_amount`/`status` не вычисляются + (0 / `MANUAL`), порог 5 % (`exceeds_threshold`) к ОФР не применяется; автосуммы ОФР (правило 1 — выручка без НДС, + правило 3 — вся сторона 1, правило 4 — `min` со знаком) считает только источник КФО (`KfoIncomeSource`) и показывает + только КФО. Единая функция для таблицы и КФО по ОФР — после доработки. 5. **Корректировка заменяет автосумму.** Применённая корректировка агрегата отменяет его автосумму целиком и к ней не прибавляется (DOC-274636, допущение 4; DOC-272613: корректировки не меняют суммы сторон). Исключения: компания 1 — статьи `debit[]` (основная, затем дочерние), компания 2 — `credit[]`, суммы как введены. Если заполнены обе стороны и их итоги разные, каждая сторона исключает **меньший** итог (ответ Лысенко п. 8): снимается со статей стороны по порядку — основная, затем дочерние в порядке ввода — до исчерпания. Пустая сторона ничего не исключает, другая — как - введена (одностороннее правило 1, правила ОФР 1–3). + введена (одностороннее правило 1, правила ОФР 1 и 3). 6. **«В т. ч. взаимные» по компании (`vgo_amount`).** Σ исключений компании по строке в рублях → тыс. руб., **до целых тысяч, половина — от нуля** (`ROUND_HALF_UP` по модулю), со **знаком строки**: у строк расходов (п. 2, они ≤ 0) `vgo_amount` отрицательный. На всех строках одно равенство `cleaned_amount = client_amount − vgo_amount`; исключение @@ -1092,6 +1148,10 @@ OQ — менять по ответу. `goodwill_adjust` (у `GOODWILL` и итогов, куда он входит), **`bank_amount = sum_amount − vgo_amount + goodwill_adjust`** (`kfo_result_line.consolidated_amount`). + **Ручная корректировка ОФР** заменяет автоисключение **только своей стороны** агрегата (DOC-277424 §7.2–7.3): сторона 2 + сохраняет автосумму или свою корректировку; суммы строк корректировки ОФР допускают отрицательные значения (у ББ — только + неотрицательные). Знак расходных строк ОФР (2120, 2210, 2220, 2330, 2350) соблюдается: исключение приближает значение к нулю. + **Итоговые строки** (1100, 1200, 1300, 1400, 1500, 1600, 1700; 2100, 2200, 2300, 2400, 2500): «по данным клиента» — итог из формы компании, а если строки итога в форме нет (упрощённая форма, `form_variant = simplified`) — Σ «по данным клиента» его слагаемых; «в т. ч. взаимные» — Σ `vgo_amount` слагаемых **с коэффициентом +1** (знак уже в @@ -1099,7 +1159,9 @@ OQ — менять по ответу. 2120; 2200 ← 2100 + 2210 + 2220; 2300 ← 2200 + 2310…2350; 2400 и 2500 — слагаемые по формулам §7 своей редакции (66н / ФСБУ 4), но **без вычитания** (формулы §7 с минусом к КФО не применяются); строки «в т. ч.» (2421 и подобные) — не слагаемые; `goodwill_adjust` — Σ слагаемых (то есть G у 1100 и 1600); очищенные — разность. Итог КФО — подписанный - клиентом итог минус взаимные, а не сумма своих строк (OQ-183). Доли компаний у 1100/1600 в сумме меньше 100 % на долю + клиентом итог минус взаимные, а не сумма своих строк (OQ-183): у итога `cleaned_amount = client_amount − vgo_amount`, а + `bank_amount = sum_amount − vgo_amount + goodwill_adjust` — на **всех** строках, включая итоговые ББ и ОФР (если подписанный + в форме итог ОФР 2200 не равен сумме слагаемых, расхождение остаётся и не «чинится» пересчётом, R-10). Доли компаний у 1100/1600 в сумме меньше 100 % на долю гудвилла — у него нет разреза по компаниям. `deviation_amount` у итоговых строк — `null`. 8. **Гудвилл** (DOC-259025, блок «Гудвилл» выше). По паре ГО → ДО таблицы гудвилла с `use_goodwill` при `goodwill_enabled` версии: исключения ГО `1170` на ФВ (все строки 58.01 и потомков пары, как в таблице гудвилла), @@ -1133,16 +1195,23 @@ OQ — менять по ответу. итогов, тыс. руб.), причины отклонений от Методики (`consolidation_version.methodology_deviations` — ввод на вкладке «КФО»), решение УОБ (номер и дата — только чтение). «Подход по Методике» и «согласовано клиентом» — не делаем до ответа заказчика (OQ-190, OQ-191). -12. **Пересчёт и заморозка.** Результат хранится у версии (`kfo_result`, одна запись). Пока версия не заморожена, - чтение отдаёт результат по текущим данным: если входы изменились после `computed_at`, сервер пересчитывает и - сохраняет до ответа (как обнаруживать изменения — AURORA-1417). Кнопки «Рассчитать» нет (OQ-192). Комментарии строк - и обоснование переносятся при пересчёте по (тип отчёта, код строки). При «Верифицирован» `is_frozen = true` (ставит - AURORA-2002) — дальше результат только читается. **Предупреждения** `{code, message, legal_entity_id?, - statement_type?, count?}` не блокируют расчёт; они и сводка корректировок — часть результата и **хранятся в - `kfo_result`** (`warnings`), чтобы замороженная версия отдавала то, что видели при верификации. Коды: +12. **Расчёт, хранение и заморозка** (решение владельца 13.11.2026, R-1; OQ-192). **Пока версия не заморожена, результат + не хранится:** каждое чтение (`GET …/kfo`) считает таблицу заново по текущим данным (формы, привязанные документы, + сводная ОСВ, агрегаты таблицы корректировок, гудвилл), `computed_at` — время запроса; признака «результат устарел» и + отдельного запроса состояния нет (`GET …/kfo/state` и поле `data_changed` убраны, R-3). Кнопки «Рассчитать» нет. + **В БД до заморозки — только ручные поля:** `kfo_result` (шапка с обоснованием порога, без расчётных сумм), + `consolidation_version.methodology_deviations` и строки `kfo_result_line` лишь у строк с комментарием + (`consolidated_amount = 0`, суммы не заполнены, строк по компаниям нет; снятие комментария строку удаляет). + **При «Верифицирован»** (переход статуса, `services/kfo.freeze`, одна транзакция с переходом) таблица считается по + текущим источникам, поверх неё накладываются ручные поля (комментарий к 1520, записанный до верификации, остаётся у + 1520, строка одна), полный снимок — все строки и разрезы по компаниям, `warnings`, сводка корректировок — пишется и + `is_frozen = true`; дальше `GET` читает сохранённое без пересчёта и первички, повторная заморозка не пересчитывает. + **Предупреждения** `{code, message, legal_entity_id?, statement_type?, count?}` не блокируют расчёт; у замороженной + версии они хранятся в `kfo_result.warnings`, чтобы версия отдавала то, что видели при верификации. Коды: `FORM_MISSING`, `FORM_AMBIGUOUS`, `FORM_UNITS_UNKNOWN`, `LINE_OUTSIDE_BLANK`, `UNALLOCATED_ROWS`, `INCOME_RULES_MISSING`, `GOODWILL_NOT_APPLIED`, `GOODWILL_ROW_REASSIGNED`, `CORRECTION_SUPERSEDED_BY_GOODWILL`. - Правка ручных полей — аудит `KFO_EDIT`. + Правка ручных полей — аудит `KFO_EDIT`. Допущение: блокировка версии на чтении (`FOR UPDATE`) остаётся — расчёт и + заморозка не идут параллельно с переходом статуса (R-9); время чтения замеряется на стенде. **Пример** (тыс. руб., если не сказано иное; те же числа — в `examples` контракта). Три компании: А «Альфа», Б «Бета», В «Гамма»; отчётная дата 01.10.2025 (ФСБУ 4). Агрегаты, рубли: diff --git a/context/07-open-questions.md b/context/07-open-questions.md index 0b1aeb3..598ff96 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -14,7 +14,7 @@ | OQ-06 | Замена одного документа внутри многодокументного файла | DBML | срез 3 | решено: замена файлом целиком | | OQ-07 | Матчинг ВГО: одна строка = одна позиция (DOC-223044) или агрегировать по (компания, контрагент, счёт) (FNR03)? | DOC-223044 vs FNR03 | срез 5 | **закрыт решением владельца 19.10.2026 (docs #136):** внутри тройки «правило × компания A × компания B» строки сопоставляются группами **1:1, 1:N, N:1** (правило охватывает несколько счетов), N:M нет; выбирается **минимальная упаковка** — меньше строк (закрывается и 1:2, и 1:3 → 1:2); закрытие — точно или ≤ 5 % к меньшей; N ≤ 5; ничья → ручное. Упаковка — детализация; в КФО — агрегат min(ΣA, ΣB) по тройке, на него способ упаковки не влияет. Алгоритм — `05-domain-rules.md` §5 | | OQ-08 | Знак порога 5 % в правиле пары (≤ или <), и к чему считать % — к меньшей сумме (Методика сноска 14) | FNR03 (знак потерян) | срез 5 | допущение: `≤ 5 %` к меньшей | -| OQ-09 | Правила элиминации ОФР (2110/2120/2310/2320/2330): точный состав субсчетов, источник средневзвешенной ставки НДС, выбор субсчетов «работником ПА самостоятельно» | FNR03 | срез 6 | не реализовывать авто-элиминацию ОФР до решения; только ручные `correction` | +| OQ-09 | Правила элиминации ОФР (2110/2120/2310/2320/2330): точный состав субсчетов, источник средневзвешенной ставки НДС, выбор субсчетов «работником ПА самостоятельно» | FNR03 | срез 6 | **допущение принято (AURORA-1999, arm-core PR #251):** правила ОФР 1–5 версии 1 заведены seed по DOC-274230 и DOC-277424 (Лысенко, 09.11.2026), §4 и §8 «КФО» п. 4. НДС правила 1 — по DOC-277424 §4.3: эффективная доля `Дт 90.03 / Кт 90.01` по общим оборотам компании 1, применяется, только если Кт 90.01 больше 2110 формы (с точностью её представления), а не средневзвешенная ставка; сумма — Кт 90.01 по покупателю без НДС, `min` не применяется. Правила 6 (2340) и 7 (2350) — только вручную. Ограничение seed: голый `76.07` у компании 2 правила 1 остаётся в сверке (маска 76). Подтверждение заказчика не получено | | OQ-10 | Гудвилл: входит ли стр. 1350 (добавочный капитал) в базу участия; УК из ББ (1310) или из ОСВ 80/83 | DBML, DOC-259025, FNR03 | срез 6 | допущение: 1310 из ББ; 1350 — настраиваемый флаг, по умолчанию выключен. **06.11:** DOC-259025 (Лысенко, 03.11) — «для УК используется 1310», 1350 в формулу не входит; так реализовано (arm-core #222), флага нет; подтверждение методологов (AURORA-1369) не получено | | OQ-11 | Строка «ППА» в КФО (правило № 1, когда у А нет ОСВ 03) — что это и куда | FNR03 | срез 6 | не реализовывать | | OQ-12 | Формулы ОФР: знаки 2410/2420/2430 по ФСБУ 4/2023 vs 66н; состав 1100 (1120–1140) | DOC-221281 | срез 3 | допущение: формулы §7, строки без значения = 0; **06.11.2026 закрыт в части 2410** (AURORA-1864, решение владельца 27.10.2026): знак 2410 не определяется, соотношения 2400 — группа альтернатив §7 (`2300 − 2410` и `2300 + 2410`); 2420/2430 и состав 1100 — открыты; **расширение 07.10.2026:** формулы и справочники по редакциям форм (`66n` / `fsbu4`) — OQ-146 | @@ -197,10 +197,11 @@ | OQ-189 | Что делает превышение порога: только подсветка, обязательное обоснование до отправки на верификацию, только до финала или блокировка верификации? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», вопрос 13; DOC-037736 («НЕ ТОЧНО»); Excel В-01 | §8 «КФО» п. 10; AURORA-2002/1414 | открыт, вопрос заказчику; допущение принято (docs #206): обоснование обязательно до отправки на «Верификацию РФ/ГО» и до финала; проверяет переход статуса | | OQ-190 | «Подход» в реквизитах КФО: пакет документов (`approach` BASIC/EXTENDED, ТЗ 5.1) или подход по Методике (сноска 15: «по Методике / индивидуальный / принята аудированная отчётность»)? Для второго нужно новое поле версии | DOC-274636 «Расчёт КФО» (черновик), «Что решить», вопрос 14 | §8 «КФО» п. 11 | открыт, вопрос заказчику; допущение принято (docs #206): показывается только `approach`; поле «подход по Методике» не заводим до ответа | | OQ-191 | Где фиксируется согласование КФО клиентом (Методика 5.8, 6.1): поле версии «да / нет / не направлялось» с датой или не вести в АРМ? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», вопрос 15 | §8 «КФО» п. 11 | открыт, вопрос заказчику; допущение принято (docs #206): не ведём до ответа (вариант черновика — поле версии) | -| OQ-192 | Пересчёт КФО: автоматически после каждого изменения (Excel Р-11, Р-23), по кнопке «Рассчитать КФО» с пометкой «устарела» (FNR03) или только при открытии? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», вопрос 16; AURORA-1417 | §8 «КФО» п. 12 | открыт, вопрос заказчику; допущение принято (docs #206): автоматически: чтение пересчитывает устаревший результат, кнопки нет | +| OQ-192 | Пересчёт КФО: автоматически после каждого изменения (Excel Р-11, Р-23), по кнопке «Рассчитать КФО» с пометкой «устарела» (FNR03) или только при открытии? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», вопрос 16; AURORA-1417 | §8 «КФО» п. 12 | **допущение принято, решение владельца 13.11.2026 (R-1):** пока версия не заморожена, результат КФО не хранится — каждое чтение считает по текущим данным, кнопки «Рассчитать» и признака «устарел» нет (`GET …/kfo/state` и `data_changed` убраны); в БД до заморозки — только ручные поля (комментарии строк, обоснование порога); полный снимок пишется при «Верифицирован», дальше только чтение. Ответ заказчика не получен — менять по нему | | OQ-193 | Правило ББ 1 (76.7.1 / 76.7.9) автоматически по DOC-274230: обе суммы — у компании 2 (Кт 76.7.1 − Дт 76.7.9, каскад 1150 → 1450 → 1550), а сторона агрегата §5 устроена «компания 1 — сторона 1». Как переложить правило в seed (стороны, режим)? | DOC-274230 правило 1; `arm-core/api/seed/core/elimination_rules_v1.yaml` | §5 п. 1, §8 «КФО» п. 4 | открыт, задача команды (follow-up AURORA-1546); до решения правило 1 — `manual`, только корректировкой | | OQ-194 | КФО при неполном комплекте документов (обязательный комплект и проверки перед расчётом отложены, Excel В-01, В-08): считать по тому, что подтверждено и привязано, с предупреждением, или не считать, пока комплект не полон? | DOC-274636 «Расчёт КФО» (черновик), «Что решить», допущение 17 | §8 «КФО» п. 2 | открыт, вопрос заказчику; допущение принято (docs #206): считать, у компании без формы — нули и `FORM_MISSING` | | OQ-195 | Кто переводит прежний срез ЕХ в архив: вручную до верификации новой версии (Excel статусно-ролевой модели, Р-35) или автоматически при записи нового среза (ЕХ ТЗ 6.4)? | DOC-274686 «Версии и прошлые периоды», вопрос 1; AURORA-1397 | §15 п. 2 | открыт, вопрос заказчику (аналитик — AURORA-1397); допущение принято (docs #208): вручную, запись нового среза прежний не архивирует | | OQ-196 | Ключ «того же периода» для одного актуального среза и блокировки верификации: ГК × дата (решения Excel Р-35/Р-36) или ГСК × дата (ЕХ ТЗ 6.2, 6.4)? | DOC-274686, вопрос 2; AURORA-1397 | §15 п. 2; AURORA-2002 | открыт, вопрос заказчику; допущение: ГК × дата для «один актуальный» и `superseded_by_snapshot_id`, нумерация — по ГСК (индекс модели) | | OQ-197 | Срез при верификации не записался: повторять запись с признаком «не передана в ЕХ», делать верификацию и запись одной операцией или не повторять? | DOC-274687, вопрос 6; Excel 13.10.2026 («механизм повторной передачи не определён») | §15 п. 9 | открыт, вопрос заказчику; допущение: верификация остаётся, признак вычисляется (VERIFIED без среза), такая версия блокирует финал следующей | | OQ-198 | Нужен ли PDF «Консолидированная отчётность» в ЕХ (макет [83-01]) и что в нём? | DOC-274687, вопрос 2; макет [83-01]; ЕХ ТЗ 6.7.3 | §15 п. 10 | открыт, вопрос заказчику (продолжение OQ-39); допущение: не формируется и не хранится, xlsx — из payload на лету | +| OQ-199 | Подтверждённая форма ББ/ОФР без колонки на конец отчётного квартала (или с несколькими такими колонками): нули в КФО молча или предупреждение (предложенный код `FORM_COLUMN_MISSING`)? | Финальное ревью arm-core #251 (I-3), `services/kfo.py` | §8 «КФО» | открыт; допущение: нули без предупреждения | diff --git a/context/08-source-docs-index.md b/context/08-source-docs-index.md index 0ec5c05..7822bf2 100644 --- a/context/08-source-docs-index.md +++ b/context/08-source-docs-index.md @@ -48,8 +48,8 @@ | Файл | Что | |---|---| -| `data-model-v5.26.dbml` | источник истины; changelog v5.1…v5.26 в шапке; блоки «ИСТОЧНИК» над каждой таблицей (v5.13…v5.25 остаются рядом как история) | -| `entities-v5.26.md` | описание сущностей по частям 01–09, таблицы «Что изменилось» по версиям, сценарий «как читать модель», открытые вопросы (v5.13…v5.24 остаются как история) | +| `data-model-v5.27.dbml` | источник истины; changelog v5.1…v5.27 в шапке; блоки «ИСТОЧНИК» над каждой таблицей (v5.13…v5.26 остаются рядом как история) | +| `entities-v5.27.md` | описание сущностей по частям 01–09, таблицы «Что изменилось» по версиям, сценарий «как читать модель», открытые вопросы (v5.13…v5.26 остаются как история) | | `diagrams/part01…09.{svg,png}` | диаграммы v5.21 по частям; перерисовываются `make diagrams` (`scripts/gen_model_diagrams.py`) | | `source/tz/data-model-part1_4.dbml` (лежит не в `model/`, а в экспорте ТЗ) | v4 часть 1 — историческое | diff --git a/context/KICKOFF-PROMPT.md b/context/KICKOFF-PROMPT.md index 1a44495..857e002 100644 --- a/context/KICKOFF-PROMPT.md +++ b/context/KICKOFF-PROMPT.md @@ -9,8 +9,8 @@ 1. `CLAUDE.md` (workspace) и `docs/context/00-workspace-layout.md` — как устроена рабочая область, статус аналитики, макет. 2. `docs/context/01-project-overview.md`, `02-glossary.md` — предметная область. -3. `docs/context/03-architecture.md`, `04-data-model-guide.md`, затем **весь** `docs/model/data-model-v5.26.dbml` и - `docs/model/entities-v5.26.md`. +3. `docs/context/03-architecture.md`, `04-data-model-guide.md`, затем **весь** `docs/model/data-model-v5.27.dbml` и + `docs/model/entities-v5.27.md`. 4. `docs/context/05-domain-rules.md` — алгоритмы (особенно §1.0 и §12 — парсеры ищут структуру по содержимому); `06-test-data.md`; `07-open-questions.md`; `10-user-journey.md`; `09-delivery-plan.md`. 5. `arm-core/`, `tabular-parser/`, `scan-gateway/` — три репозитория, развёрнутые из нулевого коммита шаблона команды, @@ -37,7 +37,7 @@ NATS, MinIO, эмулятор ИС Сканирование), контракт ` `07-open-questions.md`; если нужен ответ для среза — делай допущение по правилам из этого файла. Ограничения на всё время работы: -- имена таблиц, колонок и enum'ов — ровно как в DBML v5.26; поля, которых нет в DBML, не изобретать; +- имена таблиц, колонок и enum'ов — ровно как в DBML v5.27; поля, которых нет в DBML, не изобретать; - алгоритмы — только из `05-domain-rules.md`; из страниц ТЗ напрямую не кодировать; - парсеры: никаких координат ячеек, букв колонок, имён листов и номеров строк в коде — структура по содержимому, синонимы в `rules/*.yaml`; фикстуры — регрессия, не спецификация; diff --git a/dev/api-contract-corrections.md b/dev/api-contract-corrections.md index 8223d91..709e02f 100644 --- a/dev/api-contract-corrections.md +++ b/dev/api-contract-corrections.md @@ -11,7 +11,7 @@ 3. ТЗ 6.8, Прил. 9 — только там, где аналитика молчит. На `[design-23-10-check]` (12 пунктов + 3 по ТЗ) Лысенко ответил 29.10; ссылки вида «ответ п. N» — на этот комментарий. -Открыты: ОФР (AURORA-1396) и граница ровно 5 % (§2). Разделение работ с бэкендом — комментарии `[allocation-split]` в +Открыта граница ровно 5 % (§2); ОФР (AURORA-1396, AURORA-1999) реализован — §3.5 и DOC-277424. Разделение работ с бэкендом — комментарии `[allocation-split]` в AURORA-1540 и AURORA-1751 (29.10): разнесение строк и решения по строкам — 1751, корректировка и таблица — 1540. ## 1. Общее @@ -34,8 +34,9 @@ AURORA-1540 и AURORA-1751 (29.10): разнесение строк и реше - **Строка** — неделимая строка сводной ОСВ версии (как `ConsolidatedRowOut`): `entry_ids` — её исходные `osv_entry`. В разнесении участвуют строки уровня «счёт — контрагент», владелец и контрагент — компании периметра, тип данных — БУ - (DOC-272613 §1). Для ББ сумма строки — сальдо на конец, для ОФР — обороты по правилу (`measure`). Строка с дебетовым и - кредитовым сальдо одновременно даёт две позиции — `side = "D"` и `side = "K"`. + (DOC-272613 §1). Для ББ сумма строки — сальдо на конец, для ОФР — обороты по правилу (`measure`). Строка ОФР всегда даёт + две независимые позиции — `side = "D"` (`turnover_debit`) и `side = "K"` (`turnover_credit`); решения по ним хранятся + раздельно (§3.5). У ББ решение одно на обе позиции строки, `side` не передаётся. - **Агрегат** — правило × компания 1 × компания 2 (× тип отчётности, он следует из правила). Отдельно не хранится, считается при каждом запросе (DOC-272613 §3). Ключ агрегата — `(rule_id, company_1_id, company_2_id)`. - **Разнесение** (DOC-272613 «Распределение строк по правилам», ответы 1(б), 2(а)) — чистая функция от версии консолидации, @@ -91,8 +92,10 @@ AURORA-1540 и AURORA-1751 (29.10): разнесение строк и реше — дочерние (хранение — `correction.parent_correction_id`, глубина 1). Пустая сторона допустима, обе пустые — 422 `EMPTY_CORRECTION`. - `line_code` — код строки формы ББ (ОФР) этой компании; не из формы — 422 `INVALID_LINE_CODE` с `detail.line_code`. - Сумма — строка ≥ 0, не больше 16 цифр до запятой и двух после (`numeric(18,2)`); это уменьшение статьи. Иное — 422 - массивом pydantic. + Сумма — строка, не больше 16 цифр до запятой и двух после (`numeric(18,2)`); у ББ ≥ 0 (это уменьшение статьи), у ОФР + допустимы и отрицательные суммы (DOC-277424 §7.5). Иное — 422 массивом pydantic. У ОФР сумма — **величина исключения + в конвенции оборота**, а знак расходных строк (2120, 2210, 2220, 2330, 2350) сервер применяет сам + (`domain/kfo.py`, `apply_income_exclusions`): клиент сумму заранее не инвертирует. - `comment` обязателен всегда, пустой после обрезки — 422 `COMMENT_REQUIRED` (DOC-272613, ответ п. 6); не длиннее 1000 символов (как комментарий смены статуса), длиннее — 422 массивом pydantic. - Сохранение сразу применяет корректировку — `status = APPLIED`; поля `status` в теле нет (ответ п. 9). `DRAFT` есть в модели, @@ -105,7 +108,9 @@ AURORA-1540 и AURORA-1751 (29.10): разнесение строк и реше - Корректировку можно сохранить, даже если стороны не сошлись; ответ показывает `balance` (контроль распределения). В КФО по выбранным статьям уходит меньшая сумма (ответ п. 8). Ограничения «только при отклонении > 5 %» нет (ответ по ТЗ). - Каждое сохранение увеличивает `version_no`; аудит `CORRECTION_CREATE` (первая) / `CORRECTION_EDIT` (следующие) с `before` / - `after`; результат КФО помечается устаревшим (AURORA-1417). + `after`. КФО отдельной пометки не получает: до заморозки он считается на каждом чтении по текущим корректировкам, после + «Верифицирован» — только чтение (`dev/api-contract-kfo.md` §3.1). У ОФР корректировка заменяет автоисключение только + **своей** стороны агрегата. Ответ — пересчитанный агрегат. @@ -134,7 +139,7 @@ AURORA-1540 и AURORA-1751 (29.10): разнесение строк и реше `composition_changed = true` (ответ 5). Ответ — вся таблица (решение меняет и агрегаты, и нераспределённые строки). Аудит: реализовано существующим `audit_action.ASSIGNMENT_CHANGE` (не новым значением, как предполагалось здесь раньше) — -таблица `vgo_assignment` в DBML v5.24 (AURORA-1751); поля `side` в ней нет (решение см. там же). +таблица `vgo_assignment` в DBML v5.24 (AURORA-1751); поле `side` — только для ОФР (DBML v5.27, §3.5). ### 3.4 `GET …/export` — xlsx @@ -143,6 +148,42 @@ AURORA-1540 и AURORA-1751 (29.10): разнесение строк и реше суммы в рублях. Имя файла — `Корректировки_<название ГК>_<год>_<квартал>.xlsx` в `Content-Disposition` (`filename*`, ASCII запасное `corrections.xlsx`), как в WIP API (#208). Выбор колонок и фильтры в первой версии не передаются. Аудит `EXPORT`. +### 3.5 `…/vgo-assignments/*` — разнесение строк (как реализовано) и ОФР + +Фактический API разнесения (`arm-core#208`; ОФР — PR #251, DBML v5.27). Права — как в §1 (чтение `CONSOLIDATION_VIEW`, +правки `CONSOLIDATION_EDIT`). + +| Метод и путь | Запрос | Ответ | +|---|---|---| +| `GET /consolidations/versions/{version_id}/vgo-assignments/distribute` | query `statement_type` (`balance` по умолчанию / `income`) | `VgoAssignmentCandidatesOut`: группы-агрегаты, ручные назначения, нераспределённые строки | +| `PUT …/vgo-assignments/rule` | `VgoAssignmentAttachRuleIn {entry_ids[], rule_id, side?}` | `VgoAssignmentOut[]` | +| `DELETE …/vgo-assignments/rule` | `VgoAssignmentDetachRuleIn {statement_type, entry_ids[], side?, reset?}` | `VgoAssignmentOut[]` (при `reset = true` — `[]`) | +| `PUT …/vgo-assignments/exclude` | `VgoAssignmentExcludeIn {statement_type, entry_ids[], is_exclude, side?}` | `VgoAssignmentOut[]` | + +**`side`.** У ББ (`statement_type = balance`) `side` **не передаётся** (`NULL`): решение одно на обе позиции строки. У ОФР +(`income`) `side` **обязателен** (`"D"` или `"K"`): позиция — это «строка ОСВ + сторона», действие на оба оборота по +умолчанию не распространяется. Нарушение (ОФР без `side`, ББ с `side`) — 422 `INVALID_VGO_SIDE`. Остальные ошибки ОФР: +422 `INVALID_VGO_RULE` (правило не относится к версии правил версии консолидации), 422 `INVALID_VGO_POSITION` (позиция +или направление не подходят правилу ОФР, значения оборота нет, владелец или контрагент вне периметра), 422 +`INVALID_OSV_ENTRY_IDS` (`entry_ids` не образуют полные строки свода). `side` есть и в `VgoAssignmentOut`, +`VgoAssignmentRowOut`, `VgoUnallocatedPositionOut`. + +**Семантика ОФР** (DOC-277424 §3.4–3.5): `ASSIGN` — `rule_id`, `is_exclude = false`; `UNASSIGNED` — `rule_id = NULL`; +исключение назначенной позиции — `UNASSIGNED` + `is_exclude = true`; `reset` удаляет решение по позиции (она снова +разносится автоматически); `rule_id` вместе с `is_exclude = true` не хранятся. Решение по `D` не меняет решение по `K` той же +строки. **Смена разнесения ОФР не переводит применённые корректировки в `DRAFT`** (они сохраняют приоритет, DOC-277424 §3.5 +п. 5); у ББ — как прежде, корректировки затронутых пар становятся `DRAFT`. + +**Что приходит для ОФР в `GET …/corrections?statement_type=income` и `…/distribute?statement_type=income`:** агрегаты по +основному правилу (1, 3, 4; правила 2 и 5 — вторые стороны, собственных агрегатов нет), суммы — обороты; в строках агрегата и +в нераспределённых — `side` позиции и `company_role` (`1` или `2`: роль владельца в агрегате; для ББ `null`). Сторона может +быть без значения: `amount_present = false`, `amount`/`deviation_amount` — `null` (`NULL` не подменяется нулём, +DOC-277424 §3.1). Причина нераспределённой позиции `reason` — строка: `MULTIPLE_RULES`, `NO_RULE`, `UNASSIGNED`, +`MANUAL_REVIEW_REQUIRED`, `MISSING_AMOUNT`; у ББ — прежние `MULTIPLE_RULES` и `UNASSIGNED` (и `unassigned_reason = +PARTIAL_MANUAL_ASSIGNMENT` у строки). Правило в `rules[]` несёт `elimination_mode` (`min`, `min_or_full`, `manual`, `full`). +Корректировка ОФР вводится как у ББ (§3.2): `line_code` — статья ОФР компании, суммы со знаком; правила 6 и 7 +(2340, 2350) в seed не заведены — только ручная корректировка по статье, агрегата у них нет. + ## 4. Схемы Поля без `?` есть всегда; `null` указан явно. @@ -176,7 +217,7 @@ deviation_amount money deviation_percent percent | null к меньшей стороне; null — меньшая сторона ≤ 0 exceeds_threshold bool deviation_percent ≥ 5, или одна сторона пуста при ненулевой другой; у min_or_full — всегда false status "AUTO" | "DISCREPANCY" | "ONE_SIDED" | "MANUAL" как EliminationTotalItemOut -eliminated_amount money автосумма агрегата по режиму правила (§8 «КФО» п. 4) — её исключает КФО, если нет корректировки +eliminated_amount money автосумма агрегата по режиму правила (§8 «КФО» п. 4) — её исключает КФО, если нет корректировки; только ББ, у `income` всегда 0 (I-2) composition_changed bool состав или суммы изменились после применённой корректировки (ответ 5; ответ п. 10 — подсвечивать) correction CorrectionOut | null действующая корректировка (APPLIED; DRAFT — зарезервирован) ``` @@ -248,11 +289,8 @@ rule_id? (uuid, для ASSIGN)}`. перечитывает таблицу после вызова. - `vgo_assignment` (DBML v5.24) — **только ручные решения**, автоматическое разнесение не хранится (DECISION-5): иначе ручное решение не отличить от автоматического и при пересчёте после замены ОСВ оно затрётся. Поля: - `consolidation_version_id, statement_type, osv_entry_id, rule_id (NULL), action (ASSIGN | UNASSIGNED), is_exclude - (bool), created_by/at, updated_by/at`; `ASSIGN` — с `rule_id`, `UNASSIGNED` — без; уникальность - `(consolidation_version_id, statement_type, osv_entry_id)`. **Поля `side`, которое черновик этого пункта предполагал - для строки с одновременно дебетовым и кредитовым сальдо, в реализации нет** — решение одно на обе позиции строки, не - по стороне отдельно. + `consolidation_version_id, statement_type, osv_entry_id, side (D | K, v5.27, только ОФР), rule_id (NULL), action (ASSIGN | UNASSIGNED), is_exclude + (bool), created_by/at, updated_by/at`; `ASSIGN` — с `rule_id`, `UNASSIGNED` — без; уникальность: ББ — `(версия, тип, osv_entry_id)`, ОФР — `(версия, тип, osv_entry_id, side)`; `side` обязателен у `income` и `NULL` у `balance` (§3.5). До v5.27 поля не было — решение было одно на обе позиции строки. - Разнесение — чистая функция в `api/src/domain/` (как `elimination_packing`): строки свода × правила × ручные решения → агрегаты и нераспределённые строки (с `line_code` и `candidate_rule_ids`). Нынешняя упаковка считает каждое правило отдельно (TODO в `intercompany_matrix.build_pairs`) — для таблицы корректировок строка должна попадать не более чем в одно @@ -262,9 +300,11 @@ rule_id? (uuid, для ASSIGN)}`. `eliminated_amount = min`, `status = AUTO`; сторона 2 пуста → `eliminated_amount = amount_1`, `status = ONE_SIDED`; сторона 1 пуста → 0, `ONE_SIDED`; `exceeds_threshold = false` всегда (порога 5 % у правила нет). **Правило 1** остаётся `manual` (`status = MANUAL`, 0) до переработки seed (OQ-193). Автосумма — **та же функция, по которой считает КФО**: - что аналитик видит в `eliminated_amount`, то и исключает КФО, если корректировки нет (корректировка её заменяет). -- ОФР: тот же ответ с `statement_type = income` **(ждёт ответа в AURORA-1396)**; до него сервер может отдавать пустые - `aggregates` и `unallocated` для `income`. + что аналитик видит в `eliminated_amount`, то и исключает КФО, если корректировки нет (корректировка её заменяет) — + **для ББ**. Для ОФР (I-2, отложено) таблица автосумму не считает: `eliminated_amount = 0`, `status = MANUAL`, + `exceeds_threshold` (порог 5 %) не применяется; автосуммы ОФР (правило 1 — выручка без НДС, правило 3 — вся + сторона 1, правило 4 — `min` со знаком) вычисляет и показывает только КФО (`KfoIncomeSource`, `services/kfo.py`). +- ОФР: тот же ответ с `statement_type = income`; правила 1–5 версии 1 заведены seed, агрегаты — по оборотам, позиции D/K независимы (§3.5, `05-domain-rules.md` §4 и §5 п. 2b, DOC-277424). ## 6. Пример diff --git a/dev/api-contract-kfo.md b/dev/api-contract-kfo.md index 1a75aa7..94b58ff 100644 --- a/dev/api-contract-kfo.md +++ b/dev/api-contract-kfo.md @@ -1,15 +1,16 @@ # Контракт API результата КФО (AURORA-1546 / AURORA-1547) -Контракт вкладки «КФО» отчёта версии консолидации — **до реализации** (docs #206, OpenSpec change -`kfo-calculation-contract`). Машиночитаемая копия — `dev/api-contract-kfo.openapi.yaml` с тремя полными примерами -ответа (их можно брать как фейк для экрана); она генерируется `scripts/kfo_contract/gen_kfo_openapi.py` из -сквозного примера §8 (`scripts/kfo_contract/kfo_example.py`, там же проверки правил) — руками не править. Когда расчёт появится в `arm-core`, копия выгружается из `app.openapi()` и -сверяется тестом, как у гудвилла; если файлы разойдутся с кодом — прав код, оба файла правятся вслед за ним. +Контракт вкладки «КФО» отчёта версии консолидации; реализован в arm-core (PR #251, OpenSpec `kfo-calculation`). +Машиночитаемая копия — `dev/api-contract-kfo.openapi.yaml`: пути и схемы **выгружаются из `app.openapi()`** +(`python -m scripts.export_kfo_contract` из `api/` arm-core), три полных примера ответа и пример тела PUT (`examples`) +переносятся из прежней версии файла (числа — сквозной пример §8, `scripts/kfo_contract/kfo_example.py`; можно брать как +фейк для экрана). Руками файл не правится; сверяет его с кодом тест `api/tests/test_kfo_contract.py` (как у гудвилла: +расходится с кодом — прав код, файл выгружается заново). Источники, в порядке приоритета: 1. `05-domain-rules.md` §8 «КФО» — алгоритм (п. 1–12) и сквозной пример. Здесь формулы не повторяются — только ссылки «п. N». -2. DBML v5.26: `kfo_result`, `kfo_result_line`, `kfo_result_line_company`, `consolidation_version`. +2. DBML v5.27: `kfo_result`, `kfo_result_line`, `kfo_result_line_company`, `consolidation_version`. 3. DOC-274647 «Экран: КФО» (черновик) и макет `[31-04]`, `[31-04-01]` — состав колонок, состояния экрана. ## 1. Общее @@ -36,7 +37,7 @@ - **Строки** — бланк ББ и ОФР редакции на отчётную дату версии (66н до 31.12.2024, ФСБУ 4 позже) плюс служебная `GOODWILL` в разделе I сразу после 1170 (§8 «КФО» п. 1, 8). Приходят все строки бланка в его порядке (`ordinal`), включая нулевые; `is_total` — итоговая строка. -- **По компании** (`companies[]` строки, порядок — как `KfoResultOut.companies`): «По данным клиента» +- **По компании** (`companies[]` строки, порядок — как `KfoTableOut.companies`): «По данным клиента» (`client_amount`), «В том числе взаимные обязательства / обороты» (`vgo_amount`), «Очищенные показатели» (`cleaned_amount = client_amount − vgo_amount`), «Доля в показателе по группе, %» (`share_percent`; `null` при нулевом итоге строки или `|доля| ≥ 1000 %`). Строки расходов @@ -62,17 +63,18 @@ | Метод и путь | Право | Запрос | Ответ | Ошибки | |---|---|---|---|---| -| `GET /consolidations/versions/{version_id}/kfo` | `KFO_VIEW` | — | 200 `KfoResultOut` | 404 `NOT_FOUND` | -| `PUT /consolidations/versions/{version_id}/kfo/annotations` | `CONSOLIDATION_EDIT` | `KfoAnnotationsIn` | 200 `KfoResultOut` | 404 `NOT_FOUND`, 409 правок версии, 422 `UNKNOWN_LINE`, 422 валидации тела | +| `GET /consolidations/versions/{version_id}/kfo` | `KFO_VIEW` | — | 200 `KfoTableOut` | 404 `NOT_FOUND` | +| `PUT /consolidations/versions/{version_id}/kfo/annotations` | `CONSOLIDATION_EDIT` | `KfoAnnotationsIn` | 200 `KfoTableOut` | 404 `NOT_FOUND`, 409 правок версии, 422 `UNKNOWN_LINE`, 422 валидации тела | Выгрузки («Таблица», «Форма 2.03.01.01с») — AURORA-1565, отдельным контрактом. ### 3.1 `GET …/kfo` — результат -Весь результат одним ответом, без страниц. **Пока версия не заморожена, ответ — по текущим данным версии**: если -что-то изменилось после `computed_at` (документы, сопоставление, разнесение, корректировки, гудвилл), сервер -пересчитывает и сохраняет результат до ответа (как он это обнаруживает — AURORA-1417). Кнопки «Рассчитать» нет. У -замороженной версии (`is_frozen = true`, ставится при «Верифицирован») — сохранённый результат без пересчёта, +Весь результат одним ответом, без страниц. **Пока версия не заморожена, результат не хранится: каждый запрос считает +таблицу по текущим данным версии** (формы, привязанные документы, сводная ОСВ, агрегаты таблицы корректировок, гудвилл), +`computed_at` — время запроса; расчётные суммы в БД не пишутся (решение владельца 13.11.2026, OQ-192). Признака «результат +устарел» и отдельного запроса состояния нет — они не нужны. Кнопки «Рассчитать» нет. У замороженной версии +(`is_frozen = true`, снимок записывается при «Верифицирован») — сохранённый результат без пересчёта и чтения первички, `editable = false`. Версия без компаний или без форм — не ошибка: строки бланка с нулями и `warnings` (`FORM_MISSING` по каждой компании @@ -96,31 +98,31 @@ `detail.line_code`; ничего не сохраняется. - `threshold_justification`, `methodology_deviations` — ≤ 4000 символов; длиннее — 422 массивом pydantic. - Обоснование можно сохранить и без превышения — сервер его не стирает; `justification_required` от этого не меняется. -- Всё одной транзакцией; комментарии и обоснование переживают пересчёт (по `statement_type`, `line_code`). +- Всё одной транзакцией. До заморозки в БД сохраняются только ручные поля: комментарии строк (строка `kfo_result_line` без сумм, `consolidated_amount = 0`; снятие комментария строку удаляет), обоснование порога, отклонения от Методики; при заморозке они накладываются на рассчитанную таблицу по `(statement_type, line_code)`. Повтор кода в запросе — побеждает последний элемент; все коды проверяются до записи. - Аудит — одно событие `KFO_EDIT` на запрос (`object_type = kfo_result`, раздел `KFO`), `before` / `after` — только изменённые поля. Запрос без изменений аудит не пишет. -Ответ — `KfoResultOut` целиком (с пересчётом, если он нужен). +Ответ — `KfoTableOut` целиком (рассчитан заново, как у `GET`). ## 4. Схемы Полностью — в OpenAPI. Кратко: ``` -KfoResultOut +KfoTableOut version_id uuid; computed_at date-time; is_frozen bool; editable bool; read_only_reason enum|null units "thousand_rub" - companies[] {legal_entity_id, name, inn} — порядок колонок (по имени) + companies[] KfoCompany {legal_entity_id, name, inn} — порядок колонок (по имени) requisites KfoRequisitesOut summary {balance: KfoThresholdOut, income: KfoThresholdOut} justification_required bool; threshold_justification string|null goodwill {enabled bool, total money} - balance, income {lines: KfoLineOut[]} + balance, income KfoStatementOut {lines: KfoLine[]} warnings[] KfoWarningOut {code, message, legal_entity_id?, statement_type?, count?} -KfoLineOut +KfoLine line_code, line_name, ordinal, is_total, is_threshold_base - companies[] {legal_entity_id, client_amount, vgo_amount, cleaned_amount, share_percent|null} (у GOODWILL — []) + companies[] KfoCompanyCell {legal_entity_id, client_amount, vgo_amount, cleaned_amount, share_percent|null} (у GOODWILL — []) sum_amount, vgo_amount, eliminated_auto, eliminated_manual, goodwill_adjust, bank_amount money deviation_amount money|null (у итогов null) comment string|null @@ -142,7 +144,7 @@ KfoAnnotationsIn {comments?: [{statement_type, line_code, comment|null}], thre | Состояние | Как видно из ответа | |---|---| | Нет форм у части компаний | `warnings[].code = FORM_MISSING` с `legal_entity_id`, `statement_type`; у компании нули | -| ОФР не исключается автоматически | `INCOME_RULES_MISSING` (до AURORA-1999); взаимные обороты — только корректировками | +| У версии нет правил ОФР (старая база до seed) | `INCOME_RULES_MISSING` со `statement_type = income`: ББ считается как прежде, ОФР — без автоисключений; при заведённых правилах ОФР 1–5 (seed) предупреждения нет | | Есть нераспределённые строки | `UNALLOCATED_ROWS`, `count` | | Превышение порога | `summary.*.exceeded = true`, `is_threshold_base` у строки базы, `justification_required = true` | | Гудвилл учтён | `goodwill.enabled = true`, `goodwill.total`, строка `GOODWILL` с `bank_amount` | @@ -177,5 +179,5 @@ KfoAnnotationsIn {comments?: [{statement_type, line_code, comment|null}], thre Варианты, принятые на старте, могут измениться по ответу заказчика — контракт меняется вместе с §8: OQ-180 (строки вне бланка), OQ-181 (колонки), OQ-182 (значение без формы), OQ-183 (итоговая строка), OQ-184 (округление), OQ-185 (место гудвилла), OQ-186 (объём расхождений), OQ-187 (ЧА), OQ-188 (нулевая база), OQ-189 (действие при превышении), -OQ-190/191 (подход по Методике, согласование клиентом — полей нет), OQ-192 (пересчёт), OQ-194 (КФО при неполном +OQ-190/191 (подход по Методике, согласование клиентом — полей нет), OQ-192 (пересчёт — допущение по решению владельца 13.11.2026: считается на каждом чтении, хранится при заморозке), OQ-194 (КФО при неполном комплекте). diff --git a/dev/api-contract-kfo.openapi.yaml b/dev/api-contract-kfo.openapi.yaml index 1f619c7..4720054 100644 --- a/dev/api-contract-kfo.openapi.yaml +++ b/dev/api-contract-kfo.openapi.yaml @@ -1,36 +1,32 @@ -# Сгенерировано scripts/kfo_contract/gen_kfo_openapi.py из сквозного примера §8 «КФО» (docs #206). -# Руками не править: правка генератора (и kfo_example.py) → перегенерация; текст — dev/api-contract-kfo.md. openapi: 3.1.0 info: title: АРМ КА — результат КФО (AURORA-1546/1547) - version: '0.1' - description: 'Контракт до реализации (docs #206). Правила — context/05-domain-rules.md §8 «КФО»; текст — dev/api-contract-kfo.md. - Деньги — строки с двумя знаками в тыс. руб. (units).' + version: '1.0' paths: /api/v1/consolidations/versions/{version_id}/kfo: get: tags: - consolidations - summary: Результат КФО версии - description: 'Право KFO_VIEW. Пока версия не заморожена — результат по текущим данным: устаревший пересчитывается и - сохраняется до ответа. Замороженный — только чтение сохранённого.' - operationId: kfo_get + summary: Table + description: Возвращает актуальную таблицу; для замороженной версии — сохранённый результат. + operationId: table_api_v1_consolidations_versions__version_id__kfo_get security: - bearerAuth: [] - parameters: &id001 + parameters: - name: version_id in: path required: true schema: type: string format: uuid + title: Version Id responses: '200': - description: Результат КФО + description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/KfoResultOut' + $ref: '#/components/schemas/KfoTableOut' examples: goodwill_exceeded: summary: Гудвилл включён, превышение порога ББ (сквозной пример §8) @@ -2018,30 +2014,28 @@ paths: legal_entity_id: 33333333-3333-4333-8333-333333333333 statement_type: income count: null - '403': - description: Нет права KFO_VIEW + '422': + description: Validation Error content: application/json: schema: - $ref: '#/components/schemas/ErrorOut' - '404': - description: Версия не найдена или вне видимости — NOT_FOUND - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorOut' + $ref: '#/components/schemas/HTTPValidationError' /api/v1/consolidations/versions/{version_id}/kfo/annotations: put: tags: - consolidations - summary: Ручные поля КФО - description: Право CONSOLIDATION_EDIT, блокировка версии. Комментарии строк, обоснование порога, причины отклонений - от Методики — одной транзакцией; отсутствующее поле не меняется, null очищает. Аудит KFO_EDIT. Ответ — KfoResultOut - целиком. - operationId: kfo_annotations_put + summary: Annotations + operationId: annotations_api_v1_consolidations_versions__version_id__kfo_annotations_put security: - bearerAuth: [] - parameters: *id001 + parameters: + - name: version_id + in: path + required: true + schema: + type: string + format: uuid + title: Version Id requestBody: required: true content: @@ -2059,292 +2053,294 @@ paths: threshold_justification: Расхождение по 1520 — Бета → Альфа, сверка в работе; риск принят responses: '200': - description: Результат КФО после сохранения + description: Successful Response content: application/json: schema: - $ref: '#/components/schemas/KfoResultOut' - '403': - description: Нет права CONSOLIDATION_EDIT - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorOut' - '404': - description: NOT_FOUND - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorOut' - '409': - description: VERSION_NOT_EDITABLE | LOCK_REQUIRED | LOCKED_BY_OTHER - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorOut' + $ref: '#/components/schemas/KfoTableOut' '422': - description: UNKNOWN_LINE (detail.statement_type, detail.line_code) или массив ошибок валидации тела (длины 2000/4000) + description: Validation Error content: application/json: schema: - oneOf: - - $ref: '#/components/schemas/ErrorOut' - - type: object + $ref: '#/components/schemas/HTTPValidationError' components: schemas: - KfoLineCompanyOut: + ConsolidationApproach: + type: string + enum: + - BASIC + - EXTENDED + title: ConsolidationApproach + description: DBML `consolidation_approach`. + ConsolidationScenario: + type: string + enum: + - CREATE + - CONFIRM + title: ConsolidationScenario + description: DBML `consolidation_scenario`. + ConsolidationStatus: + type: string + enum: + - INITIATED + - DOCUMENTS_PROCESSING + - DATA_CORRECTION + - CONSOLIDATION_RF + - ADJUSTMENT + - VERIFICATION_RF + - CONSOLIDATION_GO + - VERIFICATION_GO + - VERIFIED + - CANCELLED + - REJECTED + - ARCHIVED + title: ConsolidationStatus + description: DBML `consolidation_status`. + HTTPValidationError: + properties: + detail: + items: + $ref: '#/components/schemas/ValidationError' + type: array + title: Detail type: object - additionalProperties: false + title: HTTPValidationError + KfoAnnotationsIn: + properties: + comments: + items: + $ref: '#/components/schemas/KfoCommentIn' + type: array + title: Comments + threshold_justification: + anyOf: + - type: string + maxLength: 4000 + - type: 'null' + title: Threshold Justification + methodology_deviations: + anyOf: + - type: string + maxLength: 4000 + - type: 'null' + title: Methodology Deviations + type: object + title: KfoAnnotationsIn + KfoCommentIn: + properties: + statement_type: + $ref: '#/components/schemas/StatementType' + line_code: + type: string + title: Line Code + comment: + anyOf: + - type: string + maxLength: 2000 + - type: 'null' + title: Comment + type: object + required: + - statement_type + - line_code + - comment + title: KfoCommentIn + KfoCompany: properties: legal_entity_id: type: string format: uuid - client_amount: + title: Legal Entity Id + name: type: string - pattern: ^-?\d{1,16}\.\d{2}$ - examples: &id002 - - '588500.00' - description: «По данным клиента», тыс. руб.; строки расходов приведены к −|x| (§8 «КФО» п. 2) - vgo_amount: - type: string - pattern: ^-?\d{1,16}\.\d{2}$ - examples: *id002 - description: «В т. ч. взаимные», тыс. руб. до целых, со знаком строки (п. 6) - cleaned_amount: - type: string - pattern: ^-?\d{1,16}\.\d{2}$ - examples: *id002 - description: «Очищенные показатели» = client_amount − vgo_amount - share_percent: - type: - - string - - 'null' - pattern: ^-?\d{1,3}\.\d{2}$ - description: «Доля в показателе по группе, %» от bank_amount строки, ROUND_HALF_UP; null при нулевом итоге или |доля| - ≥ 1000 % + title: Name + inn: + anyOf: + - type: string + - type: 'null' + title: Inn + type: object required: - legal_entity_id - - client_amount - - vgo_amount - - cleaned_amount - - share_percent - KfoLineOut: + - name + - inn + title: KfoCompany + KfoCompanyCell: + properties: + legal_entity_id: + type: string + format: uuid + title: Legal Entity Id + client_amount: + type: string + pattern: ^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$ + title: Client Amount + default: '0.00' + vgo_amount: + type: string + pattern: ^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$ + title: Vgo Amount + default: '0.00' + cleaned_amount: + type: string + pattern: ^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$ + title: Cleaned Amount + default: '0.00' + share_percent: + anyOf: + - type: string + pattern: ^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$ + - type: 'null' + title: Share Percent type: object - additionalProperties: false + required: + - legal_entity_id + title: KfoCompanyCell + KfoCorrectionsOut: + properties: + count: + type: integer + title: Count + amount: + type: string + title: Amount + type: object + required: + - count + - amount + title: KfoCorrectionsOut + KfoGoodwillOut: + properties: + enabled: + type: boolean + title: Enabled + total: + type: string + title: Total + type: object + required: + - enabled + - total + title: KfoGoodwillOut + KfoLine: properties: line_code: type: string - description: код строки бланка редакции на отчётную дату или GOODWILL + title: Line Code line_name: - type: string + anyOf: + - type: string + - type: 'null' + title: Line Name ordinal: type: integer - description: порядок строки в бланке + title: Ordinal is_total: type: boolean - description: итоговая строка бланка (1100, 1600, 2100 …) - is_threshold_base: - type: boolean - description: строка — база порога (1300, 1600, 2110 или 2200), подсветка при превышении + title: Is Total companies: - type: array items: - $ref: '#/components/schemas/KfoLineCompanyOut' - description: по компаниям в порядке KfoResultOut.companies; у GOODWILL — пусто + $ref: '#/components/schemas/KfoCompanyCell' + type: array + title: Companies sum_amount: type: string - pattern: ^-?\d{1,16}\.\d{2}$ - examples: *id002 - description: Σ client_amount + pattern: ^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$ + title: Sum Amount + default: '0.00' vgo_amount: type: string - pattern: ^-?\d{1,16}\.\d{2}$ - examples: *id002 - description: Σ vgo_amount = eliminated_auto + eliminated_manual - eliminated_auto: &id003 + pattern: ^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$ + title: Vgo Amount + default: '0.00' + eliminated_auto: type: string - pattern: ^-?\d{1,16}\.\d{2}$ - examples: *id002 - eliminated_manual: *id003 + pattern: ^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$ + title: Eliminated Auto + default: '0.00' + eliminated_manual: + type: string + pattern: ^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$ + title: Eliminated Manual + default: '0.00' goodwill_adjust: type: string - pattern: ^-?\d{1,16}\.\d{2}$ - examples: *id002 - description: Σ G — у GOODWILL и итогов 1100, 1600 + pattern: ^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$ + title: Goodwill Adjust + default: '0.00' bank_amount: type: string - pattern: ^-?\d{1,16}\.\d{2}$ - examples: *id002 - description: «КФО, подготовленная банком» = sum_amount − vgo_amount + goodwill_adjust + pattern: ^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$ + title: Bank Amount + default: '0.00' deviation_amount: - type: - - string - - 'null' - pattern: ^-?\d{1,16}\.\d{2}$ - description: «Расхождения» (§8 «КФО» п. 9); у итогов — null + anyOf: + - type: string + pattern: ^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$ + - type: 'null' + title: Deviation Amount + default: '0.00' + is_threshold_base: + type: boolean + title: Is Threshold Base + default: false comment: - type: - - string - - 'null' - maxLength: 2000 + anyOf: + - type: string + - type: 'null' + title: Comment + type: object required: - line_code - line_name - ordinal - is_total - - is_threshold_base - - companies - - sum_amount - - vgo_amount - - eliminated_auto - - eliminated_manual - - goodwill_adjust - - bank_amount - - deviation_amount - - comment - KfoStatementOut: - type: object - additionalProperties: false - properties: - lines: - type: array - items: - $ref: '#/components/schemas/KfoLineOut' - required: - - lines - KfoThresholdOut: - type: object - additionalProperties: false - properties: - deviation_total: *id003 - base_kind: - type: string - enum: - - NET_ASSETS - - BALANCE_TOTAL - - SALES_PROFIT - - REVENUE - base_amount: *id003 - limit_percent: - type: string - enum: - - '10.00' - - '1.00' - limit_amount: *id003 - deviation_percent: - type: - - string - - 'null' - pattern: ^-?\d+\.\d{2}$ - description: deviation_total / base_amount × 100; null при нулевой базе - exceeded: - type: boolean - description: deviation_total > limit_amount (равно — норма) - required: - - deviation_total - - base_kind - - base_amount - - limit_percent - - limit_amount - - deviation_percent - - exceeded - KfoSummaryOut: - type: object - additionalProperties: false - properties: - balance: - $ref: '#/components/schemas/KfoThresholdOut' - income: - $ref: '#/components/schemas/KfoThresholdOut' - required: - - balance - - income - KfoCompanyOut: - type: object - additionalProperties: false - properties: - legal_entity_id: - type: string - format: uuid - name: - type: string - inn: - type: string - required: - - legal_entity_id - - name - - inn - KfoCorrectionsOut: - type: object - additionalProperties: false - properties: - count: - type: integer - minimum: 0 - amount: *id003 - required: - - count - - amount - description: агрегаты с применённой корректировкой и Σ их исключений, тыс. руб. + title: KfoLine KfoRequisitesOut: - type: object - additionalProperties: false properties: company_group_name: type: string + title: Company Group Name related_group_name: - type: - - string - - 'null' + anyOf: + - type: string + - type: 'null' + title: Related Group Name reporting_date: type: string format: date + title: Reporting Date version_no: type: integer + title: Version No version_status: - type: string - description: consolidation_version.status (enum consolidation_status DBML) - enum: - - INITIATED - - DOCUMENTS_PROCESSING - - DATA_CORRECTION - - CONSOLIDATION_RF - - ADJUSTMENT - - VERIFICATION_RF - - CONSOLIDATION_GO - - VERIFICATION_GO - - VERIFIED - - CANCELLED - - REJECTED - - ARCHIVED + $ref: '#/components/schemas/ConsolidationStatus' scenario: - type: string - enum: - - CREATE - - CONFIRM + $ref: '#/components/schemas/ConsolidationScenario' approach: - type: string - enum: - - BASIC - - EXTENDED + $ref: '#/components/schemas/ConsolidationApproach' methodology_deviations: - type: - - string - - 'null' - maxLength: 4000 + anyOf: + - type: string + - type: 'null' + title: Methodology Deviations uob_decision_no: - type: - - string - - 'null' + anyOf: + - type: string + - type: 'null' + title: Uob Decision No uob_decision_date: - type: - - string - - 'null' - format: date + anyOf: + - type: string + format: date + - type: 'null' + title: Uob Decision Date corrections_balance: $ref: '#/components/schemas/KfoCorrectionsOut' corrections_income: $ref: '#/components/schemas/KfoCorrectionsOut' + type: object required: - company_group_name - related_group_name @@ -2358,190 +2354,201 @@ components: - uob_decision_date - corrections_balance - corrections_income - KfoGoodwillOut: - type: object - additionalProperties: false + title: KfoRequisitesOut + KfoStatementOut: properties: - enabled: - type: boolean - total: *id003 - required: - - enabled - - total - KfoWarningOut: + lines: + items: + $ref: '#/components/schemas/KfoLine' + type: array + title: Lines type: object - additionalProperties: false + required: + - lines + title: KfoStatementOut + KfoSummaryOut: properties: - code: - type: string - enum: - - FORM_MISSING - - FORM_AMBIGUOUS - - FORM_UNITS_UNKNOWN - - LINE_OUTSIDE_BLANK - - UNALLOCATED_ROWS - - INCOME_RULES_MISSING - - GOODWILL_NOT_APPLIED - - GOODWILL_ROW_REASSIGNED - - CORRECTION_SUPERSEDED_BY_GOODWILL - message: - type: string - legal_entity_id: - type: - - string - - 'null' - format: uuid - statement_type: - type: - - string - - 'null' - enum: - - balance - - income - - null - count: - type: - - integer - - 'null' - required: - - code - - message - KfoResultOut: + balance: + $ref: '#/components/schemas/KfoThresholdOut' + income: + $ref: '#/components/schemas/KfoThresholdOut' type: object - additionalProperties: false + required: + - balance + - income + title: KfoSummaryOut + KfoTableOut: properties: version_id: type: string format: uuid + title: Version Id computed_at: type: string format: date-time + title: Computed At is_frozen: type: boolean + title: Is Frozen editable: type: boolean + title: Editable read_only_reason: - type: - - string - - 'null' - enum: - - VERSION_NOT_EDITABLE - - LOCK_REQUIRED - - LOCKED_BY_OTHER - - null + anyOf: + - type: string + enum: + - VERSION_NOT_EDITABLE + - LOCK_REQUIRED + - LOCKED_BY_OTHER + - type: 'null' + title: Read Only Reason units: type: string const: thousand_rub + title: Units + default: thousand_rub companies: - type: array items: - $ref: '#/components/schemas/KfoCompanyOut' + $ref: '#/components/schemas/KfoCompany' + type: array + title: Companies requisites: $ref: '#/components/schemas/KfoRequisitesOut' summary: $ref: '#/components/schemas/KfoSummaryOut' justification_required: type: boolean + title: Justification Required threshold_justification: - type: - - string - - 'null' - maxLength: 4000 - goodwill: - $ref: '#/components/schemas/KfoGoodwillOut' + anyOf: + - type: string + - type: 'null' + title: Threshold Justification balance: $ref: '#/components/schemas/KfoStatementOut' income: $ref: '#/components/schemas/KfoStatementOut' warnings: - type: array items: $ref: '#/components/schemas/KfoWarningOut' + type: array + title: Warnings + goodwill: + $ref: '#/components/schemas/KfoGoodwillOut' + type: object required: - version_id - computed_at - is_frozen - editable - read_only_reason - - units - companies - requisites - summary - justification_required - threshold_justification - - goodwill - balance - income - - warnings - KfoCommentIn: - type: object - additionalProperties: false + - goodwill + title: KfoTableOut + KfoThresholdOut: properties: - statement_type: + deviation_total: + type: string + title: Deviation Total + base_kind: type: string enum: - - balance - - income - line_code: + - NET_ASSETS + - BALANCE_TOTAL + - SALES_PROFIT + - REVENUE + title: Base Kind + base_amount: type: string - comment: - type: - - string - - 'null' - maxLength: 2000 + title: Base Amount + limit_percent: + type: string + title: Limit Percent + limit_amount: + type: string + title: Limit Amount + deviation_percent: + anyOf: + - type: string + - type: 'null' + title: Deviation Percent + exceeded: + type: boolean + title: Exceeded + type: object required: - - statement_type - - line_code - - comment - KfoAnnotationsIn: - type: object - additionalProperties: false - properties: - comments: - type: array - items: - $ref: '#/components/schemas/KfoCommentIn' - threshold_justification: - type: - - string - - 'null' - maxLength: 4000 - methodology_deviations: - type: - - string - - 'null' - maxLength: 4000 - required: [] - description: Отсутствующее поле не меняется, null — очищает - ErrorDetail: - type: object - additionalProperties: false + - deviation_total + - base_kind + - base_amount + - limit_percent + - limit_amount + - deviation_percent + - exceeded + title: KfoThresholdOut + KfoWarningOut: properties: code: type: string + title: Code message: type: string - line_code: - type: - - string - - 'null' + title: Message + legal_entity_id: + anyOf: + - type: string + format: uuid + - type: 'null' + title: Legal Entity Id statement_type: - type: - - string - - 'null' + anyOf: + - $ref: '#/components/schemas/StatementType' + - type: 'null' + count: + anyOf: + - type: integer + - type: 'null' + title: Count + type: object required: - code - message - ErrorOut: - type: object - additionalProperties: false + title: KfoWarningOut + StatementType: + type: string + enum: + - balance + - income + title: StatementType + description: DBML `statement_type`. + ValidationError: properties: - detail: - $ref: '#/components/schemas/ErrorDetail' + loc: + items: + anyOf: + - type: string + - type: integer + type: array + title: Location + msg: + type: string + title: Message + type: + type: string + title: Error Type + input: + title: Input + ctx: + type: object + title: Context + type: object required: - - detail - securitySchemes: - bearerAuth: - type: http - scheme: bearer - bearerFormat: JWT + - loc + - msg + - type + title: ValidationError diff --git a/model/data-model-v5.27.dbml b/model/data-model-v5.27.dbml new file mode 100644 index 0000000..ebb1488 --- /dev/null +++ b/model/data-model-v5.27.dbml @@ -0,0 +1,1772 @@ +// ДКБ · Модель данных v5.27: справочники, файлы/распознавание, консолидация, КФО, аудит, ЕХФД, метрики качества +// Формат: DBML (dbdiagram.io). Вставить на https://dbdiagram.io/d +// >>> Источник истины по модели данных. Аналитика в Confluence ссылается сюда. +// Над каждой таблицей — блок «ИСТОЧНИК»: пункт ТЗ АРМ КА BIQ-34 / ТЗ ЕХФД (помечено «ЕХ ТЗ») / Методика; +// страница аналитики (md); экран макета dkb_24.09.26; версия модели, в которой таблица появилась (v4 = исходная DBML аналитики). +// +// v5.27 (arm-core feat/kfo-table-back, PR #251, миграция 0014_dbml_v5_27): +// 1. vgo_assignment.side: D/K для ОФР, NULL для ББ (DOC-277424 §3.4). +// Уникальность ББ — версия/тип/строка; ОФР — версия/тип/строка/сторона. +// 2. kfo_result.snapshot не вводится (решение владельца 13.11.2026, OQ-192): незамороженный результат не хранится, +// каждый GET считает по текущим данным; до заморозки сохраняются только ручные поля (kfo_result — обоснование порога +// и шапка без расчётных сумм; kfo_result_line — только строки с комментарием, суммы не заполнены, +// consolidated_amount = 0; строк kfo_result_line_company нет). Полный снимок пишет переход в «Верифицирован». +// 3. correction.ordinal не вводится: основная строка задаётся parent_correction_id, +// дополнительные строки выводятся в стабильном порядке ID, порядок ввода не хранится. +// Порядок колонок kfo_result_line_company отражает последовательность ALTER TABLE v5.26. +// +// v5.26 (расчёт КФО, AURORA-1546/1547; RSHB/arm-ka-docs#206; одна миграция arm-core — в AURORA-1546): +// 1. kfo_result_line_company: + client_amount («по данным клиента», тыс. руб.), + vgo_amount («в т. ч. взаимные», со +// знаком строки); amount — «очищенные показатели» (context/05-domain-rules.md §8 «КФО», п. 6–7). Без них колонки +// экрана КФО не восстановить из сохранённого результата. +// 2. kfo_result: + warnings jsonb (предупреждения расчёта, §8 «КФО» п. 12), + corrections_count_bb/_ofr, +// corrections_amount_bb/_ofr (сводка корректировок для реквизитов формы, п. 11) — замороженная версия отдаёт то, +// что видели при верификации. +// 3. audit_action: + KFO_EDIT — сохранение ручных полей КФО (комментарии строк, обоснование порога, причины +// отклонений от Методики), раздел KFO. +// 4. elimination_rule.elimination_mode: значение min_or_full (правило ББ 2 по DOC-274230: min при обеих сторонах, +// сторона 1 при пустой стороне 2, без порога; §5 п. 1, §8 «КФО» п. 4). varchar — схема не меняется; seed v1 +// правится на месте в AURORA-1546 (верифицированных версий нет). +// 5. kfo_result_line: пояснения приведены к §8 «КФО» (единицы — тыс. руб.; eliminated_auto / eliminated_manual — от +// агрегатов без / с корректировкой, по одному агрегату не складываются; goodwill_adjust — у GOODWILL и итогов; +// consolidated_amount = sum − vgo + goodwill_adjust). Структура не меняется. +// 6. ex_snapshot_item.item_type: + version_card — карточка версии на дату верификации (участники маршрута с ФИО, решение +// УОБ, обоснования, состав); payload — конверт contracts/ex-payload-v1.schema.json (у primary_document — null), +// payload_schema_version = "1.0" (RSHB/arm-ka-docs#208, AURORA-1560; context/05-domain-rules.md §15). +// 7. audit_action: + EX_SNAPSHOT_WRITE — запись среза ЕХ при «Верифицирован», раздел KFO. +// 8. ex_snapshot.status: пояснение под ручной архив (решение 27.10.2026) — ACTUAL «не архивирован», а не «последняя +// верифицированная»; запись среза прежний в архив не переводит. Структура не меняется. +// Миграция: новые колонки nullable, значения enum (KFO_EDIT — AURORA-1546, EX_SNAPSHOT_WRITE — AURORA-1560); откат +// удаляет колонки, значения enum при наличии событий не откатываются (как FILE_REPROCESS в v5.21). +// +// v5.25 (AURORA-1759, RSHB/arm-ka-docs#202; миграция в arm-core — отдельный change arm-core#190): +// 1. Три новые таблицы фактов качества распознавания первички: quality_intake_fact, quality_outcome_fact, +// quality_failure_fact (отдельная схема dkb_metrics, append-only, без FK на рабочие таблицы) — +// design openspec/changes/quality-metrics §1.4-1.6. +// 2. s3_bucket_role: + raw (сырой ответ ИС Сканирование, не экспирится с файлом); recognition_job.raw_s3_object_id +// — ссылка на него (отменяет решение v5.13 «сырой ответ в БД не регистрируется»). +// 3. stored_file.is_scan — признак скан/цифровой PDF (NULL для не-PDF). +// 4. permission_object: + QUALITY_METRICS (право QUALITY_METRICS_VIEW — seed в arm-core). +// +// v5.24 (AURORA-1751/1540/1541, RSHB/arm-ka-docs#196; одна миграция arm-core, уже слита main): +// 1. Новая таблица vgo_assignment: ручное решение аналитика по строке сводной ОСВ сверх автоматического разнесения +// по правилам ВГО (context/05-domain-rules.md §5) — привязка к правилу (ASSIGN), снятие привязки (UNASSIGNED), +// отметка «не учитывать в КФО» (is_exclude) у нераспределённой строки. Автоматическое разнесение по-прежнему не +// хранится (DECISION-5) — только это, поверх него. Одна действующая запись на (версия, тип отчётности, +// osv_entry_id); повторное решение обновляет её, а не добавляет новую. +// 2. Черновик контракта (dev/api-contract-corrections.md §5) предполагал отдельное поле `side` — строка с +// одновременно дебетовым и кредитовым сальдо даёт две позиции. В уже слитой миграции 0011 (arm-core) поля нет: +// решение по такой строке одно на обе позиции, не по стороне отдельно. +// Реализация полностью в коде: сервис `VgoAssignmentCrudService`, эндпоинты `PUT|DELETE …/vgo-assignments/rule`, +// `PUT …/vgo-assignments/exclude`; участвует в таблице корректировок (`CorrectionAggregateSource`, §8). +// +// v5.23 (решения владельца 23.10.2026; RSHB/arm-ka-docs#164, AURORA-1536, ревью arm-core #175; одна миграция arm-core): +// 1. consolidation_version.elimination_rules_version: not null. Версия правил элиминации (elimination_rule.version_no), +// действующая на дату создания версии консолидации (OQ-174), фиксируется при создании и дальше не меняется; по ней +// считаются шахматка-пресет и пары ВГО (context/05-domain-rules.md §4, §5). Пустое значение больше не допускается: +// «пока поле пусто — последние открытые правила» убрано, версия без правил не создаётся. +// Миграция: существующим версиям — номер версии правил, действовавшей на дату created_at (по Москве), при её отсутствии — +// последней открытой; затем NOT NULL. Откат снимает только NOT NULL, значения остаются. +// +// v5.22 (22.10.2026; AURORA-1446): +// 1. comment.object_type: убраны устаревшие chess_cell и kfo_result_line. +// Сохранено описание привязки: document | consolidation_version | …. +// Комментарий к правке пары ВГО — correction.comment; к строкам КФО — kfo_result_line.comment +// и client_kfo_line.comment (отдельные поля, не лента комментариев). +// Уточнены описание и источники comment. Структура БД не меняется, миграция не требуется. +// +// v5.21 (решение владельца 22.10.2026; RSHB/arm-ka-docs#149, AURORA-1442; одна миграция arm-core): +// 1. audit_action: значение FILE_REPROCESS — повторная обработка файла пользователем (раздел PRIMARY_DOCUMENTS; +// context/05-domain-rules.md §9.5 п. 6, п. 13). Файл не загружается заново — создаётся новое recognition_job +// (attempt + 1) и становится current_recognition_job_id, поэтому FILE_UPLOAD (приём нового файла) и FILE_REPLACE +// (автозамена другим файлом) не подходят. Событие — на файл: object_type = stored_file, before_value — прежнее +// задание, after_value — новое; пакет версии пишет по событию на каждый отправленный файл. +// Миграция только добавляет значение enum. Откат при наличии событий FILE_REPROCESS останавливается с ошибкой и ничего +// не меняет (журнал append-only). +// +// v5.20 (решения владельца 21.10.2026; RSHB/arm-ka-docs#147, OQ-161 вариант 2, AURORA-1442; одна миграция arm-core): +// 1. document: is_modified (boolean, not null, default false), original_values (jsonb), modified_by (FK → app_user), +// modified_at — уточнение периода и счёта документа ОСВ аналитиком (context/05-domain-rules.md §9.5 п. 12). +// Правка — на месте в period_year / period_quarter / is_cumulative / osv_account_code; при первой правке прежние +// значения этих четырёх полей копируются в original_values и дальше не меняются (первичка не мутируется, как +// osv_entry.original_values). Копия document не создаётся: file_ordinal — номер документа в результате +// распознавания, не номер редакции. Строки osv_entry при смене счёта не меняются. +// Миграция только добавляет колонки (is_modified = false, остальные NULL). Откат на данных с is_modified = true +// останавливается с ошибкой и ничего не меняет (потерялись бы исходные значения). +// +// v5.19 (решения владельца 17.10.2026, Р-1…Р-19; RSHB/arm-ka-docs#116, openspec/changes/archive/2026-10-19-unified-statement-normalization; +// контракт результата v2.0; одна миграция arm-core): +// 1. financial_statement_line: снята уникальность (meta_id, line_code) — повтор кода законен, хранятся все строки (Р-4); +// line_code остаётся nullable и пуст у строк unreadable / missing / detail (Р-8, отменяет OQ-36 / OQ-141). +// Новые колонки: row_index (порядок строки в документе; у шаблонных и созданных правкой — NULL), source_ref +// (положение в источнике, частичный уникальный индекс (meta_id, source_ref) WHERE source_ref IS NOT NULL), +// line_status (enum statement_line_status), line_code_text (сырой текст ячейки кода), parent_row_index (родитель +// detail), needs_review (как пришло от сервиса, пересчитывается ядром после правок), is_confirmed (код подтверждён +// аналитиком). +// 2. financial_statement_value: source_text (values[].text — сырой текст источника), needs_review (как пришло, +// пересчитывается после правок; пересматривает п. 4 v5.13 «считается, не хранится»), is_confirmed + confirmed_by / +// confirmed_at (подтверждено аналитиком без изменения значения). +// 3. financial_statement_column: period_kind (absolute | relative:<вид> | none), label_confidence. +// 4. financial_statement_meta: form_variant (enum statement_form_variant), needs_review_fields (поля шапки с +// needs_review, jsonb-массив имён). +// 5. Новая таблица document_issue (+ enum'ы document_issue_kind, issue_severity, issue_source): предупреждения и нарушения +// §7 документа как пришли от сервиса или посчитаны ядром; очищается при перезагрузке файла; при пересчёте после +// правки записи relation / soft заменяются целиком, warning источника service сохраняются, кроме снимаемых правкой. +// 6. recognition_job.statements_version — коммит копии общего кода arm_statements, которым посчитан результат. +// 7. document: уникальность (stored_file_id, document_type, osv_account_code) — только для типов кроме BALANCE / INCOME +// (частичный индекс; ОСВ по счёту, DISCLOSURE, OTHER — «один на тип на файл», §9.5 п. 2); вторая форма того же типа +// в файле хранится (Р-10, Р-15); новая колонка file_ordinal — индекс документа в documents[] результата (у всех +// документов при любом MAJOR контракта; несозданный документ номер не занимает), уникален в файле. +// 8. related_group.crm_id (varchar, nullable, unique) — ID ГСК/ГСЗ в ЕЦП CRM (AURORA-1619, arm-core#112, OQ-33). +// 9. Признак «строка создана правкой аналитика» — financial_statement_line.row_index IS NULL (заменяет прежний +// признак line_code_original_value IS NULL: у строк unreadable / missing / detail источника кода тоже нет). +// line_code_original_value — код строки из результата как пришёл (NULL у статусов кроме ok и у строк, созданных +// правкой); правками не меняется. +// 10. financial_statement_meta: header_source (jsonb — сырой текст и уверенность всех полей шапки результата, +// {"<поле>": {"text", "confidence"}}), organization_name_original_value и organization_name_modified (наименование +// правится PATCH шапки, как ИНН). +// 11. document_issue: codes (jsonb — коды цели и членов нарушения §7), column_index (столбец предупреждения вне колонок +// формы — COLUMN_SKIPPED и др.). +// Перед выкладкой база очищается от форм ББ/ОФР (решение владельца 18.10.2026, Р-24; скрипт ядра +// scripts/purge_statements.py): миграция только добавляет колонки, таблицу, enum'ы, индексы и значения по умолчанию, +// §7 по существующим формам не считает. Заполнение (миграция проходит и на непустой базе): row_index строк — NULL у +// строк с line_code_original_value IS NULL (созданных правкой), у остальных — по возрастанию line_code внутри формы с 0; +// line_status = ok, needs_review строк = false, needs_review значений = НЕ values_ok (values_ok — правлено или +// is_recognized не false и confidence 100 / NULL), source_text = string_value, is_confirmed = false, period_kind = +// absolute при period_date, иначе none, form_variant = NULL (набор §7 — полный), header_source — из *_original_value +// и inn_confidence, organization_name_original_value = organization_name, file_ordinal — по (created_at, id) внутри +// файла с 0 (у всех документов), related_group.crm_id = NULL; document_issue — пуст: корректность нарушений у форм, +// оставшихся в неочищенной базе, не гарантируется до первой правки. +// Откат гарантирован на пустой БД и на данных, которые v5.18 представляет без потери первичных данных и ручного ввода; +// downgrade останавливается с ошибкой и ничего не меняет при повторе кода в форме, строке без кода, двух формах одного +// типа в файле, непустом related_group.crm_id, organization_name_modified = true; производные и сигнальные колонки +// v5.19 и document_issue удаляет с записью числа удалённых значений в журнал миграции. +// +// v5.18 (решения владельца 09.10.2026; RSHB/arm-ka-docs#90, openspec/changes/console-files): +// 1. stored_file.consolidation_id (uuid, nullable, FK → consolidation): консолидация (ГК + год + квартал), в контексте +// которой загружен файл, — основа «рабочего стола консоли» (полный состав стола — context/05-domain-rules.md §9.5 п. 11), общего для всех версий периода. NULL — загрузка без контекста +// (страница ЮЛ, монозаёмщики, API). Пишется один раз при загрузке (POST /consolidations/{id}/legal-entities/{le}/files); +// при автозамене новый файл без контекста наследует consolidation_id заменённого. Индекс (legal_entity_id, consolidation_id). +// Что использует конкретная версия, по-прежнему определяет consolidation_document; заморозка после VERIFIED — привязки и ex_snapshot. +// Миграция 0006 arm-core: backfill по consolidation_document (при нескольких периодах — самый ранний). +// 2. Note recognition_job.reply_deadline: срок ответа OCR — 20 мин (OQ-159, решение владельца 09.10.2026), структура не меняется. +// +// v5.17 (решение владельца 07.10.2026, пересмотр OQ-31; RSHB/arm-ka-docs#53): +// 1. legal_entity.kpp и legal_entity_history.kpp (varchar, nullable): КПП юридического лица. Формат — 9 символов: +// 4 цифры, 2 символа (цифра или латинская A–Z), 3 цифры; иначе INVALID_KPP. У ИП КПП запрещён (INVALID_KPP; +// импорт — строка пропускается). Для ЮЛ необязателен: существующие записи и импорт без колонки КПП остаются валидными. +// Уникальность — только legal_entity.inn (КПП — атрибут, сопоставление контрагентов ОСВ по ИНН не меняется). +// Изменение КПП — обычная правка со снимком в legal_entity_history. Отменяет п. 2 v5.14 («КПП не вводится»). +// +// v5.16 (Срез 4, решения владельца 06.10.2026): +// 1. Новый enum counterparty_match_kind и колонка osv_entry.counterparty_match_kind (nullable): способ, которым строке ОСВ +// сопоставлен контрагент. NULL ⇔ counterparty_id NULL; INN / NAME / ALIAS — шаг автоматического резолва при маппинге +// (ИНН; точное имя; нормализованное имя или алиас); MANUAL — ручное сопоставление аналитиком; PROPAGATED — +// автоприменение по имени в версии (DOC-221278 п. 12–13). Миграция 0004 заполняет колонку по уже смапленным строкам. +// 2. Без изменений структуры, уточнение семантики consolidation_document: строка создаётся при подтверждении документа +// (по документам ЮЛ — членов периметра версии «в работе»), при подходящем подтверждённом документе — при добавлении ЮЛ +// в периметр; удаляется при отвязке документа (замена файла, удаление, исключение ЮЛ из периметра). Для одной +// (ЮЛ, document_type, osv_account_code, период, признак нарастающего) привязан один документ — самый поздний confirmed_at. +// 3. Без изменений структуры, уточнение семантики consolidation_lock: блокировка без истечения по времени — активна, пока её не +// снял владелец, администратор или перехват другим пользователем с CONSOLIDATION_EDIT по причине (OQ-119). +// +// v5.15 (Срез 2c, решения владельца 05.10.2026): +// 1. osv_data_type: добавлено NU — строки налогового учёта из ОСВ «БУ, НУ» хранятся в osv_entry как есть (OQ-45); +// контракт результата допускал NU с v1.0. +// 2. app_user.email хранится в нижнем регистре: нормализация при записи (seed, администрирование), поиск по +// lower(email) остаётся; уникальность — существующий UNIQUE по email (OQ-59). Структура не меняется. +// 3. Без изменений структуры, уточнение семантики: stored_file.status — PROCESSING ставится inbox'ом по ack сервиса, +// PROCESSED/ERROR — циклом маппинга результата (OQ-67); маппинг выполняется отдельно от inbox (OQ-69); +// recognition_job.group_schema заполняет маппер; «Итого» в osv_entry не хранится (OQ-01). +// 4. Индексы для удаления файла и обхода дерева строк: osv_entry (parent_entry_id), osv_entry (recognition_job_id), +// stored_file (current_recognition_job_id) — проверки FK при DELETE документа/файла и очистке перед повторным +// маппингом шли seq scan'ом (OQ-117). Замер: DELETE документа 1000 строк 8,6 с → 16 мс на 200 тыс. строк. +// +// v5.14 (Срез 1, решения владельца 04.10.2026): +// 1. ЮЛ может состоять ОДНОВРЕМЕННО в одной ГСК и одной ГСЗ (но только в одной группе каждого вида) — отменяет +// ANL-030/v5.8. В related_group_member возвращена денормализованная колонка kind (= related_group.kind, +// равенство проверяет ядро при записи); частичный уникальный индекс стал (legal_entity_id, kind) WHERE is_current. +// 2. КПП у ЮЛ не вводится (OQ-31: поле есть только в макете; решение владельца — не нужен). [Отменено в v5.17 — КПП введён.] +// 3. Без изменений структуры, уточнение семантики: company_group.crm_id = «iD ГК» выгрузки ЕЦП CRM +// (docs/source/biq/ecp-export-lagal-entity.xlsx), base_regional_office — «Базовый РФ ГК в CRM»; флаги «входит в ГСК/ГСЗ» +// выгрузки → членство в одной неявной ГСК/ГСЗ на ГК (правила импорта — 05-domain-rules.md §13). +// 4. audit_action: добавлены LEGAL_ENTITY_CHANGE (создание/изменение/закрытие/перевод ЮЛ, алиасы) и USER_CHANGE +// (создание/деактивация пользователя администратором) — ТЗ 8.2 «журнал изменений справочников» их требует, +// в v5.13 были только GROUP_CHANGE / MEMBERSHIP_CHANGE / USER_ROLE_CHANGE. +// +// v5.13 (ревью 30.09–01.10, замечания 1–7; п.2 parent_entry_id — отложен): +// 1. Права как данные: permission + role_permission (матрица доступов ТЗ сноска 29 — в БД, не в коде); +// права только через роли, прямых назначений пользователю нет. Отменяет решение v5.4. +// 3. osv_group_schema убрана — схема группировок шапки = recognition_job.group_schema jsonb; +// тип строки — osv_entry.group_type. +// 4. financial_statement_value.needs_review убран — производное от confidence/is_recognized, считается запросом. +// 5. recognition_job: оставлен только result_s3_object_id; сырой ответ ИС Сканирование в БД не регистрируется +// (scan-gateway пишет его в S3 по конвенции raw/{job_id}.json для разбора инцидентов). +// 6. stored_file.fields_modified убран — COUNT(is_modified) по данным документа. +// 6-бис. Разделение «файл / документ»: stored_file = физический файл (один на загрузку), document = логическая +// единица (ББ, ОФР, ОСВ по счёту, расшифровка) — один файл содержит N документов (PDF с ББ+ОФР, xlsx с +// несколькими ОСВ листами). Чек-лист, проверки периода/контрагента, подтверждение, FK разобранных данных, +// consolidation_document и ex_snapshot_item — на document. Замена (superseded_by) остаётся на файле целиком. +// 7. file_type: убран P7. +// +// Изменения относительно v4 (кратко): +// FIX — исправлены дефекты v4 (PK у meta, типы, копипаст-note про «контрагента», row_type, parent_entry_id) +// ADD — audit_event, status_history, company_group_assignment, elimination_rule, statement_check_result, +// consolidated_osv_line, chess_cell, vgo_pair_match (агрегат по ТЗ 6.4), ofr_vgo_match, kfo_result_*, +// client_kfo_line, comment, export_log/preset, ex_snapshot*, correction_version +// CHG — role → 7 ролей ТЗ + сегмент (ЗО); consolidation_version — сценарий/подход/гудвилл/решение УОБ; +// stored_file — счёт ОСВ, версии/замена; osv_entry — признак контрагента + override; legal_entity — монозаёмщик +// +// Ключи: у ВСЕХ сущностей суррогатный PK (uuid). Натуральные ключи — UNIQUE-колонки. +// (Для высокообъёмных таблиц — osv_entry, audit_event — uuid можно заменить на bigint identity.) +// ЮЛ и ГК — SCD Type 4 (живая таблица + *_history). Членство в ГСК/ГСЗ — SCD Type 2 (is_current). +// Первичка не мутируется: правки распознанных значений = поле + *_original_value + is_modified + audit_event. +// Производные объекты версии (сводная ОСВ, шахматка, КФО) — snapshot на consolidation_version: +// пересчитываются пока версия «в работе», замораживаются при VERIFIED (ТЗ 6.10, 7.2). +// +// DECISION-1 (ТЗ 6.4 vs v4): первичная модель сопоставления ВГО — АГРЕГАТ по паре компаний × правилу пары счетов +// (vgo_pair_match), как в ТЗ/Прил. 3. Строковое 1:1 сопоставление (vgo_match) сохранено как детализация агрегата. +// DECISION-2 (ЕХФД): ЕХ — те же таблицы + ex_snapshot как «замороженный указатель» на состав верифицированной версии. +// Отдельная физическая БД для ЕХ не предполагается; архив в data lake — выгрузка снапшота. +// DECISION-3: ГО — обычная запись regional_office с is_head_office=true (вместо NULL). +// DECISION-5 (производные консолидации): сводная ОСВ, пары ВГО и шахматка НЕ хранятся, пока версия в работе — +// считаются запросом из osv_entry (актуальные прогоны) × consolidation_member × elimination_rule. +// Признак контрагента ГСК/ГК/Внешний — функция от osv_entry.counterparty_id и периметра версии; ручное действие +// аналитика = сопоставление контрагента с ЮЛ (counterparty_id + алиас), комментарий — в audit_event. +// v5.16: способ сопоставления контрагента (osv_entry.counterparty_match_kind) ХРАНИТСЯ; пробелы квартальной склейки и сам свод — нет. +// При VERIFIED производные замораживаются один раз: структурированный снапшот — jsonb в ex_snapshot_item.payload +// (просмотр в ЛК ЕХ), подготовленный xlsx для выгрузки — s3_object. Отдельная NoSQL-база не вводится. +// Корректировки и гудвилл ссылаются на пару ВГО по естественному ключу (правило, компания 1, компания 2). +// DECISION-4 (распознавание): два отдельных stateless-сервиса без доступа к БД — +// tabular-parser (XLS/XLSX/XLSB) и scan-gateway (адаптер ИС Сканирование для PDF/PNG/JPG). +// Запрос: NATS recognition.{tabular|image}.request + файл в S3 uploads/. Результат: JSON в S3 results/ + +// NATS recognition.result. Единственный писатель в БД — arm-core; корреляция по recognition_job.id; +// идемпотентность по msg_id; outbox при создании recognition_job; DLQ. +// v5.13: результат одного файла содержит N документов; ядро создаёт document при маппинге результата. +// +// v5.1 (по макету dkb_24.09.26): consolidation_lock (взятие в работу), participant_disclosure (расшифровки участника), +// legal_entity_history.event_type, statement_meta реквизиты (ОКПО/ОКОПФ/ОКФС/адрес), kfo_result_line: vgo/share, +// client_kfo_line: ВГО клиента/банка + причина, audit_event: section/result/user_agent, статусы REJECTED/ARCHIVED, +// document_check (проверка периода/контрагента файла), company_group.crm_id nullable (ANL-037). +// v5.2 (DECISION-4): recognition_job: subject/msg_id/deadline/external_task_id/result_schema_version/error_code, статусы timeout/dlq. +// v5.12 (ревью part09): ЕХ самодостаточен — срез не ссылается FK на консолидацию и её результаты (их удаляют по сроку, +// срез хранится «вечно»): ex_snapshot.source_version_id/no без FK; ex_snapshot_item = stored_file_id (исходники храним) +// ИЛИ payload jsonb (сводная ОСВ, ВГО, шахматка, КФО, гудвилл, корректировки); убраны object_type/object_id, +// prepared_s3_object_id (готовые файлы не храним), ex_snapshot.source_system, audit_event.s3_object_id; +// добавлен ex_snapshot.superseded_by_snapshot_id (архивный → актуальный). +// v5.11: consolidation_version.elimination_rules_version — какая версия правил Прил. 3/4 применялась (объяснимость и +// воспроизводимость расчёта замороженной версии). +// v5.10 (DECISION-5, ревью part08): убраны consolidated_osv_line, consolidated_osv_source, vgo_pair_match, chess_sheet, chess_cell, +// osv_entry.counterparty_type (выводится); correction ссылается на пару по (rule_id, company_1_id, company_2_id); +// ex_snapshot_item получил payload jsonb. +// v5.9 (ревью part06/07): statement_check_result убрана — проверки считаются на лету, акцепт КА — на документе +// (financial_statement_meta.checks_accepted_*); reviewed_by/at перенесены в meta (документ ревьюится целиком); +// financial_statement_line: убраны ordinal/row_kind/section (порядок и группирующие строки — шаблон формы на фронте); +// consolidation: company_group_id NOT NULL, related_group_id nullable — ГСК фиксируется при VERIFIED (ANL-029); +// consolidation_version: убран package_uuid (документы в ИС Сканирование по одному, ключ = recognition_job.id), добавлен updated_by. +// v5.8 (ревью part01): убраны company_group_assignment (видимость ТЗ 8.1 выводится из company_group.base_regional_office_id, +// legal_entity.regional_office_id, app_user.regional_office_id; история перезакрепления — company_group_history), +// legal_entity.is_mono_borrower (монозаёмщик = company_group_id IS NULL), related_group_member.kind и индекс related_group(id, kind) +// (после ANL-030 partial-unique по legal_entity_id без условия на kind). +// v5.7: метрики качества распознавания — recognition_job.fields_total/low_confidence/unrecognized (что пришло от сервиса). +// (stored_file.fields_modified/fields_reviewed и financial_statement_value.needs_review — убраны в v5.9/v5.13 как производные.) +// v5.6 (ревизия 28.09, решения по каждой убранной таблице): +// account_code_alias — нормализация кодов в коде; редкие субсчета клиентов добавляются в chart_of_account +// vat_rate — константы; средневзвешенная ставка хранится в vgo_pair_match +// crm_import_batch — импорт справочников только на старте (миграция); след в audit_event(DIRECTORY_IMPORT) + файл в s3_object +// osv_entry_edit — audit_event(RECOGNIZED_DATA_EDIT) + osv_entry.original_values/modified_by/modified_at +// document_check — колонки period_check/counterparty_check (с v5.13 — на document); сверка с периодом версии вычисляется +// export_preset — выбор колонок в момент экспорта, без сохранения наборов +// export_log — audit_event(EXPORT, after_value={report_code, format, columns, rows}); файлы выгрузок не хранятся (S3 lifecycle exports/ = 7 дней) +// v5.5: убрана correction_version — история корректировок (сноска 22) ведётся audit_event(before/after), version_no в correction. +// v5.4: возвращён s3_object (реестр объектов S3 — lifecycle/ретеншн ТЗ 8.4, sha256, единая точка ключей); permission/role_permission — матрица в коде (ОТМЕНЕНО в v5.13). +// v5.3 РЕВИЗИЯ по правилу «в базе только то, что есть в ТЗ или на экране макета». Удалено 14 таблиц: +// - account_code_alias: нормализация кодов (60.1=60.01) — алгоритм в коде (ARCH-004), не справочник +// - vat_rate: ставки 0/10/20/22 — константы; средневзвешенная ставка хранится в vgo_pair_match.vat_weighted_rate +// - disclosure_item: расшифровки участника: в UI скрыты, в ТЗ только тип документа «Расшифровка» → вне scope до решения ANL-040 +// - participant_disclosure: см. disclosure_item +// - crm_import_batch: журнал импортов не в ТЗ/UI; факт импорта и ошибки — audit_event(GROUP_CHANGE/IMPORT) +// - permission / role_permission: ВОЗВРАЩЕНЫ в v5.13 (замечание 1) +// - integration_message: РЕШЕНИЕ — обмен с NATS завязан только на recognition_job (status queued→published = outbox; result_msg_id UNIQUE = inbox/дедуп); отдельная таблица появится, только если возникнет второй тип сообщений +// - osv_entry_edit: история правок сумм ОСВ — audit_event(RECOGNIZED_DATA_EDIT, before/after) +// - document_check: результат проверки периода/контрагента — две колонки (с v5.13 — в document) +// - vgo_match: строковое сопоставление 1:1 — только в аналитике, нет ни в ТЗ (агрегат по Прил. 3), ни в UI; детализация агрегата — consolidated_osv_source +// - export_preset: сохранённые пресеты колонок не в ТЗ/UI (выбор столбцов — при экспорте, без сохранения) +// - export_log: история выгрузок (ТЗ 8.2, ЕХ 6.6) — audit_event(EXPORT) с составом в after_value + +// ============ ENUMS ============ +Enum entity_type { + "ЮЛ" + "ИП" +} + +Enum related_group_kind { + gsk + gsz +} + +Enum account_kind { + A [note: 'актив'] + P [note: 'пассив'] + AP [note: 'активно-пассивный'] +} + +Enum account_row_type { + account [note: 'синтетический счёт, напр. 60'] + subaccount [note: 'субсчёт, напр. 60.01'] + analytic [note: 'подсубсчёт, напр. 60.01.1 / 76.07.1'] +} + +Enum business_segment { + DKB [note: 'крупный бизнес'] + DMSB [note: 'малый и средний'] + DM [note: 'микробизнес'] +} + +Enum document_type { + BALANCE [note: 'Бухгалтерский баланс (0710001)'] + INCOME [note: 'ОФР (0710002)'] + OSV [note: 'ОСВ по счёту — osv_account_code обязателен'] + DISCLOSURE [note: 'Расшифровка'] + OTHER [note: 'Прочий документ'] +} + +Enum file_type { + XLS + XLSX + XLSB + PDF + PNG + JPG +} + +Enum recognition_pipeline { + tabular [note: 'XLS/XLSX/XLSB — tabular-parser'] + image [note: 'PDF/PNG/JPG — ИС Сканирование через scan-gateway'] +} + +// v5.25: метрики качества первички (05-domain-rules.md §14) +Enum quality_outcome { + AUTO_CONFIRMED + CONFIRMED + DELETED + SUPERSEDED + DELETED_AFTER_CONFIRM + SUPERSEDED_AFTER_CONFIRM +} + +Enum quality_cause { + SERVICE_ERROR [note: 'есть правка, не найденная в сыром тексте сервиса'] + OUR_ERROR [note: 'есть правка, найденная в сыром тексте сервиса (наш разбор ошибся)'] + CLIENT_DOCUMENT [note: 'подтверждён с акцептом нарушенных жёстких проверок, без несписанных правок на это нарушение'] + WITHOUT_PARTICIPATION [note: 'AUTO_CONFIRMED'] + AS_IS [note: 'CONFIRMED без правок и без акцепта'] +} + +Enum quality_failure_kind { + SERVICE_ERROR [note: 'recognition_job.status = error'] + TIMEOUT + NO_ACK [note: 'dlq — redelivery после max_deliver'] + MAPPING [note: 'MappingFatal, файл → ERROR'] +} + +Enum period_quarter { + Q1 + Q2 + Q3 + Q4 +} + +// v5.13: статус ФАЙЛА — только техническая обработка. Проверка и подтверждение аналитиком — на document. +Enum file_status { + UPLOADED [note: 'Загружен: поступил, в очереди'] + PROCESSING [note: 'Обработка: распознавание/парсинг'] + PROCESSED [note: 'Результат получен, документы созданы'] + ERROR [note: 'Ошибка: нечитаемый файл, требуется повторная загрузка'] + SUPERSEDED [note: 'Заменён новой версией файла (ТЗ 8.2) — вместе со всеми документами в нём'] +} + +// v5.13: статус ДОКУМЕНТА (ANL-003: «Требуется проверка» при confidence < 100% или непройденных проверках; +// «Подтвержден» — финальный после проверки аналитиком) +Enum document_status { + RECOGNIZED [note: 'данные получены, ручная проверка не требуется по порогу'] + NEEDS_CHECK [note: 'Требуется ручная проверка: confidence < 100% / контрольные суммы / период или контрагент не совпали'] + CONFIRMED [note: 'Подтвержден аналитиком: данные зафиксированы, доступны консолидации'] + ERROR [note: 'документ не разобран (напр. не найдена шапка ОСВ)'] +} + +Enum s3_bucket_role { + uploads + results + exports + archive + raw [note: 'сырой ответ ИС Сканирование, raw/{job_id}.json; не экспирится с файлом — вход для эталонного прогона (OQ-179)'] +} + +Enum recognition_status { + queued [note: 'создано, запрос в outbox'] + published [note: 'запрос опубликован в NATS'] + processing [note: 'сервис подтвердил взятие в работу (опц. heartbeat)'] + done + needs_check [note: 'результат получен, но confidence < 100% / контрольные суммы не сошлись'] + error + timeout [note: 'reply_deadline истёк без результата'] + dlq [note: 'сообщение ушло в DLQ после max_deliver'] +} + +Enum statement_type { + balance + income +} + +// v5.19: статус строки формы ББ/ОФР (05-domain-rules.md §11.4) +Enum statement_line_status { + ok [note: 'код разобран'] + unreadable [note: 'в ячейке кода есть цифры, но код не разобран (сырой текст — line_code_text)'] + missing [note: 'строка без кода с содержимым, выше в документе нет строки с кодом'] + detail [note: 'расшифровка без кода под строкой с кодом (Р-8): в §7, агрегаты, КФО и ЕХ не входит'] +} + +// v5.19: вариант формы — по структуре кодов (§11.7) +Enum statement_form_variant { + full + simplified [note: 'упрощённая форма (приказ 66н, прил. 5): ББ без 1100/1200, ОФР без 2100/2200/2300'] +} + +// v5.19: вид записи document_issue +Enum document_issue_kind { + warning [note: 'предупреждение (05-domain-rules.md §11.9, таблица уровней)'] + relation [note: 'жёсткое соотношение §7, не сошедшееся вне допуска и округления'] + soft [note: 'мягкое соотношение или жёсткое в пределах округления (OQ-138)'] +} + +Enum issue_severity { + error [note: 'документ needs_check'] + info [note: 'на статус не влияет'] +} + +Enum issue_source { + service [note: 'пришло в результате распознавания'] + core [note: 'посчитано ядром (проверки по словарю имён и перечню кодов; пересчёт §7 после правки)'] +} + +Enum osv_group_type { + ACCOUNT + DEPARTMENT + COUNTERPARTY + CONTRACT + OTHER +} + +Enum counterparty_match_kind { + INN [note: 'автоматический резолв при маппинге: ИНН строки совпал с legal_entity.inn'] + NAME [note: 'автоматический резолв: точное совпадение имени строки с legal_entity.name'] + ALIAS [note: 'автоматический резолв: нормализованное имя ЮЛ или алиас (legal_entity_alias)'] + MANUAL [note: 'ручное сопоставление аналитиком'] + PROPAGATED [note: 'автоприменение ручного сопоставления к строкам с тем же именем в версии (DOC-221278 п. 12–13)'] +} + +Enum osv_data_type { + BU + NU [note: 'v5.15: налоговый учёт из ОСВ «БУ, НУ» (OQ-45)'] + CURRENCY + QUANTITY + SUM +} + +Enum consolidation_scenario { + CREATE [note: '«Создание КФО» при отсутствии клиентской консолидации'] + CONFIRM [note: '«Подтверждение КФО клиента»'] +} + +Enum consolidation_approach { + BASIC [note: 'базовый: ББ, ОФР, ОСВ 01,58,60,62,66,67,76,90'] + EXTENDED [note: 'опция к базовому: + ОСВ 02,03,08,44,26,91,51,52,59,80,83,86,97,001,008,009,011'] +} + +// Статусы КФО по ТЗ 6.10 + «Корректировка» при расхождении ≥5% (ТЗ 6.4) +Enum consolidation_status { + INITIATED + DOCUMENTS_PROCESSING + DATA_CORRECTION [note: 'корректировка распознанных данных'] + CONSOLIDATION_RF + ADJUSTMENT [note: '«Корректировка» ВГО (расхождение ≥ 5%); подстатус этапа консолидации'] + VERIFICATION_RF + CONSOLIDATION_GO + VERIFICATION_GO [note: 'РКА ГО, если КФО инициировано КА ГО (ТЗ 6.10 маршрут)'] + VERIFIED + CANCELLED + // v5.1 по макету — ANL-035: подтвердить с заказчиком + REJECTED [note: 'Отклонено: возвращено на доработку (макет). Альтернатива — событие в status_history без отдельного статуса'] + ARCHIVED [note: 'Архив: скрыто из основного списка (макет); применимо к VERIFIED/CANCELLED'] +} + +Enum correction_status { + DRAFT + APPLIED + REVOKED +} + +Enum document_check_result { + MATCH + MISMATCH + UNKNOWN +} + +Enum le_history_event { + CREATE + UPDATE + MOVE_TO_GROUP [note: '«Переход в другую ГК»'] + CLOSE [note: '«Закрытие» / вывод из состава ГК'] + REMOVE_FROM_GROUP +} + +Enum ex_snapshot_status { + ACTUAL + ARCHIVE +} + +// v5.13: объекты и действия матрицы прав (ТЗ Прил. 2 «Роль: перечень полномочий», сноска 29) +Enum permission_object { + DIRECTORY [note: 'справочники ГК/ГСК/ГСЗ/ЮЛ'] + DOCUMENT [note: 'первичные документы и распознанные данные'] + CONSOLIDATION [note: 'консолидация, версии, корректировки, гудвилл'] + KFO [note: 'результат КФО: утверждение, верификация'] + EX [note: 'Единое хранилище'] + EXPORT + USERS [note: 'пользователи и роли'] + AUDIT [note: 'журнал действий'] + QUALITY_METRICS [note: 'метрики качества распознавания первички'] +} + +Enum permission_action { + VIEW + EDIT + APPROVE [note: 'утверждение в РФ'] + VERIFY [note: 'верификация в ГО'] + DELETE + EXPORT + ADMIN +} + +Enum audit_section { + DIRECTORIES + CONSOLIDATION + PRIMARY_DOCUMENTS + RECOGNITION + KFO + EXPORT + ACCESS + SESSION + USERS +} + +Enum audit_action { + LOGIN + LOGIN_FAILED + LOGOUT + CONSOLIDATION_CREATE + CONSOLIDATION_STATUS_CHANGE + CONSOLIDATION_APPROVE + CONSOLIDATION_VERIFY + GROUP_CHANGE + LEGAL_ENTITY_CHANGE [note: 'v5.14: ЮЛ создано/изменено/закрыто/переведено, алиасы'] + MEMBERSHIP_CHANGE + ASSIGNMENT_CHANGE + FILE_UPLOAD + FILE_DELETE + FILE_REPLACE + FILE_REPROCESS [note: 'v5.21: повторная обработка файла — новое recognition_job (attempt + 1), не новая загрузка'] + DOCUMENT_CONFIRM + CONSOLIDATION_LOCK + CONSOLIDATION_LOCK_TAKEOVER + DISCLOSURE_EDIT + RECOGNIZED_DATA_EDIT + OSV_COUNTERPARTY_MAP + OSV_TYPE_OVERRIDE + CORRECTION_CREATE + CORRECTION_EDIT + CORRECTION_REVOKE + GOODWILL_EDIT + EX_SNAPSHOT_WRITE [note: 'v5.26: запись среза ЕХ при «Верифицирован» (§15); раздел KFO, object_type = ex_snapshot'] + KFO_EDIT [note: 'v5.26: ручные поля КФО — комментарии строк, обоснование порога, причины отклонений от Методики; раздел KFO'] + EXPORT + DIRECTORY_IMPORT + USER_ROLE_CHANGE + USER_CHANGE [note: 'v5.14: пользователь создан/изменён/деактивирован администратором'] + ROLE_PERMISSION_CHANGE +} + +// ============ СПРАВОЧНИКИ ============ + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.1 (пользователь ГО / пользователь РФ; закрепление ГК за РФ); Прил. 2 «Пользователь: привязка к РФ/ГО» +// Аналитика: md «Модель данных» (regional_office) +// Макет: справочник ГК: колонка «Основной РФ»; пользователи: «Ростовский РФ» и т.п. +// В модели: v4 +// Зачем: справочник филиалов; DECISION-3 — ГО явной записью +Table regional_office { + id uuid [pk] + name varchar [not null, unique] + is_head_office boolean [not null, default: false, note: 'DECISION-3: ГО — явная запись'] + is_active boolean [not null, default: true] + Note: 'Филиал: ГО и региональные филиалы (РФ)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.1 (справочник ГК, импорт из ЕЦП CRM, создание вручную Куратором); Прил. 2 «ГК: id, наименование, id ЕЦП CRM» +// Аналитика: md «Модель данных» (company_group, SCD4) +// Макет: «Группы компаний»: Создать ГК, CRM ID «генерируется автоматически», Архив +// В модели: v4 + v5.1 (code, crm_id nullable — ANL-037) +Table company_group { + id uuid [pk] + code varchar [not null, unique, note: 'v5.1 ANL-037: внутренний код ГК, генерируется системой'] + crm_id varchar [unique, note: 'ключ из ЕЦП CRM; NULL для ГК, созданной вручную Куратором до сверки с CRM'] + name varchar [not null] + base_regional_office_id uuid [ref: > regional_office.id, note: 'закрепление ГК за филиалом (ТЗ 6.1/8.1); перезакрепление Куратором = UPDATE + снимок в history'] + is_archived boolean [not null, default: false, note: 'v5.1 макет: «Архив» ГК'] + valid_from timestamptz [not null] + updated_by uuid [ref: > app_user.id] + created_at timestamptz + updated_at timestamptz + Note: 'ГК — ТЕКУЩЕЕ состояние (SCD4). История — company_group_history' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.10 «фиксация изменений ГК/ГСК в истории изменений»; 8.2 «изменение состава ГСК, ГК» +// Аналитика: md «Модель данных» (company_group_history) +// Макет: «История изменений» в карточке ГК +// В модели: v4 +// Зачем: прошлые снимки ГК (SCD4) +Table company_group_history { + id uuid [pk] + company_group_id uuid [not null, ref: > company_group.id] + crm_id varchar + name varchar + base_regional_office_id uuid + valid_from timestamptz + valid_to timestamptz + changed_by uuid [ref: > app_user.id] + Indexes { (company_group_id, valid_from) [unique] } +} + + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.1 (ЮЛ/ИП, только одна ГСК/ГСЗ); Прил. 2 «ЮЛ: id, наименование, ИНН, id CRM, ГК, признак»; ЕХ ТЗ 6.1 «Монозаёмщик» +// Аналитика: md «Модель данных» (legal_entity, SCD4) +// Макет: «Юридические лица», «ЮЛ вне ГК» / «Монозаемщики» +// В модели: v4 + v5 (crm_id, is_mono_borrower, ГК nullable); v5.17 — kpp +Table legal_entity { + id uuid [pk] + inn varchar [not null, unique] + crm_id varchar [unique, note: 'ADD: id ЮЛ в ЕЦП CRM (Прил. 2 ТЗ)'] + entity_type entity_type + name varchar [not null] + company_group_id uuid [ref: > company_group.id, note: 'nullable: NULL = монозаёмщик (ЕХ 6.1), признак выводится'] + regional_office_id uuid [ref: > regional_office.id] + close_date date [note: 'NULL = активна'] + valid_from timestamptz [not null] + updated_by uuid [ref: > app_user.id] + created_at timestamptz + updated_at timestamptz + kpp varchar [note: 'v5.17 (добавлена последней колонкой, как её добавляет миграция 0005): КПП, nullable; 9 символов (4 цифры, 2 цифры/латинские A–Z, 3 цифры); у ИП запрещён — проверяет ядро (INVALID_KPP). Источник: макет [50][51-02][61][79], решение владельца 07.10.2026 (OQ-31)'] + Note: 'Организация (ЮЛ/ИП) — ТЕКУЩЕЕ состояние (SCD4). История — legal_entity_history' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.2 (журнал изменений справочников); 6.10 (история изменений состава) +// Аналитика: md «Модель данных» (legal_entity_history) +// Макет: Архив ГК: события «Переход в другую ГК», «Закрытие», дата перевода +// В модели: v4 + v5.1 (event_type, effective_date — ANL-046); v5.17 — kpp +Table legal_entity_history { + id uuid [pk] + legal_entity_id uuid [not null, ref: > legal_entity.id] + inn varchar + crm_id varchar + entity_type entity_type + name varchar + company_group_id uuid + regional_office_id uuid + close_date date + event_type le_history_event [note: 'v5.1 макет: «Переход в другую ГК» / «Закрытие» — для архива ГК'] + effective_date date [note: 'дата перевода/закрытия (не раньше текущей — ANL-046)'] + valid_from timestamptz + valid_to timestamptz + changed_by uuid [ref: > app_user.id] + kpp varchar [note: 'v5.17 (последней колонкой, миграция 0005): снимок КПП на момент события'] + Indexes { (legal_entity_id, valid_from) [unique] } +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.5 «ведение справочника соответствий наименований контрагентов», «использование ранее подтверждённых сопоставлений» +// Аналитика: md «Сводная ОСВ» (алгоритм ручного сопоставления, legal_entity_alias); FNR01 п.9 +// Макет: карточка ЮЛ: «Альтернативные наименования», «Связь с компанией из ОСВ (алиас)» +// В модели: v4 +Table legal_entity_alias { + id uuid [pk] + legal_entity_id uuid [not null, ref: > legal_entity.id] + alias varchar [not null, note: 'как в ОСВ'] + normalized_alias varchar [not null, note: 'lower/trim/без пунктуации/без ОПФ'] + created_by uuid [ref: > app_user.id] + created_at timestamptz + Indexes { + (legal_entity_id, normalized_alias) [unique] + normalized_alias [note: 'ANL-028: конфликт «один алиас у разных ЮЛ» решается при ручном сопоставлении'] + } + Note: 'Словарь соответствий наименований контрагентов (ТЗ 6.5)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.1 (ГСК/ГСЗ в составе ГК; иерархия не поддерживается; в одной ГК несколько ГСК — сноска 11); Прил. 2 «ГСК/ГСЗ» +// Аналитика: md «Модель данных» (related_group, kind) +// Макет: вкладки «ГСК», «ГСЗ», коды GSK-001/GSZ-001 +// В модели: v4; v5.19 — crm_id +Table related_group { + id uuid [pk] + code varchar [not null, unique] + company_group_id uuid [not null, ref: > company_group.id] + kind related_group_kind [not null] + name varchar [not null] + created_date date + crm_id varchar [unique, note: 'v5.19: ID ГСК/ГСЗ в ЕЦП CRM (формы 51-03, 51-04, 77-02; arm-core#112, OQ-33); вводится вручную, импорт ЕЦП не заполняет (OQ-91); NULL — не задан'] + Note: 'ГСК/ГСЗ — current-state. Иерархия групп не поддерживается (ТЗ 6.1)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.1 «КА может редактировать состав ГСК»; «одно ЮЛ только в одной ГСК или ГСЗ» +// Аналитика: md «Модель данных» (related_group_member, SCD2) +// Макет: «Состав», «Входит в ГСК / ГСЗ», «Редактировать состав» +// В модели: v4 (индекс уточнён — ANL-030); v5.14 — kind и индекс (legal_entity_id, kind) +Table related_group_member { + id uuid [pk] + related_group_id uuid [not null, ref: > related_group.id] + legal_entity_id uuid [not null, ref: > legal_entity.id] + kind related_group_kind [not null, note: 'v5.14: денормализация related_group.kind ради частичного уникального индекса; равенство с группой проверяет ядро при записи'] + valid_from timestamptz [not null] + valid_to timestamptz + is_current boolean [not null] + created_by uuid [ref: > app_user.id] + Indexes { + (related_group_id, legal_entity_id) [note: 'partial UNIQUE WHERE is_current'] + (legal_entity_id, kind) [note: 'partial UNIQUE WHERE is_current — v5.14: ЮЛ в одной ГСК И одной ГСЗ одновременно (решение владельца 04.10.2026, отменяет ANL-030); ГК у ЮЛ и у группы должна совпадать (проверяет ядро)'] + } + Note: 'Состав ГСК/ГСЗ — SCD2. Исключение = закрытие версии (valid_to, is_current=false)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2/6.5 (счета и субсчета 01…90, нормализация 60.1=60.01); Прил. 3/4 (пары счетов); сноска 32 +// Аналитика: md «Модель данных» (chart_of_account); md «Шахматка» (kind A/P/AP, row_type) +// Макет: фильтр «Дебетовые/Кредитовые счета», полный план счетов 1С в бандле +// В модели: v4 + v5 (row_type, is_off_balance) +Table chart_of_account { + id uuid [pk] + code varchar [not null, unique, note: 'нормализованный код: 60.01, 76.07.1'] + parent_id uuid [ref: > chart_of_account.id] + name varchar [not null] + kind account_kind + row_type account_row_type [not null, note: 'FIX: используется в «Шахматке»'] + is_off_balance boolean [not null, default: false, note: '001, 008, 009, 011'] + Note: 'План счетов' +} + + +// ADD ТЗ Прил. 3/4 как данные (сноски 32/35: пары могут корректироваться на этапе реализации) +// ИСТОЧНИК: +// ТЗ: ТЗ 6.4 + Прил. 3 «Перечень статей ББ» и Прил. 4 «Модель расчётов ОФР»; сноски 32/35 «могут быть скорректированы на этапе реализации» +// Аналитика: md «Автоматическое сопоставление ВГО» (таблица «Правила встречных счетов» — хардкод) +// Макет: — +// В модели: v5 новая +// Зачем: правила элиминации как данные, версионируемые +Table elimination_rule { + id uuid [pk] + statement_type statement_type [not null, note: 'balance = Прил. 3, income = Прил. 4'] + rule_no smallint [not null, note: '№ строки приложения'] + article_1_line varchar [not null, note: 'статья Компании 1, код строки ББ/ОФР, напр. 1230'] + article_2_line varchar [not null, note: 'статья Компании 2, напр. 1520'] + side_1 varchar [not null, note: 'D | K — сторона Компании 1'] + measure varchar [not null, note: 'closing | turnover — сальдо на конец (ББ) или оборот (ОФР)'] + elimination_mode varchar [not null, note: 'min | min_or_full | full | manual — min(A,B) в допуске / v5.26: min(A,B) при обеих сторонах, иначе A, без порога (ББ 2, DOC-274230) / вся сумма ВГО (ОФР выручка) / вручную'] + vat_handling varchar [note: 'none | check_90_01_vs_2110 | weighted_rate — только для ОФР'] + excise_adjust boolean [not null, default: false] + comment text + version_no int [not null] + valid_from date [not null] + valid_to date + Indexes { (statement_type, rule_no, version_no) [unique] } + Note: 'Правило элиминации. Состав счетов — в elimination_rule_account' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ Прил. 3/4 (столбцы «Пары счетов Компании 1 и Компании 2», исключения «76 кроме 76.7.1») +// Аналитика: md «Автоматическое сопоставление ВГО» +// Макет: — +// В модели: v5 новая +// Зачем: состав счетов сторон правила +Table elimination_rule_account { + id uuid [pk] + rule_id uuid [not null, ref: > elimination_rule.id] + company_no smallint [not null, note: '1 или 2'] + account_id uuid [not null, ref: > chart_of_account.id] + is_excluded boolean [not null, default: false, note: 'true = исключение внутри маски (напр. «76 кроме 76.7.1»)'] + Note: 'Счета/субсчета сторон правила' +} + + +// ============ ПОЛЬЗОВАТЕЛИ И ДОСТУП ============ + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.1 (роли, ГО/РФ, ЗО); Прил. 2 «Пользователь: ФИО, роль, привязка к РФ/ГО, ЗО, табельный номер»; РАИСА +// Аналитика: md «Аутентификация и авторизация» (users: email, role, JWT РАИСА); md «Модель данных» (app_user) +// Макет: «Пользователи»: Логин, Роль, Активен, Деактивация +// В модели: v4 + v5 (employee_no, business_segment) +Table app_user { + id uuid [pk] + external_id varchar [unique, note: 'субъект РАИСА/SSO; аутентификация внешняя (ANL-018)'] + email varchar [unique, note: 'v5.15: хранится в нижнем регистре (нормализация при записи), сравнение по lower(email) (OQ-59)'] + full_name varchar [not null] + employee_no varchar [note: 'ADD: табельный номер (Прил. 2 ТЗ)'] + regional_office_id uuid [not null, ref: > regional_office.id, note: 'ГО или РФ (DECISION-3)'] + business_segment business_segment [note: 'ADD: зона ответственности ДКБ/ДМСБ/ДМ (ТЗ 8.1)'] + is_active boolean [not null, default: true] + created_at timestamptz + updated_at timestamptz +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.1 таблица ролей (КА РФ, КА ГО, РКА, КМ, Куратор, Эксперт, Администратор); Прил. 2 «Роль» +// Аналитика: md «Ролевая модель»; md «Модель данных» (role.code) +// Макет: «Изменение ролей и прав доступа» (Аналитик ДКБ, Наблюдатель… — расходится с ТЗ, ANL-034) +// В модели: v4 (коды → ТЗ в v5); v5.13 — матрица полномочий в role_permission +Table role { + id uuid [pk] + code varchar [not null, unique, note: 'ТЗ 8.1: KA_RF | KA_GO | RKA | CLIENT_MANAGER | CURATOR_GO | EXPERT | ADMIN. Seed; администратор может заводить дополнительные роли (замена исключениям по пользователю)'] + name varchar [not null] + description text + is_system boolean [not null, default: false, note: 'роли ТЗ 8.1 — не удаляются'] + is_active boolean [not null, default: true] + Note: 'Роль. Полномочия — через role_permission (v5.13); код роли в бизнес-логике больше не проверяется' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ Прил. 2 «Роль: перечень полномочий»; сноска 29 «матрица доступов будет разработана» +// Аналитика: md «Ролевая модель» +// Макет: карточка роли: «Права: Просмотр, редактирование КФО / справочники / консолидации» +// В модели: v5 новая, убрана в v5.3/v5.4, ВОЗВРАЩЕНА в v5.13 (замечание 1) +// Зачем: атомарное право «объект × действие»; seed, администратор новые коды не создаёт +Table permission { + id uuid [pk] + code varchar [not null, unique, note: 'напр. CONSOLIDATION_EDIT, KFO_VERIFY, DIRECTORY_EDIT, USERS_ADMIN'] + object_type permission_object [not null] + action permission_action [not null] + name varchar [not null] + description text + Indexes { (object_type, action) [unique] } + Note: 'Справочник прав (seed, привязан к коду). Ограничения по ЗО/филиалу и статусу КФО — отдельный policy-слой поверх прав: право говорит «может», policy — «эту ли и сейчас ли»' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ Прил. 2 «Роль: перечень полномочий» +// Аналитика: нет +// Макет: «Права» в карточке роли, «Изменение ролей и прав доступа» +// В модели: v5.13 (возвращена) +// Зачем: матрица доступов как данные; редактируется администратором, история — audit_event(ROLE_PERMISSION_CHANGE) +Table role_permission { + role_id uuid [not null, ref: > role.id] + permission_id uuid [not null, ref: > permission.id] + granted_by uuid [ref: > app_user.id] + granted_at timestamptz + Indexes { (role_id, permission_id) [pk] } + Note: 'effective_permissions(user) = ∪ permissions(roles(user)). Прямых прав у пользователя нет (решение 30.09): исключения — через заведение роли' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.1 (роль пользователя); Прил. 2 «Пользователь — Роль: многие-к-одному» +// Аналитика: md «Модель данных» v4 (user_role M:N) +// Макет: «Роль» пользователя +// В модели: v4 +// Зачем: M:N оставлен (пользователь может быть КА и Куратором); если ТЗ строго 1:1 — заменить на app_user.role_id +Table user_role { + id uuid [pk] + user_id uuid [not null, ref: > app_user.id] + role_id uuid [not null, ref: > role.id] + Indexes { (user_id, role_id) [unique] } + Note: 'M:N current-state. История назначений — audit_event(USER_ROLE_CHANGE)' +} + +// ============ ФАЙЛЫ / РАСПОЗНАВАНИЕ ============ + +// ИСТОЧНИК: +// ТЗ: 6.2 (файлы с префиксом UUID, исходные документы хранятся); 8.4 «хранение 5 лет с даты прекращения отношений, архивирование/удаление»; ЕХ 6.2 «ссылка на исходные документы с возможностью скачать», ЕХ 8.3 «архивирование в data lake» +// Аналитика: md «Модель данных» v4 (s3_key, raw_result_s3_key — строки) +// Макет: — +// В модели: v5.2 новая (DECISION-4), подтверждена в v5.4 +// Зачем: единая точка ключей всех бакетов для lifecycle/ретеншна и дедупа; без реестра ключи размазаны по 4 таблицам +Table s3_object { + id uuid [pk] + bucket_role s3_bucket_role [not null] + bucket varchar [not null] + key varchar [not null, note: 'схема ключа: {file_id}/{job_id}/{name}'] + content_type varchar + size_bytes bigint + sha256 varchar + version_id varchar [note: 'S3 object version при включённом versioning'] + created_by varchar [note: 'arm-core | tabular-parser | scan-gateway | user:{id}'] + created_at timestamptz [not null] + expires_at timestamptz [note: 'lifecycle: NULL = весь срок (ТЗ 8.4); exports/ = 7 дней; results/ — решение ARCH-010'] + archived_to varchar [note: 'ссылка в data lake после архивирования (ЕХ 8.3)'] + Indexes { (bucket, key) [unique] } + Note: 'Реестр объектов S3. stored_file / recognition_job ссылаются сюда FK' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 (типы и форматы документов, UUID); 8.2 «первичные документы не редактируются, только замена с версионностью» +// Аналитика: md «Загрузка документов» (панель, UC-01…05); FNR01 «Маппинг stored_file» +// Макет: «Первичные документы», «Загрузка документов», «Замена документа», «Удаление документа» +// В модели: v4 + v5 (version_no, superseded_by) + v5.2 (s3_object_id); v5.13 — только ФИЗИЧЕСКИЙ файл, документы → document; v5.18 — consolidation_id +Table stored_file { + id uuid [pk] + legal_entity_id uuid [not null, ref: > legal_entity.id, note: 'организация-владелец (выбрана при загрузке)'] + file_type file_type [not null] + original_name varchar + s3_object_id uuid [not null, ref: > s3_object.id, note: 'исходный файл в uploads'] + content_hash varchar [note: 'sha256, дедуп (UC-01 A2)'] + size_bytes bigint + status file_status [not null] + version_no int [not null, default: 1, note: 'ADD ТЗ 8.2: версия при замене'] + superseded_by_file_id uuid [ref: > stored_file.id, note: 'ссылка на файл-замену; статус SUPERSEDED. Замена — файл ЦЕЛИКОМ со всеми документами в нём (решение 01.10)'] + current_recognition_job_id uuid [ref: > recognition_job.id, note: 'авторитетный прогон; его результат породил актуальные document'] + uploaded_by uuid [ref: > app_user.id] + uploaded_at timestamptz + updated_at timestamptz + consolidation_id uuid [ref: > consolidation.id, note: 'v5.18: консолидация (период ГК), в контексте которой загружен файл — основа «рабочего стола консоли» (полный состав — context/05-domain-rules.md §9.5 п. 11), общего для версий периода; NULL — без контекста. Пишется при загрузке; при автозамене файл без контекста наследует значение заменённого'] + is_scan boolean [note: 'v5.25 (добавлена последней колонкой, как её добавляет миграция 0012): только PDF — признак скана по отсутствию текстового слоя при загрузке; true = скан (нет текстового слоя); false = цифровой, в т. ч. скрытый OCR-слой — допущение §14; NULL для не-PDF'] + Indexes { + (legal_entity_id, status) + (legal_entity_id, consolidation_id) [note: 'v5.18: основа рабочего стола консоли для ЮЛ (§9.5 п. 11)'] + current_recognition_job_id [note: 'v5.15: проверка FK при удалении recognition_job / файла (OQ-117)'] + } + Note: 'v5.13: физический файл одной загрузки (S3 — только ссылка). Что внутри — document (1:N). Первичка не редактируется. Удаление физическое запрещено после INITIATED — только SUPERSEDED (ANL-020). v5.18: consolidation_id — где загружен; что использует версия — consolidation_document' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 (типы документов: ББ, ОФР, ОСВ по счетам; обязательный набор); 5.1 (подход); 8.2; Прил. 2 «Документ ФО» +// Аналитика: md «Загрузка документов» (чек-лист, UC-01 A1 период, «Подтвердить данные»); md «Парсинг ОСВ» (счёт/период из шапки) +// Макет: «Чек-лист загруженных документов», «Соответствует период», «Не соответствует контрагент», «Подтвердить данные» +// В модели: v5.13 новая (замечание 6-бис); v5.19 — file_ordinal, уникальность типа только для не-форм; +// v5.20 — is_modified, original_values, modified_by/at (уточнение периода и счёта ОСВ, OQ-161) +// Зачем: один файл = N документов (PDF ББ+ОФР; xlsx с несколькими ОСВ). Чек-лист, проверки, подтверждение и +// FK разобранных данных — здесь; без этого «ОСВ 58 загружена?» по файлу не ответить +Table document { + id uuid [pk] + stored_file_id uuid [not null, ref: > stored_file.id] + legal_entity_id uuid [not null, ref: > legal_entity.id, note: 'денормализация stored_file.legal_entity_id — выборки чек-листа по ЮЛ+период'] + document_type document_type [not null] + osv_account_code varchar [note: 'для OSV — счёт по шапке (01, 60, 62…), нормализованный; NULL для прочих типов'] + period_year smallint [note: 'из шапки/формы'] + period_quarter period_quarter + is_cumulative boolean [note: 'true = нарастающий итог с начала года'] + reporting_date date [note: 'отчётная дата, если определена'] + locator varchar [note: 'где в файле: лист xlsx / диапазон страниц PDF (напр. «sheet:ОСВ 58», «pages:1-2»)'] + status document_status [not null] + period_check document_check_result [note: 'UC-01 A1 / макет «Соответствует период»'] + counterparty_check document_check_result [note: 'макет «Не соответствует контрагент» — ИНН/наименование из шапки vs legal_entity'] + confirmed_by uuid [ref: > app_user.id, note: 'кто нажал «Подтвердить данные» → audit_event(DOCUMENT_CONFIRM)'] + confirmed_at timestamptz + created_at timestamptz + updated_at timestamptz + file_ordinal smallint [not null, default: 0, note: 'v5.19: индекс документа в documents[] результата распознавания (с 0), у всех документов при любом MAJOR контракта; несозданный документ номер не занимает (номера с пропусками); различает две формы одного типа в файле (Р-10)'] + is_modified boolean [not null, default: false, note: 'v5.20: период или счёт ОСВ уточнены аналитиком и отличаются от распознанных (возврат к исходному снимает признак)'] + original_values jsonb [note: 'v5.20: как распознано, до первой правки — {period_year, period_quarter, is_cumulative, osv_account_code}; NULL = не правилось; после первой правки не меняется'] + modified_by uuid [ref: > app_user.id, note: 'v5.20: кто последним уточнил период или счёт'] + modified_at timestamptz + Indexes { + (stored_file_id, document_type, osv_account_code) [note: 'partial UNIQUE WHERE document_type NOT IN (\'BALANCE\', \'INCOME\') — NULLS NOT DISTINCT; v5.19: одна ОСВ по счёту / одна расшифровка / один прочий на файл; формы ББ/ОФР одного типа в файле могут повторяться (Р-10)'] + (stored_file_id, file_ordinal) [unique, note: 'v5.19: номер документа уникален в файле'] + (legal_entity_id, document_type, osv_account_code, period_year, period_quarter, status) [note: 'чек-лист required_document_rule'] + } + Note: 'Логический документ внутри файла. Создаётся ядром при маппинге результата recognition_job. Разобранные данные: OSV → osv_entry; BALANCE/INCOME → financial_statement_meta. Счётчики «исправлено полей» — COUNT(is_modified) по данным, не хранятся. v5.19: предупреждения и нарушения §7 — document_issue; две формы одного типа и периода в файле не автоподтверждаются (Р-18). v5.20: период и счёт ОСВ уточняются аналитиком на месте, распознанное — в original_values, + audit_event(RECOGNIZED_DATA_EDIT, before/after); уточнение подтверждённого документа сбрасывает подтверждение и снимает привязки к версиям (05-domain-rules §9.5 п. 12)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 (статусы процесса распознавания, ИС Сканирование); 6.10 «повторная обработка»; 7.1 +// Аналитика: FNR01 «Маппинг recognition_job»; md «Загрузка документов» UC-02; md «Парсинг ОСВ» (схема группировок шапки) +// Макет: «Система распознавания: получен ответ», «Обработка», «Ошибка» +// В модели: v4, переписана в v5.2 под контракт NATS (DECISION-4); v5.13 — group_schema jsonb, убран raw_result +Table recognition_job { + id uuid [pk, note: 'job_id в контракте NATS'] + file_id uuid [not null, ref: > stored_file.id] + pipeline recognition_pipeline [not null, note: 'tabular → tabular-parser; image → scan-gateway'] + request_subject varchar [not null, note: 'recognition.tabular.request | recognition.image.request'] + request_msg_id varchar [unique, note: 'Nats-Msg-Id запроса (outbox)'] + result_msg_id varchar [unique, note: 'Nats-Msg-Id результата (inbox, дедуп)'] + schema_version varchar [not null, note: 'версия контракта сообщений'] + service_name varchar [note: 'tabular-parser | scan-gateway — из результата'] + service_version varchar + status recognition_status [not null] + attempt smallint [not null, default: 1] + max_attempts smallint [not null, default: 1, note: 'OCR: 1 (0 повторов), парсер: 3'] + requested_at timestamptz + published_at timestamptz + started_at timestamptz + finished_at timestamptz + reply_deadline timestamptz [note: 'таймаут ожидания (OCR — 20 мин)'] + external_task_id varchar [note: 'id задания в ИС Сканирование (только image)'] + error_code varchar [note: 'UNSUPPORTED_FORMAT | UNREADABLE | TIMEOUT | SCHEMA_MISMATCH | …'] + error_message text + result_s3_object_id uuid [ref: > s3_object.id, note: 'нормализованный JSON в results — единственная ссылка на результат (v5.13)'] + result_schema_version varchar [note: 'версия схемы JSON результата (ARCH-008/009)'] + group_schema jsonb [note: 'v5.13 (вместо osv_group_schema): схема группировок шапки ОСВ по листам — [{\"locator\":\"sheet:ОСВ 58\",\"groups\":[{\"type\":\"ACCOUNT\"},{\"type\":\"COUNTERPARTY\",\"has_inn\":true},{\"type\":\"CONTRACT\"}]}]; только для tabular'] + metrics jsonb [note: 'duration_ms, pages, rows, warnings (v5.19 / контракт 2.0: предупреждения уровня файла — структуры {code, severity, message, locator?}; error уровня файла блокирует RECOGNIZED документов файла)'] + fields_total int [note: 'v5.7: всего извлечённых значений (ячейки формы / строки ОСВ)'] + fields_low_confidence int [note: 'v5.7: значений с confidence < 100 (ТЗ 6.2) — заполняется при маппинге результата'] + fields_unrecognized int [note: 'v5.7: значений с is_recognized = false'] + statements_version varchar [note: 'v5.19: service.statements_version результата — коммит копии общего кода arm_statements; при пересчёте после правки сравнивается с копией ядра (расхождение — document_issue info)'] + raw_s3_object_id uuid [ref: > s3_object.id, note: 'v5.25 (добавлена последней колонкой, как её добавляет миграция 0012): сырой ответ ИС Сканирование, роль raw; заполняется при приёме результата image pipeline (отменяет решение v5.13 «в БД не регистрируется»)'] + Indexes { (file_id, attempt) [unique] } + Note: 'РЕШЕНИЕ v5.4: единственная точка обмена с NATS. Запись = outbox: создаётся в queued в одной транзакции с stored_file, фоновый публикатор переводит в published (при сбое публикации — повтор по queued старше N сек); результат приходит по NATS, дедуп по result_msg_id UNIQUE; redelivery после max_deliver → status=dlq, replay = сброс в queued с attempt+1. Ядро маппит JSON из S3 в document → osv_entry / financial_statement_*' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 (Osv_report_lines); 6.5 (строка ОСВ: компания, счёт, контрагент, признак, 6 сумм); Прил. 2 «Строка сводной ОСВ», «Распознанный показатель» +// Аналитика: FNR01 «Маппинг osv_entry»; md «Парсинг ОСВ» п.11 (row_number, level, group_type, data_type) +// Макет: «Оборотно-сальдовая ведомость»: Дебет/Кредит начало/оборот/конец, «Тип контрагента» +// В модели: v4 + v5 (owner_legal_entity_id — FIX, parent_entry_id, is_modified, is_excisable); v5.13 — document_id, group_type вместо group_schema_id; v5.16 — counterparty_match_kind +Table osv_entry { + id uuid [pk] + recognition_job_id uuid [not null, ref: > recognition_job.id] + document_id uuid [not null, ref: > document.id, note: 'v5.13: ОСВ-документ (счёт + период); файл — через document.stored_file_id'] + owner_legal_entity_id uuid [not null, ref: > legal_entity.id, note: 'FIX: организация-ВЛАДЕЛЕЦ ОСВ (денормализация). В v4 называлось legal_entity_id с ошибочной note'] + parent_entry_id uuid [ref: > osv_entry.id, note: 'родитель в дереве строк ОСВ (ТЗ 6.5). ОТКРЫТО (замечание 2, отложено 01.10): хранить ли только контрагентские строки и нужно ли дерево'] + group_type osv_group_type [note: 'v5.13: тип строки по схеме шапки (recognition_job.group_schema) — ACCOUNT / DEPARTMENT / COUNTERPARTY / CONTRACT / OTHER'] + group_name varchar [note: 'исходное название группировки из шапки («Контрагенты», «Договоры»)'] + row_number int + level smallint + row_value varchar [note: 'исходный текст строки'] + data_type osv_data_type + account_code varchar [note: 'сырой код счёта'] + account_id uuid [ref: > chart_of_account.id] + account_root varchar + counterparty_name varchar [note: 'сырое имя из ОСВ, не изменяется'] + counterparty_inn varchar [note: 'сырой ИНН, не изменяется'] + counterparty_id uuid [ref: > legal_entity.id, note: 'резолв по ИНН/имени/алиасу или ручное сопоставление; NULL = не сопоставлен. Признак ГСК/ГК/Внешний выводится из периметра версии (DECISION-5)'] + opening_debit numeric(18,2) + opening_credit numeric(18,2) + turnover_debit numeric(18,2) + turnover_credit numeric(18,2) + closing_debit numeric(18,2) + closing_credit numeric(18,2) + is_modified boolean [not null, default: false, note: 'значения правились аналитиком'] + original_values jsonb [note: 'v5.6: суммы до первой ручной правки {opening_debit,…}; NULL = не правилось'] + modified_by uuid [ref: > app_user.id] + modified_at timestamptz + is_excisable boolean [note: 'ADD ТЗ 6.4: признак «подакцизный товар» для ОСВ 90'] + counterparty_match_kind counterparty_match_kind [note: 'v5.16: NULL ⇔ counterparty_id NULL; INN/NAME/ALIAS — шаг автоматического резолва при маппинге; MANUAL — ручное сопоставление; PROPAGATED — автоприменение по имени в версии (DOC-221278 п. 12–13)'] + Indexes { + (document_id, row_number) [unique] + (owner_legal_entity_id, account_id, counterparty_id) + parent_entry_id [note: 'v5.15: обход дерева строк и проверка self-FK при удалении документа (OQ-117)'] + recognition_job_id [note: 'v5.15: проверка FK при удалении recognition_job / файла (OQ-117)'] + } + Note: 'Строка ОСВ. Правки сумм аналитиком — original_values/modified_by + audit_event(RECOGNIZED_DATA_EDIT, before/after)' +} + + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 «извлекаются значения по кодам форм Приказа 157н»; 6.3 проверки единиц измерения/даты +// Аналитика: FNR01 «fin_report_header» (report_type, org, unit, currency, reporting_date); md «Модель данных» v4 +// Макет: «Реквизиты организации» (ОКПО, ОКОПФ, ОКФС, ОКУД, ОКЕИ, адрес), «Единица измерения», «Отчетная дата» +// В модели: v4 (FIX pk) + v5.1 (реквизиты — ANL-044); v5.13 — document_id; v5.19 — form_variant, needs_review_fields +Table financial_statement_meta { + id uuid [pk, note: 'FIX: был без PK'] + document_id uuid [not null, unique, ref: > document.id, note: 'v5.13: документ BALANCE/INCOME (1:1); файл — через document'] + recognition_job_id uuid [not null, ref: > recognition_job.id] + statement_type statement_type [not null] + inn varchar + inn_original_value varchar + inn_confidence numeric(5,2) + inn_verified boolean + inn_modified boolean + organization_name varchar [note: 'как в документе; сверяется с legal_entity/alias, при несовпадении — document.counterparty_check'] + report_date date + report_date_original_value varchar [note: 'FIX: строка как распознано (был `date []`)'] + report_date_verified boolean + report_date_modified boolean + units varchar [note: 'тыс. руб. / руб.'] + units_original_value varchar + units_verified boolean + units_modified boolean + currency varchar [default: 'RUB'] + okpo varchar [note: 'v5.1 ANL-044: реквизиты по макету экрана распознавания'] + okopf varchar + okfs varchar + okud varchar [note: '0710001 / 0710002'] + okei varchar + legal_form varchar [note: 'ОПФ'] + ownership_form varchar + address varchar + subject_to_audit boolean + reviewed_by uuid [ref: > app_user.id, note: 'v5.9: кто проверил данные формы; подтверждение документа — document.confirmed_by'] + reviewed_at timestamptz + checks_accepted_by uuid [ref: > app_user.id, note: 'v5.9 ТЗ 6.3 п.7: акцепт КА непройденных проверок (напр. 1370/2400) с комментарием'] + checks_accepted_at timestamptz + checks_accepted_comment text + form_variant statement_form_variant [note: 'v5.19: full | simplified — по структуре кодов (§11.7); выбирает набор соотношений §7; NULL — документ до v5.19 или результат 1.x: набор полный'] + needs_review_fields jsonb [note: 'v5.19: имена полей шапки, требующих проверки (["report_date"]), как пришло от сервиса; правка поля или подтверждение тем же значением убирает его из списка'] + header_source jsonb [note: 'v5.19: шапка результата как пришла — {"<поле>": {"text": <поле>_text, "confidence": <поле>_confidence}} для organization_name, inn, report_date, units, okud, okpo, okopf, okfs, okei, address; не меняется. *_original_value — как прежде: inn_original_value = inn, report_date_original_value = report_date_text, units_original_value = units_text; inn_confidence = header_source.inn.confidence'] + organization_name_original_value varchar [note: 'v5.19: наименование организации из результата; правками не меняется'] + organization_name_modified boolean [note: 'v5.19: наименование правилось аналитиком (PATCH шапки)'] + Note: 'Шапка распознанной формы ББ/ОФР. v5.19: результат проверок §7 и предупреждения хранятся в document_issue как пришли от сервиса и пересчитываются ядром только после правок (пересматривает «не хранятся — считаются на лету»)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.3 п.7 «стр. 1370 на конец и на начало года»; 6.7 «УК ДО на отчётную дату» +// Аналитика: FNR01 «rows[0].cells[2..4] — наименования колонок периода»; md «Логические проверки» («по каждой отчётной дате (колонке)») +// Макет: колонки «На 30 сентября 2025 г.», «На 31 декабря 2024 г.», «Выбор периодов» +// В модели: v5 новая (FIX-14) +// Зачем: дата колонки — без неё межформенная проверка и гудвилл не считаются +Table financial_statement_column { + id uuid [pk] + meta_id uuid [not null, ref: > financial_statement_meta.id] + column_index smallint [not null] + label varchar [note: 'как в форме: «На 30 сентября 2025 г.», «За 9 месяцев 2025 г.»'] + period_date date [note: 'ADD FIX-14: дата колонки — нужна для 1370(нач.года), гудвилла «на отчётную дату», сравнения версий'] + is_current boolean [note: 'колонка отчётного периода'] + period_kind varchar [note: 'v5.19: absolute | relative:current | relative:prev_year_end | relative:prev_prev_year_end | relative:prev_period | none; относительные подписи бланка 66н — period_date от эффективной даты документа, ядро досчитывает после правки даты отчёта (Р-6)'] + label_confidence numeric(5,2) [note: 'v5.19: уверенность подписи колонки (Excel — 100; подписи нет — NULL)'] + Indexes { (meta_id, column_index) [unique] } + Note: 'Колонки-периоды формы (2–3 сравнительные даты)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 (коды статей по формам 157н); 6.3 (Таблица 1 — коды строк 1100…2400) +// Аналитика: FNR01 «fin_report_rows» (row_name, row_code, section, is_total_row); md «Модель данных» v4 +// Макет: «Результат распознавания»: Наименование показателя, Код, разделы +// В модели: v4 (FIX: убраны копипаст-поля); v5.19 — строки без кода и повторы кода, статусы, порядок, needs_review +Table financial_statement_line { + id uuid [pk] + meta_id uuid [not null, ref: > financial_statement_meta.id] + line_code varchar [note: 'код строки ОКУД, напр. 1230'] + line_code_original_value varchar [note: 'v5.19: line_code строки из результата как пришёл (NULL у unreadable / missing / detail и у строк, созданных правкой); правками кода не меняется. До v5.19 NULL был признаком «создана правкой» — теперь это row_index IS NULL'] + line_code_confidence numeric(5,2) + line_code_modified boolean + line_name varchar + line_name_original_value varchar + line_name_confidence numeric(5,2) + line_name_modified boolean [note: 'FIX: был numeric'] + row_index int [note: 'v5.19: порядок строки в документе источника (с 0). NULL ⇔ строка создана правкой аналитика (шаблонная, origin = template); у строк из результата — всегда не NULL (маппер 1.x и 2.0)'] + source_ref varchar [note: 'v5.19: положение в источнике (row:8, row:14#2, Баланс!C27); NULL у строк, созданных правкой'] + line_status statement_line_status [not null, default: 'ok', note: 'v5.19: ok | unreadable | missing | detail; line_code пуст ⇔ статус не ok'] + line_code_text varchar [note: 'v5.19: сырой текст ячейки кода, как распознано'] + parent_row_index int [note: 'v5.19: row_index строки-родителя у detail; пересчитывается при правке'] + needs_review boolean [not null, default: false, note: 'v5.19: неуверенный код не подтверждён структурой (как пришло от сервиса; правка или подтверждение кода — false)'] + is_confirmed boolean [not null, default: false, note: 'v5.19: код подтверждён аналитиком без изменения (PATCH lines confirmed)'] + Indexes { + (meta_id, line_code) [note: 'v5.19: не уникален — повтор кода законен (Р-4); чтения «по коду» — агрегат формы по коду'] + (meta_id, source_ref) [note: 'partial UNIQUE WHERE source_ref IS NOT NULL — v5.19: положение в источнике уникально в форме'] + } + Note: 'Строка формы как в источнике (v5.19): строки с кодом, без кода (unreadable / missing / detail) и повторы кода; раскладка экрана по коду и шаблон формы — на фронте и в ядре по словарю имён' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.2 «уверенность распознавания ниже 100% → Требуется ручная проверка»; Прил. 2 «Распознанный показатель: значение, уверенность, признак ручной корректировки» +// Аналитика: FNR01 (value_current/prior/prior2, confidence, скобки → отрицательное); md «Логические проверки» (правка ячеек) +// Макет: подсветка ячеек, «не распознано», «Редактировать/Сохранить», «Подтвердить данные» +// В модели: v4 + v5 (column_id, confidence, original_amount, modified_by); v5.13 — убран needs_review; v5.19 — needs_review хранится, source_text, is_confirmed +Table financial_statement_value { + id uuid [pk] + line_id uuid [not null, ref: > financial_statement_line.id] + column_id uuid [not null, ref: > financial_statement_column.id] + amount numeric(18,2) + string_value varchar [note: 'как распознано, если не число'] + confidence numeric(5,2) [note: 'уверенность распознавания (Прил. 2 ТЗ). v5.19: подсветка — только needs_review (уверенность, is_recognized и §7 сведены сервисом)'] + is_recognized boolean + is_modified boolean [not null, default: false] + original_amount numeric(18,2) [note: 'значение до ручной правки'] + modified_by uuid [ref: > app_user.id] + modified_at timestamptz + source_text varchar [note: 'v5.19: сырой текст ячейки источника (values[].text), не меняется'] + needs_review boolean [not null, default: false, note: 'v5.19: как пришло от сервиса (подтверждение проверкой §7, 05-domain-rules.md §11.9), пересчитывается ядром после правок; is_modified или is_confirmed ⇒ false. Пересматривает п. 4 v5.13'] + is_confirmed boolean [not null, default: false, note: 'v5.19: подтверждено аналитиком без изменения значения (is_modified не ставится)'] + confirmed_by uuid [ref: > app_user.id] + confirmed_at timestamptz + Indexes { (line_id, column_id) [unique] } +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.3 (логические проверки, акцепт); 6.2 «уверенность ниже 100% → Требуется ручная проверка» +// Аналитика: openspec/changes/archive/2026-10-19-unified-statement-normalization (design: шаг 8, «Что уходит из ядра») +// Макет: «Результат распознавания»: ошибки проверок, подсветка ячеек +// В модели: v5.19 новая +// Зачем: предупреждения сервиса и нарушения §7 с привязкой к строкам, ячейкам и полям шапки — экран показывает их у +// места и после сохранения; ядро не пересчитывает их при загрузке (Decisions № 17, 19) +Table document_issue { + id uuid [pk] + document_id uuid [not null, ref: > document.id] + kind document_issue_kind [not null] + code varchar [not null, note: 'код предупреждения (05-domain-rules.md §11.9); у relation — RELATION_FAILED (severity error), у soft — RELATION_SOFT (severity info)'] + severity issue_severity [not null] + message text [not null] + line_ids jsonb [note: 'массив financial_statement_line.id, к которым относится запись (нарушение — все строки кодов соотношения); FK нет — при удалении шаблонной строки (назначение её кода строке источника) ядро убирает её id из line_ids всех записей документа'] + column_id uuid [ref: > financial_statement_column.id] + column_index smallint [note: 'столбец сетки из предупреждения, если колонки нет среди financial_statement_column (COLUMN_SKIPPED и др.); у колонки формы — column_id'] + codes jsonb [note: 'для relation / soft: коды цели и членов соотношения (["1600","1100","1200"]); экран подсвечивает строки этих кодов'] + field varchar [note: 'поле шапки (report_date, inn, organization_name, …)'] + source issue_source [not null] + relation varchar [note: 'для relation / soft: текст соотношения (1600 = 1100 + 1200)'] + expected numeric(18,2) [note: 'для relation / soft: цель'] + actual numeric(18,2) [note: 'для relation / soft: правая часть'] + created_at timestamptz [not null] + Indexes { document_id } + Note: 'v5.19: предупреждения и нарушения документа (формы — предупреждения и нарушения §7; ОСВ — предупреждения листа OSV_* результата 2.0). Удаляются вместе с документом при перезагрузке файла; при пересчёте после правки relation / soft заменяются целиком, warning источника service сохраняются, кроме снимаемых правкой (таблица §11.9)' +} + + +// ============ КОНСОЛИДАЦИЯ ============ + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.10 (процесс формирования КФО за отчётный период); 7.2 «формируется по требованию»; Прил. 2 «Консолидация: ГСК, отчётный период» +// Аналитика: md «Сводная ОСВ» / «Шахматка» (consolidation.year/quarter); md «Модель данных» v4 +// Макет: «Создание консолидации: ГК → ГСК → Квартал» +// В модели: v4 + v5 (reporting_date) +// Зачем: логическая консолидация ГСК+период; версии — ниже +Table consolidation { + id uuid [pk] + company_group_id uuid [not null, ref: > company_group.id, note: 'v5.9: консолидация создаётся от ГК'] + related_group_id uuid [ref: > related_group.id, note: 'v5.9: ГСК — NULL до утверждения; фиксируется/создаётся при VERIFIED по составу consolidation_member (ANL-029)'] + year smallint [not null] + quarter period_quarter [not null] + reporting_date date [not null, note: 'ADD: 01.04 / 01.07 / 01.10 / 01.01 след. года — для ЕХ и сравнений'] + created_at timestamptz + Indexes { (company_group_id, year, quarter) [unique] } + Note: 'Логическая консолидация ГК за период; версии различаются периметром (consolidation_member). Прошлые периоды — по company_group_id. v5.18: файлы, загруженные в консолидацию, — stored_file.consolidation_id (основа рабочего стола; полный состав — context/05-domain-rules.md §9.5 п. 11)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 5.1 (сценарий, подход, чекбокс Гудвилл); 6.10 (статусная модель, маршрут по инициатору, № и дата решения УОБ, «новая версия КФО» после Верифицирован); 6.4 (причины отклонений от Методики) +// Аналитика: md «Статусная модель»; md «Модель данных» v4 (consolidation_version: version_no, status varchar, initiator, approved_in_rf, verified_in_go, kfo_confirmation_enabled) +// Макет: «Версия», «Дата верификации», «Включить/Отключить подтверждение КФО», «Смена статуса отчёта», «Ответственный / Утвердил / Верифицировал» +// В модели: v4 + v5 (enum статусов, scenario, approach, goodwill_enabled, initiator_office_id, uob_*, methodology_deviations, previous_version_id) +Table consolidation_version { + id uuid [pk] + consolidation_id uuid [not null, ref: > consolidation.id] + version_no int [not null] + status consolidation_status [not null, note: 'CHG: enum вместо varchar'] + scenario consolidation_scenario [not null, note: 'ADD ТЗ 5.1'] + approach consolidation_approach [not null, default: 'BASIC', note: 'ADD ТЗ 5.1'] + goodwill_enabled boolean [not null, default: false, note: 'ADD ТЗ 5.1/6.7: чек-бокс расчёта Гудвилла'] + initiator_id uuid [not null, ref: > app_user.id] + initiator_office_id uuid [not null, ref: > regional_office.id, note: 'ГО/РФ инициатора — определяет маршрут (ТЗ 6.10)'] + approved_in_rf_by uuid [ref: > app_user.id] + approved_in_rf_at timestamptz + verified_in_go_by uuid [ref: > app_user.id] + verified_in_go_at timestamptz + uob_decision_no varchar [note: 'ADD ТЗ 6.10: № решения УОБ'] + uob_decision_date date [note: 'ADD ТЗ 6.10'] + methodology_deviations text [note: 'ADD ТЗ 6.4: причины отклонений от Методики'] + elimination_rules_version int [not null, note: 'v5.11; v5.23 not null: версия elimination_rule (version_no), действующая на дату создания версии консолидации (OQ-174); фиксируется при создании, не меняется'] + is_in_work boolean [not null, default: true] + previous_version_id uuid [ref: > consolidation_version.id, note: '«новая версия КФО» после VERIFIED'] + cancelled_reason text + created_at timestamptz + created_by uuid [ref: > app_user.id] + updated_by uuid [ref: > app_user.id] + updated_at timestamptz + Indexes { (consolidation_id, version_no) [unique] } + Note: 'Версия консолидации. После VERIFIED — read-only; изменения только через новую версию' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.10 (переходы, возврат на доработку); 8.2 «изменение статусов; согласование и утверждение КФО» +// Аналитика: md «Статусная модель» (переходы) +// Макет: «Смена статуса отчёта», «Комментарий (обязательно)», «Отклонено: отчёт возвращён на доработку» +// В модели: v5 новая +Table consolidation_status_history { + id uuid [pk] + version_id uuid [not null, ref: > consolidation_version.id] + from_status consolidation_status + to_status consolidation_status [not null] + changed_by uuid [not null, ref: > app_user.id] + changed_at timestamptz [not null] + comment text [note: 'обязателен при возврате на доработку'] + Note: 'ADD ТЗ 6.10/8.2: маршрут согласования' +} + +// ADD v5.1 (макет, ANL-036): «взять в работу» / перехват прав редактирования +// ИСТОЧНИК: +// ТЗ: нет в ТЗ (ANL-036) +// Аналитика: нет +// Макет: «Взятие КФО в работу (перехват прав редактирования)», «Причина: Отпуск», «В данный момент консолидация редактируется пользователем …» +// В модели: v5.1 новая (макет) +// Зачем: блокировка версии; подтвердить с заказчиком +// v5.16: без истечения по времени (OQ-119): активна, пока не снята владельцем/администратором или не перехвачена по причине +Table consolidation_lock { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + locked_by uuid [not null, ref: > app_user.id] + locked_at timestamptz [not null] + released_at timestamptz [note: 'NULL = активная блокировка'] + taken_over_from uuid [ref: > app_user.id, note: 'при перехвате — предыдущий владелец'] + takeover_reason text [note: 'обязателен при перехвате (макет: «Причина: Отпуск»)'] + Indexes { consolidation_version_id [note: 'partial UNIQUE WHERE released_at IS NULL'] } + Note: 'Ответственный за версию сейчас = активная блокировка' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.1 «КА в рамках консолидации редактирует состав ГСК»; 6.10 «выбор состава ГСК»; ЕХ 6.3 «Периметр на дату» +// Аналитика: md «Сводная ОСВ» п.2 («зафиксированный периметр; изменение related_group_member не меняет версию»); md «Модель данных» v4 +// Макет: «Состав ЮЛ», «Минимум для консолидации: 2», «Редактировать состав» +// В модели: v4 + v5 (is_head_entity для Гудвилла) +Table consolidation_member { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + legal_entity_id uuid [not null, ref: > legal_entity.id] + is_head_entity boolean [not null, default: false, note: 'ADD: головная организация (для Гудвилла)'] + Indexes { (consolidation_version_id, legal_entity_id) [unique] } + Note: 'Зафиксированный периметр версии. Изменение related_group_member не меняет существующие версии' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ Прил. 2 «Документ ФО: ссылка на Консолидацию и ЮЛ»; 6.4/6.5 «использование данных предыдущих периодов»; ЕХ 7 «повторное использование первички без дублей» +// Аналитика: md «Сводная ОСВ» п.3 (файлы выбираются по ЮЛ+год+квартал — без явной связи) +// Макет: «Первичные документы консолидации» +// В модели: v5 новая; v5.13 — document_id вместо stored_file_id +// Зачем: явная привязка документа к версии (переиспользование, монозаёмщики из ЕХ) +// v5.16: точки привязки/отвязки — подтверждение документа, добавление ЮЛ в периметр; замена/удаление файла, исключение ЮЛ (Срез 4, D-5) +Table consolidation_document { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + document_id uuid [not null, ref: > document.id, note: 'v5.13: логический документ (ОСВ по счёту / ББ / ОФР), не файл'] + Indexes { (consolidation_version_id, document_id) [unique] } + Note: 'ADD Прил. 2 ТЗ «Документ ФО → Консолидация»: явная привязка документов к версии (переиспользование в нескольких версиях/периодах, монозаёмщики из ЕХ). Чек-лист = required_document_rule × периметр − consolidation_document' +} + + +// ---- Назначения ВГО (AURORA-1751; context/05-domain-rules.md §5) ---- +// ИСТОЧНИК: +// ТЗ: ТЗ Прил. 3/4 (пары счетов, правила элиминации) +// Аналитика: DOC-272613 «Корректировка данных агрегатом» («Ручное добавление и перенос строк», §5); md «Автоматическое сопоставление ВГО» +// Макет: «Таблица корректировок» — перенос строки в правило / «Не учитывать в КФО» +// В модели: v5.24 новая +// Зачем: ручное решение аналитика по строке сводной ОСВ сверх автоматического разнесения по правилам (§5): +// привязка к правилу, снятие привязки, исключение/включение нераспределённой строки в КФО. Само +// разнесение по-прежнему чистая функция и не хранится (DECISION-5) — здесь только перекрывающее её решение. +Table vgo_assignment { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + statement_type statement_type [not null] + osv_entry_id uuid [not null, note: 'строка сводной ОСВ (исходная), FK → osv_entry.id, ON DELETE CASCADE — см. Ref ниже'] + rule_id uuid [ref: > elimination_rule.id, note: 'NULL при action = UNASSIGNED'] + action varchar [not null, note: 'ASSIGN | UNASSIGNED (CHECK в БД, в DBML не представлено); ASSIGN — только с rule_id, UNASSIGNED — без'] + is_exclude boolean [not null, default: true, note: '«Не учитывать в КФО»; CHECK в БД: true допустимо только при rule_id = NULL'] + created_by uuid [ref: > app_user.id] + created_at timestamptz [not null, default: `now()`] + updated_by uuid [ref: > app_user.id] + updated_at timestamptz + side varchar [note: 'v5.27: income D/K, balance NULL; CHECK side_consistency в миграции'] + Indexes { + (consolidation_version_id, statement_type, osv_entry_id) [unique, note: "partial UNIQUE WHERE statement_type = 'balance'"] + (consolidation_version_id, statement_type, osv_entry_id, side) [unique, note: "partial UNIQUE WHERE statement_type = 'income'"] + osv_entry_id + } + Note: 'Ручные решения ВГО: ББ — по исходной строке; ОФР — отдельно для D/K. Старые UNASSIGNED переносятся на обе позиции, ASSIGN — по счетам правила; неоднозначные решения блокируют миграцию.' +} +Ref: vgo_assignment.osv_entry_id > osv_entry.id [delete: cascade] + + +// ---- Корректировки ---- +// ИСТОЧНИК: +// ТЗ: ТЗ 6.8 (форма корректировок: колонки ДБ/КР/Комментарий, >5%, импорт/экспорт, отдельная форма ОФР); 6.4 (доп. корректировки ОФР); Прил. 2 «Расхождение/корректировка»; Прил. 9 +// Аналитика: md «Корректировка данных» (correction: parent_correction_id, debit/credit_line_code, adjustment); md «Модель данных» v4 +// Макет: «Таблица корректировки»: Актив/Пассив/Контроль/Корректировка, «Строка баланса Дт/Кт», «Комментарий» +// В модели: v4 + v5 (statement_type, pair_match_id, reason_code, comment NOT NULL, status, version_no) +Table correction { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + statement_type statement_type [not null, note: 'ADD: ББ или ОФР (ТЗ 6.8 — отдельные формы)'] + rule_id uuid [ref: > elimination_rule.id, note: 'v5.10: пара ВГО по естественному ключу (правило + компании); NULL = ручная без пары'] + parent_correction_id uuid [ref: > correction.id, note: 'дочерняя строка распределения по нескольким статьям; глубина 1'] + legal_entity_id uuid [ref: > legal_entity.id, note: 'компания 1 (чья первичка корректируется)'] + counterparty_id uuid [ref: > legal_entity.id, note: 'компания 2'] + debit_line_code varchar + debit_account_id uuid [ref: > chart_of_account.id] + debit_adjustment numeric(18,2) + credit_line_code varchar + credit_account_id uuid [ref: > chart_of_account.id] + credit_adjustment numeric(18,2) + deviation_amount numeric(18,2) + deviation_percent numeric(9,4) + reason_code varchar [note: 'ADD ТЗ 6.4: unrealized_revenue | excise | agency_commission | intra_lease_depreciation | intra_loan_interest | equity_income | other'] + comment text [not null, note: 'ТЗ 6.8: комментарий обязателен'] + status correction_status [not null, default: 'DRAFT'] + version_no int [not null, default: 1, note: 'сноска 22 ТЗ: история версий корректировки — audit_event(CORRECTION_EDIT, before/after); инкремент при каждом изменении'] + created_by uuid [ref: > app_user.id] + created_at timestamptz + updated_by uuid [ref: > app_user.id] + updated_at timestamptz + Indexes { + consolidation_version_id + (consolidation_version_id, rule_id, legal_entity_id, counterparty_id) + } + Note: 'Overlay поверх первички: итог = свод первички − Σ применённых корректировок. Расхождение пары (deviation_*) — справочно, пересчитывается' +} + + +// ---- Гудвилл (ТЗ 6.7, Прил. 3 п.2, Прил. 8) ---- +// ИСТОЧНИК: +// ТЗ: ТЗ 6.7 (Гудвилл: ФВ ГО − доля в капитале ДО, комментарий обязателен, отдельная строка ББ); Прил. 3 п.2 (58.01 против 80/83, стр. 1310/1350/1370); Прил. 8 +// Аналитика: md «Гудвилл» (шаги 1–10: 58.01, стр. 1310, доля владения, include_in_kfo); md «Модель данных» v4 (goodwill) +// Макет: «Таблица гудвилла», «Доля владения», «Учет в отчете», «Изменение долей владения (гудвилл)» +// В модели: v4 + v5 (additional_capital_amount, comment) +Table goodwill { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + head_legal_entity_id uuid [not null, ref: > legal_entity.id] + subsidiary_legal_entity_id uuid [not null, ref: > legal_entity.id] + ownership_share numeric(7,4) [note: 'ручной ввод, %'] + financial_investment_amount numeric(18,2) [note: 'ΣДБ 58.01 ГО по ДО'] + charter_capital_amount numeric(18,2) [note: 'стр. 1310 ББ ДО на отчётную дату'] + additional_capital_amount numeric(18,2) [note: 'ADD ТЗ Прил. 3: стр. 1350 ББ ДО'] + participation_amount numeric(18,2) [note: '(1310 + 1350) * share / 100 — DECISION: уточнить у методологов, входит ли 1350 в базу'] + goodwill_amount numeric(18,2) + include_in_kfo boolean [not null, default: false] + comment text [note: 'ТЗ 6.7: обязателен при include_in_kfo'] + created_by uuid [ref: > app_user.id] + created_at timestamptz + updated_by uuid [ref: > app_user.id] + updated_at timestamptz + Indexes { (consolidation_version_id, head_legal_entity_id, subsidiary_legal_entity_id) [unique] } +} + +// ---- Результат КФО (ТЗ 6.4, Прил. 10) ---- +// ИСТОЧНИК: +// ТЗ: ТЗ 6.4 (итоговая форма КФО: подход, Гудвилл, расхождения, корректировки, верификация); 6.8 (контрольные пороги 10% ЧА / 1% ВБ, 10% прибыли / 1% выручки); Прил. 10 +// Аналитика: нет (пробел — ANL-008/009) +// Макет: «КФО», «Сводка расхождений КФО», «Есть несохранённые изменения в КФО» +// В модели: v5 новая +// Зачем: шапка результата + пороги +Table kfo_result { + id uuid [pk] + consolidation_version_id uuid [not null, unique, ref: > consolidation_version.id] + computed_at timestamptz [not null] + is_frozen boolean [not null, default: false, note: 'true после VERIFIED (ставит переход статуса, AURORA-2002); дальше не пересчитывается'] + net_assets numeric(18,2) [note: 'ЧА ГСК = строка 1300 КФО банка (OQ-187)'] + balance_total numeric(18,2) [note: 'ВБ ГСК = строка 1600 КФО банка'] + revenue numeric(18,2) [note: 'строка 2110 КФО банка'] + sales_profit numeric(18,2) [note: 'строка 2200 КФО банка'] + total_deviation_bb numeric(18,2) [note: 'Σ расхождений по строкам бланка ББ без итогов (§8 «КФО» п. 9)'] + total_deviation_ofr numeric(18,2) + threshold_bb_exceeded boolean [note: 'ТЗ 6.8: > 10% ЧА (или > 1% ВБ, если ЧА < 10% ВБ, в т. ч. ЧА ≤ 0); равно — норма (§8 «КФО» п. 10)'] + threshold_ofr_exceeded boolean [note: '> 10% прибыли от продаж (или > 1% выручки при рентабельности < 10% или 2200 < 0; 2110 = 0 — предел 0)'] + threshold_justification text [note: 'обоснование принятия риска при превышении'] + goodwill_total numeric(18,2) [note: 'v5.26: Σ G применимых пар гудвилла, тыс. руб. = bank_amount строки GOODWILL (§8 «КФО» п. 8)'] + has_deviations boolean + warnings jsonb [note: 'v5.26: предупреждения расчёта [{code, message, legal_entity_id?, statement_type?, count?}], коды — §8 «КФО» п. 12'] + corrections_count_bb int [note: 'v5.26: число агрегатов ББ с применённой корректировкой (§8 «КФО» п. 11)'] + corrections_count_ofr int + corrections_amount_bb numeric(18,2) [note: 'v5.26: Σ исключений этих корректировок обеих сторон = Σ eliminated_manual строк бланка ББ без итогов, тыс. руб.'] + corrections_amount_ofr numeric(18,2) + Note: 'Шапка результата КФО + контрольные пороги. Все суммы — тыс. руб. (§8 «КФО»); одна запись на версию. До заморозки (is_frozen = false) хранит только ручные поля (threshold_justification, computed_at шапки), расчётные поля пусты — таблицу считает каждый GET; полный результат пишется при «Верифицирован» (v5.27, §8 «КФО» п. 12)' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.4 «шаг 1 — сложение по кодам строк; шаг 2 — элиминация; неэлиминируемая часть остаётся»; Прил. 2 «Результат КФО: статья, значение»; Прил. 10; ФСБУ 4/2023 +// Аналитика: DOC-274636 «Расчёт КФО» (черновик), DOC-274230, DOC-259025, DOC-272613; выжимка — context/05-domain-rules.md §8 «КФО» +// Макет: колонки «в т.ч. взаимные обязательства», «Очищенные показатели», «Расхождения», «источник: Баланс/ОСВ», «в т.ч. Гудвилл» +// В модели: v5 новая + v5.1 (vgo_amount, source_kind, deviation_amount, comment) +Table kfo_result_line { + id uuid [pk] + kfo_result_id uuid [not null, ref: > kfo_result.id] + statement_type statement_type [not null] + line_code varchar [not null, note: 'код строки бланка редакции на отчётную дату (66н / ФСБУ 4/2023); GOODWILL — служебная строка раздела I после 1170, без разреза по компаниям (§8 «КФО» п. 8, OQ-185)'] + line_name varchar + sum_amount numeric(18,2) [note: 'Σ client_amount по компаниям, тыс. руб.'] + eliminated_auto numeric(18,2) [note: 'v5.26: часть vgo от агрегатов без корректировки и гудвилл-пар (автосумма по elimination_mode)'] + eliminated_manual numeric(18,2) [note: 'v5.26: часть vgo от агрегатов с применённой корректировкой; корректировка заменяет автосумму агрегата, не складывается с ней'] + goodwill_adjust numeric(18,2) [note: 'v5.26: Σ G — у строки GOODWILL и у итогов, куда она входит (1100, 1600)'] + consolidated_amount numeric(18,2) [not null, note: '«КФО, подготовленная банком» = sum_amount − vgo_amount + goodwill_adjust'] + vgo_amount numeric(18,2) [note: '«в т.ч. взаимные обязательства/обороты» = Σ vgo по компаниям = eliminated_auto + eliminated_manual; со знаком строки (у расходов < 0)'] + source_kind varchar [note: 'v5.1 макет: balance | osv; v5.26: всегда balance — значение из формы (OQ-182)'] + deviation_amount numeric(18,2) [note: '«Расхождения» по статье: Σ |amount_1 − amount_2| агрегатов min на статье большей стороны (§8 «КФО» п. 9); у итогов — null'] + comment text [note: 'v5.1 макет: пояснение к строке КФО; ≤ 2000, переносится при пересчёте по (statement_type, line_code)'] + ordinal int + Note: 'v5.27: до заморозки хранятся только строки с комментарием (comment не пуст; consolidated_amount = 0, суммы и строки kfo_result_line_company не заполнены); полный набор строк пишет заморозка при «Верифицирован»' + Indexes { (kfo_result_id, statement_type, line_code) [unique] } +} + +// ИСТОЧНИК: +// ТЗ: ЕХ ТЗ 6.6 «выгрузка расшифровки статей отчётности в разрезе контрагентов»; ТЗ 6.9 (настраиваемые поля экспорта) +// Аналитика: нет +// Макет: «Доля в показателе по группе, %», расшифровка по компаниям +// В модели: v5 новая + v5.1 (share_percent) +Table kfo_result_line_company { + kfo_result_line_id uuid [not null, ref: > kfo_result_line.id] + legal_entity_id uuid [not null, ref: > legal_entity.id] + amount numeric(18,2) [note: '«Очищенные показатели» = client_amount − vgo_amount'] + share_percent numeric(7,4) [note: 'v5.1 макет: «Доля в показателе по группе, %» = amount / kfo_result_line.consolidated_amount × 100, ROUND_HALF_UP; null при нулевом итоге или |доля| ≥ 1000 % (иначе переполнение numeric(7,4))'] + client_amount numeric(18,2) [note: 'v5.26: «По данным клиента», тыс. руб.; строки расходов приведены к −|x| (§8 «КФО» п. 2)'] + vgo_amount numeric(18,2) [note: 'v5.26: «В т. ч. взаимные», тыс. руб. до целых, со знаком строки (§8 «КФО» п. 6)'] + Indexes { (kfo_result_line_id, legal_entity_id) [pk] } + Note: 'Разрез по компаниям для расшифровки статьи (ЕХ 6.6 «расшифровка в разрезе контрагентов»)' +} + +// ---- Сценарий «Подтверждение КФО клиента» (ТЗ 5.1, 6.10) ---- +// ИСТОЧНИК: +// ТЗ: ТЗ 5.1 «подтверждение консолидированной отчётности, предоставленной Клиентом»; 6.10 «ручной ввод данных КФО клиента; сравнение и анализ расхождений»; Методика сноска 14 (процент к наименьшему) +// Аналитика: md «Статусная модель» (kfo_confirmation_enabled — только флаг; ANL-006) +// Макет: «Подтверждение КФО клиента»: Версия клиента / Версия банка, ВГО клиента / ВГО Банка, Расхождение, Причина +// В модели: v5 новая + v5.1 (client_vgo/bank_vgo/deviation_reason) +Table client_kfo_line { + id uuid [pk] + consolidation_version_id uuid [not null, ref: > consolidation_version.id] + statement_type statement_type [not null] + line_code varchar [not null] + client_amount numeric(18,2) [note: 'ручной ввод из КФО клиента («Консолидированная отчетность от клиента»)'] + client_vgo_amount numeric(18,2) [note: 'v5.1 макет: «ВГО клиента» — ручной ввод'] + bank_amount numeric(18,2) [note: 'из kfo_result_line на момент сравнения'] + bank_vgo_amount numeric(18,2) [note: 'v5.1 макет: «ВГО Банка» из kfo_result_line.vgo_amount'] + deviation_amount numeric(18,2) + deviation_percent numeric(9,4) [note: 'относительно наименьшего (Методика сноска 14)'] + deviation_reason text [note: 'v5.1 макет: «Причина расхождения»'] + comment text + entered_by uuid [ref: > app_user.id] + entered_at timestamptz + Indexes { (consolidation_version_id, statement_type, line_code) [unique] } +} + + + +// ============ КОММЕНТАРИИ, АУДИТ, ЭКСПОРТ ============ + +// ИСТОЧНИК: +// ТЗ: ТЗ 6.3 «возможность добавления комментария к отчётности» +// Аналитика: md «Загрузка документов» («Комментарий к документу») +// Макет: «Новый комментарий», «Комментарий к документу» +// В модели: v5 новая +// Зачем: полиморфные комментарии к объектам; сейчас используется привязка к документу +Table comment { + id uuid [pk] + object_type varchar [not null, note: 'document | consolidation_version | …'] + object_id uuid [not null] + text text [not null] + author_id uuid [not null, ref: > app_user.id] + created_at timestamptz [not null] + Indexes { (object_type, object_id) } + Note: 'ТЗ 6.3; AURORA-1446: комментарии к документу. Правка и удаление комментариев не предусмотрены; аудит не ведётся' +} + +// ИСТОЧНИК: +// ТЗ: ТЗ 8.2 (журнал изменений, логирование всех действий, перечень событий: вход/выход, создание консолидации, ГСК/ГК, загрузка, распознанные данные, корректировки, статусы, утверждение, выгрузка; хранение ≥ срока данных); Прил. 2 «Запись журнала изменений» +// Аналитика: нет (пробел — ANL-019); md «Ролевая модель» ссылается на «журнал аудита» +// Макет: «Журнал действий»: разделы, Объект/Событие/Что изменилось/Результат/Идентификатор, IP/браузер, «неверный пароль» +// В модели: v5 новая + v5.1 (section, result, changed_field, user_agent) +Table audit_event { + id uuid [pk] + occurred_at timestamptz [not null] + user_id uuid [ref: > app_user.id, note: 'NULL для системных событий'] + action audit_action [not null] + section audit_section [not null, note: 'v5.1 макет: раздел журнала'] + object_type varchar [not null] + object_id uuid + consolidation_version_id uuid [ref: > consolidation_version.id, note: 'для выборки «история изменений КФО»'] + description text + before_value jsonb + after_value jsonb + comment text + result varchar [note: 'v5.1 макет: Успешно / Отклонено / Ошибка'] + changed_field varchar [note: 'v5.1 макет: фильтр «Изменяемое поле»'] + ip_address varchar + user_agent varchar [note: 'v5.1 макет: браузер в событиях сеанса'] + Indexes { + (consolidation_version_id, occurred_at) + (object_type, object_id) + (user_id, occurred_at) + } + Note: 'ТЗ 8.2: журнал изменений/аудит. Append-only. Хранение ≥ срока хранения консолидации. Также покрывает: историю правок распознанных данных, факт/состав выгрузок (ЕХ 6.6), импорт справочников, изменения матрицы прав' +} + + + +// ============ ЕХФД (ТЗ BIQ-34649) ============ +// DECISION-2: срез = замороженный указатель на верифицированную версию + список её артефактов. + +// ИСТОЧНИК: +// ТЗ: ЕХ ТЗ 6.2 «запись срезами: одна ГСК на одну отчётную дату, после Верифицирован»; 6.4 «версии среза Актуальная/Архив, неизменяемость»; 5.3 «архивирование в data lake» +// Аналитика: нет (пробел — ANL-032) +// Макет: «Единое хранилище», «Сформированная отчетность», «Верифицирована» +// В модели: v5 новая (DECISION-2) +// Зачем: замороженный указатель на верифицированную версию +Table ex_snapshot { + id uuid [pk] + related_group_id uuid [not null, ref: > related_group.id] + company_group_id uuid [not null, ref: > company_group.id, note: 'денормализация на момент верификации'] + reporting_date date [not null] + year smallint [not null] + quarter period_quarter [not null] + version_no int [not null] + status ex_snapshot_status [not null, note: 'v5.26: ACTUAL — не архивирован; ARCHIVE — архивирован пользователем в ЕХ (ручной архив, решение 27.10.2026); запись нового среза прежний не архивирует (§15)'] + source_version_id uuid [not null, unique, note: 'v5.12: id consolidation_version-источника БЕЗ FK — источник удаляется по сроку, срез нет'] + source_version_no int [not null] + superseded_by_snapshot_id uuid [ref: > ex_snapshot.id, note: 'v5.12: для ARCHIVE — ссылка на актуальный срез, который его заместил'] + verified_at timestamptz [not null] + written_at timestamptz [not null] + written_by uuid [ref: > app_user.id, note: 'инициатор записи = пользователь, выполнивший переход «Верифицирован» (v5.26, §15)'] + archived_at timestamptz + data_lake_ref varchar [note: 'ссылка на архив в data lake'] + Indexes { + (related_group_id, year, quarter, version_no) [unique] + (related_group_id, year, quarter, status) + } + Note: 'Срез данных ЕХ: одна ГСК на одну отчётную дату (ЕХ 6.2). Неизменяем и самодостаточен (не зависит от таблиц консолидации). Состав — ex_snapshot_item + ex_snapshot_member' +} + +// ИСТОЧНИК: +// ТЗ: ЕХ ТЗ 6.3 «Периметр на дату: состав ЮЛ/ИП группы на отчётную дату» +// Аналитика: нет +// Макет: дерево «ГК → ГСК → Организации» с ИНН +// В модели: v5 новая +// Зачем: денормализация периметра на дату среза +Table ex_snapshot_member { + ex_snapshot_id uuid [not null, ref: > ex_snapshot.id] + legal_entity_id uuid [not null, ref: > legal_entity.id] + inn varchar [not null, note: 'денормализация на дату среза'] + name varchar [not null] + Indexes { (ex_snapshot_id, legal_entity_id) [pk] } + Note: 'Периметр на дату (ЕХ 6.3)' +} + +// ИСТОЧНИК: +// ТЗ: ЕХ ТЗ 5.2 «Состав хранимых данных» (КФО, первичные, сводная ОСВ, шахматка, гудвилл, корректировки, иные отчёты); 6.7.3/6.7.4 «Сформированная отчётность / Первичные документы», «Просмотр / Выгрузка» +// Аналитика: нет +// Макет: область документов ЕХ, «Скачать», «Связать» документ с организацией +// В модели: v5 новая; v5.13 — document_id вместо stored_file_id +// Зачем: состав среза со ссылками на объекты +Table ex_snapshot_item { + id uuid [pk] + ex_snapshot_id uuid [not null, ref: > ex_snapshot.id] + item_type varchar [not null, note: 'primary_document | summary_osv | vgo_pairs | chess | kfo_balance | kfo_income | goodwill | corrections | other_report | version_card (v5.26)'] + legal_entity_id uuid [ref: > legal_entity.id, note: 'для первички — организация'] + document_id uuid [ref: > document.id, note: 'v5.13: только для primary_document — исходники хранятся весь срок, FK допустим; «Скачать» — через document.stored_file → s3_object'] + payload jsonb [note: 'v5.12 DECISION-5: замороженные данные для всех остальных типов; самодостаточно, без ссылок на таблицы консолидации; v5.26: конверт contracts/ex-payload-v1.schema.json, у primary_document — null'] + payload_schema_version varchar [note: 'v5.26: "1.0" — версия схемы ex-payload; у primary_document — null'] + Indexes { (ex_snapshot_id, item_type) } + Note: 'Состав среза: primary_document → document; всё остальное → payload. «Просмотр» в ЛК ЕХ рендерит payload по схеме; «Выгрузка» собирает xlsx из payload на лету' +} + +// ============ ПРОЧЕЕ ============ + +// ИСТОЧНИК: +// ТЗ: ТЗ 5.1 (базовый / расширенный пакет ОСВ); 6.2 «набор обязательных ОСВ»; сноска 12 «процесс останавливается, если отсутствует документ» +// Аналитика: md «Загрузка документов» (чек-лист: «ОСВ по обязательным счетам», «прочие документы») +// Макет: «Чек-лист загруженных документов», «Условные обозначения статусов» +// В модели: v5 новая +// Зачем: конфигурация чек-листа по подходу; с v5.13 сверяется с document (тип + счёт + период), а не с файлом +Table required_document_rule { + id uuid [pk] + approach consolidation_approach [not null] + document_type document_type [not null] + account_id uuid [ref: > chart_of_account.id, note: 'для OSV'] + is_mandatory boolean [not null, note: 'обязателен → блокирует переход к консолидации (сноска 12 ТЗ)'] + Indexes { (approach, document_type, account_id) [unique] } + Note: 'ADD ТЗ 5.1/6.2: состав обязательных документов по подходу — основа чек-листа. Выполнено = есть document CONFIRMED с таким типом/счётом за период у каждого ЮЛ периметра' +} + +// ============ МЕТРИКИ КАЧЕСТВА ============ + +// ИСТОЧНИК: +// ТЗ: нет (инициатива владельца, AURORA-1759) +// Аналитика: openspec/changes/quality-metrics/design.md (этот репозиторий); WHY и решения владельца — +// arm-core/openspec/changes/quality-metrics/design.md §1-3, §10 +// Макет: экран «Качество первички» — при реализации arm-core#190 +// В модели: v5.25 новая +// Зачем: подтвердить эффект АРМ КА цифрами и дать довод о качестве сервиса разбора PDF с разделением +// ошибка сервиса / наша нормализация / документ клиента (05-domain-rules.md §14) +Table quality_intake_fact { + id uuid [pk] + document_id uuid [not null, note: 'без FK; один факт на документ'] + file_id uuid [not null] + job_id uuid [not null] + attempt smallint [not null] + legal_entity_id uuid [not null] + legal_entity_name varchar [not null, note: 'копия имени на момент факта'] + group_id uuid [note: 'NULL — ЮЛ вне ГК'] + group_name varchar + document_type document_type [not null, note: 'BALANCE / INCOME / OSV — DISCLOSURE/OTHER факт не пишут'] + pipeline recognition_pipeline [not null, note: 'tabular = Excel, image = PDF'] + is_scan boolean [note: 'только pipeline = image'] + service_name varchar + service_version varchar + statements_version varchar + result_schema_version varchar + report_date date [note: 'как распознано'] + intake_at timestamptz [not null] + intake_status document_status [not null, note: 'RECOGNIZED / NEEDS_CHECK'] + line_count int + value_count int + value_needs_review_count int + line_unreadable_count int + line_missing_count int + issue_relation_count int + issue_soft_count int + warning_count int + raw_s3_key varchar [note: 'PDF; NULL для Excel'] + result_s3_key varchar + upload_s3_key varchar + payload jsonb [not null, note: 'снимок «как распознано»: header/lines/values/issues (contracts/metrics/quality-facts.schema.json#intake-payload). Для OSV — БЕЗ построчного снимка lines/values, только счётчики выше и osv_warnings[] — правок сумм ОСВ в ядре нет'] + payload_schema_version varchar + Indexes { + (document_id) [unique] + (legal_entity_id, intake_at) + (group_id, intake_at) + } + Note: 'Отдельная схема PostgreSQL dkb_metrics (имя — настройка). Append-only, без FK на рабочие таблицы: переживает удаление/перераспознавание документа. Пишется в транзакции маппинга, после map_file, до auto_confirm — один факт на созданный документ BALANCE/INCOME/OSV.' +} + +Table quality_outcome_fact { + id uuid [pk] + document_id uuid [not null, note: 'связь с quality_intake_fact.document_id по значению, без FK'] + outcome quality_outcome [not null] + outcome_at timestamptz [not null] + primary_cause quality_cause [note: 'NULL для DELETED*/SUPERSEDED* — они не входят в матрицу причин'] + fixes_value_service int [not null, default: 0] + fixes_value_ours int [not null, default: 0] + fixes_code_service int [not null, default: 0] + fixes_code_ours int [not null, default: 0] + fixes_added_row_service int [not null, default: 0] + fixes_added_row_ours int [not null, default: 0] + fixes_header_service int [not null, default: 0] + fixes_header_ours int [not null, default: 0] + fixes_osv_period_account int [not null, default: 0, note: 'всегда «наш разбор» (Excel) — не делится по источнику'] + checks_accepted boolean [not null, default: false, note: 'подтверждён с акцептом жёстких нарушений §7 — условие «документ клиента», §14'] + issue_count_at_outcome int [not null, default: 0] + report_date date [note: 'подтверждённая отчётная дата'] + confirmed_by uuid [note: 'для агрегатов не используется'] + payload jsonb [not null, note: 'подтверждённый результат + fixes[] (contracts/metrics/quality-facts.schema.json#outcome-payload)'] + payload_schema_version varchar + Indexes { + (document_id, outcome_at) + (outcome, outcome_at) + } + Note: 'НЕ unique на document_id: повторное подтверждение ОСВ и исход DELETED_AFTER_CONFIRM/SUPERSEDED_AFTER_CONFIRM пишут новый факт поверх прежнего. Метрики берут последний факт по outcome_at. Пишется при первом из событий: подтверждение (ручное/auto_confirm), удаление до подтверждения, автозамена.' +} + +Table quality_failure_fact { + id uuid [pk] + job_id uuid [not null] + file_id uuid [not null] + attempt smallint [not null] + legal_entity_id uuid [not null] + legal_entity_name varchar [not null] + group_id uuid + group_name varchar + pipeline recognition_pipeline [not null] + is_scan boolean + service_name varchar + failure_kind quality_failure_kind [not null] + error_code varchar + failed_at timestamptz [not null] + Indexes { + (job_id) [unique] + (file_id, attempt) + } + Note: 'Технический отказ задания распознавания (recognition_job.status error/timeout/dlq) или неповторяемая ошибка маппинга (MappingFatal). Считается по файлам, не по документам — знаменатель «отказавших файлов от загруженных того же pipeline» (§14), фильтр по типу документа не действует. FILE_REPROCESS: отказ первой попытки остаётся фактом, документы удачной попытки получают свои intake/outcome. Нет payload/payload_schema_version — все поля плоские.' +} diff --git a/model/diagrams/part04_files_jobs.png b/model/diagrams/part04_files_jobs.png index 10eb796..2ab0e34 100644 Binary files a/model/diagrams/part04_files_jobs.png and b/model/diagrams/part04_files_jobs.png differ diff --git a/model/diagrams/part04_files_jobs.svg b/model/diagrams/part04_files_jobs.svg index e742aab..68e0a08 100644 --- a/model/diagrams/part04_files_jobs.svg +++ b/model/diagrams/part04_files_jobs.svg @@ -4,9 +4,9 @@ - - + + dbml @@ -35,44 +35,44 @@ file_type - - - -       file_type        - - -    XLS     - - -    XLSX     - - -    XLSB     - - -    PDF     - - -    PNG     - - -    JPG     - + + + +       file_type        + + +    XLS     + + +    XLSX     + + +    XLSB     + + +    PDF     + + +    PNG     + + +    JPG     + recognition_pipeline - - - -       recognition_pipeline        - - -    tabular     - - -    image     - + + + +       recognition_pipeline        + + +    tabular     + + +    image     + @@ -167,35 +167,35 @@ recognition_status - - - -       recognition_status        - - -    queued     - - -    published     - - -    processing     - - -    done     - - -    needs_check     - - -    error     - - -    timeout     - - -    dlq     - + + + +       recognition_status        + + +    queued     + + +    published     + + +    processing     + + +    done     + + +    needs_check     + + +    error     + + +    timeout     + + +    dlq     + @@ -236,16 +236,16 @@ consolidation - - - -       consolidation        - - -id -     -uuid - + + + +       consolidation        + + +id +     +uuid + @@ -401,28 +401,28 @@ uuid -is_scan     -boolean +current_recognition_job_id     +uuid -current_recognition_job_id     -uuid +uploaded_by     +uuid -uploaded_by     -uuid +uploaded_at     +timestamptz -uploaded_at     +updated_at     timestamptz -updated_at     -timestamptz +consolidation_id     +uuid -consolidation_id     -uuid +is_scan     +boolean @@ -430,7 +430,7 @@ stored_file:e->file_type:w - + @@ -441,19 +441,19 @@ stored_file:e->app_user:w - + 1 -* +* stored_file:e->consolidation:w - - -1 -* + + +1 +* @@ -468,8 +468,8 @@ stored_file:e->s3_object:w - - + + 1 * @@ -591,36 +591,36 @@ uuid -raw_s3_object_id     -uuid +result_schema_version     +varchar -result_schema_version     -varchar +group_schema     +jsonb -group_schema     +metrics     jsonb -metrics     -jsonb +fields_total     +int -fields_total     +fields_low_confidence     int -fields_low_confidence     +fields_unrecognized     int -fields_unrecognized     -int +statements_version     +varchar -statements_version     -varchar +raw_s3_object_id     +uuid @@ -629,10 +629,10 @@ stored_file:e->recognition_job:w - - + + 1 -* +* @@ -811,19 +811,19 @@ recognition_job:e->recognition_pipeline:w - + recognition_job:e->recognition_status:w - + recognition_job:e->s3_object:w - - + + 1 * @@ -831,10 +831,10 @@ recognition_job:e->s3_object:w - - + + 1 -* +* diff --git a/model/diagrams/part08_corrections_kfo.png b/model/diagrams/part08_corrections_kfo.png index dfabe9d..6731bed 100644 Binary files a/model/diagrams/part08_corrections_kfo.png and b/model/diagrams/part08_corrections_kfo.png differ diff --git a/model/diagrams/part08_corrections_kfo.svg b/model/diagrams/part08_corrections_kfo.svg index 44d57f3..906f554 100644 --- a/model/diagrams/part08_corrections_kfo.svg +++ b/model/diagrams/part08_corrections_kfo.svg @@ -4,57 +4,57 @@ - - + + dbml statement_type - - - -       statement_type        - - -    balance     - - -    income     - + + + +       statement_type        + + +    balance     + + +    income     + correction_status - - - -       correction_status        - - -    DRAFT     - - -    APPLIED     - - -    REVOKED     - + + + +       correction_status        + + +    DRAFT     + + +    APPLIED     + + +    REVOKED     + app_user - - - -       app_user        - - -id -     -uuid - + + + +       app_user        + + +id +     +uuid + @@ -62,16 +62,16 @@ chart_of_account - - - -       chart_of_account        - - -id -     -uuid - + + + +       chart_of_account        + + +id +     +uuid + @@ -79,16 +79,16 @@ consolidation_version - - - -       consolidation_version        - - -id -     -uuid - + + + +       consolidation_version        + + +id +     +uuid + @@ -96,16 +96,16 @@ elimination_rule - - - -       elimination_rule        - - -id -     -uuid - + + + +       elimination_rule        + + +id +     +uuid + @@ -113,16 +113,16 @@ legal_entity - - - -       legal_entity        - - -id -     -uuid - + + + +       legal_entity        + + +id +     +uuid + @@ -130,541 +130,545 @@ correction - - - -       correction        - - -id -     -uuid - - -consolidation_version_id     -uuid - -(!) - - -statement_type     -statement_type - -(!) - - -rule_id     -uuid - - -parent_correction_id     -uuid - - -legal_entity_id     -uuid - - -counterparty_id     -uuid - - -debit_line_code     -varchar - - -debit_account_id     -uuid - - -debit_adjustment     -numeric(18,2) - - -credit_line_code     -varchar - - -credit_account_id     -uuid - - -credit_adjustment     -numeric(18,2) - - -deviation_amount     -numeric(18,2) - - -deviation_percent     -numeric(9,4) - - -reason_code     -varchar - - -comment     -text - -(!) - - -status     -correction_status - -(!) - - -version_no     -int - -(!) - - -created_by     -uuid - - -created_at     -timestamptz - - -updated_by     -uuid - - -updated_at     -timestamptz - + + + +       correction        + + +id +     +uuid + + +consolidation_version_id     +uuid + +(!) + + +statement_type     +statement_type + +(!) + + +rule_id     +uuid + + +parent_correction_id     +uuid + + +legal_entity_id     +uuid + + +counterparty_id     +uuid + + +debit_line_code     +varchar + + +debit_account_id     +uuid + + +debit_adjustment     +numeric(18,2) + + +credit_line_code     +varchar + + +credit_account_id     +uuid + + +credit_adjustment     +numeric(18,2) + + +deviation_amount     +numeric(18,2) + + +deviation_percent     +numeric(9,4) + + +reason_code     +varchar + + +comment     +text + +(!) + + +status     +correction_status + +(!) + + +version_no     +int + +(!) + + +created_by     +uuid + + +created_at     +timestamptz + + +updated_by     +uuid + + +updated_at     +timestamptz + correction:e->statement_type:w - + correction:e->correction_status:w - + correction:e->app_user:w - - -1 -* + + +1 +* correction:e->app_user:w - - -1 -* + + +1 +* correction:e->chart_of_account:w - - -1 -* + + +1 +* correction:e->chart_of_account:w - - -1 -* + + +1 +* correction:e->consolidation_version:w - - -1 -* + + +1 +* correction:e->elimination_rule:w - - -1 -* + + +1 +* correction:e->legal_entity:w - - -1 -* + + +1 +* correction:e->legal_entity:w - - -1 -* + + +1 +* correction:e->correction:w - - -1 -* + + +1 +* vgo_assignment - - - - -       vgo_assignment        - - -id -     -uuid - - -consolidation_version_id     -uuid - -(!) - - -statement_type     -statement_type - -(!) - - -osv_entry_id     -uuid - -(!) - - -rule_id     -uuid - - -action     -varchar - -(!) - - -is_exclude     -boolean - -(!) - - -created_by     -uuid - - -created_at     -timestamptz - -(!) - - -updated_by     -uuid - - -updated_at     -timestamptz - + + + + +       vgo_assignment        + + +id +     +uuid + + +consolidation_version_id     +uuid + +(!) + + +statement_type     +statement_type + +(!) + + +osv_entry_id     +uuid + +(!) + + +rule_id     +uuid + + +action     +varchar + +(!) + + +is_exclude     +boolean + +(!) + + +created_by     +uuid + + +created_at     +timestamptz + +(!) + + +updated_by     +uuid + + +updated_at     +timestamptz + + +side     +varchar + vgo_assignment:e->statement_type:w - + vgo_assignment:e->app_user:w - - -1 -* + + +1 +* vgo_assignment:e->app_user:w - - -1 -* + + +1 +* vgo_assignment:e->consolidation_version:w - - -1 -* + + +1 +* vgo_assignment:e->elimination_rule:w - - -1 -* + + +1 +* goodwill - - - -       goodwill        - - -id -     -uuid - - -consolidation_version_id     -uuid - -(!) - - -head_legal_entity_id     -uuid - -(!) - - -subsidiary_legal_entity_id     -uuid - -(!) - - -ownership_share     -numeric(7,4) - - -financial_investment_amount     -numeric(18,2) - - -charter_capital_amount     -numeric(18,2) - - -additional_capital_amount     -numeric(18,2) - - -participation_amount     -numeric(18,2) - - -goodwill_amount     -numeric(18,2) - - -include_in_kfo     -boolean - -(!) - - -comment     -text - - -created_by     -uuid - - -created_at     -timestamptz - - -updated_by     -uuid - - -updated_at     -timestamptz - + + + +       goodwill        + + +id +     +uuid + + +consolidation_version_id     +uuid + +(!) + + +head_legal_entity_id     +uuid + +(!) + + +subsidiary_legal_entity_id     +uuid + +(!) + + +ownership_share     +numeric(7,4) + + +financial_investment_amount     +numeric(18,2) + + +charter_capital_amount     +numeric(18,2) + + +additional_capital_amount     +numeric(18,2) + + +participation_amount     +numeric(18,2) + + +goodwill_amount     +numeric(18,2) + + +include_in_kfo     +boolean + +(!) + + +comment     +text + + +created_by     +uuid + + +created_at     +timestamptz + + +updated_by     +uuid + + +updated_at     +timestamptz + goodwill:e->app_user:w - - -1 -* + + +1 +* goodwill:e->app_user:w - - -1 -* + + +1 +* goodwill:e->consolidation_version:w - - -1 -* + + +1 +* goodwill:e->legal_entity:w - - -1 -* + + +1 +* goodwill:e->legal_entity:w - - -1 -* + + +1 +* kfo_result - - - - -       kfo_result        - - -id -     -uuid - - -consolidation_version_id     -uuid - -(!) - - -computed_at     -timestamptz - -(!) - - -is_frozen     -boolean - -(!) - - -net_assets     -numeric(18,2) - - -balance_total     -numeric(18,2) - - -revenue     -numeric(18,2) - - -sales_profit     -numeric(18,2) - - -total_deviation_bb     -numeric(18,2) - - -total_deviation_ofr     -numeric(18,2) - - -threshold_bb_exceeded     -boolean - - -threshold_ofr_exceeded     -boolean - - -threshold_justification     -text - - -goodwill_total     -numeric(18,2) - - -has_deviations     -boolean - - -warnings     -jsonb - - -corrections_count_bb     -int - - -corrections_count_ofr     -int - - -corrections_amount_bb     -numeric(18,2) - - -corrections_amount_ofr     -numeric(18,2) - + + + + +       kfo_result        + + +id +     +uuid + + +consolidation_version_id     +uuid + +(!) + + +computed_at     +timestamptz + +(!) + + +is_frozen     +boolean + +(!) + + +net_assets     +numeric(18,2) + + +balance_total     +numeric(18,2) + + +revenue     +numeric(18,2) + + +sales_profit     +numeric(18,2) + + +total_deviation_bb     +numeric(18,2) + + +total_deviation_ofr     +numeric(18,2) + + +threshold_bb_exceeded     +boolean + + +threshold_ofr_exceeded     +boolean + + +threshold_justification     +text + + +goodwill_total     +numeric(18,2) + + +has_deviations     +boolean + + +warnings     +jsonb + + +corrections_count_bb     +int + + +corrections_count_ofr     +int + + +corrections_amount_bb     +numeric(18,2) + + +corrections_amount_ofr     +numeric(18,2) + @@ -672,142 +676,145 @@ kfo_result:e->consolidation_version:w - - -1 -* + + +1 +* kfo_result_line - - - -       kfo_result_line        - - -id -     -uuid - - -kfo_result_id     -uuid - -(!) - - -statement_type     -statement_type - -(!) - - -line_code     -varchar - -(!) - - -line_name     -varchar - - -sum_amount     -numeric(18,2) - - -eliminated_auto     -numeric(18,2) - - -eliminated_manual     -numeric(18,2) - - -goodwill_adjust     -numeric(18,2) - - -consolidated_amount     -numeric(18,2) - -(!) - - -vgo_amount     -numeric(18,2) - - -source_kind     -varchar - - -deviation_amount     -numeric(18,2) - - -comment     -text - - -ordinal     -int - + + + + +       kfo_result_line        + + +id +     +uuid + + +kfo_result_id     +uuid + +(!) + + +statement_type     +statement_type + +(!) + + +line_code     +varchar + +(!) + + +line_name     +varchar + + +sum_amount     +numeric(18,2) + + +eliminated_auto     +numeric(18,2) + + +eliminated_manual     +numeric(18,2) + + +goodwill_adjust     +numeric(18,2) + + +consolidated_amount     +numeric(18,2) + +(!) + + +vgo_amount     +numeric(18,2) + + +source_kind     +varchar + + +deviation_amount     +numeric(18,2) + + +comment     +text + + +ordinal     +int + + + kfo_result_line:e->statement_type:w - + kfo_result_line:e->kfo_result:w - - -1 -* + + +1 +* kfo_result_line_company - - - -       kfo_result_line_company        - - -kfo_result_line_id -     -uuid - -(!) - - -legal_entity_id -     -uuid - -(!) - - -client_amount     -numeric(18,2) - - -vgo_amount     -numeric(18,2) - - -amount     -numeric(18,2) - - -share_percent     -numeric(7,4) - + + + +       kfo_result_line_company        + + +kfo_result_line_id +     +uuid + +(!) + + +legal_entity_id +     +uuid + +(!) + + +amount     +numeric(18,2) + + +share_percent     +numeric(7,4) + + +client_amount     +numeric(18,2) + + +vgo_amount     +numeric(18,2) + @@ -815,114 +822,114 @@ kfo_result_line_company:e->legal_entity:w - - -1 -* + + +1 +* kfo_result_line_company:e->kfo_result_line:w - - -1 -* + + +1 +* client_kfo_line - - - -       client_kfo_line        - - -id -     -uuid - - -consolidation_version_id     -uuid - -(!) - - -statement_type     -statement_type - -(!) - - -line_code     -varchar - -(!) - - -client_amount     -numeric(18,2) - - -client_vgo_amount     -numeric(18,2) - - -bank_amount     -numeric(18,2) - - -bank_vgo_amount     -numeric(18,2) - - -deviation_amount     -numeric(18,2) - - -deviation_percent     -numeric(9,4) - - -deviation_reason     -text - - -comment     -text - - -entered_by     -uuid - - -entered_at     -timestamptz - + + + +       client_kfo_line        + + +id +     +uuid + + +consolidation_version_id     +uuid + +(!) + + +statement_type     +statement_type + +(!) + + +line_code     +varchar + +(!) + + +client_amount     +numeric(18,2) + + +client_vgo_amount     +numeric(18,2) + + +bank_amount     +numeric(18,2) + + +bank_vgo_amount     +numeric(18,2) + + +deviation_amount     +numeric(18,2) + + +deviation_percent     +numeric(9,4) + + +deviation_reason     +text + + +comment     +text + + +entered_by     +uuid + + +entered_at     +timestamptz + client_kfo_line:e->statement_type:w - + client_kfo_line:e->app_user:w - - -1 -* + + +1 +* client_kfo_line:e->consolidation_version:w - - -1 -* + + +1 +* diff --git a/model/entities-v5.27.md b/model/entities-v5.27.md new file mode 100644 index 0000000..1f5bce8 --- /dev/null +++ b/model/entities-v5.27.md @@ -0,0 +1,301 @@ +# АРМ КА — описание сущностей модели данных v5.27 + +Источник истины по структуре — `data-model-v5.27.dbml` (полный). Диаграммы по частям — `diagrams/` (part01…10 svg/png; перерисовываются `make diagrams` из текущего DBML). Здесь — что каждая таблица значит для бизнеса, как связана с соседями и откуда взялась. + +## Что изменилось в v5.27 (KFO: ОФР и хранение; arm-core PR #251, миграция `0014_dbml_v5_27` после `0013_dbml_v5_26`) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `vgo_assignment.side` (D/K) | Разнесение ОФР по позициям «строка ОСВ + сторона» (DOC-277424 §3.4): `NULL` для ББ, `D`/`K` для ОФР. Уникальность: ББ — версия/тип/строка, ОФР — версия/тип/строка/сторона. Старые назначения ASSIGN переносятся по счетам правил, неоднозначные блокируют миграцию; UNASSIGNED переносится на D и K. Это отменяет пункт 2 v5.24 «поле `side` не добавлено» только для ОФР | +| 2 | `kfo_result.snapshot` не вводится | Незамороженный результат КФО не хранится (решение владельца 13.11.2026, OQ-192): каждый GET считает по текущим данным. До заморозки сохраняются только ручные поля: обоснование порога (`kfo_result`), отклонения от Методики (`consolidation_version`) и строки `kfo_result_line` лишь с комментарием (`consolidated_amount = 0`, суммы пусты, строк компаний нет). Полный снимок пишет переход версии в «Верифицирован», в одной транзакции с `is_frozen = true`; миграция удаляет прежний расчётный кэш незамороженных таблиц, замороженные суммы не меняются | +| 3 | `correction.ordinal` не вводится | Основная строка корректировки задаётся `parent_correction_id`, дополнительные упорядочены по ID; порядок ввода не хранится | + +`GET …/kfo/state` и поле `data_changed` не вводятся: при пересчёте на каждом чтении отдельного состояния «расчёт неактуален» нет. Остальные таблицы и enum — как в v5.26. + +## Что изменилось в v5.26 (расчёт КФО, AURORA-1546/1547; RSHB/arm-ka-docs#206; миграция — в AURORA-1546) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `kfo_result_line_company` + `client_amount`, `vgo_amount` | «По данным клиента» и «в т. ч. взаимные» по компании (тыс. руб.); `amount` — «очищенные» = `client_amount − vgo_amount`. Без них колонки экрана КФО `[31-04]` из сохранённого результата не восстановить. Разреза «расхождение по компании» нет: на экране «Расхождения» — общая колонка строки | +| 2 | `kfo_result` + `warnings`, `corrections_count_bb/_ofr`, `corrections_amount_bb/_ofr` | Предупреждения расчёта и сводка корректировок для реквизитов формы хранятся с результатом: замороженная версия отдаёт то, что видели при верификации (`context/05-domain-rules.md` §8 «КФО» п. 11–12) | +| 3 | `audit_action` + `KFO_EDIT` | Сохранение ручных полей КФО (комментарии строк, обоснование порога, причины отклонений от Методики) — раздел `KFO`; подходящего действия не было | +| 4 | `elimination_rule.elimination_mode` — значение `min_or_full` | Правило ББ 2 по DOC-274230: `min` при обеих сторонах, сторона 1 при пустой стороне 2, без порога 5 %; строки ДО — каскадом 1310 → 1350 → 1370. Поле varchar — схема не меняется; seed v1 правится на месте в AURORA-1546 | +| 5 | Пояснения `kfo_result_line`, `kfo_result` | Приведены к §8 «КФО»: всё в тыс. руб.; корректировка агрегата заменяет его автосумму (`eliminated_auto` и `eliminated_manual` по одному агрегату не складываются); `GOODWILL` — служебная строка раздела I, входит в 1100/1600 | +| 6 | `ex_snapshot_item.item_type` + `version_card`; payload — по `contracts/ex-payload-v1.schema.json` | Карточка версии (участники маршрута с ФИО, решение УОБ, обоснования, состав) — элементом среза, а не колонками `ex_snapshot`: версия КФО удаляется раньше среза, а схему таблицы не трогаем. Схема payload — конверт и данные по каждому типу (`context/05-domain-rules.md` §15; docs #208) | +| 7 | `audit_action` + `EX_SNAPSHOT_WRITE` | Запись среза при «Верифицирован» — раздел `KFO` (раздела `EX` нет) | +| 8 | Пояснение `ex_snapshot.status` | Архив прежнего среза ручной (решение 27.10.2026): `ACTUAL` — «не архивирован», а не «последняя верифицированная» | + +Остальные таблицы и enum — как в v5.25. + +## Что изменилось в v5.25 (AURORA-1759; RSHB/arm-ka-docs#202; миграция — отдельный change arm-core#190) + +| № | Изменение | Решение | +|---|---|---| +| 1 | Три новые таблицы `quality_intake_fact`, `quality_outcome_fact`, `quality_failure_fact` | Append-only факты качества распознавания первички (документ «как распознано», его исход, технический отказ задания), без FK на рабочие таблицы — переживают удаление/перераспознавание. Полное WHY и решения владельца — `../arm-core/openspec/changes/quality-metrics/design.md` §1-3, §10; состав столбцов — `openspec/changes/quality-metrics/design.md` §1.4-1.6 этого репозитория. Часть 10 ниже | +| 2 | `s3_bucket_role.raw` | Сырой ответ ИС Сканирование (`raw/{job_id}.json`) теперь регистрируется в `s3_object`; не экспирится с удалением файла — вход для будущего эталонного прогона перенормализации | +| 3 | `stored_file.is_scan` | Признак скан/цифровой PDF по текстовому слою при загрузке; NULL для не-PDF | +| 4 | `recognition_job.raw_s3_object_id` | Ссылка на зарегистрированный сырой ответ (image pipeline) — отменяет решение v5.13 «сырой ответ в БД не регистрируется» | +| 5 | `permission_object.QUALITY_METRICS` | Объект права экрана «Качество первички» (действие `VIEW`); право `QUALITY_METRICS_VIEW` — seed в arm-core, не в DBML | + +Остальные таблицы и enum — как в v5.24. + +## Что изменилось в v5.24 (AURORA-1751/1540/1541; RSHB/arm-ka-docs#196; миграция уже слита в arm-core main) + +| № | Изменение | Решение | +|---|---|---| +| 1 | Новая таблица `vgo_assignment` | Ручное решение аналитика по строке сводной ОСВ сверх автоматического разнесения по правилам ВГО (`context/05-domain-rules.md` §5): привязка к правилу, снятие привязки, «не учитывать в КФО» у нераспределённой строки. Само разнесение остаётся чистой функцией и не хранится (DECISION-5) — таблица только перекрывает его решением аналитика. Одна действующая запись на строку ОСВ в версии и типе отчётности | +| 2 | Поле `side` не добавлено | Черновик контракта (`dev/api-contract-corrections.md` §5) предполагал отдельную позицию для строки с одновременно дебетовым и кредитовым сальдо; в уже слитой миграции 0011 решение одно на обе позиции строки, не по стороне | + +Остальные таблицы и enum — как в v5.23. + +## Что изменилось в v5.23 (решения владельца 23.10.2026; RSHB/arm-ka-docs#164, AURORA-1536) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `consolidation_version.elimination_rules_version` — `not null` | Версия правил элиминации (`elimination_rule.version_no`), действующая на **дату создания** версии консолидации (OQ-174), фиксируется при создании и дальше не меняется. По ней считаются шахматка по правилу-пресету и пары ВГО (`context/05-domain-rules.md` §4, §5). Вариант «пусто — значит последние открытые правила» отвергнут: после публикации новой версии правил старые консолидации молча пересчитались бы по ней. Версия консолидации без действующих правил не создаётся | + +Остальные таблицы и enum — как в v5.22. Реализация — одна миграция в `arm-core`: существующим версиям проставляется номер +версии правил, действовавшей на дату `created_at` (по Москве; если такой нет — последней открытой), затем `NOT NULL`; откат +снимает только ограничение. + +## Что изменилось в v5.22 (22.10.2026; AURORA-1446) + +Из описания `comment.object_type` убраны устаревшие варианты `chess_cell` и `kfo_result_line`. +Сохранены `document | consolidation_version | …`; сейчас используется привязка к документу. Комментарий к правке пары ВГО хранится +в `correction.comment`, к строкам КФО — в `kfo_result_line.comment` и `client_kfo_line.comment`. +Структура БД не меняется, миграция не требуется. Предыдущие версии модели сохранены. + +## Что изменилось в v5.21 (решение владельца 22.10.2026; RSHB/arm-ka-docs#149, AURORA-1442) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `audit_action` + `FILE_REPROCESS` | Повторная обработка файла пользователем (`context/05-domain-rules.md` §9.5 п. 13) — отдельное событие журнала, раздел `PRIMARY_DOCUMENTS`. Файл не загружается заново: создаётся новое `recognition_job` с `attempt + 1` и становится `current_recognition_job_id`. `FILE_UPLOAD` отвергнут — это приём нового файла, и журнал показал бы вторую загрузку, которой не было; `FILE_REPLACE` — автозамена одного файла другим. Событие пишется на файл (`object_type = stored_file`): `before_value` — прежнее задание (`job_id`, `attempt`, `status`, `error_code`), `after_value` — новое (`job_id`, `attempt`); повторная обработка пакета версии — по событию на каждый отправленный файл, с `consolidation_id` в `after_value` | + +Остальные таблицы и enum — как в v5.20. Реализация — одна миграция в `arm-core`: только добавляет значение enum; откат при +наличии событий `FILE_REPROCESS` останавливается с ошибкой (журнал append-only). + +## Что изменилось в v5.20 (решения владельца 21.10.2026; RSHB/arm-ka-docs#147, OQ-161, AURORA-1442) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `document`: `is_modified` (not null, default `false`), `original_values` (jsonb), `modified_by` (FK → `app_user`), `modified_at` | Аналитик уточняет **период и счёт документа ОСВ**, если они распознаны неверно (`context/05-domain-rules.md` §9.5 п. 12; OQ-161, вариант 2). Новые значения пишутся **на месте** — в `period_year`, `period_quarter`, `is_cumulative`, `osv_account_code` той же строки `document`; при первой правке прежние значения этих четырёх полей копируются в `original_values` и дальше не меняются; `is_modified` — текущие отличаются от исходных. Так же устроена правка сумм `osv_entry` (`original_values`) — первичка не мутируется: файл, JSON результата в S3 и исходные значения остаются как пришли. Копия `document` (второй документ с другим `file_ordinal`) отвергнута: `file_ordinal` — номер документа в `documents[]` результата, а не номер редакции; копия задвоила бы строки ОСВ, подтверждение и привязки. Строки `osv_entry` при смене счёта не меняются (у них свои коды субсчетов) | + +Остальные таблицы и enum — как в v5.19. Реализация — одна миграция в `arm-core`: только добавляет колонки (`is_modified = +false`, остальные `NULL`); откат на данных с `is_modified = true` останавливается с ошибкой (потерялись бы исходные значения). + +## Что изменилось в v5.19 (решения владельца 17.10.2026, Р-1…Р-19; RSHB/arm-ka-docs#116) + +Change `openspec/changes/archive/2026-10-19-unified-statement-normalization`: формы ББ/ОФР нормализуются общим кодом сервисов, контракт +результата v2.0; ядро хранит результат как пришёл (`context/05-domain-rules.md` §11, §9.3, §9.5). + +| № | Изменение | Решение | +|---|---|---| +| 1 | `financial_statement_line`: снята уникальность `(meta_id, line_code)` (индекс остаётся неуникальным); новые `row_index`, `source_ref` (частичный уникальный `(meta_id, source_ref) WHERE source_ref IS NOT NULL`), `line_status` (enum `statement_line_status`: `ok` / `unreadable` / `missing` / `detail`), `line_code_text`, `parent_row_index`, `needs_review`, `is_confirmed` | Повтор кода — законный случай (один код, разные наименования; Р-4): хранятся все строки, чтения «по коду» идут через агрегат формы по коду (сумма строк `ok`, не хранится — DECISION-5). Строки без кода хранятся со статусом (Р-8, отменяет OQ-36 / OQ-141): `detail` — расшифровка под строкой с кодом, в §7, агрегаты, КФО и ЕХ не входит; `unreadable` / `missing` блокируют подтверждение, пока у них есть суммы (Р-13). Порядок и положение в источнике (`row_index`, `source_ref`) нужны экрану, чтобы показать строку и предупреждение у места. **Признак «строка создана правкой аналитика» — `row_index IS NULL`** (прежний признак `line_code_original_value IS NULL` не годится: у строк `unreadable` / `missing` / `detail` источника кода тоже нет); `line_code_original_value` — код из результата как пришёл, правками не меняется | +| 2 | `financial_statement_value`: `source_text`, `needs_review`, `is_confirmed`, `confirmed_by`, `confirmed_at` | `needs_review` **хранится** (пересматривает п. 4 v5.13): его считает сервис («подтверждение проверкой» §7 — Р-5, Р-7), ядро пересчитывает только после правок; подтверждение ячейки без правки аналитиком переживает пересчёт. Сырой текст источника — подсказка ячейки | +| 3 | `financial_statement_column`: `period_kind`, `label_confidence` | Относительные подписи бланка 66н («На отчётную дату», «На 31 декабря предыдущего года») — дата колонки от эффективной даты документа, ядро досчитывает после правки даты отчёта (Р-6); колонка без даты остаётся с `period_kind = none` | +| 4 | `financial_statement_meta`: `form_variant` (enum `statement_form_variant`), `needs_review_fields`, `header_source` (jsonb), `organization_name_original_value`, `organization_name_modified` | Упрощённая форма — свой набор соотношений §7 (`NULL` — документ до v5.19 или результат 1.x, набор полный); поля шапки, требующие проверки (сейчас — неуверенная дата отчёта); сырой текст и уверенность всех полей шапки результата (`<поле>_text`, `<поле>_confidence`) — подсказка у поля на экране; наименование организации правится, как ИНН | +| 5 | Новая таблица `document_issue` + enum `document_issue_kind`, `issue_severity`, `issue_source` | Предупреждения и нарушения §7 документа с уровнем и привязкой к строкам (`line_ids`), колонке (`column_id`, а вне колонок формы — `column_index`), полю шапки; у нарушения — `codes` (коды цели и членов); как пришли от сервиса или посчитаны ядром. Очищается при перезагрузке файла; при пересчёте после правки `relation` / `soft` заменяются целиком, `warning` сервиса сохраняются, кроме снимаемых правкой. **Единственный источник нарушений** для экрана и подтверждения: для результатов 1.x ядро один раз при приёме считает §7 копией `arm_statements` и пишет записи с `source = core`; формы до миграции не пересчитываются (база очищается, Р-24). Ведётся и у документов ОСВ — предупреждения листа `OSV_*` результата 2.0 | +| 6 | `recognition_job.statements_version` | Коммит копии общего кода `arm_statements`, которым посчитан результат (`service.statements_version`); расхождение с копией ядра при пересчёте — `info` | +| 7 | `document`: уникальность `(stored_file_id, document_type, osv_account_code)` — частичная, кроме `BALANCE` / `INCOME`; новая колонка `file_ordinal` + уникальный `(stored_file_id, file_ordinal)` | Вторая форма того же типа в файле хранится (Р-10, Р-15); при двух формах одного типа и периода ни одна не автоподтверждается, подтверждение второй при подтверждённой первой — 409 `DUPLICATE_FORM` (Р-18). ОСВ по счёту, расшифровка и прочий — по-прежнему один на тип на файл (§9.5 п. 2) | +| 8 | `related_group.crm_id` (`varchar`, nullable, unique) | ID ГСК/ГСЗ в ЕЦП CRM из форм `51-03`, `51-04`, `77-02` (AURORA-1619, arm-core#112, OQ-33); вводится вручную, импорт ЕЦП не заполняет (OQ-91); уникален, как `crm_id` ГК и ЮЛ | + +Остальные таблицы и enum — как в v5.18. Реализация — одна миграция в `arm-core`; перед выкладкой база очищается от форм +ББ/ОФР (решение владельца 18.10.2026, Р-24), поэтому миграция только добавляет колонки, таблицу и значения по умолчанию и +§7 по существующим формам не считает. Заполнение (миграция проходит и на непустой базе): `row_index` — `NULL` у строк, +созданных правкой (`line_code_original_value IS NULL`), у остальных — по возрастанию `line_code` в форме; `line_status = +ok`, `needs_review` значений — **НЕ** `values_ok`, `source_text = string_value`, `period_kind` — `absolute` при +`period_date`, иначе `none`, `header_source` — из `*_original_value`, `file_ordinal` — по `(created_at, id)` в файле; +`document_issue` пуст — у форм, оставшихся в неочищенной базе, нарушения до первой правки не показываются. Откат +гарантирован на пустой БД и на данных, которые v5.18 представляет без потери первичных данных и ручного ввода (без +повторов кода, строк без кода, двух форм одного типа в файле, `crm_id` групп и правленого наименования организации); на +таких данных `downgrade` останавливается с ошибкой, а производные колонки v5.19 и `document_issue` удаляет с записью в +журнал миграции. + +## Что изменилось в v5.18 (решения владельца 09.10.2026, RSHB/arm-ka-docs#90) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `stored_file.consolidation_id` (`uuid`, nullable, FK → `consolidation`) + индекс `(legal_entity_id, consolidation_id)` | Файл загружается **в консолидацию** (период ГК) и становится основой её «рабочего стола» (полный состав стола — `context/05-domain-rules.md` §9.5 п. 11), общего для всех версий периода; `NULL` — загрузка без контекста (страница ЮЛ, монозаёмщики, API). Панель «Первичные документы» в версии показывает рабочий стол консоли для ЮЛ плюс файлы с документами, привязанными к версиям консоли. Что использует конкретная версия — по-прежнему `consolidation_document`; заморозка после `VERIFIED` — привязки и `ex_snapshot`. Привязка файла к **версии** (`consolidation_file`) отвергнута: рабочие версии периода делят первичку, а новая версия после `VERIFIED` подхватывает подтверждённое (`openspec/changes/console-files/design.md`) | +| 2 | Note `recognition_job.reply_deadline` | Без изменения структуры: срок ответа OCR — 20 мин (OQ-159) | + +Остальные таблицы и enum — как в v5.17. Реализация — миграция `0006` в `arm-core` (backfill `consolidation_id` по `consolidation_document`). + +## Что изменилось в v5.17 (решение владельца 07.10.2026, пересмотр OQ-31, RSHB/arm-ka-docs#53) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `legal_entity.kpp` и `legal_entity_history.kpp` (`varchar`, nullable) | КПП юридического лица нужен (макет: колонка «КПП» на экранах `50`, `61`, поле форм `51-02`, `79`; фронтенд: «в ручке получения ЮЛ не приходит КПП»). Формат — 9 символов: 4 цифры, 2 символа (цифра или латинская A–Z), 3 цифры, иначе `INVALID_KPP`. У ИП (`entity_type = ИП`) КПП запрещён. Для ЮЛ необязателен: существующие записи и импорт без колонки остаются валидными. Уникален по-прежнему только `inn` (КПП — атрибут; сопоставление контрагентов ОСВ по ИНН не меняется). Изменение КПП — обычная правка со снимком в `legal_entity_history`. Отменяет п. 2 v5.14 («КПП не вводится») | + +Остальные таблицы и enum — как в v5.16. Реализация — миграция `0005` в `arm-core`. + +## Что изменилось в v5.16 (Срез 4, решения владельца 06.10.2026) + +| № | Изменение | Решение | +|---|---|---| +| 1 | Enum `counterparty_match_kind` (`INN`, `NAME`, `ALIAS`, `MANUAL`, `PROPAGATED`) и колонка `osv_entry.counterparty_match_kind` (nullable) | Способ сопоставления контрагента строки ОСВ хранится. `NULL` ⇔ `counterparty_id` `NULL`; `INN` / `NAME` / `ALIAS` — шаг автоматического резолва при маппинге (ИНН; точное имя; нормализованное имя ЮЛ или алиас); `MANUAL` — ручное сопоставление аналитиком; `PROPAGATED` — автоприменение по имени в версии (DOC-221278 п. 12–13). Вычислять на чтение нельзя: после ручного связывания появляется алиас, и ручное сопоставление неотличимо от `ALIAS`. Миграция `0004` заполняет колонку по уже смапленным строкам (ИНН без пробелов = `legal_entity.inn` → `INN`; `trim(name) = legal_entity.name` → `NAME`; остальные с `counterparty_id` → `ALIAS` — приближение: ручных сопоставлений до Среза 4 не было) | +| 2 | Семантика `consolidation_document` | Без изменения структуры: строка создаётся при подтверждении документа (по документам ЮЛ — членов периметра версий «в работе») и при добавлении ЮЛ в периметр (подтверждённые подходящие документы подхватываются); удаляется при замене/удалении файла и исключении ЮЛ из периметра. Для одной (ЮЛ, `document_type`, `osv_account_code`, период, признак нарастающего) привязан один документ — самый поздний `confirmed_at` (D-5 Среза 4) | +| 3 | Семантика `consolidation_lock` | Без изменения структуры: блокировка без истечения по времени — активна, пока её не снял владелец, администратор или не перехватил другой пользователь с `CONSOLIDATION_EDIT` по причине (OQ-119) | + +Пробелы квартальной склейки и сама сводная ОСВ по-прежнему не хранятся (DECISION-5). Остальные таблицы и enum — как в v5.15 (43 таблицы; enum — 28). Реализация — миграция `0004` в `arm-core`. + +## Что изменилось в v5.15 (Срез 2c, решения владельца 05.10.2026) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `osv_data_type` + `NU` (после `BU`) | Строки налогового учёта из ОСВ «БУ, НУ» хранятся в `osv_entry` как есть (OQ-45); контракт результата допускал `NU` с v1.0, ядро раньше такой строки принять не могло | +| 2 | `app_user.email` — в нижнем регистре | Без изменения структуры: нормализация при записи (seed, администрирование), поиск по `lower(email)` сохраняется, уникальность — существующий UNIQUE по `email` (OQ-59). Expression-индекс `lower(email)` не вводится: сверка ORM↔DBML не умеет expression-индексы. Миграция `0003` приводит существующие email к нижнему регистру (при дублях по регистру — падает, слияние решает человек) | +| 3 | Семантика `stored_file.status`, `recognition_job.group_schema` | Без изменения структуры: `PROCESSING` ставит inbox по ack сервиса, `PROCESSED`/`ERROR` — цикл маппинга результата (OQ-67); маппинг выполняется отдельно от inbox (OQ-69); `group_schema` заполняет маппер; «Итого» в `osv_entry` не хранится (OQ-01) | +| 4 | Индексы `osv_entry (parent_entry_id)`, `osv_entry (recognition_job_id)`, `stored_file (current_recognition_job_id)` | Индексы для удаления файла и обхода дерева строк: без них проверки FK (в том числе self-FK `parent_entry_id`) при `DELETE /files/{id}` и очистке файла перед повторным маппингом шли seq scan'ом. Замер: DELETE документа 1000 строк 8,6 с → 16 мс на 200 тыс. строк. Обычные (не unique) индексы; закрывает OQ-117 (I-1 финального ревью Среза 2c) | + +Остальные таблицы и enum — как в v5.14 (43 таблицы, 27 enum). Реализация — миграция `0003` в `arm-core`. + +## Что изменилось в v5.14 (Срез 1, решения владельца 04.10.2026) + +| № | Изменение | Решение | +|---|---|---| +| 1 | `related_group_member.kind` + индекс `(legal_entity_id, kind) WHERE is_current` | ЮЛ состоит **одновременно в одной ГСК и одной ГСЗ** (но только в одной группе каждого вида). Отменяет ANL-030 / v5.8 («только в одной ГСК или ГСЗ»). `kind` — денормализация `related_group.kind` (равенство проверяет ядро при записи, тест на рассинхрон); индекс не выразить через join, триггер не выбран. Закрывает OQ-32 | +| 2 | КПП | **Не вводится** *(отменено в v5.17: КПП введён)*. Поле есть только в макете; у `legal_entity` его нет, импорт его не читает (закрывал OQ-31 до v5.17) | +| 3 | Семантика `company_group.crm_id`, `base_regional_office_id` | Без изменения структуры: `crm_id` = «iD ГК» выгрузки ЕЦП CRM, `base_regional_office_id` = «Базовый РФ ГК в CRM»; флаги «входит в ГСК/ГСЗ» выгрузки → членство в одной неявной ГСК/ГСЗ на ГК (правила — `05-domain-rules.md` §13) | +| 4 | `audit_action` + `LEGAL_ENTITY_CHANGE`, `USER_CHANGE` | Журнал изменений ЮЛ (создание/изменение/закрытие/перевод, алиасы) и пользователей (создание/деактивация администратором) — ТЗ 8.2; в v5.13 для них не было действия (были `GROUP_CHANGE`, `MEMBERSHIP_CHANGE`, `USER_ROLE_CHANGE`) | + +Остальные таблицы и enum — как в v5.13 (43 таблицы). Реализация — миграция `0002` в `arm-core`. + +## Что изменилось в v5.13 (ревью 30.09–01.10) + +| № | Замечание | Решение | +|---|---|---| +| 1 | Вместо RBAC «в коде» — роли **и** права | `permission` + `role_permission` возвращены; права только через роли, прямых назначений пользователю нет. Исключения — заведением роли | +| 2 | `osv_entry.parent_entry_id` не нужен — храним только строки с контрагентами | **Отложено.** Открыт вопрос: хранить ли строки уровня ACCOUNT для сверки «Σ контрагентов = итог счёта»; аналитика «Парсинг ОСВ» п.11 пока говорит «все строки» | +| 3 | `osv_group_schema` не особо нужна | Убрана. Схема шапки — `recognition_job.group_schema jsonb`; тип строки — `osv_entry.group_type` | +| 4 | `financial_statement_value.needs_review` непонятен | Убран: производное от `confidence` / `is_recognized`, порог может меняться | +| 5 | В `recognition_job` не нужны и `result`, и `raw_result` | Оставлен только `result_s3_object_id`. Сырой ответ ИС Сканирование — S3 `raw/{job_id}.json` по конвенции, в БД не регистрируется | +| 6 | `stored_file.fields_modified` — что это | Счётчик «исправлено полей» с карточки документа (v5.7). Убран — `COUNT(is_modified)` по данным | +| 6-бис | `document_type` не предусматривает несколько ОСВ или ББ+ОФР в одном файле | **Разделение файл / документ**: `stored_file` — физический файл, `document` — логическая единица (1:N). Чек-лист, проверки, подтверждение, FK разобранных данных, `consolidation_document`, `ex_snapshot_item` — на `document`. Замена (`superseded_by`) — файл целиком | +| 7 | `file_type`: убрать Р7 | Убран. `xls/xlsx/xlsb → tabular-parser`, `pdf/png/jpg → scan-gateway` | + +**Принципы модели (коротко):** +- Первичка не редактируется: файлы и распознанные значения хранятся как пришли; правки аналитика — рядом (`is_modified`, `original_*`) и в аудите. +- Производные консолидации в работе (сводная ОСВ, пары ВГО, шахматка) **не хранятся** — считаются запросом. Хранятся только ручные решения (корректировки, гудвилл, подтверждение КФО клиента). +- При верификации всё замораживается в Единое хранилище (ЕХ) как самодостаточный срез, который не зависит от таблиц консолидации. +- Единственный писатель в БД — ядро `arm-core`; сервисы распознавания видят только NATS и S3. +- Права — данные (`role_permission`), ограничения по ЗО/филиалу/статусу — policy-слой поверх прав. + +--- + +## Часть 01 — Группы компаний, организации, филиалы + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `regional_office` | Филиал банка: головной офис (ГО, `is_head_office`) и региональные филиалы (РФ). Определяет зону видимости пользователя и закрепление ГК. | ← `company_group.base_regional_office_id`, `legal_entity.regional_office_id`, `app_user.regional_office_id` | ТЗ 8.1 | +| `company_group` | Группа компаний (ГК) — верхний уровень справочника клиентов. Текущее состояние (SCD4). `crm_id` = «iD ГК» из ЕЦП CRM (NULL, если создана вручную Куратором; v5.14 — семантика по выгрузке), внутренний `code`, закрепление за филиалом (`base_regional_office_id` = «Базовый РФ ГК в CRM»), флаг архива. | → `regional_office`; ← `legal_entity`, `related_group`, `consolidation` | ТЗ 6.1, Прил. 2; макет | +| `company_group_history` | Прошлые снимки ГК: каждое изменение — новая строка с `valid_from/valid_to`. | → `company_group` | ТЗ 6.10, 8.2 | +| `legal_entity` | Юридическое лицо / ИП. Натуральный ключ — ИНН; **v5.17:** `kpp` — КПП (9 символов, nullable, только у ЮЛ, не ключ). `company_group_id` = текущая ГК; **NULL = монозаёмщик**. `close_date` — закрыто/архивировано. | → `company_group`, `regional_office`; ← `related_group_member`, `stored_file`, `document`, `osv_entry.counterparty_id`, `consolidation_member` | ТЗ 6.1, Прил. 2; ЕХ 6.1 | +| `legal_entity_history` | Прошлые снимки ЮЛ + вид события (создание, изменение, переход в другую ГК, закрытие, вывод из ГК) и дата вступления в силу. | → `legal_entity` | ТЗ 8.2; макет | +| `legal_entity_alias` | Словарь написаний контрагента: как имя встречается в ОСВ и нормализованный ключ для матчинга. Пополняется при ручном сопоставлении. | → `legal_entity` | ТЗ 6.5 | +| `related_group` | ГСК или ГСЗ внутри ГК; `kind` различает. Для ГСК формируется КФО. Иерархия не поддерживается. **v5.19:** `crm_id` — ID группы в ЕЦП CRM (вручную, уникален). | → `company_group`; ← `related_group_member`, `consolidation.related_group_id` | ТЗ 6.1, Прил. 2 | +| `related_group_member` | Состав ГСК/ГСЗ — история членства (SCD2). **v5.14:** ЮЛ может одновременно входить в **одну ГСК и одну ГСЗ** (не более одной группы каждого вида): колонка `kind` = `related_group.kind` (денормализация) и частичный уникальный индекс `(legal_entity_id, kind) WHERE is_current`. Решение владельца 04.10.2026, отменяет ANL-030. | → `related_group`, `legal_entity` | ТЗ 6.1; макет [79] | + +## Часть 02 — Пользователи, роли, права + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `app_user` | Пользователь АРМ. Аутентификация внешняя (РАИСА/SSO, `external_id`). Филиал (ГО/РФ), зона ответственности (ДКБ/ДМСБ/ДМ), табельный номер. Видимость данных = права роли + филиал + ЗО. | → `regional_office`; ← `user_role`, все `*_by` поля | ТЗ 8.1, Прил. 2 | +| `role` | Роли: seed из ТЗ 8.1 (КА РФ, КА ГО, РКА, КМ, Куратор ГО, Эксперт, Администратор; `is_system`) + роли, заведённые администратором для исключений. **v5.13:** код роли в бизнес-логике не проверяется — только права. | ← `user_role`, `role_permission` | ТЗ 8.1 | +| `permission` | **v5.13.** Атомарное право «объект × действие»: `DIRECTORY/DOCUMENT/CONSOLIDATION/KFO/EX/EXPORT/USERS/AUDIT` × `VIEW/EDIT/APPROVE/VERIFY/DELETE/EXPORT/ADMIN`. Seed, привязан к коду — администратор новые коды не создаёт. Право говорит «может»; policy-слой (ЗО, филиал, статус КФО) — «эту ли и сейчас ли». | ← `role_permission` | ТЗ Прил. 2, сноска 29 | +| `role_permission` | **v5.13.** Матрица доступов как данные: `effective_permissions(user) = ∪ permissions(roles(user))`. Редактируется администратором на экране «Изменение ролей и прав доступа»; история — `audit_event(ROLE_PERMISSION_CHANGE)`. Прямых прав у пользователя нет. | → `role`, `permission` | ТЗ Прил. 2; макет | +| `user_role` | Назначение ролей пользователю (M:N). История — в аудите. | → `app_user`, `role` | ТЗ 8.1 | + +## Часть 03 — Бухгалтерские справочники и правила + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `chart_of_account` | План счетов с иерархией, тип A/P/AP, признак забалансового. Seed — полный план 1С; нестандартные субсчета клиентов добавляет администратор. | ← `osv_entry.account_id`, `elimination_rule_account`, `required_document_rule` | ТЗ 6.2/6.5, Прил. 3/4 | +| `elimination_rule` | Правило элиминации из Прил. 3 (ББ) / Прил. 4 (ОФР) как данные: пара статей, сторона, мера, режим, НДС, акцизы. Версионируется. | ← `elimination_rule_account`, `correction.rule_id`; версия фиксируется в `consolidation_version` | ТЗ 6.4, Прил. 3/4 | +| `elimination_rule_account` | Счета/субсчета каждой стороны правила, включая исключения («76 кроме 76.7.1»). | → `elimination_rule`, `chart_of_account` | ТЗ Прил. 3/4 | +| `required_document_rule` | Какие документы обязательны по подходу. **v5.13:** сверяется с `document` (тип + счёт + период), а не с файлом: выполнено = есть `document CONFIRMED` у каждого ЮЛ периметра. | → `chart_of_account` | ТЗ 5.1, 6.2, сноска 12 | + +## Часть 04 — Файлы, документы, S3, задания распознавания + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `s3_object` | Реестр всех объектов в S3: бакет-роль, ключ, размер, sha256, срок жизни, ссылка на архив в data lake. Единая точка ретеншна (5 лет по ТЗ 8.4) и дедупа. | ← `stored_file`, `recognition_job` | ТЗ 6.2, 8.4; ЕХ 8.3 | +| `stored_file` | **v5.13: только физический файл** одной загрузки: чей (ЮЛ), формат, hash, кто/когда загрузил, статус обработки (`UPLOADED → PROCESSING → PROCESSED / ERROR / SUPERSEDED`), версия при замене. **v5.18:** `consolidation_id` — консолидация, в контексте которой файл загружен (основа «рабочего стола консоли», общего для версий периода; полный состав — `context/05-domain-rules.md` §9.5 п. 11; `NULL` — без контекста). Что внутри файла — `document`. Замена — файл целиком со всеми документами. **v5.25:** `is_scan` — скан/цифровой PDF по текстовому слою при загрузке. | → `legal_entity`, `consolidation`, `s3_object`, `recognition_job`; ← `document`, `recognition_job` | ТЗ 6.2, 8.2; UC-01…05 | +| `document` | **v5.13, новая.** Логический документ внутри файла: ББ, ОФР, ОСВ по счёту (`osv_account_code`), расшифровка, прочее. Период, отчётная дата, где в файле (`locator`: лист / страницы), статус проверки (`RECOGNIZED / NEEDS_CHECK / CONFIRMED / ERROR`), результаты проверок периода и контрагента, кто подтвердил. Один файл → N документов (PDF с ББ+ОФР; xlsx с несколькими ОСВ). Создаёт ядро при маппинге результата `recognition_job`. На документ ссылаются разобранные данные, чек-лист, консолидация и ЕХ. **v5.19:** `file_ordinal` — индекс документа в `documents[]` результата (у всех документов); формы ББ/ОФР одного типа в файле могут повторяться (ОСВ по счёту, расшифровка, прочий — один на тип); предупреждения и нарушения — `document_issue`. **v5.20:** период и счёт ОСВ уточняются аналитиком на месте, распознанные — в `original_values` (`is_modified`, `modified_by/at`). | → `stored_file`, `legal_entity`, `app_user`; ← `osv_entry`, `financial_statement_meta`, `document_issue`, `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`). **v5.19:** `statements_version` — версия копии общего кода нормализации, которой посчитан результат форм. **v5.25:** сырой ответ ИС Сканирование теперь регистрируется в `s3_object` (роль `raw`) через `raw_s3_object_id` — решение v5.13 «не регистрируется» отменено. | → `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`) и акцепт КА непройденных логических проверок с комментарием. Подтверждение документа — `document.confirmed_by`. **v5.19:** `form_variant` (полная / упрощённая — набор соотношений §7), `needs_review_fields`, `header_source` (сырой текст и уверенность полей шапки), правка наименования (`organization_name_original_value`, `organization_name_modified`); результат проверок §7 и предупреждения хранятся в `document_issue` как пришли от сервиса, ядро пересчитывает их только после правок. | → `document`, `recognition_job`, `app_user`; ← `column`, `line` | ТЗ 6.2/6.3; FNR01; макет | +| `financial_statement_column` | Колонки-периоды формы с датой — нужны для межформенной проверки (1370 на начало года) и гудвилла. **v5.19:** `period_kind` (абсолютная / относительная подпись бланка / без даты), `label_confidence`. | → `meta`; ← `value` | ТЗ 6.3 п.7, 6.7 | +| `financial_statement_line` | Строка формы как в источнике: код (или его сырой текст) и наименование как распознано. **v5.19:** все строки источника — с кодом (`ok`, повторы кода тоже) и без (`unreadable`, `missing`, `detail` — расшифровка под родителем `parent_row_index`); порядок `row_index` (`NULL` ⇔ строка создана правкой аналитика), положение `source_ref`, `needs_review` неуверенного кода, подтверждение кода `is_confirmed`. Раскладка экрана — по коду «как на бумаге». | → `meta`; ← `value`, `document_issue.line_ids` | ТЗ 6.2, Прил. 2 | +| `financial_statement_value` | Число в ячейке (строка × колонка): сумма, уверенность, `is_recognized`, ручная правка с исходным значением. **v5.19:** подсветка — только `needs_review` (хранится: уверенность, `is_recognized` и §7 сведены сервисом, ядро пересчитывает после правок); `source_text` — сырой текст источника; `is_confirmed` / `confirmed_by/at` — подтверждено без правки. (v5.13 этот флаг убирала как производный.) | → `line`, `column`, `app_user` | ТЗ 6.2, Прил. 2 | +| `document_issue` | **v5.19, новая.** Предупреждение или нарушение §7 документа (у формы; у ОСВ — предупреждения листа): вид (`warning` / `relation` / `soft`), код и уровень (`error` / `info`), сообщение, привязка к строкам (`line_ids`), колонке (`column_id` или `column_index`) и полю шапки, источник (`service` / `core`); для нарушения — соотношение, `codes`, `expected`, `actual`. Экран показывает их у места. | → `document`, `financial_statement_column` | change `unified-statement-normalization` (design, шаг 8) | + +## Часть 07 — Консолидация + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `consolidation` | Логическая консолидация: **ГК** + отчётный период. ГСК фиксируется/создаётся при верификации по фактическому периметру. Уникальна по (ГК, год, квартал). **v5.18:** файлы, загруженные в консолидацию, — `stored_file.consolidation_id` (основа рабочего стола, общего для версий периода; полный состав — `context/05-domain-rules.md` §9.5 п. 11). | → `company_group`, `related_group`; ← `consolidation_version`, `stored_file` | ТЗ 6.10, 7.2 | +| `consolidation_version` | Версия консолидации: номер, статус, сценарий, подход, гудвилл, инициатор и его офис (маршрут), кто утвердил / верифицировал, решение УОБ, отклонения от Методики, версия правил элиминации, ссылка на предыдущую. После VERIFIED — read-only. | → `consolidation`, `app_user`, `regional_office`, self; ← всё в частях 07–08 | ТЗ 5.1, 6.4, 6.10; макет | +| `consolidation_status_history` | Каждый переход статуса: из → в, кто, когда, комментарий (обязателен при возврате). | → `consolidation_version`, `app_user` | ТЗ 6.10, 8.2 | +| `consolidation_lock` | «Взять в работу»: кто редактирует версию, перехват с причиной (v5.16: без истечения по времени, OQ-119). Из макета — ANL-036. | → `consolidation_version`, `app_user` | макет | +| `consolidation_member` | Периметр версии — зафиксированный список ЮЛ; `is_head_entity` для гудвилла. | → `consolidation_version`, `legal_entity` | ТЗ 6.1, 6.10 | +| `consolidation_document` | **v5.13:** какие **документы** (не файлы) участвуют в версии. Чек-лист = `required_document_rule` × периметр − `consolidation_document`. Переиспользование одного документа в нескольких версиях/периодах, монозаёмщики из ЕХ. | → `consolidation_version`, `document` | ТЗ Прил. 2; ЕХ 7 | + +*Не хранятся (DECISION-5), считаются запросом по версии:* сводная ОСВ, признак контрагента ГСК/ГК/Внешний, пары ВГО, шахматка, результирующая таблица ВГО. + +## Часть 08 — Корректировки, гудвилл, результат КФО + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `correction` | Ручная корректировка ВГО: пара (правило + компания 1 + компания 2 — естественный ключ), ББ/ОФР, строки и суммы по Дт и Кт, дочерние строки для распределения, причина, обязательный комментарий, статус, номер версии (история — в аудите). КФО = свод − Σ корректировок. | → `consolidation_version`, `elimination_rule`, `legal_entity` ×2, `chart_of_account` ×2, self | ТЗ 6.4, 6.8, Прил. 9 | +| `vgo_assignment` | Ручное решение аналитика по строке сводной ОСВ сверх автоматического разнесения по правилам ВГО: привязка к правилу / снятие привязки, «не учитывать в КФО» у нераспределённой строки; для ОФР — по стороне позиции (`side` D/K, v5.27). Разнесение само по себе не хранится — только это решение поверх него. | → `consolidation_version`, `osv_entry` (ON DELETE CASCADE), `elimination_rule`, `app_user` ×2 | DOC-272613 §5, AURORA-1751 | +| `goodwill` | Строка расчёта гудвилла: головная × дочерняя, доля владения, ФВ по 58.01, УК (1310) и добавочный капитал (1350 — вопрос методологам), гудвилл, «включить в КФО», комментарий. | → `consolidation_version`, `legal_entity` ×2, `app_user` | ТЗ 6.7, Прил. 3 п.2, Прил. 8 | +| `kfo_result` | Шапка результата КФО (тыс. руб.): когда посчитан, заморожен ли, ЧА и ВБ, выручка, прибыль, расхождения, пороги, обоснование, итог гудвилла, предупреждения расчёта, сводка корректировок. Одна запись на версию; пока не заморожена — не хранит расчётные суммы (только ручные поля), таблица считается при каждом чтении; полный результат пишется при «Верифицирован» (§8 «КФО» п. 12). | → `consolidation_version` (1:1); ← `kfo_result_line` | ТЗ 6.4, 6.8, Прил. 10 | +| `kfo_result_line` | До заморозки — только строка с комментарием (суммы пусты, `consolidated_amount = 0`). Строка консолидированного ББ/ОФР по коду бланка редакции на отчётную дату (66н / ФСБУ 4/2023) и служебная `GOODWILL`: Σ по данным клиента − в т. ч. взаимные (авто или корректировка — не обе по одному агрегату) + гудвилл = КФО банка; расхождение, комментарий. | → `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` | Полиморфный комментарий к объекту (`document | consolidation_version | …`); сейчас используется для документов. Правка и удаление не предусмотрены; аудит не ведётся. | → `app_user`; `object_type + object_id` без FK | ТЗ 6.3; AURORA-1446; макет | +| `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 | + +## Часть 10 — Метрики качества первички + +| Сущность | Что это | Ключевые связи | Откуда | +|---|---|---|---| +| `quality_intake_fact` | Документ «как распознано»: снимок на момент приёма результата (до авто/ручного подтверждения) — счётчики строк/значений/предупреждений, `payload` со строками и значениями. **Для ОСВ — без построчного снимка**: только счётчики и предупреждения `OSV_*` (правок сумм ОСВ в ядре нет). Пишется один раз на документ BALANCE/INCOME/OSV, в транзакции маппинга, до `auto_confirm`; `DISCLOSURE`/`OTHER` факт не получают. Append-only, без FK — переживает удаление/перераспознавание документа. | `document_id`/`file_id`/`job_id`/`legal_entity_id`/`group_id` — без FK, по значению | AURORA-1759; arm-core#190 | +| `quality_outcome_fact` | Исход документа: автоподтверждён / подтверждён / удалён / заменён (в т. ч. после подтверждения — отдельные значения), с атрибуцией правок по источнику (сервис / наш разбор) и причиной попадания в ячейку матрицы (`primary_cause`). **Не один факт на документ**: повторное подтверждение ОСВ и исход после подтверждения пишут новый факт — метрики берут последний по `outcome_at`. | `document_id` — связь с `quality_intake_fact` по значению, без FK | AURORA-1759; arm-core#190 | +| `quality_failure_fact` | Технический отказ задания распознавания (ошибка сервиса, таймаут, DLQ) или неповторяемая ошибка маппинга. Считается по файлам, не по документам (у отказавшего файла документов нет). Плоская таблица, без `payload`. | `job_id`/`file_id`/`legal_entity_id`/`group_id` — без FK, по значению | AURORA-1759; arm-core#190 | + +--- + +## Как читать модель по сценарию «сформировать КФО» + +1. **Справочники** (01–03): ГК → ЮЛ → ГСК/ГСЗ; план счетов; правила элиминации; что обязательно загрузить. +2. **Создание консолидации** (07): `consolidation` от ГК + период → `consolidation_version` → `consolidation_member` (периметр). +3. **Загрузка** (04): `stored_file` + `s3_object` → `recognition_job` (queued → NATS → сервис → результат в S3 → done) → ядро создаёт **`document`** на каждый ББ / ОФР / ОСВ по счёту, найденный в файле. +4. **Разбор** (05–06): ОСВ → `osv_entry`; ББ/ОФР → `financial_statement_meta/column/line/value`. Аналитик проверяет, правит, подтверждает **документ**; логические проверки — на лету; сопоставляет контрагентов (→ `osv_entry.counterparty_id`, `legal_entity_alias`). Чек-лист закрывается по `document CONFIRMED`. +5. **Консолидация** (вычисляется): сводная ОСВ, пары ВГО, шахматка — из `osv_entry` × периметр × правила. Аналитик вносит `correction`, заполняет `goodwill`, при сценарии подтверждения — `client_kfo_line`. Результат — `kfo_result(_line)`. +6. **Согласование** (07): `consolidation_status_history` по маршруту; блокировка `consolidation_lock`. Что можно — решает `role_permission` + policy-слой по статусу/ЗО. +7. **Верификация → ЕХ** (09): `ex_snapshot` + `member` + `item` (документы по ссылке, остальное — payload). Версия → read-only. Всё это время — `audit_event`. + +## Открытые вопросы после v5.14 + +- **Замечание 2** (`osv_entry.parent_entry_id`, какие строки ОСВ хранить) — отложено; затрагивает и аналитику «Парсинг ОСВ» п.11. +- Замена по-документно (одна ОСВ из файла с пятью) — сейчас не предусмотрена, замена файлом целиком. Если понадобится — `superseded_by` переезжает в `document`. +- Статус `document.RECOGNIZED` (ручная проверка не требуется по порогу) — подтвердить с аналитиками, нужен ли, или все документы проходят через `NEEDS_CHECK → CONFIRMED`. diff --git a/openspec/config.yaml b/openspec/config.yaml index a1e991d..53507e3 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -1,12 +1,12 @@ schema: spec-driven context: | - Репозиторий arm-ka-docs — контекст, модель данных (DBML v5.26), правила домена, контракты между сервисами + Репозиторий arm-ka-docs — контекст, модель данных (DBML v5.27), правила домена, контракты между сервисами (docs/contracts/*.schema.json), макет UI и экспорт ТЗ системы АРМ КА (ДКБ, Россельхозбанк). Здесь оформляются СКВОЗНЫЕ изменения: контракт + окружение разработки (docs/dev) + проверки качества (make check) + изменения документов контекста. Код сервисов живёт в соседних репозиториях (../arm-core, ../tabular-parser, ../scan-gateway), у каждого свой openspec/ и свой change; сквозной change ссылается на них. - Источники истины по приоритету: docs/model/data-model-v5.26.dbml → docs/model/entities-v5.26.md → + Источники истины по приоритету: docs/model/data-model-v5.27.dbml → docs/model/entities-v5.27.md → docs/context/05-domain-rules.md → docs/source/tz (черновик, имена v4). Открытые вопросы — docs/context/07-open-questions.md; план срезов — docs/context/09-delivery-plan.md; соглашения шаблона команды — docs/context/03-architecture.md. Язык документов и коммитов — русский, идентификаторы — английские, как в DBML. docs/source/** не редактируется. diff --git a/repo-templates/arm-core.CLAUDE.md b/repo-templates/arm-core.CLAUDE.md index ae4f3cd..f8897d4 100644 --- a/repo-templates/arm-core.CLAUDE.md +++ b/repo-templates/arm-core.CLAUDE.md @@ -16,7 +16,7 @@ - NATS subjects и схема ключей S3 — `../docs/context/03-architecture.md`. ## Правила репозитория -- ORM 1:1 с `../docs/model/data-model-v5.26.dbml`; `scripts/check_dbml_vs_orm.py` — обязательная проверка CI. +- ORM 1:1 с `../docs/model/data-model-v5.27.dbml`; `scripts/check_dbml_vs_orm.py` — обязательная проверка CI. - Алгоритмы — только из `../docs/context/05-domain-rules.md`; номер § — в docstring. - Вычисляемые объекты консолидации — `backend/app/domain/`, без ORM-таблиц. - Фронт: пользователь слаб во фронте — для TS/React изменений делай ревью сам и объясняй решения в PR. diff --git a/scripts/kfo_contract/gen_kfo_openapi.py b/scripts/kfo_contract/gen_kfo_openapi.py index e874b24..c712f2e 100644 --- a/scripts/kfo_contract/gen_kfo_openapi.py +++ b/scripts/kfo_contract/gen_kfo_openapi.py @@ -1,6 +1,10 @@ -"""Генерирует dev/api-contract-kfo.openapi.yaml: пути, схемы и examples из kfo_example.py (docs #206). +"""Схемы КФО и examples из kfo_example.py (docs #206) для gen_ex_payload.py (схема payload ЕХ). -uv run --with pyyaml --with jsonschema --with openapi-spec-validator python -I gen_kfo_openapi.py +ВНИМАНИЕ: dev/api-contract-kfo.openapi.yaml этим скриптом больше не генерируется — он выгружается из app.openapi() arm-core +(`python -m scripts.export_kfo_contract`, сверка — arm-core `api/tests/test_kfo_contract.py`); схемы ниже — прежний черновик +контракта (имена KfoResultOut/KfoLineOut), используются только в gen_ex_payload.py. + +Не запускать для выгрузки контракта: результат (черновик) не совпадёт с кодом. """ import copy import json