diff --git a/openspec/changes/slice-3b-statements-api/.openspec.yaml b/openspec/changes/slice-3b-statements-api/.openspec.yaml new file mode 100644 index 0000000..a26662f --- /dev/null +++ b/openspec/changes/slice-3b-statements-api/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-07 diff --git a/openspec/changes/slice-3b-statements-api/design.md b/openspec/changes/slice-3b-statements-api/design.md new file mode 100644 index 0000000..75fa278 --- /dev/null +++ b/openspec/changes/slice-3b-statements-api/design.md @@ -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` и + предупреждение `: дата <дата> не конец квартала`. Без даты — год и квартал `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` того же файла не создаётся: предупреждение `: вторая форма +<тип> в файле пропущена (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.` и `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=,…` — любой аутентифицированный пользователь; ≤ 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 | копии разойдутся незаметно | diff --git a/openspec/changes/slice-3b-statements-api/proposal.md b/openspec/changes/slice-3b-statements-api/proposal.md new file mode 100644 index 0000000..781109e --- /dev/null +++ b/openspec/changes/slice-3b-statements-api/proposal.md @@ -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). diff --git a/openspec/changes/slice-3b-statements-api/specs/consolidations-api/spec.md b/openspec/changes/slice-3b-statements-api/specs/consolidations-api/spec.md new file mode 100644 index 0000000..dfaa3a7 --- /dev/null +++ b/openspec/changes/slice-3b-statements-api/specs/consolidations-api/spec.md @@ -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)» diff --git a/openspec/changes/slice-3b-statements-api/specs/directories-api/spec.md b/openspec/changes/slice-3b-statements-api/specs/directories-api/spec.md new file mode 100644 index 0000000..272dabd --- /dev/null +++ b/openspec/changes/slice-3b-statements-api/specs/directories-api/spec.md @@ -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` diff --git a/openspec/changes/slice-3b-statements-api/specs/documents-api/spec.md b/openspec/changes/slice-3b-statements-api/specs/documents-api/spec.md new file mode 100644 index 0000000..947b2d1 --- /dev/null +++ b/openspec/changes/slice-3b-statements-api/specs/documents-api/spec.md @@ -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-кодированное исходное имя diff --git a/openspec/changes/slice-3b-statements-api/specs/osv-result-mapping/spec.md b/openspec/changes/slice-3b-statements-api/specs/osv-result-mapping/spec.md new file mode 100644 index 0000000..c8b88d2 --- /dev/null +++ b/openspec/changes/slice-3b-statements-api/specs/osv-result-mapping/spec.md @@ -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` нет diff --git a/openspec/changes/slice-3b-statements-api/specs/statement-checks/spec.md b/openspec/changes/slice-3b-statements-api/specs/statement-checks/spec.md new file mode 100644 index 0000000..edfe814 --- /dev/null +++ b/openspec/changes/slice-3b-statements-api/specs/statement-checks/spec.md @@ -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` diff --git a/openspec/changes/slice-3b-statements-api/specs/users-admin/spec.md b/openspec/changes/slice-3b-statements-api/specs/users-admin/spec.md new file mode 100644 index 0000000..31a4f3d --- /dev/null +++ b/openspec/changes/slice-3b-statements-api/specs/users-admin/spec.md @@ -0,0 +1,17 @@ +# Spec Delta: users-admin + +## ADDED Requirements + +### Requirement: Краткий справочник пользователей +`GET /api/v1/users/brief?ids=,…` 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` diff --git a/openspec/changes/slice-3b-statements-api/tasks.md b/openspec/changes/slice-3b-statements-api/tasks.md new file mode 100644 index 0000000..f58a0c8 --- /dev/null +++ b/openspec/changes/slice-3b-statements-api/tasks.md @@ -0,0 +1,6 @@ +# Tasks: Срез 3b — формы ББ/ОФР в ядре и API до сводной ОСВ + +Заполняется планом (`writing-plans`) после одобрения спеки владельцем. Репозиторий — arm-core; команды — из корня +(`make`), `pytest` — из `api/`. + +- [ ] 0.1 План реализации написан и одобрен; задачи перенесены сюда из плана