arm-ka-docs/dev/api-contract-files-directories.md
Andrey Epifancev e975f56386 docs: fix round 1 — автоподтверждение: §9.1/§9.3/§9.5, OQ-157, гайды QA и фронта
Правки по ревью: цепочка F3–F10 на форме с нарушением (чистая CONFIRMED),
F11 — ручное подтверждение после правки, DISCLOSURE/OTHER автоподтверждаются,
удаление — условия вне корректировки данных, W17 — файл вне демо-периметра.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:59:21 +04:00

112 lines
13 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: файлы и документы организации, справочники (для экранов до сводной ОСВ)
Источник — реальный код arm-core (`api/src/schemas/files.py`, `schemas/documents.py`, `schemas/directories.py`,
`schemas/common.py`, `api/endpoints/files/{upload,read,documents}.py`, `api/endpoints/directories/*.py`; Swagger — `/docs`).
Маршруты существуют со Срезов 1 и 2c; здесь они впервые описаны для фронта (arm-core #42, П-5). Общие правила (префикс, токен,
формат ошибок, видимость 404) — `dev/api-contract-slice-4.md` §1; форма ББ/ОФР, комментарии, исходный файл — `dev/api-contract-slice-3b.md`;
консолидации — `dev/api-contract-slice-4.md`. Экраны — `context/10-user-journey.md`, номера `[…]` — снимки макета (`mockup/INDEX.md`).
## 1. Общее
- Права: файлы и документы — чтение `DOCUMENT_VIEW`, загрузка/удаление/подтверждение — `DOCUMENT_EDIT`; справочники —
`DIRECTORY_VIEW` (импорт ЕЦП — `DIRECTORY_ADMIN`, только у администратора: кнопку «Импорт» на [34] скрывайте по `GET /me`).
- Страницы — `{items, total, limit, offset}`: у файлов ЮЛ и справочников `limit` 1…200 (по умолчанию 50), у строк ОСВ документа
1…2000 (по умолчанию 500); `offset ≥ 0`; вне предела — 422 (pydantic-массив).
- ЮЛ, файл или документ вне видимости пользователя неотличимы от несуществующих: 404 `NOT_FOUND`.
- Перечисления: `FileType` — `XLS`, `XLSX`, `XLSB`, `PDF`, `PNG`, `JPG`; `FileStatus` — `UPLOADED`, `PROCESSING`, `PROCESSED`, `ERROR`,
`SUPERSEDED`; `RecognitionStatus` (задание) — `queued`, `published`, `processing`, `done`, `needs_check`, `error`, `timeout` (нет ответа к сроку),
`dlq` (сервис не подтвердил приём после повторов); `DocumentType` —
`BALANCE`, `INCOME`, `OSV`, `DISCLOSURE`, `OTHER`; `DocumentStatus` — `RECOGNIZED`, `NEEDS_CHECK`, `CONFIRMED`, `ERROR`;
`DocumentCheckResult` — `MATCH`, `MISMATCH`, `UNKNOWN`; `EntityType` — `ЮЛ`, `ИП`; `RelatedGroupKind` — `gsk`, `gsz`.
## 2. Эндпоинты: файлы и документы
| Метод и путь | Право | Запрос | Ответ | Ошибки | Экран |
|---|---|---|---|---|---|
| `GET /legal-entities/{legal_entity_id}/files` | VIEW | query `limit`, `offset`, `include_superseded` (bool, по умолчанию `false` — заменённые скрыты) | `Page[FileOut]`, новые первыми (по убыванию `uploaded_at`) | 404 | [39] drawer «Первичные документы» |
| `POST /legal-entities/{legal_entity_id}/files` | EDIT | multipart, поле `files` (повторяется для каждого файла, не `files[]`); query `consolidation_version_id?` | 201 `UploadResult` (принят хотя бы один файл) / 422 `UploadResult` (ни один) | 404 (ЮЛ; версия — `VERSION_NOT_FOUND`), 409 `VERSION_NOT_EDITABLE`, 422 `ENTITY_NOT_IN_PERIMETER`, 503 `STORAGE_UNAVAILABLE` | [40] |
| `GET /files/{file_id}` | VIEW | — | `FileOut` | 404 | опрос строки файла |
| `DELETE /files/{file_id}` | EDIT | — | 204 | 404, 409 `FILE_HAS_CONFIRMED_DOCUMENTS`, 409 `FILE_SUPERSEDES_OTHERS` | [39] иконка «удалить» |
| `GET /files/{file_id}/content` | VIEW | query `disposition` (`attachment` по умолчанию / `inline`) | поток исходного файла | 404 (`FILE_CONTENT_NOT_FOUND` — объекта нет в хранилище), 503 | [41] «Исходный документ» (см. контракт 3b) |
| `GET /documents/{document_id}` | VIEW | — | `DocumentOut` | 404 | [41], карточка документа |
| `GET /documents/{document_id}/entries` | VIEW | query `limit` (≤ 2000, по умолчанию 500), `offset` | `Page[EntryOut]` по возрастанию `row_number`; дерево — по `parent_entry_id` | 404 | строки ОСВ документа (только чтение) |
| `POST /documents/{document_id}/confirm` | EDIT | — | `DocumentOut` | 404, 409 `ALREADY_CONFIRMED` / `FILE_SUPERSEDED` / `DOCUMENT_NOT_CONFIRMABLE`; для ББ/ОФР — `PERIOD_REQUIRED`, `CHECKS_NOT_PASSED` | «Подтвердить данные» |
Фильтра «файлы в контексте версии консолидации» **нет** (решение владельца 07.10.2026, arm-core #42): drawer показывает все
документы ЮЛ, период — в колонке «Период». Подходит ли документ версии, видно по ячейкам чек-листа версии (`document_ids`).
## 3. Схемы: файлы и документы
**`UploadResult`** `{accepted[], rejected[]}`: `accepted[]` — `{file_id, job_id, name}`; `rejected[]` — `{name, code, message,
file_id?}`, где `code` — `UNSUPPORTED_FILE` (тип не из списка или сигнатура не совпала), `FILE_TOO_LARGE` (больше `UPLOAD_MAX_BYTES`, по умолчанию 50 МиБ),
`DUPLICATE_FILE` (такой файл у ЮЛ уже есть — `file_id` указывает на него), `STORAGE_UNAVAILABLE`. **Два разных тела 422**: при
отказе всех файлов — `UploadResult` (смотрите `rejected[]`); при ЮЛ вне периметра версии — доменная ошибка
`{"detail": {"code": "ENTITY_NOT_IN_PERIMETER", "message": …}}`. Различайте по наличию `detail`.
**`FileOut`** `{id, legal_entity_id, original_name?, file_type, status (FileStatus), version_no, size_bytes?, content_hash?,
uploaded_at?, uploaded_by?, superseded_by_file_id?, job (JobBrief | null), documents[] (DocumentBrief)}` — поля с `?` могут быть
`null`. **`JobBrief`** `{id, status (RecognitionStatus), error_code?, error_message?, mapping_error?, warnings[],
mapping_warnings[], finished_at?}` — причина ошибки сервиса распознавания (`error_*`) или маппинга (`mapping_error`).
**`DocumentBrief`** `{id, document_type, osv_account_code?, period_year?, period_quarter?, is_cumulative?, status (DocumentStatus),
period_check?, counterparty_check?, locator?, comments_count}` — порядок: тип, счёт, `locator`.
**`DocumentOut`** = поля `DocumentBrief` + `{file_id, legal_entity_id, confirmed_by?, confirmed_at?, group_schema? ([{type, name?,
has_inn?}]), warnings[]}`. **`EntryOut`** `{id, row_number?, level?, parent_entry_id?, row_value?, group_type?, group_name?,
data_type?, account_code?, account_root?, account_id?, counterparty_name?, counterparty_inn?, counterparty_id?, match_kind?,
opening_debit?, opening_credit?, turnover_debit?, turnover_credit?, closing_debit?, closing_credit?}` — деньги строками.
### Как собрать строку drawer [39]
Строка — **файл** (`FileOut`); документы файла (`documents[]`) — подписи и проверки:
- «Период»: `period_year` + `period_quarter` + `is_cumulative` — квартальный «2024 К1», нарастающий Q3 — «2024 · 9 мес.»
(Q1 — «3 мес.», Q2 — «6 мес.», Q4 — «12 мес.»); `period_quarter = null` — «период не определён» (`dev/api-contract-slice-4.md`,
нюанс 17).
- «Проверка»: «Контрагент» — `counterparty_check` (`MATCH` ✓, `MISMATCH` ✗, `UNKNOWN`/`null` —); «Период» — `period_check` сейчас
всегда `UNKNOWN` → показывайте «—» (OQ-120).
- «Статус» (подписи — как в чек-листе и его CSV, `context/10-user-journey.md` «Словарь UI»): файл `UPLOADED`/`PROCESSING` —
«Обработка»; файл `ERROR` — «Ошибка» (причина — `job.error_message` / `job.mapping_error`; задание `timeout`/`dlq` — тоже
ошибка, причина в `job.error_message`); иначе по документу: `NEEDS_CHECK` и `RECOGNIZED` (старые и формы после правки) — «Требуется проверка», `CONFIRMED` (в т.ч. автоподтверждённый) —
«Загружен», `ERROR` — «Ошибка».
- Комментарий — у **документа** (`comments_count`, `GET/POST /documents/{id}/comments`, контракт 3b); у файла без документов
комментария нет.
- Опрос во время распознавания: перечитывайте `GET /legal-entities/{id}/files` (или `GET /files/{id}` для одного файла), пока
файл в `UPLOADED`/`PROCESSING`; разумный интервал — 3–5 с.
- Клик по строке ББ/ОФР-документа (PDF и Excel) — экран [41] (`GET /documents/{id}/statement`, контракт 3b); строки ОСВ — только
чтение (`GET /documents/{id}/entries`).
## 4. Эндпоинты: справочники (экраны [03], [36], [42], [60])
Все — `GET`, право `DIRECTORY_VIEW`, префикс `/api/v1/directories`.
| Путь | Query | Ответ | Экран |
|---|---|---|---|
| `/regional-offices` | — | `RegionalOfficeOut[]` `{id, name, is_head_office, is_active}` | выбор филиала в формах (имя «Основной РФ» на [03] приходит объектом в ГК, arm-core #47) |
| `/company-groups` | `q` (подстрока name / crm_id / code), `is_archived`, `limit`, `offset` | `Page[CompanyGroupOut]` по имени | [03], выбор ГК в [42] |
| `/company-groups/{group_id}` | — | `CompanyGroupOut` | заголовок [34] («N организаций» — `legal_entities_count`) |
| `/company-groups/{company_group_id}/related-groups` | — | `RelatedGroupOut[]` (ГСК и ГСЗ; ГСК — `kind = gsk`) | выбор ГСК в [42], фильтр «ГСК» в [36] |
| `/related-groups/{group_id}` | — | `RelatedGroupDetail` (+ `members[]`) | отметки [42], колонка «ГСК» в [36] |
| `/legal-entities` | `company_group_id`, `outside_group` (bool), `inn` (точно), `q` (подстрока имени/ИНН), `closed` (bool), `limit`, `offset` | `Page[LegalEntityOut]` по имени | [36] «Показать все ЮЛ в ГК», список ЮЛ в [42], монозаёмщики [60] |
| `/legal-entities/{entity_id}` | — | `LegalEntityDetail` (+ `memberships[]`, `aliases[]`) | карточка ЮЛ |
**`CompanyGroupOut`** `{id, code, crm_id?, name, base_regional_office_id?, base_regional_office? {id, name, is_head_office}, is_archived, valid_from, legal_entities_count, created_at?,
updated_at?}`. **`RelatedGroupOut`** `{id, code, company_group_id, kind, name, created_date?, members_count}`;
**`RelatedGroupDetail`** — + `members[] {legal_entity_id, inn, name, valid_from}` (текущий состав). **`LegalEntityOut`** `{id, inn,
crm_id?, entity_type?, name, company_group_id?, regional_office_id?, regional_office? {id, name, is_head_office}, close_date?, valid_from, created_at?, updated_at?,
documents_count, outside_group, closed}` (`outside_group` — нет ГК, монозаёмщик; `closed` — заполнен `close_date`; оба вычисляет сервер); **`LegalEntityDetail`** — + `memberships[] {related_group_id, code, kind, name, valid_from}`, `aliases[] {id,
alias, normalized_alias, created_at?}`.
Нюансы:
- **Модалка [42]**: ЮЛ ГК — `legal-entities?company_group_id=<ГК>&closed=false` (неархивные = без `close_date`; `limit` ≤ 200, а
периметр — до 500 ЮЛ: дочитывайте страницы по `offset`, пока не наберёте `total`), отметки — члены
выбранной ГСК из `related-groups/{id}`; создание — `POST /consolidations` с `legal_entity_ids` (`dev/api-contract-slice-4.md`,
нюанс 19).
- **Колонка «ГСК» в [36]**: у `LegalEntityOut` членства нет — возьмите `related-groups/{id}` по каждой ГСК ГК (их немного) или
`LegalEntityDetail.memberships`.
- **Монозаёмщики [60]**: `legal-entities?outside_group=true`; «Документы» — `documents_count`, статусы ОФР/ББ/ОСВ — из
`GET /legal-entities/{id}/files` по ЮЛ (сводного поля нет; экран не отнесён к срезу, П-6).
- Филиал (`base_regional_office` у ГК, `regional_office` у ЮЛ и в истории) приходит объектом с именем, `*_id` рядом остаются; неактивный
филиал тоже отдаётся (arm-core #47).
- Поиск ГК по имени РФ не поддерживается (`q` — только name/crm_id/code); пользователю филиала он не нужен (видит один РФ).