# Spec Delta ## Purpose Дельта change `quality-metrics` (метрики качества первички). ## 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): загрузка этим маршрутом — без контекста консолидации и не меняет статусы версий; загрузка в консолидацию — требование «Загрузка файлов в консолидацию». Для PDF `stored_file.is_scan` SHALL определяться при загрузке по наличию текстового слоя в первых 10 страницах (разбор pypdf — вне event loop, `asyncio.to_thread`): нет текста — скан (`true`), есть — цифровой (`false`); для не-PDF — NULL. PDF со скрытым OCR-слоем считается цифровым. #### 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 — маршрут ЮЛ периметр не проверяет; проверка периметра — только у загрузки в консолидацию #### Scenario: Признак скана - **WHEN** загружается PDF без текстового слоя - **THEN** `stored_file.is_scan = true`; у xlsx `is_scan IS NULL` ### Requirement: Удаление файла `DELETE /api/v1/files/{file_id}` (`DOCUMENT_EDIT`, policy) SHALL удалить строки ОСВ, документы, задания и `stored_file`, проставить `s3_object.expires_at = now()` (объект физически не удаляется, OQ-22) и записать `audit_event(FILE_DELETE, before_value = карточка файла, after_value = {unbound_version_ids})`. Удаление MUST быть отклонено 409 `FILE_HAS_CONFIRMED_DOCUMENTS`, если у файла есть документ `CONFIRMED`, подтверждённый пользователем (`confirmed_by` не NULL), или если автоподтверждённый документ файла (`confirmed_by` NULL) привязан к версии в статусе, отличном от `INITIATED`, `DOCUMENTS_PROCESSING`, `DATA_CORRECTION` (OQ-157, уточняет OQ-110); и 409 `FILE_SUPERSEDES_OTHERS`, если на файл ссылаются `superseded_by_file_id`. При удалении файла с автоподтверждёнными документами их привязки `consolidation_document` SHALL сниматься, статус версий MUST NOT меняться, `after_value.unbound_version_ids` — id версий, где привязки сняты (по возрастанию; пустой список, если привязок не было). Блокировки: задания → файл → документы → advisory-блокировка привязок ГК → версии по `id`. Файл `SUPERSEDED` удалить можно. Если у файла есть `quality_intake_fact` (по `file_id`), объект `uploads` файла MUST NOT получать `expires_at` — он нужен эталонному прогону качества; сырой ответ ИС (`raw`) при удалении `expires_at` не получает никогда. Для документов с исходом удаление SHALL писать факт исхода `DELETED` / `DELETED_AFTER_CONFIRM` в той же транзакции (спека `quality-metrics`). #### Scenario: Удаление ошибочного файла - **WHEN** файл в `ERROR` удаляется владельцем - **THEN** 204; `stored_file`, `recognition_job` отсутствуют; `s3_object.expires_at` заполнен; `audit_event(FILE_DELETE)` с `after_value.unbound_version_ids = []` #### Scenario: Подтверждённый документ - **WHEN** удаляется файл с документом `CONFIRMED`, у которого `confirmed_by` = пользователь - **THEN** 409 `FILE_HAS_CONFIRMED_DOCUMENTS`, ничего не изменилось #### Scenario: Автоподтверждённый документ в корректировке - **WHEN** версия 2025 Q1 в `DATA_CORRECTION`, к ней привязан автоподтверждённый документ ОСВ 60 РБ; файл удаляется - **THEN** 204; привязка снята, версия осталась в `DATA_CORRECTION`; `FILE_DELETE.after_value.unbound_version_ids` = [id версии] #### Scenario: Версия ушла на согласование - **WHEN** автоподтверждённый документ файла привязан к версии в `CONSOLIDATION_RF` - **THEN** 409 `FILE_HAS_CONFIRMED_DOCUMENTS`, привязка и файл на месте #### Scenario: Автоподтверждённый без привязок - **WHEN** удаляется файл, все документы которого автоподтверждены и ни к одной версии не привязаны - **THEN** 204; `after_value.unbound_version_ids = []` #### Scenario: Файл с фактом приёма - **WHEN** удаляется файл, по документам которого записан `quality_intake_fact` - **THEN** 204; объект `uploads` и сырой ответ остаются без `expires_at`, результат (`results`) экспирится; записан факт исхода