Пара «новый документ — прежний» с counterparty_check = MISMATCH у любого из двух в автозамене не участвует: сканы разных организаций в одной карточке больше не вытесняют друг друга. UNKNOWN заменяется как прежде. Если файл оставлен только из-за MISMATCH, в mapping_warnings нового файла — «автозамена не выполнена» с именем и версией оставленного файла. Спека primary-files дополнена. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
33 KiB
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с этим ЮЛ в периметре, файл загружен маршрутом ЮЛ с queryconsolidation_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 Q2MATCH - 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, одной транзакцией:
- Отбор: документы
BALANCE/INCOME(у файлов любого статуса, включаяSUPERSEDED). - Отказ без изменений, если хотя бы один отобранный документ упомянут в
ex_snapshot_itemили привязан (consolidation_document) к версии консолидации в статусе «Верифицирована» или дальше —VERIFIEDилиARCHIVEDверифицированной версии (естьex_snapshotсsource_version_idверсии): такая версия read-only и заморожена в ЕХ. Отчёт называет версии, решение — за владельцем. Версии в работе, на согласовании, отклонённые (REJECTED) и отменённые (CANCELLED, в том числе архивированные) очистке не мешают: привязки к ним снимаются, как к версиям «в работе» (п. 3, 4), статус версий не меняется (Р-35). - Файлы только с формами — у файла нет документов
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(повторная загрузка заменит их снова; отчёт перечисляет такие файлы, если в них есть ОСВ). - Смешанные файлы — у файла есть и ОСВ, и формы: удаляются только документы
BALANCE/INCOMEс ихfinancial_statement_*, комментариями и привязками; файл, ОСВ,DISCLOSURE/OTHERи задания остаются. Повторно загрузить такой файл нельзя (дедуп поsha256), поэтому отчёт--dry-runперечисляет смешанные файлы отдельно, и владелец решает до--apply, удалять ли их целиком (ключ--include-mixed). - Аудит: на каждый удалённый файл — системное
FILE_DELETE(user_id = NULL, описание «очистка форм перед v5.19», карточка файла вbefore_value); на документ смешанного файла — то же событие по файлу с перечнем удалённых документов. - Не трогаются: ОСВ (
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 сохраняется; дальнейшие переходы происходят по существующим событиям