Пакет: engine.edition (choose_edition) и engine.contract_document_status (status_input_from_contract) вместо сборки StoredForm ради редакции и status_input_from_v2; FailedRelation.expected/actual — Decimal | None; O-8 закрыт. API для плана 6: LineOut.line_code_confidence, StatementHeaderOut.okei, header.source.confidence — строка Decimal, StatementOut.edition_lines, DocumentBrief.issue_counts, line_id/line_code в detail ошибок элемента lines[]; P3-19…P3-24; спека arm-core и гайд §5.6.1 дополнены. Р-34: подтверждение кода не меняет уверенность — буква спеки arm-core, §9.5 п. 9 и гайда приведена; O-2 закрыт. Р-35: очистка отказывает только при верифицированной версии — P3-12, тесты A4, спека «Очистка форм» п. 2 и сценарий; O-5 закрыт. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
115 KiB
Гайд фронтенд-разработчика: 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».
Это точка входа: здесь всё, что нужно, чтобы писать клиент, — подключение, общие правила, каждый маршрут, ошибки, сценарии экранов, известные ограничения и то, что изменится в ближайших 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 | Что сделать в клиенте |
|---|---|---|
| 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 | unmapped_total), а не только без ИНН |
баннер — по unmapped_total; колонку «ГСК» на [34] не заполнять; кнопку «К отчётности» не прятать по статусу |
| 07.10, решение владельца | 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 периметра и ГСК → 409CONSOLIDATION_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— 422INVALID_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 |
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 |
— |
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) — только чтение; экрана правки ОСВ в макете нет.
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 Формы ББ/ОФР после единой нормализации — после реализации
Пока не работает на
main. Раздел описывает API после changeunified-statement-normalization(RSHB/arm-ka-docs#116; контракт распознавания v2.0, DBML v5.19; дочерние PR arm-core и arm-web ещё не созданы). Правила —context/05-domain-rules.md§7, §9.3, §9.5 пп. 2, 4, 9, §11; экран — спекаarm-webтого же change. В…/statementполя добавляются, старые не удаляются: клиент, написанный по §5.6, продолжит читать ответ. Исключение — карточка файла:FileOut.job.warningsменяет тип со строк на объекты (ниже, «Карточка файла»), это несовместимо — экран файлов обновляется в той же выкладке, что и ядро. После слияния PR раздел переедет в §5.6 и §1.
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(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: догадка, подтверждённая сошедшимся соотношением, не подсвечивается). У значений строкdetailneeds_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[](все строки кодов соотношения).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; 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} (непустая строка) |
organization_name_modified, counterparty_check; снимает NO_ORGANIZATION_NO_INN |
422 массивом |
| подтвердить дату отчёта | header: {report_date} тем же значением, если report_date в needs_review_fields |
поле уходит из needs_review_fields, снимается REPORT_DATE_UNCERTAIN |
— |
Общие ошибки 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.
Статус документа. Форма из 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):
- Блокировка обязательна для правок версии (
PUT …/members,POST …/status, сопоставление): статус «в работе» и ваша активная блокировка. Без неё — 409LOCK_REQUIRED, чужая — 409LOCKED_BY_OTHER. Создание версию не блокирует — «Взять в работу» отдельнымPOST …/lock. Блокировка не истекает по времени;CANCELLEDеё снимает. Загрузка файлов и подтверждение документов блокировкой не ограничены. Чтение — без блокировки и в любом статусе, включаяCANCELLED. LOCKED_BY_OTHERиз правок — безlocked_by_name(толькоlocked_by,locked_at); имя возьмите изlockкарточки версии илиusers/brief. ИзPOST …/lockимя есть.- Перехват:
POST …/lock {takeover_reason: "…"}при чужой блокировке; пустая причина или из пробелов — как отсутствующая. - Ручные переходы сейчас два:
CANCELLEDиз любого «в работе» (с непустымcomment, иначе 422COMMENT_REQUIRED) и возврат вINITIATEDизDOCUMENTS_PROCESSING/DATA_CORRECTION. Остальное — 409TRANSITION_NOT_ALLOWED(маршрут согласования — Срез 7). Автоматические переходы: загрузка в консолидацию →DOCUMENTS_PROCESSING; документNEEDS_CHECKпо версии или подтверждение привязанного документа →DATA_CORRECTION. - 404 в консолидациях — всегда
NOT_FOUND; отдельных кодов «версия/строка/ГСК не найдена» нет. - Ячейки чек-листа самоописывающие: ищите по
document_type/account(«Бух. баланс» —BALANCE, «ОФР» —INCOME, «ОСВ 60» —OSV+"60"). Порядокrules: ББ, ОФР, ОСВ по коду счёта натурально (58 < 58.03 < 60). ОСВ по субсчёту58.03закрывает только своё правило.PROCESSINGставится по всем недостающим ячейкам ЮЛ, пока его файл не смаплен (OQ-122). osv_accountsучастника — «что загружено» к периоду версии (любой статус документа, кроме ошибки, незаменённые файлы), а не «что готово»: готовность — в ячейкахchecklist.- Сводная ОСВ — чистая функция, в БД не хранится; одна и та же для страницы, сводки и экспорта.
match_stored = false— контрагент найден на лету (в БД не записан). Склеенная из кварталов строка несёт несколькоentry_ids(от раннего к позднему), еёmatch_kind— от последнего квартала. На лету совпадение по алиасу приоритетнее совпадения по имени: при ЮЛ-тёзке берётся владелец алиаса (ALIAS). - Ручное сопоставление: отправляйте последнюю
entry_idстроки (entry_ids.at(-1)), тогда в ответеMANUAL.propagated— число записейosv_entry, а не строк свода.rowможет бытьnull(строка привязанного документа в свод не попадает) — тогда перечитайте страницу и сводку. ПриALIAS_TAKENспросите пользователя и повторите сmove_alias: true. - Баннер «Осталось N» =
unmapped_total= число строкGET …/osv?unmapped=true— строки без сопоставленного ЮЛ и без ИНН из файла. Строка с ИНН, которого нет в справочнике, сопоставления не требует (отмена П-1, решение владельца 08.10.2026). gaps[]— пары (ЮЛ, счёт) без ОСВ за какие-то кварталы; строк таких пар в своде нет. Лист «Пробелы» экспорта от фильтров не зависит.- ГСК в версии не хранится:
related_group_idнужен только при создании. Колонку «ГСК» на [34] оставляйте пустой. «Ответственный» на [34] —lock.locked_by_name, иначеinitiator.name; «Утверждён в РФ» / «Верифицирован в ГО» — «—». 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. 404FILE_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— строки-суммы;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, relation, expected, actual} |
показать нарушения, предложить акцепт |
LINES_UNRESOLVED |
409 | подтверждение формы (после #116, §5.6.1) | lines[] {line_id, row_index, line_code_text} |
перейти к первой строке без кода, предложить назначить код или «Это расшифровка» |
DUPLICATE_FORM |
409 | подтверждение формы (после #116) | document_id |
ссылка на уже подтверждённую форму того же типа и периода в файле |
LINE_CODE_TAKEN |
409 | правка формы, назначение кода (после #116) | — | ошибка поля кода: код занят строкой, созданной правкой, с суммами |
INVALID_LINE_CODE, LINE_CODE_REQUIRED, INVALID_LINE_PATCH, UNKNOWN_LINE, UNKNOWN_COLUMN |
422 | правка формы lines / columns (после #116) |
— | ошибка поля / ошибка клиента (перечитать форму) |
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 |
— | перевести экран в просмотр |
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
period_checkвсегдаUNKNOWN(OQ-120) — показывать «—»; подходящесть документа версии — только чек-лист.- Ячейка
PROCESSINGнеточна (OQ-122): пока файл ЮЛ не смаплен, «Обработка» стоит во всех недостающих ячейках ЮЛ. /meне перечитывается после смены ролей администратором — нужна перезагрузка страницы.- CSV чек-листа не обезвреживает формулы Excel (R-9): имя ЮЛ, начинающееся с
=,+,-,@, Excel может принять за формулу. xlsx свода этим не страдает (ячейки пишутся текстом). - Статус формы после правки может стать
RECOGNIZED, даже если у файла были предупреждения маппинга (arm-core#40, R-2) — ориентируйтесь на проверки и предупреждения, а не только на статус. - Комментарии и правки нельзя удалить/отменить; подтверждение необратимо.
- Нет в API (показывать «—» или не показывать): маршрут согласования после
DATA_CORRECTION(Срез 7), шахматка и ВГО (Срез 5), КФО и ЕХ (Срезы 6–7), «Добавить версию», гудвилл, журнал аудита, шапка формы ОКЕИ/ОПФ/форма собственности/аудит (П-4). - В проде нет
/openapi.json— генерируйте типы со стенда разработки. 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 — единая нормализация ББ/ОФР (контракт v2.0, DBML v5.19) | GET/PATCH …/statement: строки со статусами и повторами кода, needs_review, issues[], lines/columns/confirmed в PATCH; подтверждение — 409 LINES_UNRESOLVED, DUPLICATE_FORM; crm_id ГСК/ГСЗ сохраняется; FileOut.job.warnings — объекты вместо строк, DocumentOut.issues[] |
§5.6.1; не держать словарь «код → строка», подсветку строить по needs_review; тип JobBrief.warnings в types/api.ts поменять вместе с выкладкой ядра |
Состояние этих задач — в Gitea (RSHB/arm-core, RSHB/arm-ka-docs); после слияния каждой этот раздел переезжает в §1.
12. Чек-лист клиента
- Единый HTTP-клиент: база,
Bearer, глобальный 401, разбор трёх формdetail(§3.2), сетевая ошибка отдельно. - Один
GET /meпри старте; видимость элементов — поpermissions, не по ролям. - Ветвление по
code, не по тексту; 404 — «не найдено» без уточнений. - Деньги — строками и decimal-библиотекой; в
PATCHформы — строкой. - PATCH — только изменённые поля; не обнулять обязательные (
name,entity_type,innформы). - Ограничения ввода как на сервере: ≤ 500 ЮЛ периметра, комментарий статуса ≤ 1000, акцепта ≤ 2000, комментарий документа ≤ 4000,
поиск ≤ 500,
takeover_reason≤ 1000,users/brief≤ 200 id. - Длинные списки — дочитывать по
offset; лимит 200 (справочники, файлы) или 2000 (строки ОСВ). - Правки версии — только со своей блокировкой; состояние кнопок — по
is_in_workиlock. - Ячейки чек-листа — по
document_type/account. - Файлы —
fetch→ blob с токеном; имя изfilename*. - После действий — перечитать по таблице §6.1; во время распознавания — опрос 3–5 с.