# Контракт 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`); иначе по документу: `RECOGNIZED` и `NEEDS_CHECK` — «Требуется проверка», `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); пользователю филиала он не нужен (видит один РФ).