arm-ka-docs/dev/frontend-api-guide.md
Andrey Epifancev 21c0f900bd §9.4–9.5, гайд фронта: повтор обработки файла и уточнение шапки ОСВ (#156, AURORA-1442)
§9.4 — автоповтор публикации не наращивает attempt, ручной повтор — новое задание.
§9.5 п. 12 — ключ автозамены после уточнения; п. 13 — коды, 202, файл из консолидации,
состав пакета (только файлы в ERROR), accepted/skipped.
frontend-api-guide — маршруты retry и osv-header, новые поля DocumentOut, коды ошибок.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 17:12:04 +04:00

124 KiB
Raw Blame History

Гайд фронтенд-разработчика: API arm-core

Состояние — arm-core на main от 08.10.2026 (коммит d8bf5b2: после PR #46 «API до сводной ОСВ», #51 «Основной РФ объектом», #52 «КПП ЮЛ», #53 «Схема БД настройкой», #59 «Алиас приоритетнее имени» и #60 «Предупреждение автозамены»). Схема сверена с GET /openapi.json (62 операции на 49 путях), коды ошибок — с api/src/services/* и arm-core/README.md «Ошибки API». Раздел 5.6.1 (формы ББ/ОФР после единой нормализации #116) приведён к фактическому API arm-core main 35a24e7a (часть А — PR #129, часть Б — PR #132; схемы сверены с pydantic-моделями api/src/schemas/{statements,files,issues}.py, ошибки — с README ядра «Ошибки API»).

Это точка входа: здесь всё, что нужно, чтобы писать клиент, — подключение, общие правила, каждый маршрут, ошибки, сценарии экранов, известные ограничения и то, что изменится в ближайших PR. Подробные контракты по областям остаются источником деталей и на них даны ссылки:

Документ Что там подробнее
frontend-auth-guide.md вход, токен, /me, 401/403, эталонный клиент web/src/{api,auth}
api-contract-files-directories.md файлы и документы ЮЛ, чтение справочников, сборка строки drawer [39]
api-contract-slice-3b.md формы ББ/ОФР, комментарии, исходный файл, users/brief, CSV чек-листа
api-contract-slice-4.md консолидации: версии, блокировка, периметр, чек-лист, сводная ОСВ, сопоставление
context/10-user-journey.md, mockup/INDEX.md пользовательский путь; номера экранов [NN] — снимки макета

Если этот гайд и OpenAPI расходятся в схемах и полях — прав OpenAPI работающего стенда (/openapi.json), а расхождение — повод для issue в RSHB/arm-ka-docs. Текстовые описания ошибок в OpenAPI местами неточны (например, до v5.18 «VERSION_NOT_FOUND» у загрузки с query consolidation_version_id — на деле код NOT_FOUND; у загрузки 422 ENTITY_NOT_IN_PERIMETER приходит объектом detail, а не UploadResult): коды ошибок сверяйте по §9.


1. Что изменилось за последние дни (05–14.10.2026)

Если клиент писался по более ранним контрактам, пройдитесь по этому списку.

Когда, PR Что изменилось в API Что сделать в клиенте
18.10, arm-core #129 и #132 (arm-ka-docs#116, DBML v5.19, контракт распознавания 2.0) Единая нормализация ББ/ОФР (подробно — §5.6.1). GET/PATCH …/statement: все строки источника со статусами (line_status) и повторами кода, needs_review/is_confirmed, issues[], edition_lines[], header.source/needs_review_fields, columns[].period_kind; PATCH — части lines, columns, confirmed в values, header.organization_name; подтверждение — новые 409 LINES_UNRESOLVED, DUPLICATE_FORM; новые 409/422 правки: LINE_CODE_TAKEN, LINE_CODE_REQUIRED, INVALID_LINE_CODE, INVALID_LINE_PATCH, UNKNOWN_LINE, UNKNOWN_COLUMN, DUPLICATE_COLUMN. Несовместимо: FileOut.job.warnings — список объектов {code, severity, message, locator}, а не строк; FileOut.documents[].issue_counts и DocumentOut.issues[]; StatementLineOut.line_code — null у строк не ok; FailedCheckOut.expected/actual — могут быть null; crm_id ГСК/ГСЗ сохраняется (#112) обновить типы JobBrief.warnings, LineOut, FailedCheckOut (в types/api.ts) вместе с выкладкой ядра; не держать словарь «код → строка», подсветку строить по needs_review; остальное — §5.6.1
16.10, arm-core #118 (AURORA-1527) GET/PATCH …/statement, POST …/statement/checks/accept: у каждой строки lines[] новое поле parent_code — у пятизначной подстроки (11101…) код её четырёхзначного родителя, иначе null; вычисляется ядром по правилу проверки «сумма подстрок = родитель», в БД не хранится выводить подстроки под родителем по parent_code, не вычисляя родителя на клиенте; пустые подстроки (нет значения ни в одной колонке) можно сворачивать
14.10, arm-core #111 crm_id у ГСК/ГСЗ — принимается, но не сохраняется. POST …/company-groups/{id}/related-groups и PATCH /directories/related-groups/{id} принимают необязательный crm_id (раньше — 422 extra_forbidden); значение отбрасывается — колонки в модели пока нет (arm-core #112). В RelatedGroupOut/RelatedGroupDetail появилось поле crm_id, сейчас всегда null формы [51-03]/[51-04]/[77-02]: поле «ID ГСК/ГСЗ в CRM» можно отправлять; после сохранения не ждать его в ответе — показывать то, что пришло (null), пока не закрыта #112
13.10, arm-core #110 (#109) Членства ГСК/ГСЗ в списке ЮЛ. memberships[] {related_group_id, code, kind, name, valid_from} перенесено из LegalEntityDetail в LegalEntityOut: приходит в GET /directories/legal-entities, в ответах move/withdraw (после них — []) и, как раньше, в карточке. Только текущие членства; нет — []. Серверного фильтра по ГСК/ГСЗ нет карточка ГК [36] и модалка [42]: колонка «ГСК»/«ГСЗ» и фильтр по ним — из memberships строк списка, без related-groups/{id} и без карточки каждого ЮЛ
09.10, docs #90 (DBML v5.18; arm-core и arm-web — дочерние PR) Файлы в контексте консолидации. Новые маршруты POST /consolidations/{cid}/legal-entities/{le}/files (загрузка в консоль: 404; 409 CONSOLIDATION_NOT_EDITABLE — нет версии «в работе»; 422 ENTITY_NOT_IN_PERIMETER) и GET /consolidations/{cid}/legal-entities/{le}/files (рабочий стол консоли для ЮЛ: файлы консоли + файлы с документами, привязанными к её версиям; include_superseded). Query consolidation_version_id у POST /legal-entities/{id}/files удалён; FileOut.consolidation_id: uuid | null. Отменяет решение 07.10 «фильтра файлов ЮЛ в контексте версии нет» панель [39] — стол консоли (current_consolidation_id из ConsolidationPeriodOut.consolidation_id строки списка / карточки версии consolidation_id), переключатель «Все документы организации» → GET /legal-entities/{id}/files с подписью консолидации у файла; загрузка [40] — новым маршрутом, кнопка недоступна, если версия не «в работе»
09.10, arm-core #85 (#84) Форма ЮЛ одним запросом (макет [51-02], [79]): POST и PATCH /directories/legal-entities принимают aliases: string[] (1–500 символов, ≤ 100), gsk_id, gsz_id и применяют их в той же транзакции, что и поля ЮЛ (ошибка — откат всего). POST: алиасы и членства создаются вместе с ЮЛ. PATCH: не переданное поле не меняется; aliases — итоговый набор (сверка по normalized_alias: совпавшие не трогаются, лишние удаляются — нужно право DIRECTORY_DELETE, иначе 403 строкой; aliases: null — 422), gsk_id/gsz_id — итоговая группа (null — выйти, другая — сменить). Новый код 422 WRONG_GROUP_KIND; дубль в списке — 409 ALIAS_EXISTS. Ответ обоих — LegalEntityDetail (было LegalEntityOut; поля прежние + memberships, aliases) «Добавить»/«Сохранить» в модалках ЮЛ — один запрос со всей формой; в PATCH слать aliases и группы целиком, как они на экране; после ответа брать карточку из него, без GET; удаление тега-алиаса без DIRECTORY_DELETE (/me.permissions) не предлагать — сервер ответит 403
08.10, arm-core #75 (#74) Отмена П-1 (решение владельца 08.10.2026): «несопоставленная» строка свода — нет ЮЛ после резолва и нет ИНН из файла. unmapped_total, GET …/osv?unmapped=true и экспорт с этим фильтром больше не включают строки с ИНН, которого нет в справочнике (unmapped=false — все остальные строки). Новое поле ConsolidatedRowOut.counterparty_entity_inn — ИНН сопоставленного ЮЛ справочника (П-8; counterparty_inn — по-прежнему ИНН из файла) «Указать» — только у строк без counterparty_id и без counterparty_inn; у строки, сопоставленной без ИНН в файле, показывать counterparty_entity_inn с возможностью пересопоставить (макет [32-osv-no-inn], osv-inn-mapped-button; повторный POST …/counterparty разрешён)
08.10, arm-core #60 (#58) Предупреждение автозамены (OQ-154): если новый файл заменил прежний целиком, а части документов прежнего в нём нет (книга «ББ + ОФР» → отдельный ОФР), в job.mapping_warnings нового файла приходит одна строка на заменённый файл: «автозамена: файл «…» (версия N) заменён этим файлом целиком, но в новом файле нет документов прежнего: …; привязки сняты с версий консолидации: …» — с пометкой CONFIRMED у подтверждённых документов. Схемы ответов не менялись; поведение замены прежнее показать предупреждение в карточке нового файла заметно (не только в общем списке подсказок): аналитик должен догрузить выпавшие документы; текст не разбирать
08.10, arm-core #59 (#57) Алиас приоритетнее имени (OQ-128): при ЮЛ-тёзке по нормализованному имени строка с совпавшим алиасом теперь сопоставляется с владельцем алиаса (match_kind = ALIAS; в своде — match_stored = false, если найдено на лету). После ручного сопоставления unmapped_total может уменьшиться больше чем на 1 + propagated после ручного сопоставления перечитывать и страницу, и …/osv/summary (как и раньше), не вычислять «Осталось N» на клиенте
07.10, arm-core #52 (#45) КПП ЮЛ (DBML v5.17, context/05-domain-rules.md §13.5): поле kpp: string | null в LegalEntityCreate, LegalEntityUpdate, LegalEntityOut/Detail, LegalEntityHistoryOut. Формат — 9 символов (4 цифры, 2 цифры или латинские A–Z, 3 цифры; пробелы убираются, буквы — в верхний регистр), иначе 422 INVALID_KPP; у ИП КПП запрещён (422), у ЮЛ необязателен; уникален только ИНН. Поиск q в списке ЮЛ ищет и по КПП; смена типа на ИП у ЮЛ с КПП без очистки КПП — 422 INVALID_KPP; kpp: null в PATCH очищает КПП; в импорте ЕЦП — необязательная колонка «КПП» и код строки INVALID_KPP поле КПП в формах ЮЛ и в карточке; в форме ИП поле скрыть; ошибку INVALID_KPP — на поле «КПП»
07.10, arm-core #53 (#38) Схема БД задаётся настройкой DB_SCHEMA — API не изменился —
07.10, arm-core #51 (#47) «Основной РФ» приходит объектом {id, name, is_head_office} или null: regional_office в LegalEntityOut/Detail/HistoryOut, base_regional_office в CompanyGroupOut/HistoryOut (в т. ч. в ответах POST/PATCH/move/withdraw/archive). Поля *_id остались; неактивный филиал тоже отдаётся; входные схемы прежние (*_id) брать имя филиала из объекта, а не склеивать с GET /directories/regional-offices
07.10, arm-core #50 (#43), #49 (#44) Зависимости под контур (PyJWT 2.8) и настройки из OpenBao — API не изменился —
07.10, arm-core #46 (#42) POST /consolidations принимает необязательный legal_entity_ids (≤ 500): выбранный в модалке [42] периметр. Без поля или null — состав ГСК; [] — явный пустой периметр → 422 PERIMETER_TOO_SMALL; ЮЛ не из ГК → 422 ENTITY_NOT_IN_GROUP модалка [42]: все ЮЛ ГК, отмечены члены ГСК, отправлять отмеченные (§6, строка [42])
07.10, #46 Ячейка чек-листа ChecklistCellOut стала самоописывающей: document_type и account своего правила искать ячейку по document_type/account, а не по индексу
07.10, #46 VersionCard.members[] (MemberOut): новые entity_type ("ЮЛ"/"ИП"/null) и osv_accounts[] — счета ОСВ, загруженные к периоду версии колонки «Тип» и «ОСВ» панели «Организации» [34]
07.10, решения владельца П-1/П-2/П-3 «Осталось N» / «Показать ЮЛ без ИНН» = все несопоставленные строки (unmapped_total), а не только без ИНН (П-1 отменён 08.10, см. arm-core #75); ГСК в версии не хранится; «К отчётности» доступна в любом статусе баннер — по unmapped_total; колонку «ГСК» на [34] не заполнять; кнопку «К отчётности» не прятать по статусу
07.10, решение владельца Фильтра «файлы ЮЛ в контексте версии» нет и не будет (пересмотрено 09.10, см. docs #90 выше) drawer [39] показывает все файлы ЮЛ; подходящесть документа версии — по-прежнему по document_ids ячеек чек-листа
07.10, arm-core #39 (Срез 3b) Новые маршруты: GET/PATCH /documents/{id}/statement, POST …/statement/checks/accept, GET/POST /documents/{id}/comments, GET /files/{id}/content, GET /users/brief, GET …/checklist?format=csv экран [41], комментарии, просмотр первички, имена пользователей, «Скачать чек-лист»
07.10, #39 POST /documents/{id}/confirm для ББ/ОФР — новые 409 PERIOD_REQUIRED и CHECKS_NOT_PASSED (detail.failures[]) обработать оба кода на «Подтвердить данные»
07.10, #39 Новые поля: comments_count у документа (в FileOut.documents[] и DocumentOut), documents_count у ЮЛ бейджи комментариев, колонка «Документы»
07.10, #39 (фиксы ревью) PATCH …/statement: header.inn: null → 422 INVALID_INN (ИНН не очищается); header.units — непустая строка ≤ 50, пробельная → 422; DELETE /files/{id} удаляет и формы ББ/ОФР, и комментарии документов файла; GET /files/{id}/content отдаёт nosniff, Cache-Control: private, no-store, при inline — Content-Security-Policy: sandbox не слать inn: null; после удаления файла не открывать его документы; проверить просмотрщик PDF под CSP sandbox
06.10, arm-core #26 Верхние границы входов: legal_entity_ids ≤ 500 (создание и PUT …/members), StatusIn.comment ≤ 1000, q у сводной ОСВ и экспорта ≤ 500 → иначе 422 pydantic-массивом (без code). Удаление алиаса, который в этот момент переносится сопоставлением, — 404 NOT_FOUND ограничить ввод теми же числами; на 404 удаления алиаса — перечитать алиасы
06.10, arm-core #30 В openapi.json есть схема bearerAuth — в Swagger (/docs) работает кнопка Authorize токен вставлять без слова Bearer
06.10, arm-core #19 + фиксы R-14/R-14a Срез 4 целиком; ОСВ за Q1 подходит версии любого квартала года, ББ/ОФР за Q1 — только версии Q1 не вычислять подходящесть на клиенте — брать из чек-листа
06.10, arm-core #32, #25 make seed-dev создаёт dev-ГК под документы fixtures («Русский Бисквит» и три ГК пакета ДКБ); план счетов 1С — 488 счетов (больше строк ОСВ с account_id) экраны не пустые сразу после сида
05.10, arm-core #10 В проде (DEBUG=false) нет /docs и /openapi.json, CORS — только CORS_ORIGINS; неверный метод — 405 с Allow; даты «не раньше сегодня» — по Москве генерировать типы со стенда разработки
docs #51 Задание распознавания бывает timeout и dlq; поле multipart — files (повторяется), не files[]; MemberOut.entity_type nullable учесть в разборе статусов файла и в форме загрузки

На клиенте нужно обойти одно ограничение сервера: кросс-доменно не читается Content-Disposition скачиваемых файлов (§7, §10 п. 9).


2. Подключение

Что Значение
База API ${REACT_APP_API_URL}/api/v1; в разработке REACT_APP_API_URL=http://localhost:8000, REACT_APP_ROOT_PATH=/ (обе обязательны при npm run start)
Аутентификация заголовок Authorization: Bearer <JWT> на каждом запросе к /api/v1/*; cookie и сессий нет
Токен в разработке POST /api/v1/dev/token {"email": "ka_rf@armka.local"} → {access_token, token_type}, HS256 на 8 часов; работает только при DEBUG=true и AUTH_DEV_ISSUER=true
Токен в проде SSO РАИСА; параметры неизвестны (OQ-54) — держите слой «получить токен» отдельно от слоя «использовать токен»
Dev-пользователи admin@, ka_go@, curator_go@, expert@, ka_rf@, rka@, client_manager@armka.local — по одному на роль (таблица — frontend-auth-guide.md §3.1)
Схема и Swagger /openapi.json, /docs, /redoc — только при DEBUG=true; в проде 404
Живость GET /healthcheck (без префикса и без токена) → {"uptime": "<время старта процесса, ISO>", "version": "1.0.0", "nats": "ok" | "down" | "off", "subjects": {"tabular_request": "…", "image_request": "…", "result": "…"}} (subjects — служебное поле для диагностики, фронту не нужно) — это не признак «вошёл»
CORS при DEBUG=true — * без credentials; в проде — источники из CORS_ORIGINS
Стенд целиком qa-guide.md (docker compose, сиды, запуск бэка и фронта)

Типы из схемы. Эталонный клиент arm-core/web/src/api/types.ts пишет типы руками и покрывает только /me и /admin. Для остальных экранов удобнее генерировать типы со стенда разработки, например npx openapi-typescript http://localhost:8000/openapi.json -o src/api/schema.d.ts (инструмент — на ваш выбор; это рекомендация, а не соглашение репозитория). Схемы в OpenAPI названы так же, как в этом гайде (VersionCard, FileOut, …).


3. Общие правила

3.1 Порядок проверок на сервере

Гарантированно: право (403) раньше видимости (404) и раньше проверки полей тела (422 массивом), а видимость объекта из пути (404) — раньше проверок состояния и блокировки (409). Поэтому по 404 нельзя понять, «нет объекта» или «нет доступа к нему». Исключения и вариации:

  • Синтаксически битый JSON или multipart разбирается до проверки токена и права: такой запрос получает 422 (или 400 у multipart) даже без токена.
  • Доменные 422 и 409 идут в разном порядке на разных маршрутах. Например, POST /directories/legal-entities: INVALID_INN (422) → ГК не видна (404) → INN_EXISTS/CRM_ID_EXISTS (409) → OFFICE_* (422); POST /consolidations: 422 периметра и ГСК → 409 CONSOLIDATION_EXISTS; ручное сопоставление: блокировка (409) → строка или ЮЛ не найдены (404). Не стройте логику на порядке — обрабатывайте каждый код независимо.

3.2 Три формы тела ошибки

Тело ошибки всегда {"detail": …}, но detail бывает трёх видов:

Форма detail Когда Пример
строка 401, 403, 404 несуществующего маршрута, 405, 413, 409 нарушения целостности БД, ошибки POST /dev/token {"detail": "недостаточно прав: требуется CONSOLIDATION_EDIT"}
объект {code, message, …} доменные ошибки 404/409/422/503 {"detail": {"code": "LOCKED_BY_OTHER", "message": "…", "locked_by": "…", "locked_at": "…"}}
массив [{loc, msg, type, …}] 422 валидации тела, query и path (pydantic) {"detail": [{"loc": ["body", "comment"], "msg": "…", "type": "string_too_long"}]}

И одно исключение: POST /legal-entities/{id}/files, когда не принят ни один файл, отвечает 422 телом UploadResult {accepted: [], rejected: […]} — без detail (§5.5).

type DomainDetail = { code: string; message: string; [extra: string]: unknown };
type ValidationItem = { loc: (string | number)[]; msg: string; type: string };

function parseError(status: number, body: unknown): { status: number; code: string; message: string; extra?: DomainDetail; fields?: ValidationItem[] } {
  const detail = (body as { detail?: unknown } | null)?.detail;
  if (typeof detail === 'string') return { status, code: `HTTP_${status}`, message: detail };
  if (Array.isArray(detail)) return { status, code: 'VALIDATION', message: 'Проверьте поля формы', fields: detail as ValidationItem[] };
  if (detail && typeof detail === 'object' && 'code' in detail) {
    const d = detail as DomainDetail;
    return { status, code: d.code, message: d.message, extra: d };
  }
  return { status, code: `HTTP_${status}`, message: 'Неизвестная ошибка' }; // в т. ч. 422 UploadResult — разбирайте отдельно
}

Правила обработки:

  • Ветвиться только по code (и HTTP-статусу), никогда по тексту message: тексты по-русски и могут меняться.
  • Доп. поля доменной ошибки — в том же объекте (locked_by, owners, failures, legal_entity_ids, missing, roles, permissions, related_group_id, legal_entity_id, from_status, to_status). Какие у какого кода — §9.
  • 401 — глобально, одним обработчиком в HTTP-клиенте: сбросить токен, показать вход.
  • 403 строкой бывает двух видов: «недостаточно прав: требуется <КОД>» и «пользователь не зарегистрирован в АРМ / учётная запись отключена / неоднозначная учётная запись» (токен валиден, человека нет в АРМ). Различать — frontend-auth-guide.md §3.3.
  • 422 массивом — показывать как ошибки полей формы: loc последним элементом называет поле; если loc = ["body"] — ошибка всего тела (например, «поле entity_type нельзя обнулить»), текст — в msg.

3.3 Видимость

Пользователь головного офиса (GET /me → regional_office.is_head_office = true) видит всё; пользователь филиала — ГК своего филиала, их ЮЛ, ГСК/ГСЗ, версии консолидации и монозаёмщиков своего филиала. Списки уже отфильтрованы сервером. Объект вне видимости по id — 404 NOT_FOUND с тем же телом, что у несуществующего. Клиент правила видимости не дублирует.

Исключение: кандидаты сопоставления GET …/counterparty-candidates?scope=all ищут по всему справочнику без фильтра видимости (контрагент может быть ЮЛ чужого филиала).

3.4 Типы значений

Что Как приходит Как работать
Деньги (numeric(18,2)) строка с двумя знаками: "1234.50", "0.00", или null не парсить во float; для сумм — decimal-библиотека (decimal.js, big.js) или работа со строкой; форматировать для показа отдельно
Деньги в запросе (PATCH …/statement, amount) число или строка, не более 2 знаков после точки и 16 цифр до; null — очистить слать строкой ("1234.50"), чтобы не терять точность
confidence строка "0.00"…"100.00" или null подсветка неуверенных: < 100
Идентификаторы uuid-строки —
Дата "2025-09-30" (ISO) —
Дата-время ISO 8601 с поясом, обычно UTC ("2026-10-07T12:34:56.789000Z") показывать в часовом поясе пользователя (МСК)
Квартал "Q1"…"Q4" подписи «1 кв.» и т. п. — на клиенте
Период строки свода "2024 Q3" —
Перечисления строки, регистр как в §3.7 (у RelatedGroupKind и RecognitionStatus — нижний) —
EntityType "ЮЛ" / "ИП" — кириллица сравнивать с кириллическими литералами

3.5 Страницы и лимиты

Страница — {items, total, limit, offset}; offset ≥ 0, больше предела — 422 массивом.

Список limit (по умолчанию / максимум) Порядок
ГК, ЮЛ, пользователи администратора, файлы ЮЛ 50 / 200 ГК и ЮЛ — по имени; пользователи — по ФИО; файлы — новые первыми
Строки ОСВ документа, сводная ОСВ версии 500 / 2000 строки документа — по row_number; свод — имя ЮЛ, счёт (натурально), номер строки

Простые массивы без пагинации: филиалы, ГСК/ГСЗ одной ГК, история ГК/ЮЛ, комментарии, периоды консолидации ГК, кандидаты (до 100), роли и права, users/brief.

Если нужен «весь список» (например, все ЮЛ ГК для модалки [42], а в ГК их может быть больше 200) — дочитывайте страницы по offset, пока offset + items.length < total.

3.6 Запросы на изменение

  • PATCH — частичный: поле, которого нет в теле, не меняется; поле с явным null — «очистить», если поле это допускает: crm_id (ГК, ЮЛ), created_date (ГСК/ГСЗ), employee_no, business_segment (пользователь), report_date (шапка формы), base_regional_office_id (ГК) и regional_office_id (ЮЛ). Филиал очищается null только у пользователя головного офиса: у пользователя филиала null превращается в его собственный филиал.
  • Явный null у обязательных полей — 422 массивом с loc: ["body"]: name (ГК, ЮЛ, ГСК/ГСЗ), entity_type (ЮЛ), full_name, regional_office_id, is_active (пользователь). У шапки формы inn: null — 422 INVALID_INN, а units: null означает «не менять» (пробельная строка — 422). Не отправляйте форму целиком «как есть» — шлите только изменённые поля.
  • Лишние поля запрещены (422 extra_forbidden) в телах создания и правки ГК, ЮЛ (в т. ч. move, withdraw), ГСК/ГСЗ и их членов, пользователей, ролей и прав, правки формы, акцепта и комментариев. Игнорируются — в теле алиаса, dev/token и во всех телах консолидаций. Не полагайтесь на второе.
  • PUT заменяет набор целиком: PUT …/members (периметр версии), PUT /admin/users/{id}/roles, PUT /admin/roles/{code}/permissions.
  • Повтор того же состояния — обычно 200 без изменений и без записи в журнал (PATCH теми же значениями, archive уже архивной ГК, повторный POST …/lock своей блокировки).
  • Каждая мутация пишет audit_event; фронту для этого ничего делать не нужно.

3.7 Перечисления

Тип Значения (подпись UI из макета, где она есть)
ConsolidationStatus INITIATED «Инициация», DOCUMENTS_PROCESSING «Загрузка и распознавание документов», DATA_CORRECTION «Корректировка данных», CONSOLIDATION_RF «Консолидация РФ», ADJUSTMENT, VERIFICATION_RF «Верификация РФ», CONSOLIDATION_GO «Консолидация ГО», VERIFICATION_GO, VERIFIED «Верифицирован», CANCELLED «Отмена КФО», REJECTED, ARCHIVED. Сейчас достижимы только первые три и CANCELLED; «в работе» = первые три (VersionCard.is_in_work)
ConsolidationScenario / ConsolidationApproach CREATE, CONFIRM / BASIC, EXTENDED (сейчас всегда CREATE и BASIC)
CellStatus (ячейка чек-листа) CONFIRMED «Загружен», NEEDS_CHECK и RECOGNIZED «Требуется проверка», PROCESSING «Обработка», ERROR «Ошибка загрузки», MISSING «Отсутствует»
FileStatus UPLOADED, PROCESSING, PROCESSED, ERROR, SUPERSEDED
RecognitionStatus (задание, FileOut.job.status) 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 —
CounterpartyType GSK «ГСК», GK «ГК», EXTERNAL «Внешний»
CounterpartyMatchKind INN, NAME, ALIAS (автоматически), MANUAL (вручную), PROPAGATED (автоприменение по имени)
EntityType ЮЛ, ИП
RelatedGroupKind gsk, gsz
LeHistoryEvent CREATE, UPDATE, MOVE_TO_GROUP, CLOSE, REMOVE_FROM_GROUP
OsvGroupType / OsvDataType ACCOUNT, DEPARTMENT, COUNTERPARTY, CONTRACT, OTHER / BU, NU, CURRENCY, QUANTITY, SUM
BusinessSegment DKB, DMSB, DM
PermissionObject / PermissionAction DIRECTORY, DOCUMENT, CONSOLIDATION, KFO, EX, EXPORT, USERS, AUDIT / VIEW, EDIT, APPROVE, VERIFY, DELETE, EXPORT, ADMIN
OnExisting (импорт) skip, update

Автоподтверждение документа (OQ-157). После маппинга чистый документ сразу приходит CONFIRMED с confirmed_by: null: кнопку «Подтвердить» показывать неактивной; повторный POST …/confirm — 409 ALREADY_CONFIRMED. Удаление файла с таким документом возможно (204), кроме случаев, когда документ подтвердил пользователь или документ привязан к версии вне корректировки данных (не INITIATED/DOCUMENTS_PROCESSING/DATA_CORRECTION): тогда 409 FILE_HAS_CONFIRMED_DOCUMENTS.


4. Права

UI прячет элементы по GET /me → permissions (объединение прав всех ролей); код роли для ветвления не используется. Сервер всё равно проверяет право и отвечает 403.

Область Чтение Изменение Особые
Справочники /directories/* DIRECTORY_VIEW DIRECTORY_EDIT удалить ГСК/ГСЗ и алиас — DIRECTORY_DELETE; импорт ЕЦП — DIRECTORY_ADMIN
Файлы и документы /legal-entities/{id}/files, /consolidations/{cid}/legal-entities/{le}/files (GET — DOCUMENT_VIEW, POST — DOCUMENT_EDIT), /files/*, /documents/* DOCUMENT_VIEW DOCUMENT_EDIT (загрузка, удаление, подтверждение, правка формы, акцепт, комментарий) —
Консолидации /consolidations* (кроме …/legal-entities/{le}/files), /company-groups/{id}/consolidations CONSOLIDATION_VIEW (в т. ч. экспорт xlsx и CSV чек-листа) CONSOLIDATION_EDIT (создание, блокировка, статус, периметр, сопоставление) перенос чужого алиаса при сопоставлении (move_alias) права DIRECTORY_DELETE не требует
Администрирование /admin/* USERS_ADMIN USERS_ADMIN без права — 403 до любых проверок тела
/me, /users/brief любой вошедший — —

Права KFO_*, EX_VIEW, EXPORT, AUDIT_VIEW есть в каталоге, но маршрутов под них пока нет. Полный каталог прав и ролей — frontend-auth-guide.md §4.


5. Маршруты

Префикс /api/v1 опущен. Колонка «Ошибки» — доменные коды (и 422 массивом там, где у маршрута есть явные границы); 401/403 и 404 NOT_FOUND у маршрутов с объектом в пути есть всегда и не повторяются. Описание схем — §5.8.

5.1 Профиль, пользователи, вход

Метод и путь Право Запрос Ответ Ошибки
POST /dev/token без токена; только dev {email} {access_token, token_type: "bearer"} 404 строка «пользователь не найден или отключён», 409 строка «неоднозначная учётная запись»; в проде маршрута нет — 404 {"detail":"Not Found"}
GET /me любой вошедший — MeResponse {id, email, full_name, regional_office {id, name, is_head_office}, business_segment?, roles[], permissions[]} 401; 403 строкой «не зарегистрирован / отключена / неоднозначная»
GET /users/brief?ids=<uuid>,<uuid> любой вошедший ids через запятую, ≤ 200 UserBriefOut[] {id, full_name, is_active} — только найденные (включая неактивных), по id, не в порядке запроса; ?ids= (пусто) → []; без параметра — 422 массивом 422 INVALID_IDS (проверяется первым), 422 TOO_MANY_IDS (дубли считаются)

/users/brief — способ показать имена вместо uuid (uploaded_by, confirmed_by, modified_by, checks.accepted.by, changed_by, history[].changed_by) без права администратора. Собирайте id со всей страницы в один запрос и кэшируйте. locked_by_name блокировки, initiator.name версии и author.full_name комментария уже приходят с именем.

5.2 Справочники: чтение

Метод и путь Query Ответ
GET /directories/regional-offices — RegionalOfficeOut[] {id, name, is_head_office, is_active} — все, включая Головной офис, по имени
GET /directories/company-groups q (подстрока name/crm_id/code), is_archived, limit, offset Page[CompanyGroupOut]
GET /directories/company-groups/{group_id} — CompanyGroupOut
GET /directories/company-groups/{group_id}/history — CompanyGroupHistoryOut[] (прошлые снимки, новые первыми)
GET /directories/company-groups/{company_group_id}/related-groups — RelatedGroupOut[] — ГСК и ГСЗ ГК (kind), по имени, с members_count; crm_id — пока всегда null (arm-core #112)
GET /directories/related-groups/{group_id} — RelatedGroupDetail (+ members[] {legal_entity_id, inn, name, valid_from} — текущий состав)
GET /directories/legal-entities company_group_id, outside_group (монозаёмщики), inn (точно), q (подстрока имени / ИНН / КПП), closed, limit, offset Page[LegalEntityOut] (с memberships[] — текущие ГСК/ГСЗ)
GET /directories/legal-entities/{entity_id} — LegalEntityDetail (+ aliases[])
GET /directories/legal-entities/{entity_id}/history — LegalEntityHistoryOut[] (снимки с event_type и effective_date, новые первыми)

Нюансы:

  • «Основной РФ» приходит объектом: base_regional_office {id, name, is_head_office} у ГК, regional_office у ЮЛ и в снимках истории (null, если филиала нет; неактивный филиал тоже отдаётся — имя нужно для истории). *_id рядом остаются. GET /directories/regional-offices нужен только для выпадающих списков выбора филиала.
  • LegalEntityOut.outside_group = нет ГК (монозаёмщик); closed = заполнен close_date. Оба вычисляются сервером.
  • documents_count у ЮЛ — число документов (не файлов) его незаменённых файлов: файл с ББ и ОФР даёт 2.
  • memberships у ЮЛ — текущие членства в ГСК/ГСЗ (kind: gsk / gsz), во всех ответах ЮЛ, включая список: колонку «ГСК» и фильтр по ГСК/ГСЗ стройте по ним. Серверного фильтра по ГСК/ГСЗ нет — фильтруйте страницу ЮЛ на клиенте.
  • Поиска ГК по имени филиала нет.

5.3 Справочники: изменение

Метод и путь Право Запрос Ответ Ошибки
POST /directories/company-groups EDIT {name (1–500), crm_id?, base_regional_office_id?} 201 CompanyGroupOut (код GK-NNNNNN назначает сервер) 422 OFFICE_NOT_FOUND, 422 OFFICE_NOT_ALLOWED (филиал не свой), 409 CRM_ID_EXISTS
PATCH /directories/company-groups/{group_id} EDIT {name?, crm_id?, base_regional_office_id?} (частично) CompanyGroupOut те же
POST /directories/company-groups/{group_id}/archive · /unarchive EDIT — CompanyGroupOut (повтор — без изменений) —
POST /directories/legal-entities EDIT {inn, entity_type, name, kpp?, company_group_id?, regional_office_id?, crm_id?, aliases?, gsk_id?, gsz_id?}; без company_group_id — монозаёмщик (тогда без ГСК/ГСЗ); aliases — строки 1–500, ≤ 100 201 LegalEntityDetail (с aliases, memberships) 422 INVALID_INN (ЮЛ — 10 цифр, ИП — 12; пробелы внутри убираются), 422 INVALID_KPP (формат или КПП у ИП), 404 (ГК не видна), 409 INN_EXISTS, 409 CRM_ID_EXISTS, 422 OFFICE_NOT_FOUND / OFFICE_NOT_ALLOWED — в этом порядке; затем алиасы: 422 EMPTY_ALIAS, 409 ALIAS_EXISTS (повтор в списке), 409 ALIAS_TAKEN (legal_entity_id); затем ГСК, ГСЗ: 404 (группа не видна), 422 WRONG_GROUP_KIND, 422 DIFFERENT_COMPANY_GROUP. Любая ошибка — ничего не создано
PATCH /directories/legal-entities/{entity_id} EDIT {name?, entity_type?, crm_id?, regional_office_id?, kpp?, aliases?, gsk_id?, gsz_id?}; kpp: null очищает КПП; ИНН и ГК так не меняются; aliases — итоговый набор (совпавшие по normalized_alias остаются как были, лишние удаляются, новые добавляются); gsk_id/gsz_id — итоговая группа, null — выйти; не переданное поле не меняется LegalEntityDetail 422 INVALID_INN (ИНН не подходит новому типу), 422 INVALID_KPP (формат; КПП у ИП, в т. ч. смена типа на ИП без очистки КПП), 422 OFFICE_*, 409 CRM_ID_EXISTS; name/entity_type/aliases: null — 422 массивом; алиасы и группы — коды как у POST; удаление алиаса без DIRECTORY_DELETE — 403 строкой «недостаточно прав: требуется DIRECTORY_DELETE». Любая ошибка — ничего не изменено
POST /directories/legal-entities/{entity_id}/move EDIT {company_group_id, effective_date} (дата ≥ сегодня по Москве) LegalEntityOut; текущие членства ГСК/ГСЗ закрываются 422 EFFECTIVE_DATE_IN_PAST, 404 (ГК назначения не видна), 409 LEGAL_ENTITY_CLOSED, 409 SAME_COMPANY_GROUP
POST /directories/legal-entities/{entity_id}/withdraw EDIT {event: "CLOSE" | "REMOVE_FROM_GROUP", effective_date, comment?} LegalEntityOut 422 EFFECTIVE_DATE_IN_PAST, 409 ALREADY_CLOSED, 409 NOT_IN_COMPANY_GROUP
POST /directories/legal-entities/{entity_id}/aliases EDIT {alias (1–500)} — как в ОСВ 201 AliasOut {id, alias, normalized_alias, created_at} 422 EMPTY_ALIAS, 409 ALIAS_EXISTS, 409 ALIAS_TAKEN (legal_entity_id владельца)
DELETE /directories/legal-entities/{entity_id}/aliases/{alias_id} DELETE — 204 404, если алиас уже убран/перенесён — перечитать алиасы
POST /directories/company-groups/{company_group_id}/related-groups EDIT {kind: "gsk" | "gsz", name, created_date?, crm_id?}; crm_id принимается и не сохраняется (arm-core #112) 201 RelatedGroupOut (код GSK-/GSZ-NNNNNN) —
PATCH /directories/related-groups/{group_id} EDIT {name?, created_date?, crm_id?} (вид и ГК не меняются; crm_id не сохраняется и изменением не считается) RelatedGroupOut —
DELETE /directories/related-groups/{group_id} DELETE — 204 409 GROUP_NOT_EMPTY (сначала исключите членов)
POST /directories/related-groups/{group_id}/members EDIT {legal_entity_id} 201 RelatedGroupMemberOut 422 DIFFERENT_COMPANY_GROUP, 409 MEMBER_EXISTS, 409 MEMBER_OF_OTHER_GROUP (related_group_id — где состоит)
DELETE /directories/related-groups/{group_id}/members/{legal_entity_id} EDIT — 204 (членство закрывается, остаётся в истории) —
POST /directories/import ADMIN multipart: file (xlsx/csv), dry_run (по умолчанию true), on_existing (skip по умолчанию / update) ImportReportOut {rows, groups_*, entities_*, memberships_created, offices_created, errors, lines[] {row, inn, code, message}} 413 строкой (> 20 МиБ), 422 MISSING_COLUMNS (missing[]), 422 UNSUPPORTED_FILE

Импорт — всегда два шага: пробный прогон (dry_run=true, ничего не пишет, отчёт тот же), затем запись тем же файлом (dry_run=false). Коды строк отчёта (ImportCode) и формат файла — arm-core/README.md «Импорт из ЕЦП CRM» и context/05-domain-rules.md §13. Кнопку «Импорт» показывать только с DIRECTORY_ADMIN.

При гонке двух одинаковых запросов 409 приходит с тем же кодом, но без доп. полей (legal_entity_id, related_group_id; у LOCKED_BY_OTHER гонки взятия блокировки — без locked_by/locked_at) — не рассчитывайте, что доп. поля есть всегда.

5.4 Администрирование (USERS_ADMIN)

Метод и путь Запрос Ответ Ошибки
GET /admin/users q (email/ФИО), is_active, limit, offset Page[UserOut] (по ФИО; regional_office — объект целиком, roles[] — коды) —
POST /admin/users {email, full_name, regional_office_id, employee_no?, business_segment?, roles? (по умолчанию [])} 201 UserOut 409 EMAIL_EXISTS (без учёта регистра), 422 UNKNOWN_ROLE (roles), 422 OFFICE_NOT_FOUND; email на *.local — 422 массивом (валидатор email)
PATCH /admin/users/{user_id} {full_name?, employee_no?, regional_office_id?, business_segment?, is_active?} UserOut 409 LAST_ADMIN, 422 OFFICE_NOT_FOUND
PUT /admin/users/{user_id}/roles {roles[]} — набор целиком UserOut 422 UNKNOWN_ROLE, 409 LAST_ADMIN
GET /admin/roles — RoleOut[] {code, name, description?, is_system, is_active, permissions[]} —
GET /admin/permissions — PermissionOut[] {code, object_type, action, name, description?} —
PUT /admin/roles/{code}/permissions {permissions[]} — набор целиком RoleOut 404 (роли нет), 422 UNKNOWN_PERMISSION (permissions), 409 LAST_ADMIN

Профиль /me после смены своих ролей не перечитывается сам — перезагрузка страницы. Форма создания пользователя берёт филиалы из GET /directories/regional-offices (нужно DIRECTORY_VIEW). Эталонная реализация экрана — arm-core/web/src/widgets/Admin/.

5.5 Файлы и документы

Метод и путь Право Запрос Ответ Ошибки
POST /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files EDIT multipart, поле files повторяется на каждый файл (не files[]) — загрузка в консолидацию (панель [40] внутри версии) 201 UploadResult {accepted[] {file_id, job_id, name}, rejected[] {name, code, message, file_id?}}, если принят ≥ 1 файл; 422 тем же телом, если ни одного 404 (консолидация или ЮЛ), 409 CONSOLIDATION_NOT_EDITABLE (нет версии «в работе»), 422 ENTITY_NOT_IN_PERIMETER (объектом detail), 503 STORAGE_UNAVAILABLE
POST /legal-entities/{legal_entity_id}/files EDIT то же, без контекста (страница ЮЛ, монозаёмщики): consolidation_id = null как выше 404, 503 STORAGE_UNAVAILABLE
GET /legal-entities/{legal_entity_id}/files VIEW limit, offset, include_superseded (по умолчанию false) Page[FileOut], новые первыми —
GET /consolidations/{consolidation_id}/legal-entities/{legal_entity_id}/files VIEW limit, offset, include_superseded Page[FileOut] — рабочий стол консоли для ЮЛ: файлы с этим consolidation_id плюс файлы, чьи документы привязаны к любой версии консолидации; новые первыми; SUPERSEDED — только при include_superseded=true (скрыт по умолчанию) 404
GET /files/{file_id} VIEW — FileOut —
DELETE /files/{file_id} EDIT — 204 (удаляет документы, строки ОСВ, формы ББ/ОФР, комментарии, задания) 409 FILE_HAS_CONFIRMED_DOCUMENTS, 409 FILE_SUPERSEDES_OTHERS
POST /files/{file_id}/retry EDIT — 202 FileOut — новое задание (job.attempt + 1), файл снова UPLOADED 409 FILE_NOT_RETRYABLE, MAX_ATTEMPTS_REACHED; для файла консолидации — 409 VERSION_NOT_EDITABLE (нет версии «в работе» с его ЮЛ)
POST /consolidations/versions/{version_id}/files/retry EDIT — 202 RetryBatchOut {accepted[] {file_id, job_id, attempt}, skipped[] {file_id, code}} 409 VERSION_NOT_EDITABLE (версия не «в работе»)
GET /files/{file_id}/content VIEW disposition = attachment (по умолчанию) / inline поток байт; Content-Type по типу файла 404 FILE_CONTENT_NOT_FOUND (в БД есть, в хранилище нет), 503 STORAGE_UNAVAILABLE
GET /documents/{document_id} VIEW — DocumentOut —
PATCH /documents/{document_id}/osv-header EDIT любые из {period_year, period_quarter, is_cumulative, osv_account_code}; отсутствующее поле не меняется DocumentOut по порядку: 409 DOCUMENT_NOT_OSV, FILE_SUPERSEDED; 422 INVALID_PERIOD, INVALID_ACCOUNT_CODE; 409 DUPLICATE_ACCOUNT (document_id), VERSION_NOT_EDITABLE (version_ids); 422 массивом — неверный тип поля
GET /documents/{document_id}/entries VIEW limit (≤ 2000, 500), offset Page[EntryOut] по row_number; дерево — по parent_entry_id —
POST /documents/{document_id}/confirm EDIT — DocumentOut 409 FILE_SUPERSEDED, ALREADY_CONFIRMED, DOCUMENT_NOT_CONFIRMABLE; для ББ/ОФР — PERIOD_REQUIRED, CHECKS_NOT_PASSED (failures[]) — именно в этом порядке, если совпало несколько
GET /documents/{document_id}/comments VIEW — CommentOut[] {id, text, author {id, full_name}, created_at} по возрастанию —
POST /documents/{document_id}/comments EDIT {text} (1–4000 после обрезки) 201 CommentOut 422 массивом

Загрузка:

  • Принимаются xls, xlsx, xlsb, pdf, png, jpg (и jpeg) до 50 МиБ на файл (по умолчанию, настройка UPLOAD_MAX_BYTES). Тип и период не спрашиваются — их определяет парсер по содержимому.
  • Коды rejected[].code: UNSUPPORTED_FILE (тип не из списка, сигнатура не совпала с расширением, пустое имя), FILE_TOO_LARGE, DUPLICATE_FILE (тот же файл у ЮЛ уже есть — file_id указывает на него), STORAGE_UNAVAILABLE.
  • Два разных тела 422: все файлы отклонены — UploadResult (смотрите rejected[]); ЮЛ вне периметра всех версий консолидации «в работе» — {"detail": {"code": "ENTITY_NOT_IN_PERIMETER", …}}. Различайте по наличию detail.
  • Загрузка в консолидацию: консолидация видима, есть версия «в работе», ЮЛ в её периметре; первый принятый файл переводит каждую версию консолидации «в работе» с этим ЮЛ из INITIATED в DOCUMENTS_PROCESSING. Блокировка версии для загрузки не нужна. FileOut.consolidation_id — куда загружен файл (null — без контекста).
  • Общий размер запроса приложение не ограничивает (лимит ставит ingress), число файлов — только предел Starlette: больше 1000 в одном запросе — 400 строкой; большие пачки лучше слать по нескольку файлов.

Жизненный цикл файла и что показывать (подписи — как в чек-листе):

FileOut.status Что происходит Строка в drawer [39]
UPLOADED принят, ждёт отправки на распознавание «Обработка», опрашивать
PROCESSING распознаётся или маппится; documents[] ещё пуст «Обработка», опрашивать
PROCESSED документы созданы статус — по документу: NEEDS_CHECK и RECOGNIZED (старые и формы после правки) «Требуется проверка», CONFIRMED «Загружен» (в т.ч. автоподтверждённый), ERROR «Ошибка»
ERROR ошибка распознавания (job.status error/timeout/dlq, причина — job.error_code/error_message) или маппинга (job.mapping_error) «Ошибка» + причина
SUPERSEDED заменён более новым файлом с теми же документами (автозамена); в списке скрыт без include_superseded=true —

Опрос: перечитывайте GET /legal-entities/{id}/files (или GET /files/{id}), пока есть файлы UPLOADED/PROCESSING; интервал 3–5 с. Предупреждения — job.warnings (сервис распознавания) и job.mapping_warnings (ядро), у документа — DocumentOut.warnings; это подсказки, тексты не разбирать. Предупреждение автозамены (строка job.mapping_warnings, начинается с «автозамена:») — исключение по важности: в нём документы, которые выпали из заменённого файла и сняты с версий консолидации, — показывать его заметно.

Документ:

  • Один файл = N документов: лист ОСВ по счёту — документ OSV (osv_account_code, locator — лист), форма ББ/ОФР — документ BALANCE/INCOME, расшифровки — один документ DISCLOSURE/OTHER на тип (без данных; статус RECOGNIZED при маппинге, затем автоподтверждается → CONFIRMED).
  • Период: period_year + period_quarter + is_cumulative. Квартальный — «2024 К1», нарастающий — «2024 · 9 мес.»; period_quarter = null — «период не определён» (например, ОСВ со скользящим периодом) — не подставлять квартал самим.
  • Проверки: counterparty_check — сверка шапки с ЮЛ-владельцем; period_check всегда UNKNOWN — показывать «—» (OQ-120). Подходит ли документ версии — только по чек-листу версии.
  • Подтверждённый документ отменить нельзя; исправление — загрузкой нового файла (автозамена). Исключение — уточнение периода и счёта ОСВ (ниже): оно снимает подтверждение.
  • Строки ОСВ (entries) — только чтение; экрана правки ОСВ в макете нет.

Повторная обработка (…/retry, AURORA-1442, правила — 05-domain-rules.md §9.5 п. 13):

  • Доступна только для файла в ERROR, у которого текущее задание табличного парсера (job.pipeline = tabular) упало на обмене: job.status = timeout (error_code = TIMEOUT) или dlq (NO_ACK). PDF/сканы (OCR) — нет. Кнопку «Повторно обработать» показывать только при этом условии; остальное — 409 FILE_NOT_RETRYABLE.
  • Попыток у парсера 3 (job.max_attempts); на последней — 409 MAX_ATTEMPTS_REACHED, кнопку прятать по job.attempt >= job.max_attempts.
  • Ответ 202: файл снова UPLOADED с новым заданием — опрашивать как после загрузки. Прежнее задание остаётся в истории.
  • Файл, загруженный в консолидацию (consolidation_id не null), повторяется, только пока есть версия «в работе» с его ЮЛ в периметре (иначе 409 VERSION_NOT_EDITABLE); файл без контекста — без этого условия.
  • Пакет версии: рассматриваются файлы в ERROR, загруженные в консолидацию этой версии, ЮЛ которых в её периметре. Подошедшие — в accepted, остальные файлы в ERROR — в skipped с кодом FILE_NOT_RETRYABLE или MAX_ATTEMPTS_REACHED. Пустой пакет — 202 с двумя пустыми списками. Версия не «в работе» — 409 VERSION_NOT_EDITABLE. Блокировка версии не нужна.
  • В журнал пишется FILE_REPROCESS на каждый принятый файл.

Уточнение периода и счёта ОСВ (PATCH …/osv-header, правила — §9.5 п. 12):

  • Только документ OSV. Если передано хоть одно поле периода, после правки все три должны быть заполнены, у Q1 — is_cumulative = true (иначе 422 INVALID_PERIOD). Счёт нормализуется как при разборе (60.1 → 60.01).
  • Новые поля DocumentOut: is_modified (текущие значения отличаются от распознанных), original_values (распознанные period_year, period_quarter, is_cumulative, osv_account_code — заполняется при первой правке и дальше не меняется), modified_by, modified_at. Возврат к исходным значениям снимает is_modified.
  • Подтверждённый документ после правки — NEEDS_CHECK, привязки к версиям «в работе» сняты; подтверждать заново вручную.
  • Запрос без изменений — 200 без записи в журнал. Строки ОСВ не меняются.

5.6 Формы ББ/ОФР

Метод и путь Право Запрос Ответ Ошибки
GET /documents/{document_id}/statement VIEW — StatementOut 409 DOCUMENT_NOT_STATEMENT (ОСВ, расшифровка, ББ/ОФР без данных формы)
PATCH /documents/{document_id}/statement EDIT `{header?: {inn?, report_date?, units?}, values?: [{line_id line_code, column_id, amount}]}(до 5000 ячеек; для достроенной строки сid = null—line_code`) StatementOut — состояние после правки
POST /documents/{document_id}/statement/checks/accept EDIT {comment} (1–2000) StatementOut 409 NOTHING_TO_ACCEPT и те же 409

Главное (подробно — api-contract-slice-3b.md §3–§5):

  • StatementOut = {document, edition, header, columns[], lines[], checks}. lines[].parent_code — код четырёхзначной строки-родителя у пятизначной подстроки (null у остальных, в том числе у достроенных строк); считает ядро (то же правило, что у проверки «сумма подстрок = родитель»), родитель берётся только из этого поля. У каждой пары «строка × колонка» есть ValueOut. Ядро достраивает полный состав формы редакции (справочник statement_lines.yaml, arm-core#80): lines[].origin — source (из источника) или template (достроена); у достроенных строк и ячеек id = null, в БД их нет. Для правки передавайте {line_id | line_code, column_id, amount}: у существующей строки — line_id; у достроенной (id = null) — line_code, первая непустая сумма создаёт строку и ячейку (is_modified = true). Очистка (amount: null) возвращает созданную ячейку в пустую. UNKNOWN_CELL — и новые случаи: line_code уже есть в форме (передайте line_id), код вне справочника редакции, у документа нет редакции (нет даты периода — строки не достраиваются; текст ошибки тот же, что для кода вне справочника). Пустые достроенные строки не участвуют в проверках §7 и статусе.
  • Подсветка: confidence < 100, is_recognized = false, is_modified = true (исходное — original_amount). Возврат ячейки к исходному значению снимает is_modified.
  • Правка даты отчёта пересчитывает document.period_*, reporting_date и edition; правка ИНН — counterparty_check; любая правка пересчитывает статус и сбрасывает акцепт. Запрос без изменений — 200 без эффектов.
  • checks.failed — жёсткие нарушения (блокируют подтверждение, пока не акцептованы), soft_failed — мягкие (в т. ч. «в пределах округления»), cross_form — межформенная ББ↔ОФР (мягкая, column_index = -1), accepted — акцепт или null. Акцепт не очищает failed: «нарушения есть и приняты» = failed непуст и accepted не null.
  • Подтверждение блокируют только PERIOD_REQUIRED (поправить дату отчёта) и CHECKS_NOT_PASSED (показать detail.failures[] или акцептовать). Не блокируют: мягкие и межформенная проверки, counterparty_check = MISMATCH, низкая уверенность. Формы из OCR почти всегда NEEDS_CHECK — подтверждать из него можно.
  • cross_form зависит от второго документа ЮЛ за тот же период: появилась ОФР отдельным файлом — перечитайте ББ.
  • Документ формы появляется после того, как файл стал PROCESSED; до этого …/statement — 404.

5.6.1 Формы ББ/ОФР после единой нормализации (arm-core main 35a24e7a)

Работает на main ядра: change unified-statement-normalization (RSHB/arm-ka-docs#116; контракт распознавания v2.0, DBML v5.19), части А (arm-core #129) и Б (#132). Фронт arm-core/web эти типы уже использует (план 6, сделан в arm-web и перенесён синхронизацией до 21.10.2026; дальше фронт ведётся в arm-core). Правила — context/05-domain-rules.md §7, §9.3, §9.5 пп. 2, 4, 9, §11; экран — спеки document-review-ui и primary-files-ui в arm-core/openspec/specs/. В …/statement поля добавлены, старые не удалены: клиент, написанный по §5.6, продолжит читать ответ. Исключение — карточка файла: FileOut.job.warnings сменил тип со строк на объекты (ниже, «Карточка файла»), это несовместимо — экран файлов обновляется в той же выкладке, что и ядро. Раздел 5.6 ниже описывает базовые маршруты; где он расходится с этим разделом, прав этот раздел.

GET /documents/{id}/statement — новые поля StatementOut:

  • document.form_variant — full | simplified | null (упрощённая форма — свой набор проверок §7).
  • header.needs_review_fields[] — поля шапки, которые держат документ в «Требует проверки» (сейчас бывает только report_date; подсветить поле). Сомнения в ИНН и организации — не здесь, а в issues с field (INN_UNCERTAIN, ORGANIZATION_UNCERTAIN, уровень info). header.source — {<поле>: {text, confidence}}: как поле распознано (сырой текст и уверенность; confidence — строка Decimal "61.54" или null, как прочие уверенности) — в подсказку у поля; header.organization_name_modified — наименование правилось; header.okei — код ОКЕИ (только чтение).
  • columns[].period_kind — absolute | relative:current | relative:prev_year_end | relative:prev_prev_year_end | relative:prev_period | none; columns[].label_confidence. Колонка с period_kind = none и period_date = null — нужна дата от аналитика (ввод даты у колонки).
  • lines[] — все строки источника, а не только строки с кодом: row_index (порядок в файле; null — и только тогда — у строк, созданных правкой, и у достроенных шаблонных; origin = template ⇔ row_index = null), source_ref, line_status (ok | unreadable | missing | detail), line_code (string | null: null у статусов кроме ok), line_code_text (сырой текст ячейки кода — показывать серым у unreadable / missing), line_code_confidence (строка Decimal или null; подтверждение кода её не меняет), line_name (наименование из файла), reference_name (справочное имя по коду или null — его показывать основным, файловое рядом, если отличается), parent_row_index (родитель расшифровки detail), needs_review (подсветить строку), is_confirmed, line_code_modified. Один код может стоять у нескольких строк (повтор кода законен): не строить словарь «код → строка», ключ строки — id (у достроенных шаблонных — line_code, их у кода с реальной строкой ok нет).
  • values[] — text (сырой текст источника; в подсказку ячейки вместе с confidence), needs_review, is_confirmed. Подсветка ячейки — только по needs_review; по confidence < 100 и is_recognized = false больше не подсвечивать (сервис уже свёл их с проверками §7: догадка, подтверждённая сошедшимся соотношением, не подсвечивается). У значений строк detail needs_review всегда false (расшифровки статус не держат) — подсказку с text и уверенностью показывать и у них.
  • issues[] — предупреждения и нарушения документа: {id, kind: "warning" | "relation" | "soft", code, severity: "error" | "info", message, line_ids[], column_id?, column_index?, field?, source: "service" | "core", relation?, codes?, expected?, actual?}; показывать у места (строки, ячейки, поле шапки) или у документа; error отличать от info. column_index без column_id — столбец, которого нет среди колонок формы (пропущенная колонка «Изменение, %»): показывать у документа. Нарушения §7 приходят и здесь (kind = relation / soft, code = RELATION_FAILED / RELATION_SOFT) — у любой формы, в том числе загруженной до перехода.
  • checks.failed[] / soft_failed[] — у нарушения ещё codes[] и line_ids[] (все строки кодов соотношения). expected и actual — строки-суммы или null: expected = null, если у соотношения пустая цель при непустых членах (форма потеряла итоговую строку; такое нарушение блокирует подтверждение, пока не исправлено или не акцептовано). column_index — число; -1 у межформенной проверки cross_form и у нарушения без колонки (P3-60). В detail.failures[] отказа 409 CHECKS_NOT_PASSED у записи без колонки column_index — null, не -1; обрабатывайте оба значения.
  • edition_lines[] {code, name} — справочник строк формы для подсказки кода при назначении: коды редакции формы по одному (имя — первое из словаря), у формы без даты (редакции нет) — коды всех редакций по одному.
  • Порядок lines — по возрастанию кода, как сейчас (одинаковые коды — по row_index); unreadable / missing — после ближайшей предыдущей по row_index строки с кодом (без такой — в начале), detail — сразу после родителя. Разделы и размещение итогов «как на бумаге» строит экран, как и раньше (formLayout). Расшифровки (detail) по умолчанию скрывать за переключателем «Показать расшифровки (N)».

PATCH /documents/{id}/statement — новые части тела (можно вместе в одном запросе; один запрос — один пересчёт):

Часть Тело Эффект Ошибки
подтвердить ячейку / снять подтверждение values: [{line_id, column_id, confirmed: true | false}] is_confirmed, needs_review = false (при false — пересчитывается), сумма не меняется, акцепт не сбрасывается 422 UNKNOWN_CELL
дата колонки columns: [{column_id, period_date}] (дата обязательна) дата колонки, is_current, период документа, period_kind = absolute (правка даты отчёта её больше не пересчитывает); снимает COLUMN_WITHOUT_DATE / COLUMN_DATE_UNCERTAIN; та же дата у колонки с таким предупреждением — подтверждение (акцепт не сбрасывает) 422 UNKNOWN_COLUMN; колонка дважды в columns[] — 422 DUPLICATE_COLUMN; period_date: null — 422 массивом
назначить / сменить код lines: [{line_id, line_code}] unreadable / missing / detail / ok → ok с этим кодом, line_code_confidence = 100 422 INVALID_LINE_CODE (не 4–5 цифр), 409 LINE_CODE_TAKEN (код занят строкой, созданной правкой, с суммами — сначала очистить её)
отметить расшифровкой lines: [{line_id, line_status: "detail"}] строка выходит из сумм и не блокирует подтверждение; обратно — только назначением кода 422 LINE_CODE_REQUIRED (line_status: "ok" — в ok переводит только line_code)
подтвердить код / снять подтверждение lines: [{line_id, confirmed: true | false}] is_confirmed, needs_review строки = false (при false — пересчитывается); line_code_confidence и line_code_modified не меняются — в подсказке остаётся исходная уверенность —
наименование организации header: {organization_name} (непустая строка после обрезки пробелов, до 1000 символов) organization_name_modified, counterparty_check; снимает NO_ORGANIZATION_NO_INN; равное текущему — без эффекта 422 массивом; organization_name: null — 422 (как inn: null; «не менять» — отсутствие поля)
подтвердить дату отчёта header: {report_date} тем же значением, если report_date в needs_review_fields поле уходит из needs_review_fields, снимается REPORT_DATE_UNCERTAIN —

Части применяются в порядке lines → values → columns → header; ошибка адресации или 409 откатывает весь запрос (ничего не применено). Общие ошибки lines: 422 UNKNOWN_LINE (строка не этого документа), 422 INVALID_LINE_PATCH (в элементе нет действия или их несколько). У ошибок элемента lines[] (LINE_CODE_TAKEN, LINE_CODE_REQUIRED, INVALID_LINE_CODE, INVALID_LINE_PATCH, UNKNOWN_LINE) в detail есть line_id и line_code первого неверного элемента — показывать ошибку у поля этой строки. Правка суммы по line_code (достроенная строка) для кода, у которого уже есть строка ok, — 422 UNKNOWN_CELL: передайте line_id. Правка header.report_date пересчитывает даты колонок с period_kind = relative:*. Удалять строки нельзя — для этого «Сделать расшифровкой». Акцепт проверок сбрасывает правка данных (сумма, код, статус строки, дата колонки, поле шапки), подтверждения (confirmed) его не сбрасывают. Ответ — StatementOut после пересчёта.

POST /documents/{id}/confirm для формы — порядок 409: FILE_SUPERSEDED → ALREADY_CONFIRMED → DOCUMENT_NOT_CONFIRMABLE → PERIOD_REQUIRED → LINES_UNRESOLVED (detail.lines[] {line_id, row_index, line_code_text} — строки unreadable / missing, у которых в ячейках есть сумма или нераспознанный текст; кнопку «Подтвердить данные» отключать заранее с подсказкой «Разберите строки без кода: N») → DUPLICATE_FORM (detail.document_id — в этом файле уже подтверждена форма того же типа, года и квартала) → CHECKS_NOT_PASSED (detail.failures[] {column_index | null, relation, expected | null, actual | null}).

Статус документа. Форма из OCR может стать RECOGNIZED (и сразу автоподтвердиться), если сервис всё подтвердил проверками: сомнительные ячейки расшифровок (detail) статус не держат, поэтому чистые сканы 1С (например, ББ и ОФР за 1 кв. 2025 в fixtures) могут прийти RECOGNIZED. NEEDS_CHECK остаётся, когда есть что смотреть: подсвеченные ячейки и строки, issues уровня error, неразобранные строки. Две формы одного типа и периода в одном файле приходят двумя документами; ни одна не автоподтверждается.

Карточка файла и документа (несовместимое изменение). FileOut.job.warnings — список объектов {code, severity: "error" | "info", message, locator} вместо строк (у заданий до выкладки ядро отдаёт строки, обёрнутые в {code: "LEGACY_WARNING", severity: "info", message, locator: null}); показывать message и locator, error отличать от info. FileOut.job.mapping_warnings — по-прежнему строки. FileOut.documents[].issue_counts и DocumentOut.issue_counts — {error, info}: сколько записей issues у документа (для значка в панели файлов без запроса документа). DocumentOut.issues[] — те же записи, что issues формы, у любого документа, в том числе ОСВ (предупреждения листа OSV_*); DocumentOut.warnings (строки маппинга) остаётся на окно перехода.

ГСК/ГСЗ. crm_id в POST …/related-groups и PATCH /directories/related-groups/{id} сохраняется и возвращается в RelatedGroupOut / RelatedGroupDetail; повтор — 409 CRM_ID_EXISTS (arm-core#112, DBML v5.19).

5.7 Консолидации

Метод и путь Право Запрос Ответ Ошибки
POST /consolidations EDIT {company_group_id, related_group_id, year (2000–2100), quarter, legal_entity_ids? (≤ 500)} 201 VersionCard (версия №1, INITIATED, lock = null) 404 (ГК/ГСК не видна), 409 CONSOLIDATION_EXISTS, 422 RELATED_GROUP_MISMATCH (ГСК чужой ГК или gsz), 422 ENTITY_NOT_IN_GROUP (legal_entity_ids), 422 PERIMETER_TOO_SMALL
GET /company-groups/{group_id}/consolidations VIEW in_work_only (по умолчанию false) ConsolidationPeriodOut[] — периоды по убыванию, в каждом versions[] (VersionBrief) по убыванию номера —
GET /consolidations/versions/{version_id} VIEW — VersionCard —
POST /consolidations/versions/{version_id}/lock EDIT тело необязательно: {takeover_reason? (≤ 1000)} 201 LockOut (взята или перехвачена) / 200 LockOut (уже ваша) 409 VERSION_NOT_EDITABLE (CANCELLED), 409 LOCKED_BY_OTHER (locked_by, locked_by_name, locked_at)
DELETE /consolidations/versions/{version_id}/lock EDIT — 204 (и когда блокировки нет) 409 NOT_LOCK_OWNER (locked_by, locked_at)
POST /consolidations/versions/{version_id}/status EDIT {to_status, comment? (≤ 1000)} VersionCard 409 VERSION_NOT_EDITABLE, LOCK_REQUIRED, LOCKED_BY_OTHER, TRANSITION_NOT_ALLOWED (from_status, to_status); 422 COMMENT_REQUIRED
PUT /consolidations/versions/{version_id}/members EDIT {legal_entity_ids[] (≤ 500)} — новый состав целиком VersionCard 409 VERSION_NOT_EDITABLE, LOCK_REQUIRED, LOCKED_BY_OTHER; 422 ENTITY_NOT_IN_GROUP, PERIMETER_TOO_SMALL
GET /consolidations/versions/{version_id}/checklist VIEW format = json (по умолчанию) / csv ChecklistOut {rules[], rows[]} или файл CSV —
GET /consolidations/versions/{version_id}/osv VIEW limit (≤ 2000, 500), offset, legal_entity_id, account_root, counterparty_type, unmapped, match_kind (повторяемый: ?match_kind=NAME&match_kind=ALIAS), q (≤ 500; имя контрагента — подстрока без регистра, ИНН контрагента — префикс) Page[ConsolidatedRowOut] —
GET /consolidations/versions/{version_id}/osv/summary VIEW — OsvSummaryOut {rows_total, unmapped_total, auto_matched_total, documents_total, gaps[]} —
GET /consolidations/versions/{version_id}/osv/export VIEW те же фильтры без limit/offset файл xlsx: листы «Сводная ОСВ» и «Пробелы» —
GET /consolidations/versions/{version_id}/counterparty-candidates VIEW q (≤ 500), scope = group (по умолчанию: периметр и ГК) / all (весь справочник), entry_id (для recommended) CandidateOut[] (до 100; рекомендуемые первыми) 422 QUERY_TOO_SHORT (scope=all и q < 3 символов)
POST /consolidations/versions/{version_id}/osv/entries/{entry_id}/counterparty EDIT {legal_entity_id, move_alias? = false} MapCounterpartyOut {row?, propagated, alias_created, alias_moved_from?} 409 VERSION_NOT_EDITABLE, LOCK_REQUIRED, LOCKED_BY_OTHER, ALIAS_TAKEN (owners[] {legal_entity_id, name})

Правила, без которых экран будет работать неправильно (полный список — api-contract-slice-4.md §4):

  1. Блокировка обязательна для правок версии (PUT …/members, POST …/status, сопоставление): статус «в работе» и ваша активная блокировка. Без неё — 409 LOCK_REQUIRED, чужая — 409 LOCKED_BY_OTHER. Создание версию не блокирует — «Взять в работу» отдельным POST …/lock. Блокировка не истекает по времени; CANCELLED её снимает. Загрузка файлов и подтверждение документов блокировкой не ограничены. Чтение — без блокировки и в любом статусе, включая CANCELLED.
  2. LOCKED_BY_OTHER из правок — без locked_by_name (только locked_by, locked_at); имя возьмите из lock карточки версии или users/brief. Из POST …/lock имя есть.
  3. Перехват: POST …/lock {takeover_reason: "…"} при чужой блокировке; пустая причина или из пробелов — как отсутствующая.
  4. Ручные переходы сейчас два: CANCELLED из любого «в работе» (с непустым comment, иначе 422 COMMENT_REQUIRED) и возврат в INITIATED из DOCUMENTS_PROCESSING/DATA_CORRECTION. Остальное — 409 TRANSITION_NOT_ALLOWED (маршрут согласования — Срез 7). Автоматические переходы: загрузка в консолидацию → DOCUMENTS_PROCESSING; документ NEEDS_CHECK по версии или подтверждение привязанного документа → DATA_CORRECTION.
  5. 404 в консолидациях — всегда NOT_FOUND; отдельных кодов «версия/строка/ГСК не найдена» нет.
  6. Ячейки чек-листа самоописывающие: ищите по document_type/account («Бух. баланс» — BALANCE, «ОФР» — INCOME, «ОСВ 60» — OSV + "60"). Порядок rules: ББ, ОФР, ОСВ по коду счёта натурально (58 < 58.03 < 60). ОСВ по субсчёту 58.03 закрывает только своё правило. PROCESSING ставится по всем недостающим ячейкам ЮЛ, пока его файл не смаплен (OQ-122).
  7. osv_accounts участника — «что загружено» к периоду версии (любой статус документа, кроме ошибки, незаменённые файлы), а не «что готово»: готовность — в ячейках checklist.
  8. Сводная ОСВ — чистая функция, в БД не хранится; одна и та же для страницы, сводки и экспорта. match_stored = false — контрагент найден на лету (в БД не записан). Склеенная из кварталов строка несёт несколько entry_ids (от раннего к позднему), её match_kind — от последнего квартала. На лету совпадение по алиасу приоритетнее совпадения по имени: при ЮЛ-тёзке берётся владелец алиаса (ALIAS).
  9. Ручное сопоставление: отправляйте последнюю entry_id строки (entry_ids.at(-1)), тогда в ответе MANUAL. propagated — число записей osv_entry, а не строк свода. row может быть null (строка привязанного документа в свод не попадает) — тогда перечитайте страницу и сводку. При ALIAS_TAKEN спросите пользователя и повторите с move_alias: true.
  10. Баннер «Осталось N» = unmapped_total = число строк GET …/osv?unmapped=true — строки без сопоставленного ЮЛ и без ИНН из файла. Строка с ИНН, которого нет в справочнике, сопоставления не требует (отмена П-1, решение владельца 08.10.2026).
  11. gaps[] — пары (ЮЛ, счёт) без ОСВ за какие-то кварталы; строк таких пар в своде нет. Лист «Пробелы» экспорта от фильтров не зависит.
  12. ГСК в версии не хранится: related_group_id нужен только при создании. Колонку «ГСК» на [34] оставляйте пустой. «Ответственный» на [34] — lock.locked_by_name, иначе initiator.name; «Утверждён в РФ» / «Верифицирован в ГО» — «—».
  13. warnings строк — диагностика, не контракт: показывать как подсказку.

5.8 Схемы ответов (кратко)

Поля с ? могут быть null, но ключ присутствует всегда. Полные схемы — /openapi.json и контракты по областям.

  • 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?}.
  • LegalEntityOut {id, inn, kpp?, 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}; LegalEntityDetail — + memberships[] {related_group_id, code, kind, name, valid_from}, aliases[] {id, alias, normalized_alias, created_at?}.
  • RelatedGroupOut {id, code, company_group_id, kind, name, created_date?, members_count}; RelatedGroupDetail — + members[] {legal_entity_id, inn, name, valid_from}.
  • FileOut {id, legal_entity_id, original_name?, file_type, status, version_no, size_bytes?, content_hash?, uploaded_at?, uploaded_by?, superseded_by_file_id?, consolidation_id: uuid | null (консолидация, в которую загружен файл; v5.18), job?, documents[]}; JobBrief {id, status, error_code?, error_message?, mapping_error?, warnings[], mapping_warnings[], finished_at?}; DocumentBrief {id, document_type, osv_account_code?, period_year?, period_quarter?, is_cumulative?, status, period_check?, counterparty_check?, locator?, comments_count}.
  • 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?}.
  • VersionBrief {id, version_no, status, scenario, approach, lock?, created_at?, members_count}; LockOut {locked_by, locked_by_name, locked_at, taken_over_from?, takeover_reason?}.
  • VersionCard = VersionBrief + {consolidation_id, year, quarter, reporting_date, company_group {id, name}, initiator {id, name}, initiator_office {id, name}, goodwill_enabled, is_in_work, previous_version_id?, members[], history[]}; MemberOut {id, name, inn, entity_type?, is_head_entity, checklist[] (ChecklistCellOut), osv_accounts[]}; HistoryOut {from_status?, to_status, changed_by, changed_at, comment?}.
  • ChecklistOut {rules[] {document_type, account?, is_mandatory}, rows[] {legal_entity_id, legal_entity_name, legal_entity_inn, cells[]}}; ChecklistCellOut {document_type, account?, status, missing_quarters[], document_ids[]}.
  • ConsolidatedRowOut {entry_ids[], legal_entity_id, legal_entity_name, legal_entity_inn, account_code?, account_root?, period, is_cumulative, row_number?, counterparty_name?, counterparty_inn?, counterparty_id?, counterparty_entity_name?, counterparty_entity_inn?, counterparty_type, match_kind?, match_stored, opening_debit?, opening_credit?, turnover_debit?, turnover_credit?, closing_debit?, closing_credit?, warnings[]}.
  • CandidateOut {id, inn, name, company_group?, gsk?, gsz? ({id, name}), counterparty_type, recommended}.
  • StatementOut — api-contract-slice-3b.md §3.

6. Экраны: какие вызовы и в каком порядке

Экран Вызовы
Старт приложения токен → GET /me (профиль и права в контекст); GET /directories/regional-offices — только для выбора филиала в формах (кэш на сессию)
[03] Группы компаний GET /directories/company-groups?q=&is_archived=&limit=&offset=; «Основной РФ» — base_regional_office.name
Карточка ГК / состав ГК GET /directories/company-groups/{id}, GET /directories/legal-entities?company_group_id=…, GET …/company-groups/{id}/related-groups; правки — §5.3
[51-02] Добавить ЮЛ / [79] Изменить ЮЛ списки «Входит в ГСК/ГСЗ» — GET …/company-groups/{id}/related-groups (по kind); «Добавить» — POST /directories/legal-entities со всей формой (aliases, gsk_id, gsz_id); «Сохранить» — PATCH …/{id} с итоговыми aliases и группами; форму заполнять из GET …/{id} (aliases[].alias, memberships[] по kind)
[60] Монозаёмщики GET /directories/legal-entities?outside_group=true; «Документы» — documents_count, статусы форм — GET /legal-entities/{id}/files по ЮЛ
[34] Консолидации ГК GET /company-groups/{id}/consolidations?in_work_only=; клик по версии — GET /consolidations/versions/{id} (панель «Организации»: members[] — тип, ячейки ББ/ОФР, osv_accounts)
[42] Новая консолидация ГК → ГСК (…/related-groups, kind = gsk) → все незакрытые ЮЛ ГК (legal-entities?company_group_id=<ГК>&closed=false, дочитывать по offset) с отметками членов ГСК (related-groups/{id} → members[]) → «Создать» (активна при ≥ 2 отмеченных) → POST /consolidations с legal_entity_ids
[35] Взять в работу POST …/versions/{id}/lock; на 409 LOCKED_BY_OTHER — диалог перехвата → тот же запрос с takeover_reason; «Отпустить» — DELETE …/lock
[33-01] «Изменить статус» / «Отмена КФО» POST …/versions/{id}/status {to_status, comment}
[36] Состав GET …/versions/{id} (члены) + все ЮЛ ГК → «Сохранить» — PUT …/members
[37] Чек-лист GET …/checklist; «Скачать» — GET …/checklist?format=csv (blob, §7)
[39] Первичные документы ЮЛ GET /consolidations/{cid}/legal-entities/{id}/files (+ опрос при UPLOADED/PROCESSING); переключатель «Все документы организации» — GET /legal-entities/{id}/files, подпись консолидации у файла по consolidation_id (год и квартал — из списка консолидаций ГК, null — «вне консолидации»); «Загрузил» — GET /users/brief?ids=…; удалить — DELETE /files/{id}
[40] Загрузить документы POST /consolidations/{cid}/legal-entities/{id}/files → показать accepted/rejected → опрос списка; кнопка недоступна, если версия не «в работе»
[41] Проверка распознавания ББ/ОФР GET /documents/{id}/statement; исходник — GET /files/{file_id}/content?disposition=inline (blob); «Сохранить» [41-02] — PATCH …/statement; акцепт — POST …/statement/checks/accept; «Подтвердить данные» — POST /documents/{id}/confirm; комментарии — GET/POST /documents/{id}/comments
Строки ОСВ документа GET /documents/{id}/entries (только чтение)
[30] Сводная ОСВ GET …/osv с фильтрами, GET …/osv/summary; «Экспорт» — GET …/osv/export (blob)
[32-osv-no-inn], [32-osv-map-counterparty] GET …/osv?unmapped=true → GET …/counterparty-candidates?entry_id=…&q=… (scope=all для поиска по всему справочнику) → «Связать» — POST …/osv/entries/{entry_id}/counterparty
[31-00] Уход с вкладки GET …/osv/summary → предупредить, если unmapped_total > 0
/admin §5.4

6.1 Что перечитать после действия

Сервер не присылает событий; после действия перечитайте то, что могло измениться.

Действие Ответ уже содержит Перечитать
Загрузка файлов accepted/rejected список файлов ЮЛ (опрос до завершения), карточку версии (статус мог стать DOCUMENTS_PROCESSING, потом DATA_CORRECTION), чек-лист
Подтверждение документа новый DocumentOut чек-лист и карточку версии (документ привязался ко всем версиям «в работе» ГК; статус мог стать DATA_CORRECTION), список файлов (статус строки)
Правка формы / акцепт новый StatementOut ничего (кроме статуса строки в drawer, если он открыт)
Удаление файла — (204) список файлов, чек-лист
Автозамена (новый файл тех же документов) — прежний файл исчезает из списка (SUPERSEDED); его документы отвязаны от версий — перечитать чек-лист
Сопоставление контрагента row (может быть null), propagated GET …/osv/summary; страницу свода, если propagated > 0 или row = null
Смена периметра / статуса новый VersionCard список версий ГК (…/consolidations), если он на экране
Взятие / снятие блокировки LockOut / — карточку версии (или обновить lock локально)
Правки справочника новый объект (у POST/PATCH ЮЛ — карточка с алиасами и членствами) списки, где он показан; карточку ЮЛ — после отдельных ручек алиасов и состава ГСК/ГСЗ; состав ГСК/ГСЗ (related-groups/{id}), если он на экране, — после смены групп в форме ЮЛ

7. Скачивание файлов

Три маршрута отдают файл: GET /files/{id}/content, GET …/checklist?format=csv, GET …/osv/export. Все требуют Authorization, поэтому <a href> и <img src> не работают — забирайте fetch → blob. Имя файла — из filename* заголовка Content-Disposition (RFC 5987, UTF-8, percent-кодировка); filename= там — ASCII-заглушка (file.pdf, checklist.csv, svod-osv.xlsx).

Кросс-доменно заголовок не виден. CORS ядра не выставляет Access-Control-Expose-Headers, поэтому при npm run start (фронт на :3000, API на :8000) res.headers.get('Content-Disposition') вернёт null. В сборке, которую раздаёт само ядро (SPA и API на одном источнике), заголовок читается. Пока это не исправлено (§10 п. 9), держите запасное имя на клиенте — его можно собрать из того, что уже есть на экране (checklist-<ГК>-<год>-<квартал>.csv, svod-osv_<ГК>_<год>-<Qn>_v<n>.xlsx, FileOut.original_name).

async function download(path: string, fallbackName: string): Promise<void> {
  const res = await fetch(`${API}/api/v1${path}`, { headers: { Authorization: `Bearer ${getToken()}` } });
  if (!res.ok) throw parseError(res.status, await res.json().catch(() => null));
  const cd = res.headers.get('Content-Disposition') ?? '';
  const star = /filename\*=UTF-8''([^;]+)/i.exec(cd);
  const name = star ? decodeURIComponent(star[1]) : fallbackName;
  const url = URL.createObjectURL(await res.blob());
  const a = Object.assign(document.createElement('a'), { href: url, download: name });
  a.click();
  URL.revokeObjectURL(url);
}

Особенности:

  • Первичка (/files/{id}/content): disposition=inline — для просмотра (PDF/PNG/JPG во фрейме или просмотрщике из blob URL); ответ несёт Content-Security-Policy: sandbox — проверьте, что выбранный просмотрщик PDF под ним работает. Cache-Control: private, no-store — не кэшируйте байты. Работает и для SUPERSEDED. 404 FILE_CONTENT_NOT_FOUND — запись есть, байтов в хранилище нет (истёк срок хранения): «Файл больше недоступен».
  • CSV чек-листа: UTF-8 с BOM, разделитель ;, CRLF; подписи статусов как в UI; имя checklist-<ГК>-<год>-<квартал>.csv.
  • xlsx свода: имя svod-osv_<ГК>_<год>-<Qn>_v<n>.xlsx; строки — после тех же фильтров, что на экране, без пагинации.

8. Деньги, проверки и прочие тонкости, которые ломают экраны

  • Склеенные квартальные строки свода не содержат null (пустое = "0.00"); строки нарастающего документа — как в файле, могут быть null. Показывайте null как пусто, а не «0,00».
  • FailedCheckOut.expected/actual — строки-суммы или null (пустая цель при непустых членах); relation — готовая читаемая запись соотношения («1600 = 1100 + 1200»).
  • edition формы ("66n", "fsbu4", null) — только подсказка для шаблона строк, на проверки не влияет.
  • Строки формы без кода (расшифровочные «в том числе») в ответ не попадают.
  • ОСВ со скользящим периодом: period_year есть, period_quarter и is_cumulative — null; документ можно подтвердить, но в версии он не участвует.
  • Строки osv_entry общие у версий одного документа: сопоставление контрагента в одной версии видно в другой (OQ-123).
  • Даты effective_date (перевод и вывод ЮЛ) сравниваются с «сегодня» по Москве; ограничьте датапикер минимумом «сегодня (МСК)».

9. Каталог доменных кодов ошибок

Код HTTP Где Доп. поля Что делать в UI
NOT_FOUND 404 любой объект в пути или теле, вне видимости или несуществующий — «Не найдено»; не ветвиться по тексту
OFFICE_NOT_FOUND 422 ГК, ЮЛ, пользователь: филиала нет — ошибка поля «Филиал»
OFFICE_NOT_ALLOWED 422 ГК/ЮЛ: пользователь филиала закрепляет за чужим филиалом — ошибка поля «Филиал»
INVALID_INN 422 ЮЛ (10/12 цифр по типу), шапка формы (10 или 12) — ошибка поля «ИНН»
INVALID_KPP 422 ЮЛ: формат КПП, КПП у ИП (в т. ч. смена типа на ИП); в импорте — код строки отчёта — ошибка поля «КПП»
EFFECTIVE_DATE_IN_PAST 422 перевод/вывод ЮЛ — ошибка поля «Дата»
EMPTY_ALIAS 422 алиас из одних ОПФ и знаков — ошибка поля
DIFFERENT_COMPANY_GROUP 422 член ГСК/ГСЗ из другой ГК (в т. ч. ГСК/ГСЗ в форме монозаёмщика) — сообщение
WRONG_GROUP_KIND 422 форма ЮЛ: в gsk_id передана ГСЗ или в gsz_id — ГСК — ошибка поля «Входит в ГСК/ГСЗ»
UNKNOWN_ROLE / UNKNOWN_PERMISSION 422 администрирование roles / permissions подсветить коды
MISSING_COLUMNS 422 импорт missing[] (поле есть и у UNSUPPORTED_FILE импорта — пустое) перечислить колонки
UNSUPPORTED_FILE 422 импорт (файл целиком); загрузка — в rejected[] — сообщение
FILE_TOO_LARGE, DUPLICATE_FILE — только в rejected[] загрузки file_id у дубля строка «не принят» со ссылкой на существующий файл
STORAGE_UNAVAILABLE 503 / в rejected[] загрузка, первичка — «Хранилище недоступно, повторите позже»
CRM_ID_EXISTS, INN_EXISTS, EMAIL_EXISTS 409 создание/правка — ошибка поля
LEGAL_ENTITY_CLOSED, SAME_COMPANY_GROUP 409 перевод ЮЛ — сообщение
ALREADY_CLOSED, NOT_IN_COMPANY_GROUP 409 вывод ЮЛ — сообщение
GROUP_NOT_EMPTY 409 удаление ГСК/ГСЗ — «Сначала исключите членов»
MEMBER_EXISTS 409 ЮЛ уже в этой группе — сообщение
MEMBER_OF_OTHER_GROUP 409 ЮЛ уже в другой группе этого вида related_group_id (не при гонке) сообщение со ссылкой на группу
ALIAS_EXISTS 409 алиас у этого ЮЛ уже есть; в форме ЮЛ — два тега совпадают после нормализации — сообщение (в форме — подсветить повтор)
ALIAS_TAKEN 409 алиас у другого ЮЛ справочник: legal_entity_id; сопоставление: owners[] {legal_entity_id, name} в сопоставлении — предложить «Перенести алиас» (move_alias: true)
LAST_ADMIN 409 администрирование — «Нельзя оставить систему без администратора»
ALREADY_CONFIRMED, FILE_SUPERSEDED, DOCUMENT_NOT_CONFIRMABLE 409 подтверждение, правка формы — перечитать документ
PERIOD_REQUIRED 409 подтверждение формы — «Укажите дату отчёта» → режим правки шапки
CHECKS_NOT_PASSED 409 подтверждение формы failures[] {column_index (число или null), relation, expected (или null), actual (или null)} показать нарушения, предложить акцепт
LINES_UNRESOLVED 409 подтверждение формы lines[] {line_id, row_index, line_code_text} перейти к первой строке без кода, предложить назначить код или «Это расшифровка»
DUPLICATE_FORM 409 подтверждение формы document_id ссылка на уже подтверждённую форму того же типа и периода в файле
LINE_CODE_TAKEN 409 правка формы, назначение кода — ошибка поля кода: код занят строкой, созданной правкой, с суммами
INVALID_LINE_CODE, LINE_CODE_REQUIRED, INVALID_LINE_PATCH, UNKNOWN_LINE, UNKNOWN_COLUMN, DUPLICATE_COLUMN 422 правка формы lines / columns — ошибка поля / ошибка клиента (перечитать форму)
DOCUMENT_NOT_STATEMENT 409 …/statement не по форме ББ/ОФР — не открывать экран [41] для такого документа
DOCUMENT_READONLY 409 правка/акцепт подтверждённой формы — перевести экран в просмотр
UNKNOWN_CELL, DUPLICATE_CELL 422 правка формы — ошибка клиента (перечитать форму); UNKNOWN_CELL также: line_code уже есть в форме (слать line_id), код вне справочника редакции, нет редакции
NOTHING_TO_ACCEPT 409 акцепт без нарушений — перечитать форму
FILE_HAS_CONFIRMED_DOCUMENTS, FILE_SUPERSEDES_OTHERS 409 удаление файла — скрывать «Удалить» у таких файлов (есть документ, подтверждённый пользователем, или автоподтверждённый документ в версии вне корректировки данных (не INITIATED/DOCUMENTS_PROCESSING/DATA_CORRECTION), см. §3.7; или на файл ссылаются superseded_by_file_id)
FILE_CONTENT_NOT_FOUND 404 первичка — «Файл больше недоступен»
TOO_MANY_IDS, INVALID_IDS 422 users/brief — ошибка клиента (бейте на пачки ≤ 200)
CONSOLIDATION_EXISTS 409 создание консолидации — «Консолидация за этот период уже есть» → открыть её
PERIMETER_TOO_SMALL 422 создание, PUT …/members — «Нужно минимум два ЮЛ»
RELATED_GROUP_MISMATCH 422 создание — ошибка поля «ГСК»
ENTITY_NOT_IN_GROUP 422 создание с legal_entity_ids, PUT …/members legal_entity_ids[] подсветить ЮЛ
ENTITY_NOT_IN_PERIMETER 422 загрузка в консолидацию — «ЮЛ не входит в периметр ни одной версии консолидации «в работе»» (05-domain-rules.md §9.5 п. 8)
QUERY_TOO_SHORT 422 кандидаты scope=all — «Введите не менее 3 символов»
COMMENT_REQUIRED 422 отмена версии — ошибка поля «Комментарий»
VERSION_NOT_EDITABLE 409 правки версии, взятие CANCELLED; повтор обработки; уточнение шапки ОСВ у шапки ОСВ — version_ids перевести экран в просмотр
FILE_NOT_RETRYABLE, MAX_ATTEMPTS_REACHED 409 / в skipped[] повторная обработка — спрятать кнопку повтора, показать причину
DOCUMENT_NOT_OSV 409 уточнение шапки ОСВ — правка только для ОСВ
INVALID_PERIOD, INVALID_ACCOUNT_CODE 422 уточнение шапки ОСВ — ошибка поля
DUPLICATE_ACCOUNT 409 уточнение шапки ОСВ: счёт уже есть у другой ОСВ файла document_id ссылка на документ с этим счётом
CONSOLIDATION_NOT_EDITABLE 409 загрузка в консолидацию, у которой нет версии «в работе» (POST /consolidations/{cid}/legal-entities/{le}/files) — заблокировать кнопку загрузки
LOCK_REQUIRED 409 правки версии — «Возьмите версию в работу»
LOCKED_BY_OTHER 409 взятие и правки версии locked_by, locked_at, из POST …/lock ещё locked_by_name «Версию держит <имя>», предложить перехват
NOT_LOCK_OWNER 409 снятие блокировки locked_by, locked_at перечитать карточку
TRANSITION_NOT_ALLOWED 409 ручной статус from_status, to_status перечитать карточку

Не доменные ответы: 401 строкой (WWW-Authenticate: Bearer), 403 строкой, 405 строкой с Allow, 404 {"detail": "Not Found"} (нет маршрута), 400 строкой (не разобран multipart, больше 1000 файлов), 413 строкой (импорт > 20 МиБ), 409 строкой «нарушение целостности данных в БД» (редкая гонка), 500 строкой («внутренняя ошибка сервера», «ошибка базы данных»).


10. Известные ограничения и ошибки на main

  1. period_check всегда UNKNOWN (OQ-120) — показывать «—»; подходящесть документа версии — только чек-лист.
  2. Ячейка PROCESSING неточна (OQ-122): пока файл ЮЛ не смаплен, «Обработка» стоит во всех недостающих ячейках ЮЛ.
  3. /me не перечитывается после смены ролей администратором — нужна перезагрузка страницы.
  4. CSV чек-листа не обезвреживает формулы Excel (R-9): имя ЮЛ, начинающееся с =, +, -, @, Excel может принять за формулу. xlsx свода этим не страдает (ячейки пишутся текстом).
  5. Статус формы после правки может стать RECOGNIZED, даже если у файла были предупреждения маппинга (arm-core#40, R-2) — ориентируйтесь на проверки и предупреждения, а не только на статус.
  6. Комментарии и правки нельзя удалить/отменить; подтверждение необратимо.
  7. Нет в API (показывать «—» или не показывать): маршрут согласования после DATA_CORRECTION (Срез 7), шахматка и ВГО (Срез 5), КФО и ЕХ (Срезы 6–7), «Добавить версию», гудвилл, журнал аудита, шапка формы ОКЕИ/ОПФ/форма собственности/аудит (П-4).
  8. В проде нет /openapi.json — генерируйте типы со стенда разработки.
  9. Content-Disposition не читается кросс-доменно (нет Access-Control-Expose-Headers в CORS ядра): при фронте на другом порту/домене имя скачиваемого файла берите запасное (§7). Нужна правка в arm-core (expose_headers=["Content-Disposition"]).

11. Что изменится в ближайших PR

Задача Что поменяется для фронта Как подготовиться
OQ-54 — SSO РАИСА Меняется только получение токена (вместо dev-формы) держать слой получения токена изолированным (frontend-auth-guide.md §9)
arm-ka-docs#116 — единая нормализация ББ/ОФР Выполнено в arm-core main (#129, #132) — см. §1 и §5.6.1; фронт (план 6) — в arm-core/web; выкладка сервисов на контракт 2.0 — qa-statements-v2.md типы из §5.6.1
arm-core#161 — повторная обработка файла и уточнение шапки ОСВ (AURORA-1442) Маршруты и коды в §5.5 и §9 уже описаны; на main появятся после слияния PR кнопки — AURORA-1679 (повтор), правка шапки ОСВ — отдельной задачей

Состояние этих задач — в Gitea (RSHB/arm-core, RSHB/arm-ka-docs); после слияния каждой этот раздел переезжает в §1.


12. Чек-лист клиента

  1. Единый HTTP-клиент: база, Bearer, глобальный 401, разбор трёх форм detail (§3.2), сетевая ошибка отдельно.
  2. Один GET /me при старте; видимость элементов — по permissions, не по ролям.
  3. Ветвление по code, не по тексту; 404 — «не найдено» без уточнений.
  4. Деньги — строками и decimal-библиотекой; в PATCH формы — строкой.
  5. PATCH — только изменённые поля; не обнулять обязательные (name, entity_type, inn формы).
  6. Ограничения ввода как на сервере: ≤ 500 ЮЛ периметра, комментарий статуса ≤ 1000, акцепта ≤ 2000, комментарий документа ≤ 4000, поиск ≤ 500, takeover_reason ≤ 1000, users/brief ≤ 200 id.
  7. Длинные списки — дочитывать по offset; лимит 200 (справочники, файлы) или 2000 (строки ОСВ).
  8. Правки версии — только со своей блокировкой; состояние кнопок — по is_in_work и lock.
  9. Ячейки чек-листа — по document_type/account.
  10. Файлы — fetch → blob с токеном; имя из filename*.
  11. После действий — перечитать по таблице §6.1; во время распознавания — опрос 3–5 с.