Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
91 lines
8.0 KiB
Markdown
91 lines
8.0 KiB
Markdown
# 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`) и `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)`. Ответ SHALL
|
||
перечислять принятые файлы (`accepted[]` с `file_id`, `job_id`, `name`) и отклонённые (`rejected[]` с `name`, `code`,
|
||
`message`); код ответа 201, если принят хотя бы один, иначе 422. Тип документа и период при загрузке MUST NOT
|
||
запрашиваться (макет `40`). Без доступного S3 — 503 `STORAGE_UNAVAILABLE`.
|
||
|
||
#### 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 файла; `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, объектов и записей нет
|
||
|
||
### 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. Документы и строки заменённых файлов SHALL сохраняться (история), но по умолчанию не показываться.
|
||
Аудит `FILE_REPLACE` (`object_id` заменённого, `after_value` с новым файлом, в `description` — перечень заменённых
|
||
документов и признак `confirmed`, если заменён документ `CONFIRMED`). Решение владельца 05.10.2026: замена происходит
|
||
и для `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`; список файлов ЮЛ по умолчанию показывает только новый
|
||
|
||
### 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 = карточка файла)`. Удаление MUST быть отклонено 409 `FILE_HAS_CONFIRMED_DOCUMENTS`, если у файла есть
|
||
документ `CONFIRMED`, и 409 `FILE_SUPERSEDES_OTHERS`, если на файл ссылаются `superseded_by_file_id`. Файл
|
||
`SUPERSEDED` удалить можно.
|
||
|
||
#### Scenario: Удаление ошибочного файла
|
||
- **WHEN** файл в `ERROR` удаляется владельцем
|
||
- **THEN** 204; `stored_file`, `recognition_job` отсутствуют; `s3_object.expires_at` заполнен; `audit_event(FILE_DELETE)`
|
||
|
||
#### Scenario: Подтверждённый документ
|
||
- **WHEN** удаляется файл с документом `CONFIRMED`
|
||
- **THEN** 409 `FILE_HAS_CONFIRMED_DOCUMENTS`, ничего не изменилось
|