Andrey Epifancev 353b562dfc AURORA-1644 автозамена не вытесняет документ другой организации (#181)
Пара «новый документ — прежний» с counterparty_check = MISMATCH у любого из двух
в автозамене не участвует: сканы разных организаций в одной карточке больше не
вытесняют друг друга. UNKNOWN заменяется как прежде. Если файл оставлен только
из-за MISMATCH, в mapping_warnings нового файла — «автозамена не выполнена»
с именем и версией оставленного файла. Спека primary-files дополнена.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 00:54:27 +04:00

321 lines
33 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.

# primary-files Specification
## Purpose
Загрузка файлов первичной отчётности в ядро: физический файл в S3 и `stored_file`, задание распознавания в outbox,
дедуп, жизненный цикл статуса файла (§9.2), автозамена прежних версий и удаление.
## 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: Дедуп по содержимому
Файл с тем же sha256, что у существующего `stored_file` того же ЮЛ со статусом не `SUPERSEDED`, SHALL отклоняться с
кодом `DUPLICATE_FILE` и `file_id` существующего. Тот же файл у другого ЮЛ SHALL приниматься.
#### Scenario: Повторная загрузка того же файла
- **WHEN** тот же xls загружается во второй раз для того же ЮЛ
- **THEN** `rejected[0].code = DUPLICATE_FILE`, `rejected[0].file_id` — прежний файл; новых записей нет
### Requirement: Жизненный цикл статуса файла
`stored_file.status` SHALL меняться только по заданию `current_recognition_job_id`: `UPLOADED` при загрузке и при принятом ручном повторе с новым текущим заданием;
`PROCESSING` при ack сервиса; `ERROR` при финальной ошибке задания (`error`, `timeout`, `dlq`) и при невозможности
маппинга; `PROCESSED` после успешного маппинга; `SUPERSEDED` при автозамене. Карточка файла SHALL показывать причину
`ERROR` (`error_code`/`error_message` задания или ошибку маппинга).
#### Scenario: Парсер вернул ошибку
- **WHEN** конверт `error UNSUPPORTED_FORMAT` для задания файла
- **THEN** `stored_file.status = ERROR`, в карточке файла `error_code = UNSUPPORTED_FORMAT`
#### Scenario: Сервис не ответил
- **WHEN** задание переведено вахтёром в `timeout`
- **THEN** файл — `ERROR` с `error_code = TIMEOUT`
### 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`) и версии консолидации «в работе», с которых сняты
привязки этих документов. Поведение замены и статусы документов нового файла от предупреждения не меняются.
Пара «новый документ — прежний» с `counterparty_check = MISMATCH` у любого из двух SHALL NOT участвовать в замене
(AURORA-1644, решение владельца 21.10.2026): документы другой организации, загруженные в карточку ЮЛ, не вытесняют друг
друга и документы организации карточки. `UNKNOWN` (пустая шапка) несовпадением не считается. Если у ЮЛ есть файл с
документом того же ключа, оставленный только из-за `MISMATCH`, ядро SHALL добавить в `metrics.mapping_warnings` нового
файла по одному предупреждению на такой файл: «автозамена не выполнена», имя и версия оставленного файла, его документы
этого ключа (тип, счёт, период, статус).
#### 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`
#### Scenario: Документы разных организаций в одной карточке
- **WHEN** в карточку ЮЛ (ИНН 6609493015) последовательно загружены и смаплены три скана ББ за 2026 Q2 разных
организаций (в шапках другие ИНН, `counterparty_check = MISMATCH`)
- **THEN** ни один файл не `SUPERSEDED`, список файлов ЮЛ показывает все три, у каждого `version_no = 1`, `FILE_REPLACE`
нет; у второго и третьего в `metrics.mapping_warnings` — «автозамена не выполнена» с именами и версиями оставленных файлов
#### Scenario: Документ организации карточки не вытесняет чужой
- **WHEN** у ЮЛ есть файл с ББ 2026 Q2 `MISMATCH`, смаплен новый файл с ББ 2026 Q2 `MATCH`
- **THEN** прежний файл остаётся `PROCESSED`, у нового `version_no = 1` и предупреждение «автозамена не выполнена»
#### Scenario: Пустая шапка заменяется как прежде
- **WHEN** у прежнего или нового документа того же ключа `counterparty_check = UNKNOWN`, у другого — не `MISMATCH`
- **THEN** прежний файл — `SUPERSEDED`, у нового `version_no = 2`, предупреждения «автозамена не выполнена» нет
### 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` удалить можно.
#### 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 = []`
### 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`
### Requirement: Очистка форм перед выкладкой
Перед выкладкой ядра с миграцией v5.19 на контур (шаг 3 порядка выкладки) база SHALL очищаться от форм ББ/ОФР (Р-24):
формы пересоздаются повторной загрузкой файлов уже по новым правилам. Очистку выполняет владелец или платформенная
команда по его заявке, на контуре — после резервной копии БД и до `alembic upgrade` v5.19 (на стендах разработчиков — по
желанию). Инструмент — скрипт ядра `scripts/purge_statements.py` (пишется в PR шага 3; описание здесь, кода нет) с
режимами `--dry-run` (по умолчанию: только отчёт) и `--apply`, одной транзакцией:
1. **Отбор:** документы `BALANCE` / `INCOME` (у файлов любого статуса, включая `SUPERSEDED`).
2. **Отказ без изменений**, если хотя бы один отобранный документ упомянут в `ex_snapshot_item` или привязан
(`consolidation_document`) к версии консолидации в статусе «Верифицирована» или дальше — `VERIFIED` или `ARCHIVED`
верифицированной версии (есть `ex_snapshot` с `source_version_id` версии): такая версия read-only и заморожена в ЕХ.
Отчёт называет версии, решение — за владельцем. Версии в работе, на согласовании, отклонённые (`REJECTED`) и
отменённые (`CANCELLED`, в том числе архивированные) очистке не мешают: привязки к ним снимаются, как к версиям
«в работе» (п. 3, 4), статус версий не меняется (Р-35).
3. **Файлы только с формами** — у файла нет документов `OSV`: удаляется файл целиком, как `DELETE /files/{id}` (§9.5 п. 5),
но без запрета `FILE_HAS_CONFIRMED_DOCUMENTS`: привязки документов к неверифицированным версиям (п. 2) снимаются
(статус версий не меняется), удаляются документы всех типов файла (`BALANCE`, `INCOME`, `DISCLOSURE`, `OTHER`) с
`financial_statement_*` и комментариями документов, задания `recognition_job` файла и `stored_file`; объектам S3
ставится `expires_at = now()`; ссылка `superseded_by_file_id` других файлов на удаляемый обнуляется, а заменённые им
файлы остаются `SUPERSEDED` (повторная загрузка заменит их снова; отчёт перечисляет такие файлы, если в них есть ОСВ).
4. **Смешанные файлы** — у файла есть и ОСВ, и формы: удаляются только документы `BALANCE` / `INCOME` с их
`financial_statement_*`, комментариями и привязками; файл, ОСВ, `DISCLOSURE` / `OTHER` и задания остаются. Повторно
загрузить такой файл нельзя (дедуп по `sha256`), поэтому отчёт `--dry-run` перечисляет смешанные файлы отдельно, и
владелец решает до `--apply`, удалять ли их целиком (ключ `--include-mixed`).
5. **Аудит:** на каждый удалённый файл — системное `FILE_DELETE` (`user_id = NULL`, описание «очистка форм перед v5.19»,
карточка файла в `before_value`); на документ смешанного файла — то же событие по файлу с перечнем удалённых
документов.
6. **Не трогаются:** ОСВ (`osv_entry`), консолидации и их версии, корректировки, гудвилл, справочники, пользователи,
журнал аудита. Результаты КФО неверифицированных версий, посчитанные с удалёнными формами, пересчитываются, как после
удаления файла.
#### Scenario: Отчёт перед очисткой
- **WHEN** владелец запускает скрипт без ключей
- **THEN** скрипт ничего не меняет и печатает число файлов только с формами, смешанных файлов, документов форм,
подтверждённых документов и привязок к неверифицированным версиям
#### Scenario: Форма в верифицированной версии
- **WHEN** документ ББ привязан к версии `VERIFIED`
- **THEN** `--apply` отказывает с перечнем версий, база не изменена
#### Scenario: Форма в версии на согласовании или отклонённой
- **WHEN** документ ББ привязан к версии `VERIFICATION_RF`, а ОФР — к версии `REJECTED`; верифицированных версий нет
- **THEN** `--apply` выполняется: привязки сняты, статусы версий не изменились
#### Scenario: Очистка и повторная загрузка
- **WHEN** очистка выполнена, ядро v5.19 выложено, аналитик загружает тот же PDF с ББ и ОФР
- **THEN** дедуп не мешает (файла больше нет), формы создаются по мапперу 1.x или 2.0 — смотря что шлёт сервис
### Requirement: Ручной повтор обработки файла
`POST /api/v1/files/{file_id}/retry` SHALL требовать DOCUMENT_EDIT и видимость ЮЛ. Повтор разрешён для
текущего задания tabular в timeout/TIMEOUT или dlq/NO_ACK при attempt < max_attempts, отсутствии документов
и файле ERROR. Успех SHALL вернуть 202 с карточкой файла и новым текущим заданием. Несуществующий или чужой файл — 404.
#### Scenario: После таймаута
- **WHEN** табличный файл ОСВ РБ завершился timeout/TIMEOUT, attempt = 1, max_attempts = 3
- **THEN** 202, создано новое задание attempt = 2, прежнее осталось, в карточке новое текущее задание
#### Scenario: Неподходящее состояние
- **WHEN** повтор запрошен для OCR, done, needs_check, error сервиса, ошибки маппинга либо файла с документами
- **THEN** 409 FILE_NOT_RETRYABLE без нового задания и аудита
#### Scenario: Лимит исчерпан
- **WHEN** подходящее задание имеет attempt = max_attempts
- **THEN** 409 MAX_ATTEMPTS_REACHED, состояние не меняется
### Requirement: Пакет повторной обработки версии
`POST /api/v1/consolidations/versions/{version_id}/files/retry` SHALL выбирать видимые ERROR-файлы
консолидации по периметру версии. Другие статусы не входят в пакет и не блокируются.
Ответ — 202 с accepted[{file_id, job_id, attempt}] и skipped[{file_id, code}]. В skipped SHALL входить
только ERROR-файлы, которые нельзя повторить (FILE_NOT_RETRYABLE, MAX_ATTEMPTS_REACHED).
Файлы без контекста консолидации MUST NOT включаться по совпадению ЮЛ.
#### Scenario: Смешанный пакет
- **WHEN** у версии есть файл timeout/TIMEOUT, OCR, успешный файл и файл с исчерпанным лимитом
- **THEN** новый job создаётся только для первого; OCR-файл в skipped с `FILE_NOT_RETRYABLE`, файл с исчерпанным
лимитом — с `MAX_ATTEMPTS_REACHED`; успешный (`PROCESSED`) файл не попадает ни в accepted, ни в skipped
#### Scenario: Периметр и контекст
- **WHEN** файл загружен в другую консолидацию либо его ЮЛ не входит в выбранную версию
- **THEN** файл не участвует ни в accepted, ни в skipped этой версии
#### Scenario: Общие файлы разных версий
- **WHEN** две версии одной консолидации содержат одно ЮЛ и вызывают пакетный повтор
- **THEN** они используют общий набор файлов консолидации; уже повторяемый файл второй раз не запускается
#### Scenario: Пустой пакет
- **WHEN** подходящих файлов нет
- **THEN** 202 с пустым accepted; в skipped — только файлы в `ERROR`, которые повторить нельзя (без них список пуст);
аудита повторов нет
### Requirement: Ограничения версии при повторе
Повтор в контексте версии SHALL разрешаться только для версий «в работе» (INITIATED, DOCUMENTS_PROCESSING,
DATA_CORRECTION). Иначе SHALL возвращаться 409 VERSION_NOT_EDITABLE. consolidation_lock MUST NOT требоваться.
Одиночный повтор файла с consolidation_id SHALL требовать наличия версии «в работе» с ЮЛ файла в периметре;
без consolidation_id SHALL применяться только policy ЮЛ. Повтор MUST NOT изменять статусы версий.
#### Scenario: Проверенная версия
- **WHEN** пакет запрошен для VERIFIED
- **THEN** 409 VERSION_NOT_EDITABLE, ни один файл не изменён
#### Scenario: Консолидация уже закрыта для обработки
- **WHEN** одиночный повтор файла консолидации запрошен без версии «в работе» с его ЮЛ
- **THEN** 409 VERSION_NOT_EDITABLE; другая версия консолидации не открывает недоступный пакет версии
#### Scenario: Сохранение статуса
- **WHEN** пакет допустимой версии INITIATED принят без пользовательской блокировки
- **THEN** попытки созданы, INITIATED сохраняется; дальнейшие переходы происходят по существующим событиям