openspec: спека Среза 3b — формы ББ/ОФР в ядре и API до сводной ОСВ (#36)

proposal, design (D-1…D-14, решения без обсуждения №1–10), дельты osv-result-mapping (маппинг форм, статус),
statement-checks (новая: §7, межформенная, акцепт), documents-api (форма, правка, акцепт, подтверждение, комментарии,
исходный файл), consolidations-api (CSV чек-листа), directories-api (documents_count), users-admin (справочник имён).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-07 08:21:57 +04:00
parent b89d7d9322
commit dd454983e6
10 changed files with 660 additions and 0 deletions

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-07

View File

@ -0,0 +1,172 @@
# Design: Срез 3b — формы ББ/ОФР в ядре и API до сводной ОСВ
## Context
Срез 2c построил маппинг результата распознавания в ядре (фоновый цикл, `map_file`, автозамена, `counterparty_check`,
статусы документа) только для ОСВ. Срез 4 построил привязку подтверждённых документов к версиям консолидации и чек-лист;
обе механики уже различают `BALANCE`/`INCOME` (нарастающие, `Q1` только своей версии — решения №10, R-14a Среза 4).
Модели `financial_statement_*` и `comment` есть в ORM и миграциях (DBML v5.16), но нигде не пишутся. Срезы 2b и 3a
отдают `statementDocument` v1.2 из Excel и PDF; самопроверку §7 tabular-parser кладёт в `checks`, scan-gateway — нет
(3a D-5: проверки считает ядро для обоих источников).
## Goals / Non-Goals
Goals: формы ББ/ОФР из обоих источников в БД; API экрана проверки с правкой, проверками §7, акцептом и подтверждением;
закрыть дыры API пути пользователя до сводной ОСВ. Non-goals — `proposal.md`.
## Decisions
### D-1. Маппинг форм — ветка `map_file`, а не отдельный цикл (решение владельца, подход 1)
`domain/statement_mapping.py` — чистые функции (черновики документа, шапки, колонок, строк, значений; период; статус);
`map_file` после листов ОСВ вызывает их и вставляет строки в той же транзакции. Порядок: очистка прежних данных файла
(в т.ч. `financial_statement_*` его документов) → документы ОСВ и форм → строки → статус файла → автозамена →
`on_documents_mapped`. `_load_result` и `map_file` бросают `MappingFatal` только при пустом `documents`
(«в результате нет документов»). Отдельный цикл отвергнут: два цикла делили бы файл, его статус и автозамену.
### D-2. Документ формы, период, шапка
- `document`: `document_type` = тип формы, `osv_account_code = NULL`, `is_cumulative = NULL` (нарастающий, №10 Среза 4),
`locator` из результата, `period_check = UNKNOWN`.
- **Период**: дата = `header.report_date`, иначе самая поздняя `columns[].period_date`; `period_year` = год даты;
`period_quarter` по месяцу, если дата — последний день марта, июня, сентября или декабря; иначе квартал `NULL` и
предупреждение `<locator>: дата <дата> не конец квартала`. Без даты — год и квартал `NULL`, предупреждение.
`reporting_date` = эта дата.
- **Шапка** (`financial_statement_meta`): `statement_type` (`balance`/`income`); `inn` = `header.inn`,
`inn_original_value` = `header.inn`, `inn_confidence`, `inn_verified = (inn_confidence == 100)`; `organization_name`;
`report_date` = дата из шапки (не из колонок), `report_date_original_value = header.report_date_text`,
`report_date_verified = (report_date is not None)`; `units` = `units_original_value` = `header.units`,
`units_verified = (units is not None)`; `okud`, `okpo`, `okopf`, `okfs`, `address` — как пришли;
`*_modified = false`. Поля `*_original_value` заполняются при маппинге и больше не меняются (DBML: «как распознано»).
- **Колонки**: `column_index`, `label`, `period_date`; `is_current = true` у колонки с самой поздней `period_date`
(при равенстве — с меньшим `column_index`), у остальных `false`; без дат — у всех `NULL`.
- **Строки**: `line_code`, `line_code_original_value = line_code`, `line_code_confidence`, `line_name`,
`line_name_original_value = line_name`, `line_name_confidence`, `*_modified = false`. Повтор кода в результате (контракт
этого не запрещает) — остаётся первая строка, предупреждение (DBML: `(meta_id, line_code)` уникален).
- **Значения**: на каждую пару «строка × колонка» одна запись; из результата — `amount`, `string_value`, `confidence`,
`is_recognized`; ячейки нет в результате — `amount = NULL`, `string_value = NULL`, `confidence = NULL`,
`is_recognized = NULL`. `is_modified = false`, `original_amount = NULL` до первой правки (DBML: «значение до ручной
правки»). Значение с `column_index` вне колонок формы — пропускается с предупреждением.
- `checks` результата игнорируются: ядро считает §7 само (D-4).
### D-3. Повтор типа формы в файле, `DISCLOSURE`/`OTHER`
Индекс `document (stored_file_id, document_type, osv_account_code)` NULLS NOT DISTINCT разрешает один документ каждого
типа формы на файл. Второй `BALANCE`/`INCOME` того же файла не создаётся: предупреждение `<locator>: вторая форма
<тип> в файле пропущена (OQ-77)`, первый документ — `NEEDS_CHECK`. `DISCLOSURE` и `OTHER` — один документ на тип на файл:
`locator` — `locator` всех таких документов результата через запятую, статус `RECOGNIZED`, данных нет (OQ-23). Документ
`BALANCE`/`INCOME` без строк (форма без колонки кодов, `st03`) создаётся с шапкой и колонками, статус `NEEDS_CHECK`.
### D-4. Проверки §7 — перенос tabular-parser, словарь — копия
`domain/statement_checks.py`: `run_checks(form, columns, amounts, rules) -> ChecksReport` — дословный по семантике
перенос `tabular-parser/api/src/parser/statements/checks.py`: соотношение по каждой колонке, только если присутствует
его цель (левая часть); отсутствующий код = 0; допуск `tolerance` (1) в единицах формы; жёсткое нарушение не больше
`rounding_tolerance_percent` (0,001 %) от наибольшего по модулю числа соотношения — мягкое «в пределах округления»;
правая часть считается как записано и с `abs_terms` по модулю, выполнено, если хоть один вариант в допуске; мягкая
проверка «сумма пятизначных подстрок = родитель». Словарь — копия `tabular-parser/api/rules/statement_checks.yaml` в
`api/rules/statement_checks.yaml` с пометкой источника; тест идентичности между репозиториями не ставится (урок #33:
связка репозиториев ломала main), расхождение копий — известный риск, отмечается в `03-architecture.md`.
**Суммы** — текущие (после правок) `financial_statement_value.amount`.
### D-5. Межформенная проверка — мягкая
`1370(колонка отчётной даты ББ) − 1370(колонка 31 декабря прошлого года ББ) = 2400(колонка отчётного периода ОФР)` с
тем же допуском. Пара: другой документ формы того же ЮЛ, `period_year` и `period_quarter`, файла не `SUPERSEDED`
(подтверждённый предпочтительнее, затем самый поздний `created_at`). Считается, только если найдены пара и обе колонки;
нарушение — мягкое (в `checks.cross_form`), подтверждение не блокирует: ОФР часто приходит отдельным файлом позже.
### D-6. Статус документа формы
`RECOGNIZED`, только если одновременно: задание `done`; нет непройденных неакцептованных жёстких проверок;
`counterparty_check ≠ MISMATCH`; `period_year` и `period_quarter` определены; у каждого значения либо `is_modified`,
либо `is_recognized` не `false` и `confidence` равна 100 или `NULL`; нет предупреждений маппинга по документу; есть хотя
бы одна строка. Иначе `NEEDS_CHECK`. Статус пересчитывается при маппинге, после каждой правки и после акцепта (кроме
`CONFIRMED`).
### D-7. Правка формы — значения и три поля шапки, только до подтверждения (решения владельца, вопросы 1 и 2)
`PATCH /documents/{id}/statement`: `header.inn` (10 или 12 цифр, иначе 422 `INVALID_INN`; пересчёт
`counterparty_check`), `header.report_date` (дата; пересчёт `period_year`, `period_quarter`, `reporting_date` по D-2),
`header.units` (непустая строка ≤ 50); `values[] {line_id, column_id, amount}` (Decimal, 2 знака; `null` допустим).
Значение: если `original_amount` пуст и значение ещё не правилось — `original_amount` = прежняя сумма; `amount` =
новая; `is_modified = (amount != original_amount)`; `modified_by/at`. Поле шапки: `*_modified = true`, исходник в
`*_original_value` не меняется. Разрешено только для `RECOGNIZED`/`NEEDS_CHECK` незаменённого файла: `CONFIRMED` —
409 `DOCUMENT_READONLY`, файл `SUPERSEDED` — 409 `FILE_SUPERSEDED`, документ не ББ/ОФР — 409 `DOCUMENT_NOT_STATEMENT`;
`line_id`/`column_id` не этого документа — 422 `UNKNOWN_CELL`. После правки: сброс акцепта, пересчёт проверок и
статуса; одно `audit_event(RECOGNIZED_DATA_EDIT, RECOGNITION)` на запрос с `before/after` изменённых полей и ячеек.
Пустой запрос (ничего не меняет) — 200 без аудита. Блокировки — порядок подтверждения: файл `FOR UPDATE` → документ
`FOR NO KEY UPDATE`.
### D-8. Акцепт проверок
`POST /documents/{id}/statement/checks/accept {comment}` (`DOCUMENT_EDIT`; комментарий 1–2000 символов): разрешён при
непройденных жёстких проверках, иначе 409 `NOTHING_TO_ACCEPT`; те же 409, что у правки. Пишет `checks_accepted_by/at/
comment`, пересчитывает статус, `audit_event(RECOGNIZED_DATA_EDIT, RECOGNITION)` с `before/after` полей акцепта.
Отдельного `audit_action` нет; новое значение enum потребовало бы DBML v5.17 без пользы.
### D-9. Подтверждение формы
`POST /documents/{id}/confirm` для `BALANCE`/`INCOME` дополнительно: непройденные неакцептованные жёсткие проверки —
409 `CHECKS_NOT_PASSED` (в `details` — список нарушений); `period_year` или `period_quarter` пуст — 409
`PERIOD_REQUIRED`. Порядок проверок: `FILE_SUPERSEDED` → `ALREADY_CONFIRMED` → `DOCUMENT_NOT_CONFIRMABLE` →
`PERIOD_REQUIRED` → `CHECKS_NOT_PASSED`. Пишет `financial_statement_meta.reviewed_by/at`. Несовпадение контрагента и
низкая уверенность подтверждение не блокируют (как у ОСВ). `DISCLOSURE`/`OTHER` — без условий.
### D-10. Отдача исходного файла
`GET /files/{id}/content?disposition=attachment|inline` (`DOCUMENT_VIEW`, policy по ЮЛ): поток объекта `uploads/…`
файла из S3 частями (без чтения целиком в память); `Content-Type` по `file_type` (`application/pdf`, `image/png`,
`image/jpeg`, xls/xlsx/xlsb — свои MIME); `Content-Disposition` — ASCII-имя `file.<ext>` и `filename*` RFC 5987 с
исходным именем (как экспорт сводной ОСВ); работает и для `SUPERSEDED`; объект истёк или пропал — 404
`FILE_CONTENT_NOT_FOUND`. Аудит не пишется (как экспорт xlsx). Прямая ссылка на S3 отвергнута: в контуре фронт может
не видеть MinIO, а через ядро действует policy.
### D-11. Комментарии к документу
`comment(object_type = 'document', object_id, text, author_id, created_at)`. `POST /documents/{id}/comments {text}`
(`DOCUMENT_EDIT`, 1–4000 символов после обрезки пробелов) и `GET /documents/{id}/comments` (`DOCUMENT_VIEW`, по
`created_at`, без пагинации — комментариев к документу единицы) — ответ `{id, text, author: {id, full_name},
created_at}`. Правки и удаления нет. Комментировать можно документ в любом статусе и файла `SUPERSEDED`. В карточке
документа (`DocumentBrief` и `DocumentOut`) — `comments_count`. Аудит не пишется: `comment` сам хранит автора и время.
### D-12. Справочник имён пользователей
`GET /users/brief?ids=<uuid>,<uuid>…` — любой аутентифицированный пользователь; ≤ 200 id (иначе 422
`TOO_MANY_IDS`); ответ `[{id, full_name, is_active}]` по найденным id, включая неактивных; неизвестные id не
возвращаются. Админский `GET /admin/users` не меняется.
### D-13. Число документов у ЮЛ и CSV чек-листа
`LegalEntityOut.documents_count` — число документов файлов ЮЛ со статусом файла не `SUPERSEDED`, подзапросом на
страницу списка и в карточке. `GET …/versions/{id}/checklist?format=csv` — `text/csv; charset=utf-8` с BOM,
разделитель `;`, строка заголовка «Организация;ИНН;<правило>…» (правило — «Бух. баланс», «ОФР», «ОСВ <счёт>»), ячейки —
подписи макета: `CONFIRMED` «Загружен», `PROCESSING` «Обработка», `NEEDS_CHECK`/`RECOGNIZED` «Требуется проверка»,
`ERROR` «Ошибка загрузки», `MISSING` «Отсутствует» (+ « (нет: Q1, Q2)» при `missing_quarters`); имя файла
`checklist-<ГК>-<год>-<квартал>.csv` в RFC 5987. Аудит не пишется.
### D-14. Редакция формы в ответе
`GET …/statement` отдаёт `edition`: `66n`, если дата периода документа (D-2) ≤ 2024-12-31, `fsbu4` — если позже,
`null` без даты. Фронту — для выбора шаблона строк; на проверки §7 в 3b не влияет (OQ-146).
## Risks / Trade-offs
- OCR-формы почти всегда `NEEDS_CHECK` (уверенность < 100, мусорный ИНН) — ожидаемо, аналитик правит.
- Копия словаря §7 может разойтись с tabular-parser; документировано.
- Правка даты отчёта может дать документу период, совпадающий с другим файлом ЮЛ; автозамена при правке не
запускается (только при маппинге) — в чек-листе привязывается подтверждённый позже.
- Межформенная проверка мягкая — расхождение 1370/2400 не блокирует подтверждение.
## Migration Plan
Миграции нет. Уже смапленные файлы с формами (до 3b формы игнорировались) форм не получают: повторный маппинг —
повторная загрузка файла пользователем (автозамена) или ручной сброс файла в `PROCESSING`; в dev-стенде — `make
seed-dev` с нуля.
## Open Questions
1. OQ-146: формулы §7 и справочники по редакциям `66n` / `fsbu4`.
2. OQ-152 / I-2 из 3a: повтор кода 2410 в OCR — ядро получает уже отфильтрованную строку.
3. Нужен ли аудит скачивания первички (ТЗ 8.2 «выгрузка») — сейчас нет, как у экспорта.
## Decisions made without discussion
| № | Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|---|
| 1 | 2026-10-07 | Период формы без даты отчёта — по самой поздней дате колонок; квартал только по концу квартала | дата отчёта в OCR часто мусор (`aep_6`), колонки надёжнее; иначе привязка к неверной версии | неверный период при перепутанных колонках — аналитик правит дату |
| 2 | 2026-10-07 | `*_original_value` шапки заполняются при маппинге, `original_amount` — при первой правке | так в DBML: «как распознано» у шапки и «до ручной правки» у значения | — |
| 3 | 2026-10-07 | Значение на каждую ячейку «строка × колонка», отсутствующие — `NULL` | у любой ячейки есть `id` для правки; фронту не нужно создавать значения | лишние строки в БД (единицы процентов) |
| 4 | 2026-10-07 | Межформенная проверка мягкая | ОФР часто приходит позже; жёсткая блокировала бы подтверждение ББ | расхождение 1370/2400 не останавливает аналитика |
| 5 | 2026-10-07 | Акцепт сбрасывается любой правкой | данные изменились — проверку нужно смотреть заново | лишний повторный акцепт |
| 6 | 2026-10-07 | Аудит акцепта — `RECOGNIZED_DATA_EDIT` | нового `audit_action` без DBML v5.17 нет | фильтр журнала по акцепту — по `object_type`/полям |
| 7 | 2026-10-07 | Контрагент и уверенность не блокируют подтверждение | паритет с ОСВ; шум OCR в ИНН | подтверждение чужой формы — защищает статус `NEEDS_CHECK` и подсветка |
| 8 | 2026-10-07 | Скачивание файла, CSV, комментарии — без аудита | паритет с экспортом xlsx; `comment` хранит автора | журнал не покажет скачивания (OQ в docs) |
| 9 | 2026-10-07 | `GET /users/brief` без отдельного права | имена нужны всем экранам; данные — только ФИО и активность | ФИО видны всем аутентифицированным |
| 10 | 2026-10-07 | Нет теста идентичности словаря §7 с tabular-parser | урок #33: межрепозиторные тесты ломают main | копии разойдутся незаметно |

View File

@ -0,0 +1,73 @@
# Proposal: Срез 3b — формы ББ/ОФР в ядре и API до сводной ОСВ
**Срез:** 3b — вторая половина Среза 3 «ББ/ОФР: scan-gateway, маппинг форм, логические проверки, подтверждение
документа» (`../docs/context/09-delivery-plan.md`). **Репозиторий:** arm-core. **Issue:** RSHB/arm-core#36.
**Соседние:** Срез 3a (scan-gateway, `statementDocument` из OCR, закрыт 07.10.2026), Срез 2b (tabular-parser, формы из
Excel, контракт v1.2). Документация — bounded-PR в docs (§7, §9.5, OQ, `03-architecture.md`, `09-delivery-plan.md`), без
сквозного change: контракт и DBML не меняются. Объём по решению владельца 07.10.2026: **весь API пути пользователя до
сводной ОСВ включительно**; UI делает другой разработчик по этим спекам.
## Why
Оба сервиса распознавания отдают формы ББ/ОФР (`statementDocument` контракта v1.2), а ядро их игнорирует: документы
`BALANCE`/`INCOME` не создаются, `financial_statement_*` пусты, а файл, где есть только формы (выгрузка ББ/ОФР из 1С,
скан PDF), падает в `ERROR` с «в результате нет документов ОСВ». Поэтому столбцы «Бух. баланс» и «ОФР» чек-листа никогда
не закрываются, экрана «Проверка распознавания» нечем кормить, а консолидация не может стартовать по правилам
`required_document_rule`. Сверка пользовательского пути (`10-user-journey.md`, пути 1–2 и шаг 3.1) с API нашла и
мелкие дыры: нет отдачи исходного файла, комментариев к документу, имён пользователей для не-админов, числа документов
у ЮЛ и CSV чек-листа.
## What Changes
- **Маппинг форм** (`osv-result-mapping`): в той же транзакции `map_file` после листов ОСВ каждый `statementDocument`
`BALANCE`/`INCOME` становится `document` + `financial_statement_meta` + `financial_statement_column` +
`financial_statement_line` + `financial_statement_value` (значение на каждую пару «строка × колонка»); период — по дате
отчёта, без неё — по самой поздней дате колонок; `counterparty_check` — как у ОСВ; `DISCLOSURE`/`OTHER` — один документ
на тип на файл без данных. Файл без ОСВ, но с формами — не ошибка; ошибка — только пустой результат.
- **Проверки §7** (новая спека `statement-checks`): чистые функции `domain/statement_checks.py` — перенос проверок
tabular-parser (допуск ±1, округление 0,001 %, `abs_terms`, подстроки) со словарём `api/rules/statement_checks.yaml`;
межформенная `1370 − 1370(начало года) = 2400` — мягкая. Результат не хранится; акцепт непройденных жёстких проверок
с комментарием — `checks_accepted_*`.
- **API формы** (`documents-api`): `GET /documents/{id}/statement` (шапка с исходниками и признаками правки, редакция
формы, колонки, строки со значениями, проверки), `PATCH /documents/{id}/statement` (значения и ИНН / дата отчёта /
единицы; исходники сохраняются, `audit_event(RECOGNIZED_DATA_EDIT)`, пересчёт проверок и статуса, сброс акцепта),
`POST /documents/{id}/statement/checks/accept`. Подтверждённая форма — только для чтения (решение владельца).
- **Подтверждение ББ/ОФР**: дополнительно требует пройденных или акцептованных жёстких проверок и определённого периода;
пишет `financial_statement_meta.reviewed_by/at`. Привязка к версиям и переходы статуса версии — без изменений.
- **Мелкие дыры**: `GET /files/{id}/content` (поток исходного файла из S3 через ядро), комментарии
`POST/GET /documents/{id}/comments` (таблица `comment`) и `comments_count` в карточке документа,
`GET /users/brief?ids=` для любого аутентифицированного пользователя, `documents_count` в списке ЮЛ,
`GET …/checklist?format=csv`.
## Таблицы DBML v5.16
Пишутся: `document` (типы `BALANCE`, `INCOME`, `DISCLOSURE`, `OTHER`), `financial_statement_meta`,
`financial_statement_column`, `financial_statement_line`, `financial_statement_value`, `comment`, `audit_event`.
Читаются: `stored_file`, `s3_object`, `recognition_job`, `legal_entity`, `legal_entity_alias`, `app_user`,
`consolidation_*`, `required_document_rule`. Новых таблиц, колонок и значений enum нет; миграции нет.
## Параграфы `05-domain-rules.md`
§7 (логические проверки, акцепт), §9.3 (статус документа), §9.5 (маппинг, подтверждение), §11 и §11.1 (форма из OCR),
§12 (форма из Excel; п. 5 — повтор кода, п. 7 — самопроверка), §2 и §3 п. 1 (период и привязка к версии). Правила,
которых в `05` нет и которые вводит этот change (будут дописаны в §7 и §9.5 bounded-PR docs): период ББ/ОФР по самой
поздней дате колонок при пустой дате отчёта; межформенная проверка мягкая; правка формы только до подтверждения;
условия подтверждения ББ/ОФР.
## Допущения по открытым вопросам
- ASSUMPTION(OQ-12, OQ-146): формулы §7 одни для редакций `66n` и `fsbu4`; варианты ФСБУ 4/2023 для 2400 — мягкие.
- ASSUMPTION(OQ-23): `DISCLOSURE` только хранится — документ без данных.
- ASSUMPTION(OQ-36, OQ-141, OQ-149): строки без кода не хранятся (их нет в результате).
- ASSUMPTION(OQ-77): вторая форма того же типа в файле не создаётся; первая — `NEEDS_CHECK` с предупреждением.
- ASSUMPTION(OQ-140): страницы формы из разных файлов не склеиваются.
- ASSUMPTION(OQ-145): редакцию формы ядро определяет само по дате отчёта; контракт не меняется.
## Non-goals
- UI (экраны делает другой разработчик).
- Правка строк ОСВ, добавление и удаление строк формы, правка кода и названия строки.
- Отмена подтверждения документа (исправление — новым файлом через автозамену).
- Шахматка, корректировки, КФО, ЕХ, журнал действий, «Добавить версию» (Срезы 5–7).
- Изменение контракта и DBML; хранение результатов проверок; разбор расшифровок.
- Заполнение редакции `fsbu4` и словарей по редакциям (OQ-146).

View File

@ -0,0 +1,35 @@
# Spec Delta: consolidations-api
## MODIFIED Requirements
### Requirement: Чек-лист комплекта
`GET /consolidations/versions/{id}/checklist` (`CONSOLIDATION_VIEW`, policy) SHALL вернуть `rules[]` (правила
`required_document_rule` подхода версии: `document_type`, `account`, `is_mandatory`; порядок — `BALANCE`, `INCOME`,
затем ОСВ по коду счёта) и `rows[]` по ЮЛ периметра (по имени) с `cells[]` в порядке `rules`. Ячейка `{status,
missing_quarters, document_ids}`: `CONFIRMED` — пара (ЮЛ, правило) полностью покрыта привязанными документами (ОСВ: по
правилу выбора источников `consolidated-osv`; ББ/ОФР: документ за квартал версии); `NEEDS_CHECK` / `RECOGNIZED` —
есть неподтверждённый документ ЮЛ того же типа, счёта и года, подходящий версии (приоритет `NEEDS_CHECK`);
`PROCESSING` — у ЮЛ есть файл `UPLOADED` или `PROCESSING`; `ERROR` — последний по `uploaded_at` файл ЮЛ в `ERROR`;
иначе `MISSING`. `missing_quarters` SHALL перечислять кварталы без квартальной ОСВ при частичном покрытии. Правило ОСВ
SHALL закрываться только документом с `osv_account_code`, равным `account` правила (OQ-04). Те же ячейки SHALL
отдаваться в карточке версии у каждого члена. С `?format=csv` SHALL отдаваться тот же чек-лист файлом `text/csv;
charset=utf-8` с BOM и разделителем `;`: заголовок «Организация;ИНН;<правило>…» (правила — «Бух. баланс», «ОФР»,
«ОСВ <счёт>»), ячейки — подписи макета: `CONFIRMED` «Загружен», `PROCESSING` «Обработка», `NEEDS_CHECK`/`RECOGNIZED`
«Требуется проверка», `ERROR` «Ошибка загрузки», `MISSING` «Отсутствует» (с « (нет: Q1, Q2)» при `missing_quarters`);
`Content-Disposition` — `attachment` с именем `checklist-<ГК>-<год>-<квартал>.csv` в RFC 5987.
#### Scenario: Пробел Q2
- **WHEN** у ЮЛ привязаны квартальные ОСВ 60 за Q1 и Q3 2024, версия за 2024 Q3
- **THEN** ячейка ОСВ 60: `status = MISSING`, `missing_quarters = ["Q2"]`, `document_ids` — два документа
#### Scenario: Субсчёт не закрывает правило
- **WHEN** у ЮЛ подтверждена только ОСВ 58.03 за квартал версии
- **THEN** ячейка ОСВ 58 — `MISSING`
#### Scenario: Файл в обработке
- **WHEN** у ЮЛ загружен файл, задание ещё не завершено, других документов нет
- **THEN** все ячейки ЮЛ — `PROCESSING`
#### Scenario: Чек-лист в CSV
- **WHEN** запрошен чек-лист версии с `format=csv`, у ЮЛ подтверждён ББ, ОСВ 60 отсутствует за Q2
- **THEN** файл начинается с BOM, первая строка — «Организация;ИНН;Бух. баланс;ОФР;ОСВ 60…», в строке ЮЛ — «Загружен» и «Отсутствует (нет: Q2)»

View File

@ -0,0 +1,25 @@
# Spec Delta: directories-api
## MODIFIED Requirements
### Requirement: Юридическое лицо / ИП — создание, чтение, изменение
`POST /api/v1/directories/legal-entities` (`DIRECTORY_EDIT`) SHALL создавать запись с `inn` (ЮЛ — ровно 10 цифр, ИП — 12;
иначе 422 `INVALID_INN`), `entity_type` (`ЮЛ` | `ИП`), `name`, `company_group_id` (NULL = монозаёмщик), `regional_office_id`
(по умолчанию — филиал ГК), `crm_id`; `inn` уникален (409 `INN_EXISTS`). `PATCH …/{id}` SHALL менять `name`, `entity_type`,
`crm_id`, `regional_office_id` со снимком в `legal_entity_history(event_type = UPDATE)` и `audit_event(LEGAL_ENTITY_CHANGE)`.
`GET …` SHALL давать список с фильтрами `company_group_id`, `outside_group` (`company_group_id IS NULL`), `inn`
(точно), `q` (подстрока имени/ИНН), `closed` (по `close_date`), пагинацией; `GET …/{id}` — карточку с текущими
членствами ГСК/ГСЗ и алиасами; `GET …/{id}/history` — снимки. В списке и карточке SHALL быть `documents_count` — число
документов файлов ЮЛ, кроме файлов `SUPERSEDED`.
#### Scenario: Неверная длина ИНН
- **WHEN** создаётся ИП с ИНН из 10 цифр
- **THEN** ответ 422 с кодом `INVALID_INN`, запись не создана
#### Scenario: Монозаёмщик
- **WHEN** ЮЛ создано без `company_group_id`
- **THEN** оно отдаётся в списке с `outside_group = true` и видно пользователям филиала `regional_office_id`
#### Scenario: Число документов
- **WHEN** у монозаёмщика файл с книгой НПЗ (ББ и ОФР) и заменённый файл с одной ОСВ
- **THEN** `documents_count = 2`

View File

@ -0,0 +1,164 @@
# Spec Delta: documents-api
## MODIFIED Requirements
### Requirement: Файлы организации и карточка файла
`GET /api/v1/legal-entities/{id}/files` (`DOCUMENT_VIEW`, policy; `include_superseded=false` по умолчанию) SHALL
возвращать `Page` файлов ЮЛ по убыванию `uploaded_at`: поля `stored_file`, текущее задание (`status`, `error_code`,
`error_message`, `mapping_error` (`metrics.mapping_error`: причина `ERROR` файла после неудачного маппинга, когда у задания
`error_code` пуст), `metrics.warnings`, `metrics.mapping_warnings`, `finished_at`), документы файла (`id`, `document_type`,
`osv_account_code`, `period_year`, `period_quarter`, `is_cumulative`, `status`, `period_check`, `counterparty_check`,
`comments_count`). `GET /api/v1/files/{file_id}` SHALL отдавать ту же карточку; чужой или несуществующий файл — 404.
#### Scenario: Список после обработки
- **WHEN** у ЮЛ два файла: `PROCESSED` с документом и `ERROR`
- **THEN** оба в списке; у первого `documents` непустой, у второго `job.error_code` заполнен
#### Scenario: Счётчик комментариев
- **WHEN** к документу файла добавлены два комментария
- **THEN** в карточке файла у документа `comments_count = 2`
### Requirement: Подтверждение документа
`POST /api/v1/documents/{document_id}/confirm` (`DOCUMENT_EDIT`, policy) SHALL переводить документ из `RECOGNIZED`
или `NEEDS_CHECK` в `CONFIRMED` (`confirmed_by`, `confirmed_at`) с `audit_event(DOCUMENT_CONFIRM, PRIMARY_DOCUMENTS)`;
из `CONFIRMED` повторно — 409 `ALREADY_CONFIRMED`; документ файла `SUPERSEDED` — 409 `FILE_SUPERSEDED`; документ в любом
другом статусе (например, `ERROR`) — 409 `DOCUMENT_NOT_CONFIRMABLE`. Для `BALANCE`/`INCOME` дополнительно: пустые
`period_year` или `period_quarter` — 409 `PERIOD_REQUIRED`; непройденные неакцептованные жёсткие проверки §7 — 409
`CHECKS_NOT_PASSED` со списком нарушений в `details`; при подтверждении SHALL записываться
`financial_statement_meta.reviewed_by/at`. Несовпадение контрагента и уверенность значений подтверждение MUST NOT
блокировать. Если совпали несколько условий, порядок: `FILE_SUPERSEDED`, `ALREADY_CONFIRMED`,
`DOCUMENT_NOT_CONFIRMABLE`, `PERIOD_REQUIRED`, `CHECKS_NOT_PASSED`. Подтверждение берёт блокировки в порядке «файл →
документ → (advisory-блокировка привязок ГК) → версии консолидации (по возрастанию `id`)»: файл — `FOR UPDATE`,
документ — `FOR NO KEY UPDATE` (R-3; совместима со вставкой привязки `FOR KEY SHARE`), версии — `FOR UPDATE`; состояние
перечитывается под этими блокировками; документ или файл, исчезнувшие за время ожидания блокировки, — 404 `NOT_FOUND`.
В той же транзакции документ SHALL привязываться (`consolidation_document`) ко всем версиям «в работе» (`INITIATED`,
`DOCUMENTS_PROCESSING`, `DATA_CORRECTION`), которым он подходит: ЮЛ документа ∈ `consolidation_member` версии,
`period_year` = год версии, `period_quarter` не NULL и (`is_cumulative` true или NULL → `period_quarter` = квартал
версии; false → `period_quarter` ≤ квартала версии). ОСВ за `Q1` равнозначна нарастающей и квартальной (R-14): она
подходит любой версии года при любом `is_cumulative`; ББ/ОФР за `Q1` — отчёт на конец квартала и подходят только версии
за `Q1` (R-14a). Привязанный ранее документ того же ЮЛ с тем же ключом (`document_type`, `osv_account_code`,
`period_quarter`, признак нарастающего: `is_cumulative` true или NULL; для ОСВ за `Q1` признак не различается) в той же
версии SHALL отвязываться (R-1). Каждая версия, получившая
привязку, из `INITIATED` или `DOCUMENTS_PROCESSING` SHALL переходить в `DATA_CORRECTION` (`changed_by` =
подтвердивший). Блокировка версии (`consolidation_lock`) для подтверждения MUST NOT требоваться.
#### Scenario: Причина ошибки маппинга в карточке файла
- **WHEN** файл в `ERROR` после неудачного маппинга (задание `done`, `error_code` пуст)
- **THEN** `job.mapping_error` содержит причину, `job.error_code` пуст
#### Scenario: Документ не подтверждается
- **WHEN** подтверждается документ в статусе `ERROR`
- **THEN** 409 `DOCUMENT_NOT_CONFIRMABLE`, статус не изменился
#### Scenario: Подтверждение
- **WHEN** `ka_rf` подтверждает документ `NEEDS_CHECK` своего филиала
- **THEN** `status = CONFIRMED`, `confirmed_by` = пользователь, `audit_event(DOCUMENT_CONFIRM)` с `before_value.status = NEEDS_CHECK`
#### Scenario: Привязка к версии и переход статуса
- **WHEN** версия ГК 2024 Q3 в `INITIATED` с ЮЛ в периметре; подтверждается квартальная ОСВ 60 этого ЮЛ за 2024 Q2
- **THEN** `consolidation_document(version, document)` создан; версия в `DATA_CORRECTION`; запись истории
`INITIATED → DATA_CORRECTION` с `changed_by` = пользователь
#### Scenario: Не подходит по периоду
- **WHEN** подтверждается нарастающая ОСВ 60 за 2024 Q2 при версии за 2024 Q3
- **THEN** привязки нет, статус версии не изменился
#### Scenario: Версия в CANCELLED
- **WHEN** подходящий документ подтверждается при версии `CANCELLED`
- **THEN** привязки нет
#### Scenario: Форма с нарушением проверок
- **WHEN** подтверждается `BALANCE` с непройденным жёстким соотношением без акцепта
- **THEN** 409 `CHECKS_NOT_PASSED`, в `details` — нарушение; после акцепта подтверждение проходит
#### Scenario: Форма без периода
- **WHEN** подтверждается форма из OCR, у которой нет ни даты отчёта, ни дат колонок
- **THEN** 409 `PERIOD_REQUIRED`; после правки даты отчёта подтверждение проходит
#### Scenario: Форма в чек-листе
- **WHEN** версия ГК 2025 Q3 `INITIATED`, ЮЛ в периметре; подтверждается его `BALANCE` за 2025 Q3
- **THEN** привязка создана, ячейка «Бух. баланс» чек-листа ЮЛ — `CONFIRMED`, `reviewed_by` шапки = пользователь
## ADDED Requirements
### Requirement: Форма ББ/ОФР для экрана проверки
`GET /api/v1/documents/{document_id}/statement` (`DOCUMENT_VIEW`, policy по ЮЛ) SHALL отдавать для документа
`BALANCE`/`INCOME`: `document` (`id`, `document_type`, `status`, `period_year`, `period_quarter`, `reporting_date`,
`counterparty_check`, `locator`, `file_id`); `edition` (`66n` при дате периода ≤ 2024-12-31, `fsbu4` — позже, `null` без
даты); `header` — для `inn`, `report_date`, `units`: `value`, `original_value`, `confidence` (у ИНН), `verified`,
`modified`; плюс `organization_name`, `okud`, `okpo`, `okopf`, `okfs`, `address`; `columns[]` (`id`, `column_index`,
`label`, `period_date`, `is_current`); `lines[]` по `line_code` (`id`, `line_code`, `line_code_confidence`, `line_name`,
`line_name_confidence`, `values[]` по `column_index`: `id`, `column_id`, `amount`, `string_value`, `confidence`,
`is_recognized`, `is_modified`, `original_amount`, `modified_by`, `modified_at`); `checks` (`failed`, `soft_failed`,
`cross_form`, `accepted`: `by`, `at`, `comment` или `null`). Документ другого типа — 409 `DOCUMENT_NOT_STATEMENT`;
чужой или несуществующий — 404.
#### Scenario: Форма для экрана
- **WHEN** запрошена форма `BALANCE` из `mezheninovskaya/2025-Q3/1_Форма1_бух_09`
- **THEN** `edition = fsbu4`, 37 строк по коду, у каждой строки значения по всем колонкам, `checks.failed` пуст
#### Scenario: Не форма
- **WHEN** запрошена форма документа `OSV`
- **THEN** 409 `DOCUMENT_NOT_STATEMENT`
### Requirement: Правка формы
`PATCH /api/v1/documents/{document_id}/statement` (`DOCUMENT_EDIT`, policy) SHALL принимать частичное тело `header`
(`inn` — 10 или 12 цифр, иначе 422 `INVALID_INN`; `report_date` — дата; `units` — непустая строка ≤ 50) и `values[]`
(`line_id`, `column_id`, `amount` — число с ≤ 2 знаками или `null`) и в одной транзакции: при первой правке значения
записывать `original_amount` = прежняя сумма; ставить `amount`, `is_modified = (amount ≠ original_amount)`,
`modified_by/at`; для поля шапки — `*_modified = true`, `*_original_value` не менять; после `inn` — пересчитать
`counterparty_check`; после `report_date` — пересчитать `period_year`, `period_quarter`, `reporting_date`; сбросить
акцепт проверок; пересчитать статус; записать одно `audit_event(RECOGNIZED_DATA_EDIT, RECOGNITION)` с `before/after`
изменённых полей и ячеек; вернуть форму, как `GET`. Документ `CONFIRMED` — 409 `DOCUMENT_READONLY`; файл `SUPERSEDED` —
409 `FILE_SUPERSEDED`; не форма — 409 `DOCUMENT_NOT_STATEMENT`; ячейка не этого документа — 422 `UNKNOWN_CELL`. Запрос
без изменений — 200 без аудита. Блокировки — «файл `FOR UPDATE` → документ `FOR NO KEY UPDATE`».
#### Scenario: Правка значения
- **WHEN** аналитик меняет сумму строки 1150 колонки 0 с 100 на 120
- **THEN** `amount = 120`, `original_amount = 100`, `is_modified = true`, аудит `RECOGNIZED_DATA_EDIT` с `before 100`, `after 120`
#### Scenario: Возврат к исходному
- **WHEN** затем та же ячейка меняется обратно на 100
- **THEN** `is_modified = false`, `original_amount = 100`
#### Scenario: Правка даты отчёта
- **WHEN** у формы OCR без периода дата отчёта правится на 2025-06-30
- **THEN** `period_year = 2025`, `period_quarter = Q2`, `report_date_modified = true`, `report_date_original_value` не изменился
#### Scenario: Подтверждённая форма
- **WHEN** правится форма `CONFIRMED`
- **THEN** 409 `DOCUMENT_READONLY`, данные не изменились
### Requirement: Акцепт проверок формы
`POST /api/v1/documents/{document_id}/statement/checks/accept` (`DOCUMENT_EDIT`, policy) с `{comment}` (1–2000 символов)
SHALL выполнять акцепт (`statement-checks`), пересчитывать статус и писать `audit_event(RECOGNIZED_DATA_EDIT,
RECOGNITION)` с `before/after` полей акцепта; без непройденных жёстких проверок — 409 `NOTHING_TO_ACCEPT`; те же 409,
что у правки формы.
#### Scenario: Акцепт
- **WHEN** у формы нарушено 1600 = 1100 + 1200, аналитик акцептует с комментарием «округление в форме клиента»
- **THEN** `checks.accepted` заполнен, документ `RECOGNIZED`, если других причин `NEEDS_CHECK` нет
### Requirement: Комментарии к документу
`POST /api/v1/documents/{document_id}/comments` (`DOCUMENT_EDIT`, policy) с `{text}` (1–4000 символов после обрезки
пробелов) SHALL создавать `comment(object_type = 'document')` с автором и временем; `GET …/comments` (`DOCUMENT_VIEW`)
SHALL отдавать комментарии по `created_at`: `{id, text, author: {id, full_name}, created_at}`. Правки и удаления нет;
комментировать можно документ в любом статусе, в том числе файла `SUPERSEDED`.
#### Scenario: Комментарий
- **WHEN** аналитик добавляет комментарий «нет подписи руководителя» к подтверждённому документу
- **THEN** 201, комментарий в списке с ФИО автора, `comments_count` документа = 1
### Requirement: Исходный файл
`GET /api/v1/files/{file_id}/content` (`DOCUMENT_VIEW`, policy) SHALL отдавать содержимое исходного объекта файла из S3
потоком, без чтения целиком в память: `Content-Type` по `file_type`, `Content-Disposition` (`attachment` по умолчанию,
`inline` при `?disposition=inline`) с ASCII-именем и `filename*` RFC 5987 исходного имени; работает и для файла
`SUPERSEDED`. Объект не найден в S3 — 404 `FILE_CONTENT_NOT_FOUND`; чужой файл — 404.
#### Scenario: PDF для просмотра
- **WHEN** запрошено содержимое загруженного `aep_6.pdf` с `disposition=inline`
- **THEN** 200, `Content-Type: application/pdf`, `Content-Disposition: inline; filename="file.pdf"; filename*=UTF-8''aep_6.pdf`, тело байт в байт равно загруженному
#### Scenario: Кириллица в имени
- **WHEN** запрошен файл `ОСВ_66 за 2024.xls`
- **THEN** `filename*` содержит percent-кодированное исходное имя

View File

@ -0,0 +1,108 @@
# Spec Delta: osv-result-mapping
## MODIFIED Requirements
### Requirement: Цикл маппинга отделён от inbox
Фоновая задача ядра SHALL с периодом `MAPPING_POLL_SECONDS` (5) выбирать одно задание со `status ∈ {done,
needs_check}`, чей файл имеет `status = PROCESSING` и `current_recognition_job_id` = это задание (`FOR UPDATE SKIP
LOCKED`), проверять, что `result_s3_object` относится к бакету хранилища и его ключ начинается с
`results/{file_id}/{job_id}/` (OQ-70), читать объект из S3 **вне** транзакции, валидировать `RecognitionResult` v1.x и
в одной транзакции создать документы и строки (листы ОСВ и формы ББ/ОФР), записать `recognition_job.group_schema`,
перевести файл в `PROCESSED` и выполнить автозамену. Результат без документов SHALL давать неповторяемую ошибку
маппинга «в результате нет документов»; результат с формами, но без ОСВ, ошибкой MUST NOT быть. Inbox MUST NOT читать
файл результата и MUST NOT создавать документы.
#### Scenario: Результат смапплен
- **WHEN** в S3 лежит результат парсера для `rb/2024-FY/ОСВ_66 за 2024.xls`, задание `done`, файл `PROCESSING`
- **THEN** не позднее чем через `MAPPING_POLL_SECONDS` + время маппинга файл — `PROCESSED`, создан один `document`
(`OSV`, `osv_account_code = "66"`, `period_year = 2024`, `period_quarter = Q4`, `is_cumulative = true`, `locator = sheet:Лист_1`),
`osv_entry` — все строки результата кроме `TOTAL`, `group_schema` задания содержит `[{"locator": "sheet:Лист_1", "groups": [...]}]`
#### Scenario: Ключ результата вне папки задания
- **WHEN** `result_s3_object.key = results/<другой file_id>/.../result.json`
- **THEN** файл — `ERROR` с ошибкой маппинга, документы не созданы, повтор не выполняется
#### Scenario: Сбой маппинга с повтором
- **WHEN** вставка строк падает по ошибке БД
- **THEN** транзакция откачена, `metrics.mapping_attempts = 1`, файл остаётся `PROCESSING`; на третьей неудаче — `ERROR`
с `metrics.mapping_error`
#### Scenario: Повторный маппинг идемпотентен
- **WHEN** маппинг прерван после частичной вставки (имитация) и выполнен снова
- **THEN** у файла ровно один набор документов и строк, дублей по `(document_id, row_number)` нет
#### Scenario: Файл только с формами
- **WHEN** смаплен golden-снимок `mezheninovskaya/2025-Q3/1_Форма1_бух_09` (одна форма `BALANCE`, ОСВ нет)
- **THEN** файл `PROCESSED`, создан документ `BALANCE`, ошибки «нет документов ОСВ» нет
#### Scenario: Пустой результат
- **WHEN** результат валиден по схеме, но `documents = []`
- **THEN** файл `ERROR` с `metrics.mapping_error = "в результате нет документов"`, повтор не выполняется
## ADDED Requirements
### Requirement: Документ формы ББ/ОФР
Каждый `statementDocument` с `document_type ∈ {BALANCE, INCOME}` SHALL давать `document` (`osv_account_code = NULL`,
`is_cumulative = NULL`, `locator` из результата, `period_check = UNKNOWN`) и `financial_statement_meta` 1:1. Дата
периода — `header.report_date`, при её отсутствии — самая поздняя `columns[].period_date`; `period_year` — год даты,
`period_quarter` — квартал, если дата — последний день марта, июня, сентября или декабря, иначе `NULL` с
предупреждением; `reporting_date` — эта дата. Шапка: `inn`, `inn_original_value` = `header.inn`, `inn_confidence`,
`inn_verified = (inn_confidence = 100)`; `organization_name`; `report_date` = `header.report_date`,
`report_date_original_value` = `header.report_date_text`; `units` = `units_original_value` = `header.units`; `okud`,
`okpo`, `okopf`, `okfs`, `address` — как пришли; `statement_type` — `balance`/`income`. Колонки — по `columns[]`,
`is_current = true` у колонки с самой поздней `period_date`. Строки — по `lines[]` с исходниками кода и названия; повтор
кода — первая строка и предупреждение. Значение SHALL создаваться на каждую пару «строка × колонка»: из результата —
`amount`, `string_value`, `confidence`, `is_recognized`; отсутствующая ячейка — все поля `NULL`; `is_modified = false`,
`original_amount = NULL`. `checks` результата MUST NOT использоваться для статуса. `counterparty_check` — тем же
правилом, что у листа ОСВ (ИНН шапки, иначе имя или алиас владельца).
#### Scenario: Форма из Excel
- **WHEN** смаплен golden-снимок `mezheninovskaya/2025-Q3/1_Форма1_бух_09`
- **THEN** документ `BALANCE`, `period_year = 2025`, `period_quarter = Q3`, `reporting_date = 2025-09-30`; у `financial_statement_meta` 37 строк и по значению на каждую пару строка × колонка
#### Scenario: Книга с двумя формами
- **WHEN** смаплен golden-снимок книги НПЗ `npz/2025-Q3/…НОВОРОССИЙСКИЙ ПРОКАТНЫЙ ЗАВОД`
- **THEN** у файла документы `BALANCE` (132 строки) и `INCOME` (43 строки), оба за 2025 Q3
#### Scenario: Форма из OCR без даты отчёта
- **WHEN** смаплен эталон нормализатора `ocr/expected_normalize/aep_6` (дата отчёта `null`, колонки баланса 2025-06-30, 2024-12-31, 2023-12-31)
- **THEN** документ `BALANCE` за 2025 Q2, `is_current` — у колонки 2025-06-30, `report_date` в шапке `NULL`, `inn` `NULL`
#### Scenario: Дата не конец квартала
- **WHEN** дата отчёта формы — 2025-10-31
- **THEN** `period_year = 2025`, `period_quarter = NULL`, предупреждение маппинга, документ `NEEDS_CHECK`
### Requirement: Повтор формы в файле и прочие документы
Второй документ `BALANCE` или `INCOME` в одном файле MUST NOT создаваться: предупреждение маппинга, первый документ —
`NEEDS_CHECK`. Документы `DISCLOSURE` и `OTHER` результата SHALL давать по одному `document` каждого типа на файл без
данных (`locator` — все `locator` этого типа через запятую, статус `RECOGNIZED`). Форма `BALANCE`/`INCOME` без строк
SHALL создаваться с шапкой и колонками.
#### Scenario: Расшифровки
- **WHEN** смаплен синтетический снимок `st06_header_variants` (форма `INCOME` и пять `DISCLOSURE`)
- **THEN** у файла два документа: `INCOME` и один `DISCLOSURE` с пятью `locator` через запятую
#### Scenario: Форма без колонки кодов
- **WHEN** смаплен синтетический снимок `st03_no_codes` (`BALANCE`, строк 0)
- **THEN** документ `BALANCE` `NEEDS_CHECK` с шапкой и колонками, строк нет
### Requirement: Статус документа формы
Документ `BALANCE`/`INCOME` SHALL быть `RECOGNIZED`, только если одновременно: задание `done`; нет непройденных
неакцептованных жёстких проверок §7 (`statement-checks`); `counterparty_check ≠ MISMATCH`; `period_year` и
`period_quarter` определены; у каждого значения `is_modified = true` либо (`is_recognized` не `false` и `confidence`
равна 100 или `NULL`); нет предупреждений маппинга по документу; есть хотя бы одна строка. Иначе `NEEDS_CHECK`. Статус
SHALL пересчитываться при маппинге, после правки формы и после акцепта, кроме документа `CONFIRMED`. Документ формы
`NEEDS_CHECK` SHALL участвовать в переходе версии в `DATA_CORRECTION` наравне с ОСВ (требование «Документ NEEDS_CHECK
переводит версию в корректировку»).
#### Scenario: Чистая форма Excel
- **WHEN** смаплен `mezheninovskaya/2025-Q3/1_Форма2_бух_09` (ОФР, `done`, соотношения сходятся, ИНН шапки = ИНН владельца файла)
- **THEN** документ `INCOME` `RECOGNIZED`
#### Scenario: Форма OCR
- **WHEN** смаплен эталон `ocr/expected_normalize/aep_3` (уверенность значений < 100, ИНН шапки ≠ ИНН владельца)
- **THEN** документ `BALANCE` `NEEDS_CHECK`, `counterparty_check = MISMATCH`
#### Scenario: Форма двигает версию
- **WHEN** версия ГК 2025 Q3 в `DOCUMENTS_PROCESSING`, в периметре ЮЛ файла; смаплена форма `BALANCE` за 2025 Q3 в `NEEDS_CHECK`
- **THEN** версия в `DATA_CORRECTION`, привязки `consolidation_document` нет

View File

@ -0,0 +1,58 @@
# Spec Delta: statement-checks
## Purpose
Логические проверки форм ББ/ОФР §7 в ядре: чистые функции над текущими суммами документа, без хранения результата;
акцепт непройденных проверок аналитиком.
## ADDED Requirements
### Requirement: Соотношения §7 по колонкам
Проверки SHALL считаться чистой функцией по текущим `financial_statement_value.amount` документа для каждой колонки
отдельно по словарю `api/rules/statement_checks.yaml` (копия словаря tabular-parser): соотношение проверяется, только
если присутствует его левая часть; отсутствующий код и пустая сумма — 0; выполнено, если `|левая − правая| ≤ tolerance`
(1, в единицах формы) хотя бы в одном варианте правой части — как записано или с кодами `abs_terms` по модулю. Нарушение
жёсткого (`strict`) соотношения, не превышающее `rounding_tolerance_percent` (0,001 %) от наибольшего по модулю числа
соотношения, SHALL считаться мягким («в пределах округления»). Мягкие: соотношения `strict: false` и «сумма пятизначных
подстрок = четырёхзначный родитель». Результат — `{failed: [...], soft_failed: [...]}`, элемент — `{column_index,
relation, expected, actual}`; MUST NOT храниться в БД.
#### Scenario: Сходящийся баланс
- **WHEN** проверяется форма `mezheninovskaya/2025-Q3/1_Форма1_бух_09`
- **THEN** `failed` пуст
#### Scenario: Нарушение жёсткого соотношения
- **WHEN** у баланса 1600 = 1000, 1100 = 300, 1200 = 600 в колонке 0
- **THEN** `failed` содержит `{column_index: 0, relation: "1600 = 1100 + 1200", expected: 1000, actual: 900}`
#### Scenario: Расходы со знаком минус
- **WHEN** ОФР 2110 = 100, 2120 = −60, 2100 = 40
- **THEN** `2100 = 2110 − 2120` выполнено через вариант с `abs_terms`
#### Scenario: Правка исправляет нарушение
- **WHEN** аналитик правит 1200 с 600 на 700 в форме из предыдущего сценария
- **THEN** проверка по текущим суммам: `failed` пуст
### Requirement: Межформенная проверка
Для документа `BALANCE` или `INCOME` SHALL считаться мягкая проверка `1370(колонка отчётной даты ББ) − 1370(колонка
31 декабря прошлого года ББ) = 2400(колонка отчётного периода ОФР)` с допуском `tolerance`, если найдена форма-пара:
документ другого типа формы того же ЮЛ, `period_year` и `period_quarter`, файла не `SUPERSEDED` (подтверждённый, затем
самый поздний по `created_at`), и все три колонки. Нарушение SHALL попадать в `cross_form` и MUST NOT блокировать
подтверждение.
#### Scenario: Пара найдена
- **WHEN** у ЮЛ ББ за 2025 Q3 (1370 = 500 на 30.09.2025, 300 на 31.12.2024) и ОФР за 2025 Q3 (2400 = 150)
- **THEN** `cross_form` — нарушение `expected 200, actual 150`; подтверждение ББ разрешено
#### Scenario: Пары нет
- **WHEN** у ЮЛ есть только ББ за период
- **THEN** `cross_form` — `null`
### Requirement: Акцепт непройденных проверок
Акцепт SHALL записывать `financial_statement_meta.checks_accepted_by`, `checks_accepted_at`,
`checks_accepted_comment`; документ с акцептом SHALL считаться прошедшим жёсткие проверки для статуса и подтверждения.
Любая правка значений или шапки формы SHALL сбрасывать акцепт (три поля — `NULL`).
#### Scenario: Акцепт и правка
- **WHEN** аналитик акцептовал нарушение 1600 и затем поправил любое значение
- **THEN** поля акцепта пусты, при непройденной проверке документ `NEEDS_CHECK`

View File

@ -0,0 +1,17 @@
# Spec Delta: users-admin
## ADDED Requirements
### Requirement: Краткий справочник пользователей
`GET /api/v1/users/brief?ids=<uuid>,…` SHALL быть доступен любому аутентифицированному пользователю без отдельного
права и отдавать `[{id, full_name, is_active}]` для найденных id, включая неактивных; неизвестные id MUST NOT
возвращаться и MUST NOT давать ошибку; больше 200 id — 422 `TOO_MANY_IDS`; некорректный UUID — 422. Админский список
пользователей не меняется.
#### Scenario: Имя загрузившего
- **WHEN** аналитик без права `USERS_ADMIN` запрашивает `ids` из `uploaded_by` двух файлов и неизвестный UUID
- **THEN** 200, два элемента с ФИО, неизвестный UUID отсутствует
#### Scenario: Слишком много id
- **WHEN** передан 201 id
- **THEN** 422 `TOO_MANY_IDS`

View File

@ -0,0 +1,6 @@
# Tasks: Срез 3b — формы ББ/ОФР в ядре и API до сводной ОСВ
Заполняется планом (`writing-plans`) после одобрения спеки владельцем. Репозиторий — arm-core; команды — из корня
(`make`), `pytest` — из `api/`.
- [ ] 0.1 План реализации написан и одобрен; задачи перенесены сюда из плана