# 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` со списком нарушений в `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`, в `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-кодированное исходное имя