## MODIFIED Requirements ### Requirement: Загрузка файлов организации `POST /api/v1/legal-entities/{legal_entity_id}/files` (multipart, одно или несколько полей `files`; право `DOCUMENT_EDIT`; ЮЛ в области видимости пользователя, иначе 404) SHALL для каждого файла в отдельной транзакции: определить тип по расширению и сверить с сигнатурой содержимого (`xls` — BIFF8, `xlsx`/`xlsb` — zip, `pdf`, `png`, `jpg`); отвергнуть файл больше `UPLOAD_MAX_BYTES` (50 МиБ) или неподдерживаемого типа; вычислить sha256; создать `s3_object` (`bucket_role = uploads`, ключ `uploads/{file_id}/{job_id}/{безопасное имя}`, `sha256`, `size_bytes`, `content_type`, `created_by = user:{id}`), положить объект в S3, создать `stored_file` (`status = UPLOADED`, `version_no = 1`, `uploaded_by`, `uploaded_at`, `content_hash`, `original_name`, `consolidation_id = NULL`) и `recognition_job` (`status = queued`, `pipeline` по типу: xls/xlsx/xlsb → `tabular`, pdf/png/jpg → `image`; `max_attempts` 3 / 1), установить `stored_file.current_recognition_job_id`, записать `audit_event(FILE_UPLOAD, PRIMARY_DOCUMENTS)` с `after_value.consolidation_id = null`. Ответ SHALL перечислять принятые файлы (`accepted[]` с `file_id`, `job_id`, `name`) и отклонённые (`rejected[]` с `name`, `code`, `message`); код ответа 201, если принят хотя бы один, иначе 422. Тип документа и период при загрузке MUST NOT запрашиваться (макет `40`). Без доступного S3 — 503 `STORAGE_UNAVAILABLE`. Query-параметр `consolidation_version_id` MUST NOT приниматься (удалён в v5.18): загрузка этим маршрутом — без контекста консолидации и не меняет статусы версий; загрузка в консолидацию — требование «Загрузка файлов в консолидацию». #### Scenario: Реальная ОСВ принята и ушла в outbox - **WHEN** `ka_rf` загружает `rb/2024-FY/ОСВ_66 за 2024.xls` для ЮЛ своего филиала - **THEN** 201, `accepted[0].file_id` существует; `stored_file.status = UPLOADED`, `file_type = XLS`, `content_hash` = sha256 файла, `consolidation_id IS NULL`; `recognition_job.status = queued`, `pipeline = tabular`; объект в S3 по ключу `uploads/{file_id}/{job_id}/ОСВ_66 за 2024.xls`; публикатор outbox публикует запрос с этим ключом #### Scenario: Смесь принятых и отклонённых - **WHEN** в одном запросе xlsx, файл `.docx` и файл 51 МиБ - **THEN** 201, `accepted` — один, `rejected` — два с кодами `UNSUPPORTED_FILE` и `FILE_TOO_LARGE` #### Scenario: Расширение не совпадает с содержимым - **WHEN** загружается файл `osv.xlsx`, байты которого — BIFF8 - **THEN** файл отклонён с кодом `UNSUPPORTED_FILE` #### Scenario: Чужое ЮЛ - **WHEN** `ka_rf` загружает файл для ЮЛ другого филиала - **THEN** 404, объектов и записей нет #### Scenario: Загрузка в контексте версии - **WHEN** у ГК ЮЛ есть версия `INITIATED` с этим ЮЛ в периметре, файл загружен маршрутом ЮЛ с query `consolidation_version_id` (как делал клиент до v5.18) - **THEN** 201, параметр игнорируется; `stored_file.consolidation_id IS NULL`; версия остаётся `INITIATED` #### Scenario: ЮЛ вне периметра - **WHEN** ЮЛ не входит в периметр ни одной версии, файл загружен маршрутом ЮЛ - **THEN** 201 — маршрут ЮЛ периметр не проверяет; проверка периметра — только у загрузки в консолидацию ### Requirement: Автозамена прежних версий После успешного маппинга файла ядро SHALL найти другие файлы того же ЮЛ со статусом не `SUPERSEDED`, у которых есть документ с теми же `document_type`, `osv_account_code`, `period_year`, `period_quarter`, и перевести их в `SUPERSEDED` (`superseded_by_file_id` = новый файл, `updated_at`); новый файл получает `version_no` = максимум среди заменённых + 1. Если у нового файла `consolidation_id IS NULL`, он SHALL получить `consolidation_id` первого по `id` заменённого файла, у которого он задан (v5.18); файл, загруженный в консолидацию, сохраняет свой `consolidation_id`. Документы и строки заменённых файлов SHALL сохраняться (история), но по умолчанию не показываться. Привязки `consolidation_document` документов заменённых файлов к версиям «в работе» SHALL удаляться в той же транзакции (версии блокируются `FOR UPDATE` по возрастанию `id` после старых файлов); привязки к версиям в других статусах не трогаются; статус версии не меняется. Аудит `FILE_REPLACE` (`object_id` заменённого, `after_value` с новым файлом и `unbound_version_ids`, в `description` — перечень заменённых документов и признак `confirmed`, если заменён документ `CONFIRMED`). Решение владельца 05.10.2026: замена происходит и для `CONFIRMED`. Замена остаётся на уровне файла (OQ-115), но если у заменённого файла есть документ, ключа которого (`document_type`, `osv_account_code`, `period_year`, `period_quarter`) нет среди документов нового файла (в т. ч. документ без года или квартала), ядро SHALL добавить в `metrics.mapping_warnings` задания нового файла (карточка файла, `JobBrief.mapping_warnings`) по одному предупреждению на заменённый файл (OQ-154): имя и версия заменённого файла, выпавшие документы (тип, счёт, период, статус — в т. ч. `CONFIRMED`) и версии консолидации «в работе», с которых сняты привязки этих документов. Поведение замены и статусы документов нового файла от предупреждения не меняются. #### Scenario: Новая ОСВ 60 за 1 кв. 2025 - **WHEN** у ЮЛ есть `PROCESSED` файл с документом OSV 60 / 2025 / Q1 (`CONFIRMED`), загружен и смапплен новый файл с таким же документом - **THEN** прежний файл — `SUPERSEDED`, `superseded_by_file_id` = новый, у нового `version_no = 2`; `audit_event(FILE_REPLACE)` с `description`, содержащим `confirmed=true`; список файлов ЮЛ по умолчанию показывает только новый #### Scenario: Заменённый документ был в версии - **WHEN** заменённый `CONFIRMED` документ ОСВ 60 за 2024 Q2 был привязан к версии 2024 Q3 в `DATA_CORRECTION` - **THEN** привязка удалена; в чек-листе ячейка ОСВ 60 показывает `missing_quarters = ["Q2"]` (или `NEEDS_CHECK` по новому документу), пока новый документ не подтверждён; статус версии прежний #### Scenario: Отдельный ОФР заменил книгу «ББ + ОФР» - **WHEN** у ЮЛ есть файл с `CONFIRMED` ББ и ОФР за Q3 2025, ББ привязан к версии 2025 Q3 в `DATA_CORRECTION`; смаплен новый файл только с ОФР за Q3 2025 - **THEN** прежний файл — `SUPERSEDED`, привязка ББ снята; в `metrics.mapping_warnings` нового задания одно предупреждение: имя прежнего файла, `BALANCE 2025 Q3 (CONFIRMED)` и версия «ГК» 2025 Q3, версия 1; ОФР в предупреждении нет #### Scenario: Новый файл без контекста наследует консолидацию - **WHEN** прежний файл загружен в консолидацию 2025 Q1 (`consolidation_id = C`), новый файл с тем же документом загружен маршрутом ЮЛ (`consolidation_id IS NULL`) и смаплен - **THEN** у нового файла `consolidation_id = C` #### Scenario: Новый файл с контекстом сохраняет свой - **WHEN** прежний файл в консолидации 2025 Q1 (`C1`), новый с тем же документом загружен в консолидацию 2025 Q2 (`C2`) - **THEN** у нового файла `consolidation_id = C2` ## ADDED Requirements ### Requirement: Загрузка файлов в консолидацию `POST /api/v1/consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files` (multipart `files`, право `DOCUMENT_EDIT`; §9.5 п. 8 v5.18) SHALL до приёма файлов проверить: консолидация видима (ГК в области пользователя; чужая и несуществующая — одинаковый 404 `NOT_FOUND` «консолидация не найдена»), ЮЛ видимо (404), у консолидации есть хотя бы одна версия со статусом «в работе» (`INITIATED`, `DOCUMENTS_PROCESSING`, `DATA_CORRECTION`; иначе 409 `CONSOLIDATION_NOT_EDITABLE`), ЮЛ — член периметра хотя бы одной такой версии (иначе 422 `ENTITY_NOT_IN_PERIMETER` объектом `detail`). Приём файлов, дедуп, ответ и коды `rejected[]` — как у маршрута ЮЛ; принятый файл SHALL получить `stored_file.consolidation_id = consolidation_id`, `FILE_UPLOAD.after_value.consolidation_id` = его строка. Если принят хотя бы один файл, ядро SHALL в отдельной транзакции после приёма заблокировать `FOR UPDATE ORDER BY id` версии этой консолидации со статусом `INITIATED` и ЮЛ в периметре и перевести каждую в `DOCUMENTS_PROCESSING` (`changed_by` = загрузивший, запись `consolidation_status_history` и `CONSOLIDATION_STATUS_CHANGE`); сбой перехода логируется и не меняет ответ. Блокировка версии (`consolidation_lock`) для загрузки MUST NOT требоваться. #### Scenario: Загрузка в консолидацию переводит версию - **WHEN** у консолидации 2024 Q3 версия 1 в `INITIATED`, ЮЛ в периметре, загружен один принятый файл - **THEN** 201; `stored_file.consolidation_id` = консолидация; версия в `DOCUMENTS_PROCESSING`, история с `changed_by` = пользователь; `FILE_UPLOAD.after_value.consolidation_id` = id консолидации #### Scenario: Версия в другом рабочем статусе - **WHEN** единственная версия консолидации в `DATA_CORRECTION`, загружен принятый файл - **THEN** 201; статус версии не меняется; файл получил `consolidation_id` #### Scenario: Нет версии в работе - **WHEN** единственная версия консолидации `CANCELLED` - **THEN** 409 `CONSOLIDATION_NOT_EDITABLE`, файлы не приняты, объектов в S3 нет #### Scenario: ЮЛ вне периметра - **WHEN** ЮЛ той же ГК не входит в периметр ни одной версии «в работе» - **THEN** 422 `ENTITY_NOT_IN_PERIMETER`, файлы не приняты #### Scenario: Чужая и несуществующая консолидация - **WHEN** `ka_rf` загружает файл в консолидацию ГК другого филиала и в несуществующий id - **THEN** оба ответа 404 с одинаковым телом (`code = NOT_FOUND`), записей нет #### Scenario: Все файлы отклонены - **WHEN** в консолидацию с версией `INITIATED` загружен только `.docx` - **THEN** 422 телом `UploadResult`, версия остаётся `INITIATED` #### Scenario: Загрузка не требует блокировки версии - **WHEN** версию держит другой пользователь (`consolidation_lock`), ЮЛ в периметре - **THEN** 201; версия `INITIATED` → `DOCUMENTS_PROCESSING`