arm-ka-docs/dev/api-contract-slice-3b.md
Andrey Epifancev 2b3ead461a docs: исправить вставку в §11 и сигнатуры контракта 3b (#81)
M-7: вставка про достройку строк разорвала перечисление в 05-domain-rules.md §11, вынесена отдельным предложением.
M-8: в api-contract-slice-3b.md у LineOut/ValueOut добавлены id | null и origin, у StatementPatch.values - line_id | line_code.

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

169 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Контракт 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-массив) | [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 | null, origin: 'source' | 'template', line_code | null, line_code_confidence | null, line_name |
null, line_name_confidence | null, values[]}`; `values[]` — по одной записи на **каждую** колонку формы (`ValueOut {id | null, 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 | line_code, 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` |