From 8122205357c2014100a8b197bc12108d56fe2243 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 8 Oct 2026 17:50:37 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D1=84=D0=B8=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE?= =?UTF-8?q?=D0=BC=D1=83=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20=D0=B0=D0=B2?= =?UTF-8?q?=D1=82=D0=BE=D0=BF=D0=BE=D0=B4=D1=82=D0=B2=D0=B5=D1=80=D0=B6?= =?UTF-8?q?=D0=B4=D0=B5=D0=BD=D0=B8=D1=8F=20(I-1,=20M-1,=20M-2,=20M-4..M-6?= =?UTF-8?q?,=20T6.x)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 +- README.md | 19 +++++++++++-------- api/src/api/endpoints/files/read.py | 2 +- api/src/services/primary_files.py | 2 +- api/src/services/statements.py | 2 +- api/tests/test_auto_confirm.py | 9 ++++++++- api/tests/test_result_mapping.py | 12 ++++++++++-- .../auto-confirm-clean-documents/design.md | 9 ++++++--- .../auto-confirm-clean-documents/proposal.md | 6 ++++-- 9 files changed, 43 insertions(+), 20 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 886a01e..8882051 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,7 +76,7 @@ advisory-lock, SAVEPOINT на файл, `delete`), `services/file_status.py` (единственное место смены `stored_file.status` по ТЕКУЩЕМУ заданию; `PROCESSED`/`SUPERSEDED` обменом не перезаписываются), `services/result_mapping.py` (цикл маппинга: `pick_candidate` → проверка ключа результата (OQ-70) → чтение S3 вне транзакции → `map_file` в одной транзакции → - автозамена `FILE_REPLACE`), `services/documents.py` (карточки, `confirm`), `services/auto_confirm.py` (автоподтверждение чистых документов в транзакции маппинга: `_map_candidate`: `map_file` → `auto_confirm` → `on_documents_mapped`; `confirmed_by` NULL, `DOCUMENT_CONFIRM` без пользователя, переход версии — от `uploaded_by`; OQ-157), `domain/osv_mapping.py` (чистые функции: + автозамена `FILE_REPLACE`), `services/documents.py` (карточки, `confirm`), `services/auto_confirm.py` (автоподтверждение чистых документов в транзакции маппинга: `_map_candidate`: `map_file` → автозамена → `auto_confirm` → `on_documents_mapped`; `confirmed_by` NULL, `DOCUMENT_CONFIRM` без пользователя, переход версии — от `uploaded_by`; OQ-157), `domain/osv_mapping.py` (чистые функции: `normalize_account_code`, `build_entries`, `counterparty_check`, `document_status`), `repositories/counterparties.py` (`CounterpartyIndex` — резолв контрагента без policy). **Фоновых циклов три** (запускаются в lifespan при `NATS_ENABLED`; цикл маппинга — ещё и при подключённом S3 (storage)): публикатор outbox, вахтёр сроков и цикл маппинга diff --git a/README.md b/README.md index b279f89..5019e7c 100644 --- a/README.md +++ b/README.md @@ -423,6 +423,9 @@ POST /legal-entities/{id}/files → S3 uploads/… + stored_file(UPLOADED) + rec → автозамена прежних версий (SUPERSEDED) → GET /files/{id}, GET /documents/{id}/entries → POST /documents/{id}/confirm ``` +Чистый документ подтверждается самим маппингом (автоподтверждение, OQ-157); `POST /documents/{id}/confirm` нужен для +`NEEDS_CHECK` и для форм ББ/ОФР после правки. + Загрузка — `multipart/form-data`, одно или несколько полей `files`; право `DOCUMENT_EDIT`, ЮЛ в области видимости пользователя (чужое — 404, как и несуществующее). Поддерживаются `xls` (BIFF8), `xlsx`, `xlsb`, `pdf`, `png`, `jpg` (`.jpeg` — как `jpg`): тип определяется по расширению и сверяется с сигнатурой содержимого. Таблицы (`xls/xlsx/xlsb`) @@ -466,11 +469,10 @@ curl -s "localhost:8000/api/v1/files/" -H "Authorization: Bearer $TOKEN `PROCESSED` и `SUPERSEDED` события обмена не перезаписывают; запоздавший ack не воскрешает `ERROR`. **Документ** (`document.status`) создаётся маппером на каждый лист ОСВ, на каждую форму ББ/ОФР (Срез 3b, D-2) и по одному на -тип `DISCLOSURE`/`OTHER` результата (D-3; без данных, сразу `RECOGNIZED`). Условия для листа ОСВ: +тип `DISCLOSURE`/`OTHER` результата (D-3; без данных, при маппинге `RECOGNIZED`, затем автоподтверждаются системой). Условия для листа ОСВ: - `RECOGNIZED` — задание `done`, проверки парсера без нарушений, шапка не противоречит владельцу (`counterparty_check ≠ MISMATCH`), предупреждений маппинга у листа нет; -- чистый документ (`RECOGNIZED`) подтверждается системой сразу в маппинге; `RECOGNIZED` остаётся у документов с замечаниями и у форм, не прошедших условия подтверждения (после правки формы автоподтверждения нет); - `NEEDS_CHECK` — иначе: задание `needs_check`, нарушения `checks`, `counterparty_check = MISMATCH`, повтор счёта в файле, строки без родителя, нарастающая по данным Q1; - для формы ББ/ОФР условия `RECOGNIZED`/`NEEDS_CHECK` — правило D-6 (раздел «Формы ББ/ОФР»): проверки §7 по суммам формы вместо @@ -478,6 +480,8 @@ curl -s "localhost:8000/api/v1/files/" -H "Authorization: Bearer $TOKEN - `CONFIRMED` — аналитик нажал «Подтвердить» (`POST /documents/{id}/confirm`, `confirmed_by/at`, аудит) либо система при автоподтверждении чистого документа (`confirmed_by = null`, аудит `DOCUMENT_CONFIRM` без пользователя; OQ-157); - `ERROR` — зарезервирован: в Срезе 2c ядро документ в `ERROR` не создаёт (результат без документов — «в результате нет документов» — переводит в `ERROR` файл). +Чистый документ (`RECOGNIZED` при маппинге) подтверждается системой сразу в маппинге. `RECOGNIZED` остаётся у форм, ставших чистыми после правки/акцепта (автоподтверждения после правки нет), у форм, не прошедших условия подтверждения (`form_confirmable`), и у документов, созданных до автоподтверждения; у документов с замечаниями статус `NEEDS_CHECK`. + `counterparty_check` (`MATCH` / `MISMATCH` / `UNKNOWN`) сверяет шапку (ИНН, иначе имя или алиас) с ЮЛ — владельцем файла; `period_check` — `UNKNOWN`: подходящесть документа версии консолидации — функция (документ, версия), а не свойство документа (Срез 4, OQ-120). Допущения помечены `ASSUMPTION(OQ-nn)` в коде. @@ -524,7 +528,7 @@ S3 (storage); без S3 — предупреждение в логе, цикл | `DELETE /files/{file_id}` | `DOCUMENT_EDIT` | удалить файл (см. «Удаление и замена»), 204 | | `GET /documents/{document_id}` | `DOCUMENT_VIEW` | документ: статус, проверки, `locator`, `group_schema` листа, `warnings`, `file_id` | | `GET /documents/{document_id}/entries` | `DOCUMENT_VIEW` | строки ОСВ по `row_number` (`limit` ≤ 2000, по умолчанию 500); дерево — по `parent_entry_id` | -| `POST /documents/{document_id}/confirm` | `DOCUMENT_EDIT` | `RECOGNIZED`/`NEEDS_CHECK` → `CONFIRMED`, аудит `DOCUMENT_CONFIRM`; для формы ББ/ОФР — ещё `PERIOD_REQUIRED` и `CHECKS_NOT_PASSED` | +| `POST /documents/{document_id}/confirm` | `DOCUMENT_EDIT` | `RECOGNIZED`/`NEEDS_CHECK` → `CONFIRMED`, аудит `DOCUMENT_CONFIRM`; для автоподтверждённого документа — 409 `ALREADY_CONFIRMED` (D-6); для формы ББ/ОФР — ещё `PERIOD_REQUIRED` и `CHECKS_NOT_PASSED` | | `GET /documents/{document_id}/statement` | `DOCUMENT_VIEW` | форма ББ/ОФР: шапка, колонки, строки со значениями, редакция, проверки §7 (см. «Формы ББ/ОФР») | | `PATCH /documents/{document_id}/statement` | `DOCUMENT_EDIT` | правка значений и шапки (`inn`, `report_date`, `units`) до подтверждения; аудит `RECOGNIZED_DATA_EDIT` | | `POST /documents/{document_id}/statement/checks/accept` | `DOCUMENT_EDIT` | акцепт непройденных жёстких проверок с комментарием (`{comment}`) | @@ -601,9 +605,8 @@ advisory-блокировкой. **Удаление.** `DELETE /files/{id}` удаляет формы ББ/ОФР (шапки, колонки, строки, значения), комментарии документов, строки ОСВ, документы, задания и `stored_file`, проставляет объектам S3 (`uploads`, `results`) из папок этого файла (`uploads/{file_id}/`, `results/{file_id}/`) `expires_at = now()` -(физически объект не удаляется — ретеншн OQ-22, OQ-111) и пишет `FILE_DELETE` с карточкой файла в `before_value`. Нельзя удалить файл с документом, подтверждённым пользователем (`confirmed_by` задан), и с автоподтверждённым документом, привязанным к версии вне статусов «в работе» `INITIATED`/`DOCUMENTS_PROCESSING`/`DATA_CORRECTION` (`409 FILE_HAS_CONFIRMED_DOCUMENTS`); автоподтверждённые привязки к версиям «в работе» снимаются (`FILE_DELETE.after_value.unbound_version_ids`, OQ-157) -и файл, который заменил другие (`409 FILE_SUPERSEDES_OTHERS`); `SUPERSEDED` файл удалить можно. Блокировки — в порядке -«задания → файл → документы». +(физически объект не удаляется — ретеншн OQ-22, OQ-111) и пишет `FILE_DELETE` с карточкой файла в `before_value`. Нельзя удалить файл с документом, подтверждённым пользователем (`confirmed_by` задан), и с автоподтверждённым документом, привязанным к версии вне статусов «в работе» `INITIATED`/`DOCUMENTS_PROCESSING`/`DATA_CORRECTION` (`409 FILE_HAS_CONFIRMED_DOCUMENTS`); автоподтверждённые привязки к версиям «в работе» снимаются (`FILE_DELETE.after_value.unbound_version_ids`, OQ-157). Нельзя также удалить файл, который заменил другие (`409 FILE_SUPERSEDES_OTHERS`); `SUPERSEDED` файл удалить можно. Блокировки — в порядке +«задания → файл → документы → advisory ГК → версии по id». ## Ограничения Среза 2c @@ -613,7 +616,7 @@ advisory-блокировкой. - Общий размер multipart-запроса `POST /legal-entities/{id}/files` и число файлов в нём приложением не ограничены (Starlette спулит части на диск, `UPLOAD_MAX_BYTES` — лимит одного файла); лимит запроса задаётся ingress платформы. - Маппер Среза 2c разбирал только листы ОСВ (`osvDocument`); с Среза 3b он мапит и формы ББ/ОФР (раздел «Формы ББ/ОФР»). - Документы `DISCLOSURE`/`OTHER` результата не игнорируются: на каждый тип создаётся один документ без данных (`RECOGNIZED`, + Документы `DISCLOSURE`/`OTHER` результата не игнорируются: на каждый тип создаётся один документ без данных (`RECOGNIZED` при маппинге, затем автоподтверждение; `locator` — перечень листов через запятую; OQ-23). Отдельной обработки по конвейеру (`tabular`/`image`) в маппере нет: разбирается любой результат, проходящий схему `recognition-result`. - Корневой сегмент счёта (`account_root`) — только цифровой (`ASSUMPTION(OQ-103)`): забалансовые «МЦ.04», «ГТД» @@ -967,7 +970,7 @@ email без учёта регистра проверяет только сер Тест `api/tests/e2e/test_osv_ingest_e2e.py` (маркеры `integration`, `e2e`, `fixtures`) проверяет весь путь Среза 2c с РЕАЛЬНЫМ tabular-parser: `POST /legal-entities/{id}/files` (ОСВ 66 за 2024 из `../fixtures`) → outbox → NATS → tabular-parser (MinIO) → inbox → маппинг → `GET /files/{id}` = `PROCESSED`, документ 66/2024/Q4 (нарастающий, -`RECOGNIZED`, `counterparty_check = MATCH`) и 9 строк ОСВ. Приложение — настоящий `src.app.app` с lifespan на +`CONFIRMED` автоподтверждением, `counterparty_check = MATCH`) и 9 строк ОСВ. Приложение — настоящий `src.app.app` с lifespan на отдельной временной БД с seed, NATS и MinIO — из `api/.env.test`; в конце файл удаляется через `DELETE /files/{id}` (объекту S3 ставится `expires_at`), БД снимается. diff --git a/api/src/api/endpoints/files/read.py b/api/src/api/endpoints/files/read.py index 6205f84..6ac1a27 100644 --- a/api/src/api/endpoints/files/read.py +++ b/api/src/api/endpoints/files/read.py @@ -32,7 +32,7 @@ DELETE_RESPONSES: dict[int | str, dict[str, Any]] = { status.HTTP_409_CONFLICT: { "model": ErrorDetail, "description": "FILE_HAS_CONFIRMED_DOCUMENTS (документ подтверждён пользователем или автоподтверждённый " - "документ в версии на согласовании) или FILE_SUPERSEDES_OTHERS " + "документ привязан к версии вне корректировки данных) или FILE_SUPERSEDES_OTHERS " "(файл заменил более ранние версии)", }, } diff --git a/api/src/services/primary_files.py b/api/src/services/primary_files.py index df13a32..7454edf 100644 --- a/api/src/services/primary_files.py +++ b/api/src/services/primary_files.py @@ -318,7 +318,7 @@ class PrimaryFilesService: if any(status not in IN_WORK_STATUSES for _, status in bound): raise ConflictError( FILE_HAS_CONFIRMED_DOCUMENTS, - "файл нельзя удалить: документ уже участвует в версии консолидации на согласовании", + "файл нельзя удалить: документ привязан к версии консолидации вне корректировки данных", ) unbound_version_ids = sorted({version_id for version_id, _ in bound}) await consolidation_documents.unbind_documents( diff --git a/api/src/services/statements.py b/api/src/services/statements.py index 9288c12..774aeb8 100644 --- a/api/src/services/statements.py +++ b/api/src/services/statements.py @@ -59,7 +59,7 @@ async def statement_hard_failures( async def form_confirmable(session: AsyncSession, document: Document) -> bool: - """Условия подтверждения формы ББ/ОФР без исключений (D-9 Среза 3b; автоподтверждение — design D-3, OQ-157): + """Условия подтверждения формы ББ/ОФР без исключений (§7, D-9 Среза 3b; автоподтверждение — design D-3, OQ-157): период определён и жёстких нарушений §7 нет или они акцептованы. Не-форма и форма без шапки (созданная до Среза 3b) — `True`: условий у них нет. Ручной путь (`DocumentService._check_statement`) проверяет то же, но отвечает 409.""" if document.document_type not in FORM_TYPES: diff --git a/api/tests/test_auto_confirm.py b/api/tests/test_auto_confirm.py index 2fb4e37..4c731ea 100644 --- a/api/tests/test_auto_confirm.py +++ b/api/tests/test_auto_confirm.py @@ -5,6 +5,7 @@ import uuid from pathlib import Path import pytest +from loguru import logger from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -143,10 +144,16 @@ async def test_file_without_uploader_binds_without_transition(seeded: AsyncSessi version = await _version(seeded, group, entities, ConsolidationStatus.INITIATED) file, [document] = await _file_with(seeded, entities[0], [DocumentStatus.RECOGNIZED], None) - await auto_confirm(seeded, file=file, documents=[document], now=utc_now()) + messages: list[str] = [] + sink = logger.add(lambda message: messages.append(str(message)), level="INFO") + try: + await auto_confirm(seeded, file=file, documents=[document], now=utc_now()) + finally: + logger.remove(sink) assert document.status is DocumentStatus.CONFIRMED assert await _bound(seeded, version.id) == {document.id} + assert any("Переход версии по подтверждению пропущен" in m and str(version.id) in m for m in messages) refreshed = await seeded.get(ConsolidationVersion, version.id, populate_existing=True) assert refreshed is not None and refreshed.status is ConsolidationStatus.INITIATED diff --git a/api/tests/test_result_mapping.py b/api/tests/test_result_mapping.py index 84d28ed..973f7ab 100644 --- a/api/tests/test_result_mapping.py +++ b/api/tests/test_result_mapping.py @@ -1562,6 +1562,11 @@ async def test_mixed_file_confirms_clean_and_moves_version_once( assert statuses == [("60", DocumentStatus.CONFIRMED), ("62", DocumentStatus.NEEDS_CHECK)] history = await _history_of(maker, version_id) assert [h.to_status for h in history].count(ConsolidationStatus.DATA_CORRECTION) == 1 + # переход сделал именно автоподтверждение (T4.2): при перестановке с on_documents_mapped комментарий иной + assert history[-1].to_status is ConsolidationStatus.DATA_CORRECTION + assert history[-1].comment == "автоподтверждение документа OSV 60" + clean = next(d for d in await _documents(maker, file_id) if d.osv_account_code == "60") + assert await _bound_ids(maker, version_id) == {clean.id} # NEEDS_CHECK-документ 62 не привязан async def test_supersede_rebinds_auto_confirmed_document(committed_sessionmaker: async_sessionmaker[AsyncSession]): @@ -1657,8 +1662,9 @@ async def _audit_count(maker: async_sessionmaker[AsyncSession]) -> int: async def test_exchange_and_mapping_write_no_audit_events(committed_sessionmaker: async_sessionmaker[AsyncSession]): """spec audit-trail «События файлов и документов»: смена статусов задания и файла (ack, результат, маппинг) и - автозамена без заменяемых файлов `audit_event` не пишут: путь `UPLOADED → PROCESSING → PROCESSED` оставляет таблицу - аудита неизменной.""" + автозамена без заменяемых файлов `audit_event` не пишут: путь `UPLOADED → PROCESSING → PROCESSED` для документа + `NEEDS_CHECK` (у владельца нет ИНН) оставляет таблицу аудита неизменной. Автоподтверждение чистого документа пишет + `DOCUMENT_CONFIRM` — это проверяет отдельный тест.""" maker, storage = committed_sessionmaker, FakeStorage() file_id, job_id, _ = await _prepare( maker, storage, _minimal(), job_status=RecognitionStatus.published, file_status=FileStatus.UPLOADED @@ -1674,6 +1680,8 @@ async def test_exchange_and_mapping_write_no_audit_events(committed_sessionmaker assert await process_once(maker, storage, _settings()) is True assert (await _file(maker, file_id)).status is FileStatus.PROCESSED + [document] = await _documents(maker, file_id) + assert document.status is DocumentStatus.NEEDS_CHECK assert await _audit_count(maker) == before diff --git a/openspec/changes/auto-confirm-clean-documents/design.md b/openspec/changes/auto-confirm-clean-documents/design.md index 0ec6c0a..c8dd7c3 100644 --- a/openspec/changes/auto-confirm-clean-documents/design.md +++ b/openspec/changes/auto-confirm-clean-documents/design.md @@ -58,10 +58,10 @@ ### D-4. Удаление: только автоподтверждённое и только до согласования `PrimaryFilesService.delete` под существующими блокировками (задания → файл → документы) проверяет: если у файла есть `CONFIRMED` документ с `confirmed_by IS NOT NULL` — 409 `FILE_HAS_CONFIRMED_DOCUMENTS` (как сейчас). Если все -`CONFIRMED` документы автоподтверждены — дальше блокируются версии ЮЛ (`lock_versions_for_entity` по всем статусам, где -есть привязки этих документов; порядок «… → документы → advisory ГК → версии» соблюдён) и проверяется, что каждая +`CONFIRMED` документы автоподтверждены — дальше блокируются версии ЮЛ «в работе» (`lock_versions_for_entity` берёт только `IN_WORK_STATUSES`; версии вне +«в работе» читаются без блокировки: удаление из них запрещено, возврата в «в работе» нет; порядок «… → документы → advisory ГК → версии» соблюдён) и проверяется, что каждая версия с привязкой в `INITIATED`/`DOCUMENTS_PROCESSING`/`DATA_CORRECTION`; иначе 409 `FILE_HAS_CONFIRMED_DOCUMENTS` с -текстом «документ уже участвует в версии на согласовании». Затем привязки снимаются (`consolidation_document`), файл +текстом «документ привязан к версии консолидации вне корректировки данных». Затем привязки снимаются (`consolidation_document`), файл удаляется как сейчас; `FILE_DELETE.after_value = {unbound_version_ids: [...]}` (по возрастанию; пусто — пустой список), `before_value` — карточка файла, как сейчас. Статус версии не меняется (свод — чистая функция, пересчитается). `FILE_SUPERSEDES_OTHERS` — без изменений. @@ -113,3 +113,6 @@ | 9 | 2026-10-08 | Тесты автоподтверждения, маппинга и удаления берут `uploaded_by` у реального пользователя (FK `app_user`), а не случайный uuid (ошибка плана) | случайный uuid не проходит FK | нет | | 10 | 2026-10-08 | В `tests/test_api_files_delete.py` тесты, имитирующие подтверждение пользователем (`test_confirmed_document_blocks_delete`, `test_delete_rereads_state_after_locks`), получили `confirmed_by` | иначе по правилу удаления (D-4) документ считается автоподтверждённым и удаление проходит | нет | | 11 | 2026-10-08 | Импорт `auto_confirm` в `result_mapping._map_candidate` — локальный | цикл `documents` → `result_mapping` → `auto_confirm` → `statements` → `documents` | техдолг: вынести `form_confirmable`/`FORM_TYPES` в модуль без обратной зависимости | +| 12 | 2026-10-08 | `make check-full` запускается один раз в конце, после финального ревью и fix-волны (а не внутри Task 6 плана) | не гонять полную проверку дважды | если check-full что-то найдёт — правка после ревью | +| 13 | 2026-10-08 | M-3 (гонка в `bound_versions` при будущем маршруте «вернуть на доработку» Среза 7) и вынос `form_confirmable` из цикла импортов оставлены техдолгом (follow-up) | сейчас возврата версии в «в работе» нет, цикл импортов обойдён локальным импортом | при добавлении возврата на доработку надо отвечать 409 для версий, не попавших в множество, заблокированное `lock_versions_for_entity` | +| 14 | 2026-10-08 | Отменённая/отклонённая/архивная версия с привязкой автоподтверждённого документа блокирует удаление файла (по спеке «вне INITIATED/DOCUMENTS_PROCESSING/DATA_CORRECTION»), привязки при отмене версии не снимаются | буквальное чтение спеки | файл из отменённой версии удалить нельзя — вопрос владельцу | diff --git a/openspec/changes/auto-confirm-clean-documents/proposal.md b/openspec/changes/auto-confirm-clean-documents/proposal.md index 919db8d..cf957ff 100644 --- a/openspec/changes/auto-confirm-clean-documents/proposal.md +++ b/openspec/changes/auto-confirm-clean-documents/proposal.md @@ -49,7 +49,9 @@ DBML и контракты обмена не меняются. - Код: `services/result_mapping.py` (`_map_candidate`), `services/consolidation_documents.py` (`on_document_confirmed` с внешним `changed_by`/`comment`, новый `auto_confirm`), `services/documents.py` (общая - проверка условий формы), `services/primary_files.py` (`delete`), `repositories/files.py` (снятие привязок). -- Спеки: `osv-result-mapping`, `documents-api`, `primary-files`, `audit-trail`. + проверка условий формы), `services/primary_files.py` (`delete`; привязки снимает существующий + `consolidation_documents.unbind_documents`). +- Спеки (дельты): `osv-result-mapping`, `primary-files`, `audit-trail`. Ручной путь `POST /documents/{id}/confirm` не + меняется, поэтому дельты `documents-api` нет. - Поведение для фронта: чистые документы приходят сразу `CONFIRMED` («Загружен»); `confirmed_by = null` — признак автоподтверждения.