From 89abf0a8ec66a0089188f5c53a9e25eaab354461 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 7 Oct 2026 15:23:35 +0400 Subject: [PATCH] =?UTF-8?q?openspec:=20=D0=B0=D1=80=D1=85=D0=B8=D0=B2=20ch?= =?UTF-8?q?ange=20slice-3b-statements-api=20(2026-10-07)=20=E2=80=94=20?= =?UTF-8?q?=D1=81=D0=BF=D0=B5=D0=BA=D0=B8=20documents-api,=20osv-result-ma?= =?UTF-8?q?pping,=20statement-checks=20=D0=B8=20=D0=B4=D1=80.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ссылки README и CLAUDE.md переведены на архив. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- CLAUDE.md | 2 +- README.md | 2 +- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/consolidations-api/spec.md | 0 .../specs/directories-api/spec.md | 0 .../specs/documents-api/spec.md | 0 .../specs/osv-result-mapping/spec.md | 0 .../specs/statement-checks/spec.md | 0 .../specs/users-admin/spec.md | 0 .../tasks.md | 0 openspec/specs/consolidations-api/spec.md | 10 +- openspec/specs/directories-api/spec.md | 7 +- openspec/specs/documents-api/spec.md | 115 +++++++++++++++++- openspec/specs/osv-result-mapping/spec.md | 80 +++++++++++- openspec/specs/statement-checks/spec.md | 57 +++++++++ openspec/specs/users-admin/spec.md | 14 +++ 18 files changed, 275 insertions(+), 12 deletions(-) rename openspec/changes/{slice-3b-statements-api => archive/2026-10-07-slice-3b-statements-api}/.openspec.yaml (100%) rename openspec/changes/{slice-3b-statements-api => archive/2026-10-07-slice-3b-statements-api}/design.md (100%) rename openspec/changes/{slice-3b-statements-api => archive/2026-10-07-slice-3b-statements-api}/proposal.md (100%) rename openspec/changes/{slice-3b-statements-api => archive/2026-10-07-slice-3b-statements-api}/specs/consolidations-api/spec.md (100%) rename openspec/changes/{slice-3b-statements-api => archive/2026-10-07-slice-3b-statements-api}/specs/directories-api/spec.md (100%) rename openspec/changes/{slice-3b-statements-api => archive/2026-10-07-slice-3b-statements-api}/specs/documents-api/spec.md (100%) rename openspec/changes/{slice-3b-statements-api => archive/2026-10-07-slice-3b-statements-api}/specs/osv-result-mapping/spec.md (100%) rename openspec/changes/{slice-3b-statements-api => archive/2026-10-07-slice-3b-statements-api}/specs/statement-checks/spec.md (100%) rename openspec/changes/{slice-3b-statements-api => archive/2026-10-07-slice-3b-statements-api}/specs/users-admin/spec.md (100%) rename openspec/changes/{slice-3b-statements-api => archive/2026-10-07-slice-3b-statements-api}/tasks.md (100%) create mode 100644 openspec/specs/statement-checks/spec.md diff --git a/CLAUDE.md b/CLAUDE.md index 7c0f9ba..455fe1d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,7 +105,7 @@ `ChainedStream` закрывают источник и при обрыве клиента). Скачивание первички, CSV и комментарии аудит не пишут (в отличие от экспорта сводной ОСВ в xlsx — тот пишет `EXPORT`; OQ-153). `HeaderPatch.units` — непустая строка после обрезки (пробельная — 422). `GET /users/brief` — `current_user` без права; `documents_count` у ЮЛ — `repositories/directories`; CSV чек-листа — `?format=csv` у `GET …/checklist`. Контракт для фронта — `../docs/dev/api-contract-slice-3b.md`; решения — - `openspec/changes/slice-3b-statements-api/design.md`. + `openspec/changes/archive/2026-10-07-slice-3b-statements-api/design.md`. - **Консолидации (Срез 4)** — `api/endpoints/consolidations/` (periods, versions, locks, status, members, checklist, osv, export, mapping), сервисы: `consolidations` (создание, список, карточка, периметр `PUT …/members`), `consolidation_guard` (`lock_visible_version`/`lock_editable_version`: версия `FOR UPDATE` → видимость 404 → статус «в работе» → своя блокировка), diff --git a/README.md b/README.md index 1430fac..8d1e09c 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ PostgreSQL 16, core NATS без JetStream), `web/` — фронтенд (React + Срез 3b («формы ББ/ОФР в ядре и API до сводной ОСВ»): маппинг форм ББ/ОФР из результата парсера в ядро, форма для экрана «Проверка распознавания» с проверками §7, правка формы до подтверждения и акцепт проверок, условия подтверждения формы, комментарии к документам, исходный файл потоком, имена пользователей, `documents_count` у ЮЛ, CSV чек-листа. -Спецификация — `openspec/changes/slice-3b-statements-api/`; разделы ниже: «API файлов и документов», «Формы ББ/ОФР (Срез 3b)», +Спецификация — `openspec/changes/archive/2026-10-07-slice-3b-statements-api/`; разделы ниже: «API файлов и документов», «Формы ББ/ОФР (Срез 3b)», «Ошибки API». Срез 4 («консолидация»): схема по DBML v5.16, версии консолидации ГК за период, периметр, блокировка «взять в работу», diff --git a/openspec/changes/slice-3b-statements-api/.openspec.yaml b/openspec/changes/archive/2026-10-07-slice-3b-statements-api/.openspec.yaml similarity index 100% rename from openspec/changes/slice-3b-statements-api/.openspec.yaml rename to openspec/changes/archive/2026-10-07-slice-3b-statements-api/.openspec.yaml diff --git a/openspec/changes/slice-3b-statements-api/design.md b/openspec/changes/archive/2026-10-07-slice-3b-statements-api/design.md similarity index 100% rename from openspec/changes/slice-3b-statements-api/design.md rename to openspec/changes/archive/2026-10-07-slice-3b-statements-api/design.md diff --git a/openspec/changes/slice-3b-statements-api/proposal.md b/openspec/changes/archive/2026-10-07-slice-3b-statements-api/proposal.md similarity index 100% rename from openspec/changes/slice-3b-statements-api/proposal.md rename to openspec/changes/archive/2026-10-07-slice-3b-statements-api/proposal.md diff --git a/openspec/changes/slice-3b-statements-api/specs/consolidations-api/spec.md b/openspec/changes/archive/2026-10-07-slice-3b-statements-api/specs/consolidations-api/spec.md similarity index 100% rename from openspec/changes/slice-3b-statements-api/specs/consolidations-api/spec.md rename to openspec/changes/archive/2026-10-07-slice-3b-statements-api/specs/consolidations-api/spec.md diff --git a/openspec/changes/slice-3b-statements-api/specs/directories-api/spec.md b/openspec/changes/archive/2026-10-07-slice-3b-statements-api/specs/directories-api/spec.md similarity index 100% rename from openspec/changes/slice-3b-statements-api/specs/directories-api/spec.md rename to openspec/changes/archive/2026-10-07-slice-3b-statements-api/specs/directories-api/spec.md diff --git a/openspec/changes/slice-3b-statements-api/specs/documents-api/spec.md b/openspec/changes/archive/2026-10-07-slice-3b-statements-api/specs/documents-api/spec.md similarity index 100% rename from openspec/changes/slice-3b-statements-api/specs/documents-api/spec.md rename to openspec/changes/archive/2026-10-07-slice-3b-statements-api/specs/documents-api/spec.md diff --git a/openspec/changes/slice-3b-statements-api/specs/osv-result-mapping/spec.md b/openspec/changes/archive/2026-10-07-slice-3b-statements-api/specs/osv-result-mapping/spec.md similarity index 100% rename from openspec/changes/slice-3b-statements-api/specs/osv-result-mapping/spec.md rename to openspec/changes/archive/2026-10-07-slice-3b-statements-api/specs/osv-result-mapping/spec.md diff --git a/openspec/changes/slice-3b-statements-api/specs/statement-checks/spec.md b/openspec/changes/archive/2026-10-07-slice-3b-statements-api/specs/statement-checks/spec.md similarity index 100% rename from openspec/changes/slice-3b-statements-api/specs/statement-checks/spec.md rename to openspec/changes/archive/2026-10-07-slice-3b-statements-api/specs/statement-checks/spec.md diff --git a/openspec/changes/slice-3b-statements-api/specs/users-admin/spec.md b/openspec/changes/archive/2026-10-07-slice-3b-statements-api/specs/users-admin/spec.md similarity index 100% rename from openspec/changes/slice-3b-statements-api/specs/users-admin/spec.md rename to openspec/changes/archive/2026-10-07-slice-3b-statements-api/specs/users-admin/spec.md diff --git a/openspec/changes/slice-3b-statements-api/tasks.md b/openspec/changes/archive/2026-10-07-slice-3b-statements-api/tasks.md similarity index 100% rename from openspec/changes/slice-3b-statements-api/tasks.md rename to openspec/changes/archive/2026-10-07-slice-3b-statements-api/tasks.md diff --git a/openspec/specs/consolidations-api/spec.md b/openspec/specs/consolidations-api/spec.md index 01479b5..af897b3 100644 --- a/openspec/specs/consolidations-api/spec.md +++ b/openspec/specs/consolidations-api/spec.md @@ -136,7 +136,11 @@ missing_quarters, document_ids}`: `CONFIRMED` — пара (ЮЛ, правило `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 @@ -150,6 +154,10 @@ SHALL закрываться только документом с `osv_account_c - **WHEN** у ЮЛ загружен файл, задание ещё не завершено, других документов нет - **THEN** все ячейки ЮЛ — `PROCESSING` +#### Scenario: Чек-лист в CSV +- **WHEN** запрошен чек-лист версии с `format=csv`, у ЮЛ подтверждён ББ, ОСВ 60 отсутствует за Q2 +- **THEN** файл начинается с BOM, первая строка — «Организация;ИНН;Бух. баланс;ОФР;ОСВ 60…», в строке ЮЛ — «Загружен» и «Отсутствует (нет: Q2)» + ### Requirement: Границы входных данных Входы консолидаций SHALL иметь верхние границы: `legal_entity_ids` тела `PUT …/members` — не больше 500 идентификаторов, `comment` тела `POST …/status` — не больше 1000 символов, `q` фильтров `GET …/osv` и экспорта — не больше 500 символов. diff --git a/openspec/specs/directories-api/spec.md b/openspec/specs/directories-api/spec.md index ab75633..ad52e67 100644 --- a/openspec/specs/directories-api/spec.md +++ b/openspec/specs/directories-api/spec.md @@ -46,7 +46,8 @@ API справочников ядра: группы компаний (ГК), ю `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` — снимки. +членствами ГСК/ГСЗ и алиасами; `GET …/{id}/history` — снимки. В списке и карточке SHALL быть `documents_count` — число +документов файлов ЮЛ, кроме файлов `SUPERSEDED`. #### Scenario: Неверная длина ИНН - **WHEN** создаётся ИП с ИНН из 10 цифр @@ -56,6 +57,10 @@ API справочников ядра: группы компаний (ГК), ю - **WHEN** ЮЛ создано без `company_group_id` - **THEN** оно отдаётся в списке с `outside_group = true` и видно пользователям филиала `regional_office_id` +#### Scenario: Число документов +- **WHEN** у монозаёмщика файл с книгой НПЗ (ББ и ОФР) и заменённый файл с одной ОСВ +- **THEN** `documents_count = 2` + ### Requirement: Перевод ЮЛ в другую ГК и вывод из состава `POST /api/v1/directories/legal-entities/{id}/move` (`DIRECTORY_EDIT`; тело `company_group_id`, `effective_date` ≥ сегодня) SHALL сменить ГК, записать `legal_entity_history(event_type = MOVE_TO_GROUP, effective_date)`, закрыть все текущие diff --git a/openspec/specs/documents-api/spec.md b/openspec/specs/documents-api/spec.md index 2ebac25..5ef99a6 100644 --- a/openspec/specs/documents-api/spec.md +++ b/openspec/specs/documents-api/spec.md @@ -10,13 +10,17 @@ возвращать `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`). -`GET /api/v1/files/{file_id}` SHALL отдавать ту же карточку; чужой или несуществующий файл — 404. +`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: Документ и строки `GET /api/v1/documents/{document_id}` (`DOCUMENT_VIEW`, policy по ЮЛ владельца) SHALL отдавать документ, статус, проверки, `locator`, схему группировок листа из `recognition_job.group_schema` и `file_id`. @@ -31,10 +35,15 @@ `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`. Если совпали несколько условий, приоритет у -`FILE_SUPERSEDED`. Подтверждение берёт блокировки в порядке «файл → документ → (advisory-блокировка привязок ГК) → версии консолидации -(по возрастанию `id`)»: файл — `FOR UPDATE`, документ — `FOR NO KEY UPDATE` (R-3; совместима со вставкой привязки -`FOR KEY SHARE`), версии — `FOR UPDATE`; состояние перечитывается под этими блокировками; документ или файл, исчезнувшие за время ожидания блокировки, — 404 `NOT_FOUND`. +другом статусе (например, `ERROR`) — 409 `DOCUMENT_NOT_CONFIRMABLE`. Для `BALANCE`/`INCOME` дополнительно: пустые +`period_year` или `period_quarter` — 409 `PERIOD_REQUIRED`; непройденные неакцептованные жёсткие проверки §7 — 409 +`CHECKS_NOT_PASSED` со списком нарушений в `detail.failures`; при подтверждении 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` = квартал @@ -71,6 +80,18 @@ - **WHEN** подходящий документ подтверждается при версии `CANCELLED` - **THEN** привязки нет +#### Scenario: Форма с нарушением проверок +- **WHEN** подтверждается `BALANCE` с непройденным жёстким соотношением без акцепта +- **THEN** 409 `CHECKS_NOT_PASSED`, в `detail.failures` — нарушение; после акцепта подтверждение проходит + +#### Scenario: Форма без периода +- **WHEN** подтверждается форма из OCR, у которой нет ни даты отчёта, ни дат колонок +- **THEN** 409 `PERIOD_REQUIRED`; после правки даты отчёта подтверждение проходит + +#### Scenario: Форма в чек-листе +- **WHEN** версия ГК 2025 Q3 `INITIATED`, ЮЛ в периметре; подтверждается его `BALANCE` за 2025 Q3 +- **THEN** привязка создана, ячейка «Бух. баланс» чек-листа ЮЛ — `CONFIRMED`, `reviewed_by` шапки = пользователь + ### Requirement: Подхват документов при создании версии и расширении периметра При создании версии и при добавлении ЮЛ в периметр ядро SHALL привязать уже подтверждённые документы этого ЮЛ из незаменённых файлов, подходящие версии (условия — как при подтверждении); на один ключ (`document_type`, @@ -85,3 +106,85 @@ #### Scenario: Две подтверждённые версии одного документа - **WHEN** у ЮЛ два незаменённых файла с `CONFIRMED` ОСВ 62 за 2024 Q3 (разные `locator`), создана версия 2024 Q3 - **THEN** привязан документ с более поздним `confirmed_at` + +### 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/specs/osv-result-mapping/spec.md b/openspec/specs/osv-result-mapping/spec.md index 3c77f02..aa34a57 100644 --- a/openspec/specs/osv-result-mapping/spec.md +++ b/openspec/specs/osv-result-mapping/spec.md @@ -12,8 +12,10 @@ 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` -и выполнить автозамену. Inbox MUST NOT читать файл результата и MUST NOT создавать документы. +в одной транзакции создать документы и строки (листы ОСВ и формы ББ/ОФР), записать `recognition_job.group_schema`, +перевести файл в `PROCESSED` и выполнить автозамену. Результат без документов SHALL давать неповторяемую ошибку +маппинга «в результате нет документов»; результат с формами, но без ОСВ, ошибкой MUST NOT быть. Inbox MUST NOT читать +файл результата и MUST NOT создавать документы. #### Scenario: Результат смапплен - **WHEN** в S3 лежит результат парсера для `rb/2024-FY/ОСВ_66 за 2024.xls`, задание `done`, файл `PROCESSING` @@ -34,6 +36,14 @@ LOCKED`), проверять, что `result_s3_object` относится к б - **WHEN** маппинг прерван после частичной вставки (имитация) и выполнен снова - **THEN** у файла ровно один набор документов и строк, дублей по `(document_id, row_number)` нет +#### Scenario: Файл только с формами +- **WHEN** смаплен golden-снимок `mezheninovskaya/2025-Q3/1_Форма1_бух_09` (одна форма `BALANCE`, ОСВ нет) +- **THEN** файл `PROCESSED`, создан документ `BALANCE`, ошибки «нет документов ОСВ» нет + +#### Scenario: Пустой результат +- **WHEN** результат без документов (`documents = []`): контракт v1.x такого результата не допускает — его останавливает проверка схемы, а прямой вызов `map_file` даёт `MappingFatal` +- **THEN** файл `ERROR`, причина в `metrics.mapping_error` (при проверке схемы — ошибка схемы, в `map_file` — «в результате нет документов»), повтор не выполняется + ### Requirement: Документ по листу ОСВ На каждый `osvDocument` ядро SHALL создать `document` с `document_type = OSV`, `osv_account_code` = нормализованный код из шапки (`60.1 → 60.01`, `58.03`, буквенные сегменты как есть), `period_year`, `period_quarter`, `is_cumulative` @@ -110,3 +120,69 @@ SHALL пропускаться с записью в лог. Привязка `co #### Scenario: Документ RECOGNIZED - **WHEN** смапплен файл с единственным документом `RECOGNIZED` - **THEN** статус версии не изменился + +### 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/specs/statement-checks/spec.md b/openspec/specs/statement-checks/spec.md new file mode 100644 index 0000000..1e104c5 --- /dev/null +++ b/openspec/specs/statement-checks/spec.md @@ -0,0 +1,57 @@ +# statement-checks Specification + +## Purpose +Логические проверки форм ББ/ОФР §7 в ядре: чистые функции над текущими суммами документа, без хранения результата; +акцепт непройденных проверок аналитиком. + +## 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/specs/users-admin/spec.md b/openspec/specs/users-admin/spec.md index bde6843..bd20572 100644 --- a/openspec/specs/users-admin/spec.md +++ b/openspec/specs/users-admin/spec.md @@ -67,3 +67,17 @@ SPA SHALL содержать маршрут `/admin` (пункт «Систем #### Scenario: Нет права - **WHEN** пользователь без `USERS_ADMIN` открывает `/admin` - **THEN** страница показывает сообщение об отсутствии доступа и не делает запросов к `/admin/*` + +### 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` -- 2.47.2