## ADDED Requirements ### 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 сохраняется; дальнейшие переходы происходят по существующим событиям ## MODIFIED Requirements ### 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`