- Ссылки на источник истины v5.13/v5.14/v5.15 → v5.17: openspec/config.yaml, KICKOFF-PROMPT, 00-workspace-layout, 08-source-docs-index, заголовок 05-domain-rules, dev/checks.md; спека quality-checks — без привязки к версии (путь — переменная DBML Makefile). - Диаграммы model/diagrams/part01…09 перерисованы по v5.17 (были v5.12): scripts/gen_model_diagrams.py (разбиение DBML на части + dbml-renderer + resvg через npx, версии закреплены), цель make diagrams; описание в 04-data-model-guide, 06-test-data, 08-source-docs-index. - dev/frontend-api-guide.md: КПП (arm-core #52) и DB_SCHEMA (#53) — из §11 в §1, КПП в маршрутах §5.2/§5.3, схеме §5.8 и каталоге ошибок §9 (сверено с кодом 99669cd). - Ссылки на заархивированные change'и (dev/README, dev/checks, api-contract-slice-3b/4), путь к v4 DBML в индексе. - 09-delivery-plan: статусы Срезов 0, 2d, 3b, 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
25 KiB
Контракт 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— 409DOCUMENT_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[]} |
200 StatementOut (состояние после правки) |
404, 409 FILE_SUPERSEDED / DOCUMENT_READONLY / DOCUMENT_NOT_CONFIRMABLE / DOCUMENT_NOT_STATEMENT, 422 INVALID_INN / UNKNOWN_CELL / DUPLICATE_CELL, 422 (pydantic-массив) |
[41-02] «Сохранить» |
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 ячеек; одна ячейка дважды в запросе — 422
DUPLICATE_CELL; line_id/column_id не этого документа — 422 UNKNOWN_CELL. 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. Нюансы, которые легко пропустить
- Форма создаётся при маппинге, а не при загрузке. Пока файл
UPLOADED/PROCESSING, документов формы нет. Документ появляется в карточке файла (documents[]) после статуса файлаPROCESSED;GET …/statementдо этого — 404. - Значение на каждую ячейку. У каждой пары «строка × колонка» есть
ValueOutс собственнымid, даже если в результате распознавания ячейки не было (тогдаamount = null,confidence = null,is_recognized = null). Для правки не нужно создавать значения: передавайтеline_idиcolumn_idсуществующей ячейки. Строк без кода (расшифровочных, «в том числе …») в ответе нет (OQ-36, OQ-141, OQ-149). - Подтверждённая форма — только для чтения.
PATCHиacceptпо документуCONFIRMED— 409DOCUMENT_READONLY; отмены подтверждения нет, исправление — новым файлом (автозамена,05-domain-rules.md§9.5 п. 3). По файлуSUPERSEDED— 409FILE_SUPERSEDED; по документу в статусеERROR— 409DOCUMENT_NOT_CONFIRMABLE. Комментарии при этом разрешены всегда. - Правка значения:
is_modifiedснимается возвратом к исходному. При первой правке ячейкиoriginal_amount= прежняя сумма;is_modified = (amount ≠ original_amount). Верните исходную сумму —is_modified = false,original_amountостаётся. Правка шапки:modified = trueу поля,original_valueне меняется (возврат значенияmodifiedне снимает). - Правка даты отчёта пересчитывает период. После
PATCH header.report_dateвdocumentменяютсяperiod_year,period_quarter(только если дата — последний день квартала, иначеnull),reporting_date;edition— по новой дате. Правкаinnпересчитываетcounterparty_check. Статус документа (RECOGNIZED/NEEDS_CHECK) пересчитывается после каждой правки. - Акцепт сбрасывается любой правкой формы. После
PATCH, который что-то изменил,checks.acceptedсноваnull. Запрос, который ничего не меняет (те же значения, пустое тело), — 200 с неизменным состоянием, без записи в журнал и без сброса акцепта. Если акцепт был, в записи журналаRECOGNIZED_DATA_EDITего прежнее значение сохраняется вbefore.checks_accepted(by,at,comment), поэтому текст сброшенного акцепта можно восстановить из аудита. checks.failedне очищается акцептом. Акцепт не скрывает нарушения:failedостаётся заполненным, аacceptedнепусто — это сигнал «нарушения приняты аналитиком». Блокируют подтверждение только нарушения без акцепта.- Что блокирует подтверждение (
POST …/confirm): период не определён — 409PERIOD_REQUIRED(исправить датой отчёта); жёсткие нарушения без акцепта — 409CHECKS_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подтверждаются без условий. - Статус документа формы — подсказка экрану, а не замок:
RECOGNIZEDтолько если нет жёстких нарушений без акцепта, контрагент неMISMATCH, период определён, все значения уверенные или правленые и нет предупреждений маппинга. Формы из OCR почти всегдаNEEDS_CHECK(уверенность < 100, ИНН распознан неуверенно) — это ожидаемо; подтвердить можно и изNEEDS_CHECK. - Межформенная проверка зависит от другого документа.
cross_formсчитается, только если у ЮЛ есть документ-пара другого типа формы за тот же год и квартал (файл неSUPERSEDED; подтверждённый предпочтительнее) и найдены обе колонки; иначеnull. Появление ОФР отдельным файлом позже меняетcross_formу уже открытого ББ — перечитайте форму. Нарушение мягкое. - Редакция формы (
edition) — только подсказка. Определяется ядром по дате периода документа; для документа без даты —null. Контракт обмена с сервисами распознавания редакцию не передаёт (OQ-145). - Исходный файл.
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. Объекта нет в хранилище (убран ретеншном) — 404FILE_CONTENT_NOT_FOUND(не путать сNOT_FOUND: файл в БД есть, а байтов нет); хранилище не подключено — 503STORAGE_UNAVAILABLE. Скачивание в журнал не пишется (OQ-153). - Комментарии. Только добавление и чтение; правки и удаления нет.
created_at— порядок показа (по возрастанию).textобрезается по краям, пустой после обрезки или длиннее 4000 символов — 422 в формате pydantic. Автор — пользователь запроса.comments_countв карточках — для бейджа; список перечитывается отдельным запросом. Удаление файла (DELETE /files/{id}) удаляет и комментарии его документов, и данные форм ББ/ОФР (financial_statement_*): после этогоGET …/commentsиGET …/statementпо документам файла — 404. - Имена пользователей.
GET /users/briefнужен, чтобы показать «Загрузил» (uploaded_by) и авторов правок (modified_by,checks.accepted.by,confirmed_by) без праваUSERS_ADMIN. Отдаёт только найденные id (включая неактивных,is_active = false), упорядоченные поid, а не в порядке запроса; неизвестные пропускаются без ошибки; пустойids—[]; больше 200 id — 422TOO_MANY_IDS; не-UUID — 422INVALID_IDS. Собирайте id пачкой и кэшируйте. - 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) маршрут отдаёт JSONChecklistOut(Срез 4); значениеformat, кромеjsonиcsv, — 422 (pydantic-массив); право —CONSOLIDATION_VIEW, версия вне видимости — 404NOT_FOUND. documents_countу ЮЛ считается на странице списка и в карточке одним запросом; заменённые файлы (SUPERSEDED) не входят. Число документов, а не файлов: один файл с ББ и ОФР даёт 2.- Что не вошло в срез. Правка кода и названия строки, добавление и удаление строк и колонок, отмена подтверждения, смена типа формы, склейка страниц одной формы из разных файлов (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 |