arm-ka-docs/dev/api-contract-slice-3b.md
Andrey Epifancev 1851c248fa docs: полный состав строк ББ/ОФР — §7, OQ-150, гайды (#81)
- 05-domain-rules: §7 абзац «Полный состав формы», §11 пометка про достройку строк
- 07-open-questions: OQ-150 — решение владельца 09.10.2026
- frontend-api-guide, api-contract-slice-3b: values[] с line_code, origin, id = null, новые случаи UNKNOWN_CELL
- qa-guide: F8a, F8b, W28a, W28b

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-09 12:21:41 +04:00

26 KiB
Raw Blame History

Контракт API Среза 3b (формы ББ/ОФР и путь пользователя до сводной ОСВ)

Источник — реальный код arm-core (api/src/schemas/statements.py, schemas/comments.py, api/endpoints/files/statements.py, comments.py, read.py, users.py; Swagger — /docs) и спека arm-core/openspec/changes/archive/2026-10-07-slice-3b-statements-api/ (design D-1…D-14, «Decisions made without discussion» №1–№22). Правила предметной области — context/05-domain-rules.md §7, §9.3, §9.5 (п. 2, 4, 9, 10); экраны — context/10-user-journey.md (пути 1–2, шаг 3.1), номера в […] — снимки макета (mockup/INDEX.md). Общие правила (префикс, токен, формат ошибок, права) — как в dev/api-contract-slice-4.md §1; здесь — то, что добавил Срез 3b. Файлы, документы и справочники (схемы FileOut/DocumentBrief, загрузка, удаление) — dev/api-contract-files-directories.md.

1. Общее

  • Префикс /api/v1, Authorization: Bearer <JWT>. Права: чтение формы, комментариев и исходного файла — DOCUMENT_VIEW; правка формы, акцепт проверок, комментарий, подтверждение — DOCUMENT_EDIT; CSV чек-листа — CONSOLIDATION_VIEW; GET /users/brief — любой аутентифицированный пользователь, отдельного права нет. Нет права — 403 строкой; нет токена — 401.
  • Порядок проверок на маршрутах документа: право (403) → валидация схемы запроса (422 массивом pydantic, до обращения к данным) → видимость (404) → состояние (409) → доменные 422 (INVALID_INN, UNKNOWN_CELL, DUPLICATE_CELL). Документ, файл или ЮЛ вне области видимости неотличимы от несуществующих: 404 с кодом NOT_FOUND.
  • Доменные ошибки: {"detail": {"code": "...", "message": "на русском", ...доп. поля}}; ветвиться клиент должен по code. 422 валидации тела/параметров схемы — массив pydantic (detail: [{loc, msg, type}]) без code; доменные 422 (INVALID_INN, UNKNOWN_CELL, …) — с code. Каталог кодов — arm-core/README.md, «Ошибки API».
  • Деньги — строки JSON ("1234.50", numeric(18,2)), не float: amount, original_amount, expected, actual; в запросе amount — число с не более чем двумя знаками и не более 16 цифр до запятой, либо null (очистить ячейку). confidence — строка ("100.00"), null у ячеек, которых нет в результате распознавания.
  • Идентификаторы — uuid-строки; даты — ISO (2025-09-30); время — ISO с часовым поясом.
  • Типы форм: document_type — BALANCE (ББ), INCOME (ОФР); DISCLOSURE и OTHER — документ без данных, …/statement для них и для OSV — 409 DOCUMENT_NOT_STATEMENT.
  • DocumentStatus — RECOGNIZED, NEEDS_CHECK, CONFIRMED, ERROR; DocumentCheckResult (counterparty_check) — MATCH, MISMATCH, UNKNOWN; PeriodQuarter — Q1…Q4.

2. Эндпоинты

Метод и путь Право Запрос Ответ Ошибки Экран
GET /documents/{document_id}/statement VIEW — 200 StatementOut 404 NOT_FOUND, 409 DOCUMENT_NOT_STATEMENT [41]
PATCH /documents/{document_id}/statement EDIT StatementPatch {header?, values[]} (values[] — `{line_id line_code, column_id, amount}`) 200 StatementOut (состояние после правки) 404, 409 FILE_SUPERSEDED / DOCUMENT_READONLY / DOCUMENT_NOT_CONFIRMABLE / DOCUMENT_NOT_STATEMENT, 422 INVALID_INN / UNKNOWN_CELL / DUPLICATE_CELL, 422 (pydantic-массив)
POST /documents/{document_id}/statement/checks/accept EDIT ChecksAcceptIn {comment} (1–2000 символов после обрезки) 200 StatementOut 404, 409 NOTHING_TO_ACCEPT и те же 409, что у PATCH, 422 (pydantic-массив) [41] блок «Логические ошибки»
POST /documents/{document_id}/confirm EDIT — 200 DocumentOut (как в Срезе 2c) 404, 409 FILE_SUPERSEDED / ALREADY_CONFIRMED / DOCUMENT_NOT_CONFIRMABLE / PERIOD_REQUIRED / CHECKS_NOT_PASSED [41] «Подтвердить данные»
POST /documents/{document_id}/comments EDIT CommentIn {text} (1–4000 символов после обрезки пробелов) 201 CommentOut 404, 422 (pydantic-массив) [41] «Добавить комментарий»
GET /documents/{document_id}/comments VIEW — 200 CommentOut[] по возрастанию created_at, без пагинации 404 [41]
GET /files/{file_id}/content VIEW query disposition = attachment (по умолчанию) или inline 200 поток байт файла 404 NOT_FOUND, 404 FILE_CONTENT_NOT_FOUND, 503 STORAGE_UNAVAILABLE, 422 (pydantic-массив) при другом disposition [41] «Исходный документ»
GET /users/brief?ids=<uuid>,<uuid>… любой аутентифицированный query ids (обязателен, ≤ 200) 200 UserBriefOut[] 422 TOO_MANY_IDS, 422 INVALID_IDS [39] «Загрузил»
GET /consolidations/versions/{version_id}/checklist?format=csv CONSOLIDATION_VIEW query format = json (по умолчанию, ChecklistOut) или csv 200 text/csv; charset=utf-8 404 NOT_FOUND, 422 (pydantic-массив) при другом format [37] «Скачать»

Изменения в уже существующих ответах: comments_count (целое, 0 по умолчанию) — у документа в карточке файла (GET /legal-entities/{id}/files, GET /files/{id}: documents[]) и в DocumentOut (GET /documents/{id}); documents_count (целое) — в LegalEntityOut (GET /directories/legal-entities, GET …/{id}): число документов файлов ЮЛ, кроме файлов в статусе SUPERSEDED.

Подтверждение формы ББ/ОФР привязывает её к версиям консолидации «в работе» так же, как ОСВ (Срез 4): ББ/ОФР — отчёт на конец квартала и подходит версии за тот же квартал (форма за Q1 — только версиям за Q1); ячейка «Бух. баланс» / «ОФР» чек-листа после этого — CONFIRMED.

3. Схемы

Поля без ? присутствуют всегда; null-able поля помечены | null.

StatementOut {document, edition, header, columns[], lines[], checks}:

  • document — StatementDocumentOut {id, document_type, status, period_year | null, period_quarter | null, reporting_date | null, counterparty_check | null, locator | null, file_id}.
  • edition — "66n" (дата периода документа ≤ 2024-12-31), "fsbu4" (позже) или null (даты нет): для выбора шаблона строк формы; на проверки §7 не влияет (OQ-146).
  • header — StatementHeaderOut: inn, report_date, units — каждый HeaderFieldOut {value | null, original_value | null, confidence | null, verified | null, modified | null} (confidence есть только у ИНН; report_date.value — ISO-дата строкой; original_value — как распознано, при правке не меняется), плюс строки organization_name, okud, okpo, okopf, okfs, address (все | null).
  • columns[] — ColumnOut {id, column_index, label | null, period_date | null, is_current | null}; is_current = true у колонки с самой поздней датой.
  • lines[] — по коду строки (по возрастанию line_code): LineOut {id, line_code | null, line_code_confidence | null, line_name | null, line_name_confidence | null, values[]}; values[] — по одной записи на каждую колонку формы (ValueOut {id, column_id, column_index, amount | null, string_value | null, confidence | null, is_recognized | null, is_modified, original_amount | null, modified_by | null, modified_at | null}). amount — текущее значение (после правок); original_amount заполняется с первой правки; string_value — неразобранный текст ячейки (когда amount пуст).
  • checks — ChecksOut {failed[], soft_failed[], cross_form | null, accepted | null}: FailedCheckOut {column_index, relation, expected, actual} (relation — читаемая запись соотношения, expected и actual — суммы строками; column_index = -1 — у межформенной проверки колонки нет); accepted — {by, at, comment} (в коде схема ChecksAcceptedOut) или null.

StatementPatch {header?: {inn?, report_date?, units?}, values?: [{line_id, column_id, amount}]} — частичное тело, лишние поля запрещены (422). header.inn — строка из 10 или 12 цифр (любое другое значение, включая null, — 422 INVALID_INN: ИНН очистить нельзя); header.report_date — дата, null очищает; header.units — непустая строка после обрезки пробелов, ≤ 50 (null — «не менять», единицы очистить нельзя; пробельная строка — 422 pydantic-массивом). Поле шапки, которого нет в теле, не меняется. values — до 5000 ячеек: {line_id | line_code, column_id, amount} (ровно одно из line_id/line_code); одна ячейка дважды в запросе — 422 DUPLICATE_CELL; line_id/column_id не этого документа — 422 UNKNOWN_CELL. Полный состав формы (arm-core#80): в StatementOut lines[].origin (source | template), у достроенных по справочнику редакции строк и ячеек id = null; line_code создаёт строку (line_code_original_value = NULL) при первой непустой сумме. Новые случаи UNKNOWN_CELL: код уже есть в форме (передайте line_id), код вне справочника, нет редакции. ChecksAcceptIn {comment}.

CommentIn {text}. CommentOut {id, text, author: {id, full_name}, created_at}. UserBriefOut {id, full_name, is_active}.

DocumentOut (из Среза 2c) + comments_count. Ошибка CHECKS_NOT_PASSED несёт доп. поле failures[] {column_index, relation, expected, actual} (суммы — строками) внутри detail: {"detail": {"code": "CHECKS_NOT_PASSED", "message": "...", "failures": [...]}}.

4. Нюансы, которые легко пропустить

  1. Форма создаётся при маппинге, а не при загрузке. Пока файл UPLOADED/PROCESSING, документов формы нет. Документ появляется в карточке файла (documents[]) после статуса файла PROCESSED; GET …/statement до этого — 404.
  2. Значение на каждую ячейку. У каждой пары «строка × колонка» есть ValueOut с собственным id, даже если в результате распознавания ячейки не было (тогда amount = null, confidence = null, is_recognized = null). Для правки не нужно создавать значения: передавайте line_id и column_id существующей ячейки. Строк без кода (расшифровочных, «в том числе …») в ответе нет (OQ-36, OQ-141, OQ-149).
  3. Подтверждённая форма — только для чтения. PATCH и accept по документу CONFIRMED — 409 DOCUMENT_READONLY; отмены подтверждения нет, исправление — новым файлом (автозамена, 05-domain-rules.md §9.5 п. 3). По файлу SUPERSEDED — 409 FILE_SUPERSEDED; по документу в статусе ERROR — 409 DOCUMENT_NOT_CONFIRMABLE. Комментарии при этом разрешены всегда.
  4. Правка значения: is_modified снимается возвратом к исходному. При первой правке ячейки original_amount = прежняя сумма; is_modified = (amount ≠ original_amount). Верните исходную сумму — is_modified = false, original_amount остаётся. Правка шапки: modified = true у поля, original_value не меняется (возврат значения modified не снимает).
  5. Правка даты отчёта пересчитывает период. После PATCH header.report_date в document меняются period_year, period_quarter (только если дата — последний день квартала, иначе null), reporting_date; edition — по новой дате. Правка inn пересчитывает counterparty_check. Статус документа (RECOGNIZED/NEEDS_CHECK) пересчитывается после каждой правки.
  6. Акцепт сбрасывается любой правкой формы. После PATCH, который что-то изменил, checks.accepted снова null. Запрос, который ничего не меняет (те же значения, пустое тело), — 200 с неизменным состоянием, без записи в журнал и без сброса акцепта. Если акцепт был, в записи журнала RECOGNIZED_DATA_EDIT его прежнее значение сохраняется в before.checks_accepted (by, at, comment), поэтому текст сброшенного акцепта можно восстановить из аудита.
  7. checks.failed не очищается акцептом. Акцепт не скрывает нарушения: failed остаётся заполненным, а accepted непусто — это сигнал «нарушения приняты аналитиком». Блокируют подтверждение только нарушения без акцепта.
  8. Что блокирует подтверждение (POST …/confirm): период не определён — 409 PERIOD_REQUIRED (исправить датой отчёта); жёсткие нарушения без акцепта — 409 CHECKS_NOT_PASSED (список — в detail.failures). Не блокируют: мягкие нарушения (soft_failed, в том числе «в пределах округления»), межформенная проверка (cross_form), counterparty_check = MISMATCH, низкая уверенность значений. Порядок 409, если совпало несколько: FILE_SUPERSEDED, ALREADY_CONFIRMED, DOCUMENT_NOT_CONFIRMABLE, PERIOD_REQUIRED, CHECKS_NOT_PASSED. DISCLOSURE/OTHER подтверждаются без условий.
  9. Статус документа формы — подсказка экрану, а не замок: RECOGNIZED только если нет жёстких нарушений без акцепта, контрагент не MISMATCH, период определён, все значения уверенные или правленые и нет предупреждений маппинга. Формы из OCR почти всегда NEEDS_CHECK (уверенность < 100, ИНН распознан неуверенно) — это ожидаемо; подтвердить можно и из NEEDS_CHECK.
  10. Межформенная проверка зависит от другого документа. cross_form считается, только если у ЮЛ есть документ-пара другого типа формы за тот же год и квартал (файл не SUPERSEDED; подтверждённый предпочтительнее) и найдены обе колонки; иначе null. Появление ОФР отдельным файлом позже меняет cross_form у уже открытого ББ — перечитайте форму. Нарушение мягкое.
  11. Редакция формы (edition) — только подсказка. Определяется ядром по дате периода документа; для документа без даты — null. Контракт обмена с сервисами распознавания редакцию не передаёт (OQ-145).
  12. Исходный файл. GET /files/{id}/content отдаёт байты через ядро; прямой ссылки на S3 нет. Забирайте как blob с Authorization (<a href> и <img src> без заголовка не сработают). Content-Type — по типу файла (application/pdf, image/png, image/jpeg, для xls/xlsx/xlsb — свои MIME). Content-Disposition: attachment (по умолчанию) или inline (для просмотра в браузере) с ASCII-именем file.<ext> и filename*=UTF-8''… — исходным именем в percent-кодировке; берите имя из filename*. Ответ несёт X-Content-Type-Options: nosniff (браузер не угадывает тип по содержимому; тип задаёт Content-Type) и Cache-Control: private, no-store (не кэшируйте байты); при disposition=inline — ещё Content-Security-Policy: sandbox (активное содержимое файла в браузере не исполняется; проверьте, что выбранный просмотрщик PDF работает при таком заголовке). Работает и для файла SUPERSEDED. Объекта нет в хранилище (убран ретеншном) — 404 FILE_CONTENT_NOT_FOUND (не путать с NOT_FOUND: файл в БД есть, а байтов нет); хранилище не подключено — 503 STORAGE_UNAVAILABLE. Скачивание в журнал не пишется (OQ-153).
  13. Комментарии. Только добавление и чтение; правки и удаления нет. created_at — порядок показа (по возрастанию). text обрезается по краям, пустой после обрезки или длиннее 4000 символов — 422 в формате pydantic. Автор — пользователь запроса. comments_count в карточках — для бейджа; список перечитывается отдельным запросом. Удаление файла (DELETE /files/{id}) удаляет и комментарии его документов, и данные форм ББ/ОФР (financial_statement_*): после этого GET …/comments и GET …/statement по документам файла — 404.
  14. Имена пользователей. GET /users/brief нужен, чтобы показать «Загрузил» (uploaded_by) и авторов правок (modified_by, checks.accepted.by, confirmed_by) без права USERS_ADMIN. Отдаёт только найденные id (включая неактивных, is_active = false), упорядоченные по id, а не в порядке запроса; неизвестные пропускаются без ошибки; пустой ids — []; больше 200 id — 422 TOO_MANY_IDS; не-UUID — 422 INVALID_IDS. Собирайте id пачкой и кэшируйте.
  15. CSV чек-листа. text/csv; charset=utf-8, BOM в начале, разделитель ;, переводы строк CRLF. Первая строка — «Организация;ИНН;<правило>…», где правило — «Бух. баланс», «ОФР», «ОСВ <счёт>» (в порядке rules[] чек-листа). Ячейки — подписи макета: CONFIRMED «Загружен», PROCESSING «Обработка», NEEDS_CHECK и RECOGNIZED «Требуется проверка», ERROR «Ошибка загрузки», MISSING «Отсутствует»; при пропущенных кварталах — с хвостом « (нет: Q1, Q2)». Заголовок ответа — Content-Disposition: attachment; filename="checklist.csv"; filename*=UTF-8''checklist-<ГК>-<год>-<квартал>.csv (ASCII-имя фиксированное; настоящее имя — в filename*, percent-кодировка целиком, кириллица и пробелы экранируются; берите имя оттуда). Забирайте как blob с Authorization. Правило подписи — по document_type правила чек-листа; ячейки с текстом (имя ЮЛ) в CSV не нейтрализуются от формул Excel (design №19, R-9): значение, начинающееся с =, +, -, @, Excel может принять за формулу. Без format=csv (по умолчанию format=json) маршрут отдаёт JSON ChecklistOut (Срез 4); значение format, кроме json и csv, — 422 (pydantic-массив); право — CONSOLIDATION_VIEW, версия вне видимости — 404 NOT_FOUND.
  16. documents_count у ЮЛ считается на странице списка и в карточке одним запросом; заменённые файлы (SUPERSEDED) не входят. Число документов, а не файлов: один файл с ББ и ОФР даёт 2.
  17. Что не вошло в срез. Правка кода и названия строки, добавление и удаление строк и колонок, отмена подтверждения, смена типа формы, склейка страниц одной формы из разных файлов (OQ-140), формулы §7 по редакциям (OQ-146).

5. Сквозной сценарий экрана «Проверка распознавания» [41]

Шаг Вызовы
Открыть документ из панели [39] список документов — GET /legal-entities/{id}/files (documents[]: id, document_type, status, comments_count); имена загрузивших — GET /users/brief?ids=…
Левая панель «Исходный документ» GET /files/{file_id}/content?disposition=inline (blob с токеном), file_id — из StatementOut.document.file_id или карточки файла
Правая панель «Результат распознавания» GET /documents/{id}/statement: шапка, колонки, строки со значениями; блок «Логические ошибки» — checks.failed / soft_failed / cross_form; подсветка — confidence < 100, is_recognized = false, is_modified
Переход между документами организации (выпадающий список «Документ») тот же GET /documents/{id}/statement по другому document_id
«Редактировать» → «Сохранить» [41-02] PATCH /documents/{id}/statement {values:[…], header:{…}} → в ответе новое состояние формы (проверки, статус, период пересчитаны)
Нарушены жёсткие проверки, данные верны POST /documents/{id}/statement/checks/accept {comment} → checks.accepted заполнен
«Подтвердить данные» POST /documents/{id}/confirm; 409 PERIOD_REQUIRED — предложить поправить дату отчёта; 409 CHECKS_NOT_PASSED — показать detail.failures
«Добавить комментарий» POST /documents/{id}/comments {text}, список — GET /documents/{id}/comments
После подтверждения — чек-лист [37] GET /consolidations/versions/{id}/checklist (ячейка «Бух. баланс»/«ОФР» — CONFIRMED), «Скачать» — тот же маршрут с format=csv