2026-10-10 10:04:07 +04:00

277 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# documents-api Specification
## Purpose
Чтение файлов, документов и строк ОСВ для панели «Первичные документы» (подсрез 2d) и подтверждение документа.
## Requirements
### Requirement: Файлы организации и карточка файла
`GET /api/v1/legal-entities/{id}/files` (`DOCUMENT_VIEW`, policy; `include_superseded=false` по умолчанию) SHALL
возвращать `Page` файлов ЮЛ по убыванию `uploaded_at`: поля `stored_file` (в том числе `consolidation_id: uuid | null`,
v5.18 — консолидация, в контексте которой файл загружен), текущее задание (`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`
#### Scenario: Консолидация в карточке
- **WHEN** один файл ЮЛ загружен в консолидацию `C`, другой — без контекста
- **THEN** в списке ЮЛ и в `GET /files/{id}` у первого `consolidation_id = C`, у второго `consolidation_id = null`
### Requirement: Документ и строки
`GET /api/v1/documents/{document_id}` (`DOCUMENT_VIEW`, policy по ЮЛ владельца) SHALL отдавать документ, статус,
проверки, `locator`, схему группировок листа из `recognition_job.group_schema` и `file_id`.
`GET /api/v1/documents/{document_id}/entries` SHALL отдавать `Page` строк по возрастанию `row_number` (`limit` ≤ 2000,
по умолчанию 500) с полями `osv_entry`, включая `parent_entry_id`, `account_id`, `counterparty_id`.
#### Scenario: Строки деревом
- **WHEN** запрошены строки документа ОСВ 66
- **THEN** первая строка — счёт `66` уровня 0 без родителя, у строк уровня 2 `parent_entry_id` указывает на субсчёт
### 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` со списком нарушений в `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` = квартал
версии; 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`, в `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`,
`osv_account_code`, `period_quarter`, признак «нарастающий»; NULL признака — нарастающий; для ОСВ за Q1 признак не
различается, R-1, R-14) — документ с самым поздним `confirmed_at`. Удаление ЮЛ из периметра SHALL
снимать привязку всех его документов в этой версии. Подхват и отвязка статус версии MUST NOT менять.
#### Scenario: Фикстуры, подтверждённые до консолидации
- **WHEN** ОСВ 60 РБ за Q1, Q2, Q3 2024 подтверждены, затем создана консолидация ГК за 2024 Q3
- **THEN** три `consolidation_document` на эти документы; в чек-листе ячейка ОСВ 60 РБ — `CONFIRMED`; версия в `INITIATED`
#### 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`, `origin`, `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.
Форма SHALL достраиваться в ответе по справочнику строк редакции `edition` (`api/rules/statement_lines.yaml`, свой
список кодов и наименований для `BALANCE` и `INCOME` каждой редакции): строки справочника, кода которых нет в форме,
добавляются с `id = null`, `origin = "template"`, наименованием из справочника и `*_confidence = null`; у каждой строки
SHALL быть значение по каждой колонке формы — недостающее добавляется с `id = null`, `amount = null`,
`string_value = null`, `confidence = null`, `is_recognized = null`, `is_modified = false`, `original_amount = null`,
`modified_by = null`, `modified_at = null`. При `edition = null` строки не достраиваются, недостающие значения —
достраиваются. Строка из БД имеет `origin = "source"`, если пришла из источника, и `"template"`, если создана правкой
аналитика (`line_code_original_value IS NULL`). Строки источника, которых нет в справочнике, отдаются как есть.
Достроенные строки и значения MUST NOT записываться в БД при чтении.
#### Scenario: Форма для экрана
- **WHEN** запрошена форма `BALANCE` из `mezheninovskaya/2025-Q3/1_Форма1_бух_09`
- **THEN** `edition = fsbu4`, все 37 строк файла — `origin = source` с `id`, каждый код справочника `fsbu4` / `BALANCE`
есть среди `lines`, у каждой строки значения по всем колонкам, `checks.failed` пуст
#### Scenario: Строка, потерянная источником
- **WHEN** у формы `BALANCE` за 2025 Q1 в БД нет строки 1700 (OCR склеил «1500\n1700» до OQ-156)
- **THEN** в `lines` есть 1700: `id = null`, `origin = template`, `line_name = "БАЛАНС"`, значения по всем колонкам
`id = null`, `amount = null`, `is_recognized = null`; статус документа и `checks` — те же, что без достройки
(`1600 = 1700` нарушено: 1700 отсутствует и считается 0)
#### Scenario: Недостающая ячейка
- **WHEN** у строки 1150 в БД есть значение только для колонки 0, а у формы две колонки
- **THEN** у 1150 два значения; значение колонки 1 — `id = null`, `amount = null`
#### Scenario: Нет даты периода
- **WHEN** у формы OCR не определена дата периода (`edition = null`)
- **THEN** `lines` — только строки из БД, у каждой значения по всем колонкам
#### 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` | `line_code`, плюс `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`».
Ячейка, которой нет в БД, SHALL создаваться при правке:
- по `line_id` строки формы и `column_id` колонки формы, если у строки нет значения в этой колонке;
- по `line_code`, если строки с этим кодом в форме нет, а код есть в справочнике редакции документа для типа формы:
создаётся `financial_statement_line` (`line_code`, `line_name` из справочника, `line_code_original_value = NULL`,
`line_name_original_value = NULL`, `*_confidence = NULL`, `*_modified = false`); несколько ячеек одной новой строки в
запросе создают одну строку.
Созданная ячейка — `original_amount = NULL`, `is_recognized = NULL`, `confidence = NULL`, `string_value = NULL`, далее
общая логика правки. `line_code`, строка с которым в форме уже есть, код вне справочника редакции или редакция не
определена — 422 `UNKNOWN_CELL`; колонка не этой формы — 422 `UNKNOWN_CELL`; одна ячейка дважды в запросе — 422
`DUPLICATE_CELL`. `amount = null` для ячейки, которой нет в БД, — не изменение: ничего не создаётся и не аудируется.
#### 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** у формы `BALANCE` редакции `fsbu4` нет строки 1700, аналитик передаёт `{line_code: "1700", column_id: <колонка 0>, amount: 2371333}`
- **THEN** в БД создана строка 1700 (`line_code_original_value = NULL`, `line_name = "БАЛАНС"`) и значение
`amount = 2371333`, `original_amount = NULL`, `is_modified = true`; в ответе у 1700 `id` заполнен, `origin = template`;
аудит `RECOGNIZED_DATA_EDIT` с `before.amount = null`; акцепт сброшен, статус пересчитан
#### Scenario: Код, который уже есть в форме
- **WHEN** передан `line_code: "1150"`, а строка 1150 в форме есть
- **THEN** 422 `UNKNOWN_CELL`, данные не изменились
#### Scenario: Код вне справочника
- **WHEN** передан `line_code: "1105"` для формы редакции `66n`
- **THEN** 422 `UNKNOWN_CELL`
#### Scenario: Пустая сумма в несуществующую ячейку
- **WHEN** передан `{line_code: "1530", column_id: <колонка 0>, amount: null}` и строки 1530 нет
- **THEN** 200, строка не создана, аудита нет
#### 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-кодированное исходное имя
### Requirement: Рабочий стол консолидации для ЮЛ
`GET /api/v1/consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` (`DOCUMENT_VIEW`; `limit`,
`offset`, `include_superseded=false` по умолчанию; §9.5 п. 11 v5.18) SHALL отвечать 404 `NOT_FOUND` на консолидацию вне
видимости или несуществующую («консолидация не найдена») и на ЮЛ вне видимости, иначе возвращать `Page[FileOut]` —
объединение файлов ЮЛ, у которых `stored_file.consolidation_id = consolidation_id`, и файлов ЮЛ, у которых есть документ
с записью `consolidation_document` к любой версии этой консолидации; порядок и карточка — как у списка файлов ЮЛ;
`SUPERSEDED` — только при `include_superseded=true` (в обоих множествах). Принадлежность ЮЛ к ГК консолидации не
проверяется: чужому ЮЛ соответствует пустой список.
#### Scenario: Стол показывает только файлы консоли
- **WHEN** у ЮЛ три файла: загружен в консолидацию 2025 Q1, загружен в консолидацию 2025 Q2, без контекста
- **THEN** стол 2025 Q1 содержит только первый; стол 2025 Q2 — только второй; `GET /legal-entities/{id}/files` — все три
#### Scenario: Подхваченный файл виден через привязку
- **WHEN** файл с `CONFIRMED` ОСВ за 2025 Q1 загружен без контекста и его документ привязан к версии консолидации 2025 Q2
(подхват при создании версии)
- **THEN** файл есть в столе 2025 Q2, хотя `consolidation_id IS NULL`
#### Scenario: Заменённый файл скрыт по умолчанию
- **WHEN** файл консолидации заменён (`SUPERSEDED`) новым файлом той же консолидации
- **THEN** по умолчанию в столе только новый; с `include_superseded=true` — оба
#### Scenario: Чужая консолидация
- **WHEN** `ka_rf` запрашивает стол консолидации ГК другого филиала и несуществующий id
- **THEN** оба ответа 404 с одинаковым телом