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

33 KiB
Raw Blame History

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