dev/frontend-api-guide.md — точка входа для фронта: подключение, общие правила (три формы detail, видимость, деньги строками, PATCH-семантика, лишние поля, лимиты), все 62 операции по областям с правами и кодами ошибок, сценарии экранов и что перечитывать после действий, скачивание файлов, каталог кодов, известные ограничения, изменения 05–07.10.2026 и ближайшие (КПП, #45). Сверено с OpenAPI и кодом arm-core main (f4a7806, после #46 и #47). api-contract-files-directories.md: is_head_office у филиала, outside_group/closed у ЮЛ (M-5), FILE_TOO_LARGE — настройка UPLOAD_MAX_BYTES (M-6), дочитывание ЮЛ модалки [42] по offset (M-7), филиал объектом (arm-core #47). README: ссылка на гайд и на контракт файлов и справочников. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
82 KiB
Гайд фронтенд-разработчика: API arm-core
Состояние — arm-core на main от 07.10.2026 (коммит f4a7806: после PR #46 «API до сводной ОСВ» и #51 «Основной РФ объектом»).
Схема сверена с 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 местами неточны (например, «VERSION_NOT_FOUND» у
загрузки — на деле код NOT_FOUND; у загрузки 422 ENTITY_NOT_IN_PERIMETER приходит объектом detail, а не UploadResult):
коды ошибок сверяйте по §9.
1. Что изменилось за последние дни (05–07.10.2026)
Если клиент писался по более ранним контрактам, пройдитесь по этому списку.
| Когда, PR | Что изменилось в 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), а не только без ИНН; ГСК в версии не хранится; «К отчётности» доступна в любом статусе |
баннер — по unmapped_total; колонку «ГСК» на [34] не заполнять; кнопку «К отчётности» не прятать по статусу |
| 07.10, решение владельца | Фильтра «файлы ЮЛ в контексте версии» нет и не будет | 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"} — это не признак «вошёл» |
| 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 |
4. Права
UI прячет элементы по GET /me → permissions (объединение прав всех ролей); код роли для ветвления не используется.
Сервер всё равно проверяет право и отвечает 403.
| Область | Чтение | Изменение | Особые |
|---|---|---|---|
Справочники /directories/* |
DIRECTORY_VIEW |
DIRECTORY_EDIT |
удалить ГСК/ГСЗ и алиас — DIRECTORY_DELETE; импорт ЕЦП — DIRECTORY_ADMIN |
Файлы и документы /legal-entities/{id}/files, /files/*, /documents/* |
DOCUMENT_VIEW |
DOCUMENT_EDIT (загрузка, удаление, подтверждение, правка формы, акцепт, комментарий) |
— |
Консолидации /consolidations*, /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 |
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] |
GET /directories/legal-entities/{entity_id} |
— | LegalEntityDetail (+ memberships[] в ГСК/ГСЗ, 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.- Членства ЮЛ в ГСК в
LegalEntityOutнет: для колонки «ГСК» в списке беритеrelated-groups/{id}каждой ГСК ГК (их немного) илиLegalEntityDetail.memberships. - Поиска ГК по имени филиала нет.
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, company_group_id?, regional_office_id?, crm_id?}; без company_group_id — монозаёмщик |
201 LegalEntityOut |
422 INVALID_INN (ЮЛ — 10 цифр, ИП — 12; пробелы внутри убираются), 404 (ГК не видна), 409 INN_EXISTS, 409 CRM_ID_EXISTS, 422 OFFICE_NOT_FOUND / OFFICE_NOT_ALLOWED — в этом порядке |
PATCH /directories/legal-entities/{entity_id} |
EDIT | {name?, entity_type?, crm_id?, regional_office_id?}; ИНН и ГК так не меняются |
LegalEntityOut |
422 INVALID_INN (ИНН не подходит новому типу), 422 OFFICE_*, 409 CRM_ID_EXISTS; name/entity_type: null — 422 массивом |
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?} |
201 RelatedGroupOut (код GSK-/GSZ-NNNNNN) |
— |
PATCH /directories/related-groups/{group_id} |
EDIT | {name?, created_date?} (вид и ГК не меняются) |
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 /legal-entities/{legal_entity_id}/files |
EDIT | multipart, поле files повторяется на каждый файл (не files[]); query consolidation_version_id? |
201 UploadResult {accepted[] {file_id, job_id, name}, rejected[] {name, code, message, file_id?}}, если принят ≥ 1 файл; 422 тем же телом, если ни одного |
404 (ЮЛ или версия), 409 VERSION_NOT_EDITABLE, 422 ENTITY_NOT_IN_PERIMETER (объектом detail), 503 STORAGE_UNAVAILABLE |
GET /legal-entities/{legal_entity_id}/files |
VIEW | limit, offset, include_superseded (по умолчанию false) |
Page[FileOut], новые первыми |
— |
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. - С
consolidation_version_idверсия должна быть видима, «в работе» и содержать ЮЛ; первый принятый файл переводит версиюINITIATED→DOCUMENTS_PROCESSING. Блокировка версии для загрузки не нужна. - Общий размер запроса приложение не ограничивает (лимит ставит ingress), число файлов — только предел Starlette: больше 1000 в одном запросе — 400 строкой; большие пачки лучше слать по нескольку файлов.
Жизненный цикл файла и что показывать (подписи — как в чек-листе):
FileOut.status |
Что происходит | Строка в drawer [39] |
|---|---|---|
UPLOADED |
принят, ждёт отправки на распознавание | «Обработка», опрашивать |
PROCESSING |
распознаётся или маппится; documents[] ещё пуст |
«Обработка», опрашивать |
PROCESSED |
документы созданы | статус — по документу: RECOGNIZED/NEEDS_CHECK «Требуется проверка», 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; это подсказки, тексты не разбирать.
Документ:
- Один файл = N документов: лист ОСВ по счёту — документ
OSV(osv_account_code,locator— лист), форма ББ/ОФР — документBALANCE/INCOME, расшифровки — один документDISCLOSURE/OTHERна тип (без данных, сразуRECOGNIZED). - Период:
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, column_id, amount}]} (до 5000 ячеек) |
StatementOut — состояние после правки |
409 FILE_SUPERSEDED, DOCUMENT_READONLY (уже CONFIRMED), DOCUMENT_NOT_CONFIRMABLE (статус ERROR), DOCUMENT_NOT_STATEMENT; 422 INVALID_INN, UNKNOWN_CELL, DUPLICATE_CELL; 422 массивом |
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}. У каждой пары «строка × колонка» естьValueOutсо своимid— для правки передавайтеline_idиcolumn_idсуществующей ячейки, создавать значения не нужно.- Подсветка:
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.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— от последнего квартала. - Ручное сопоставление: отправляйте последнюю
entry_idстроки (entry_ids.at(-1)), тогда в ответеMANUAL.propagated— число записейosv_entry, а не строк свода.rowможет бытьnull(строка привязанного документа в свод не попадает) — тогда перечитайте страницу и сводку. ПриALIAS_TAKENспросите пользователя и повторите сmove_alias: true. - Баннер «Осталось N» =
unmapped_total= число строкGET …/osv?unmapped=true— все строки без сопоставленного ЮЛ, в том числе с ИНН, которого нет в справочнике. 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, 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?, 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_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 |
| [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 /legal-entities/{id}/files (+ опрос при UPLOADED/PROCESSING); «Загрузил» — GET /users/brief?ids=…; удалить — DELETE /files/{id} |
| [40] Загрузить документы | POST /legal-entities/{id}/files?consolidation_version_id=… → показать 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 локально) |
| Правки справочника | новый объект | списки, где он показан; карточку ЮЛ после алиасов |
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) | — | ошибка поля «ИНН» |
EFFECTIVE_DATE_IN_PAST |
422 | перевод/вывод ЮЛ | — | ошибка поля «Дата» |
EMPTY_ALIAS |
422 | алиас из одних ОПФ и знаков | — | ошибка поля |
DIFFERENT_COMPANY_GROUP |
422 | член ГСК/ГСЗ из другой ГК | — | сообщение |
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} |
показать нарушения, предложить акцепт |
DOCUMENT_NOT_STATEMENT |
409 | …/statement не по форме ББ/ОФР |
— | не открывать экран [41] для такого документа |
DOCUMENT_READONLY |
409 | правка/акцепт подтверждённой формы | — | перевести экран в просмотр |
UNKNOWN_CELL, DUPLICATE_CELL |
422 | правка формы | — | ошибка клиента (перечитать форму) |
NOTHING_TO_ACCEPT |
409 | акцепт без нарушений | — | перечитать форму |
FILE_HAS_CONFIRMED_DOCUMENTS, FILE_SUPERSEDES_OTHERS |
409 | удаление файла | — | скрывать «Удалить» у таких файлов (есть CONFIRMED документ или на файл ссылаются 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 | загрузка с версией | — | «ЮЛ не входит в периметр версии» |
QUERY_TOO_SHORT |
422 | кандидаты scope=all |
— | «Введите не менее 3 символов» |
COMMENT_REQUIRED |
422 | отмена версии | — | ошибка поля «Комментарий» |
VERSION_NOT_EDITABLE |
409 | правки версии, загрузка с версией, взятие CANCELLED |
— | перевести экран в просмотр |
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), ИНН сопоставленного контрагента в строке свода отдельным полем (П-8, есть толькоcounterparty_entity_name). - В проде нет
/openapi.json— генерируйте типы со стенда разработки. Content-Dispositionне читается кросс-доменно (нетAccess-Control-Expose-Headersв CORS ядра): при фронте на другом порту/домене имя скачиваемого файла берите запасное (§7). Нужна правка в arm-core (expose_headers=["Content-Disposition"]).
11. Что изменится в ближайших PR
| Задача | Что поменяется для фронта | Как подготовиться |
|---|---|---|
arm-core #45 — КПП ЮЛ (модель уже утверждена: DBML v5.17, docs #54 слит; правила — 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); в импорте ЕЦП — необязательная колонка «КПП» и код строки INVALID_KPP |
заложить поле и код ошибки; в форме ИП поле КПП скрыть |
| arm-core #38 | Схема БД настройкой — API не меняется | — |
| OQ-54 — SSO РАИСА | Меняется только получение токена (вместо dev-формы) | держать слой получения токена изолированным (frontend-auth-guide.md §9) |
Состояние этих задач — в 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 с.