arm-ka-docs/dev/frontend-api-guide.md
Andrey Epifancev 9ba8d29033 docs: QA-гайд, OQ-160 (целевая модель версий), план срезов — файлы консолидации (#90)
QA-гайд переведён на маршрут консоли (POST /consolidations/{cid}/legal-entities/{le}/files),
OQ-159 закрыт, OQ-160 добавлен, Срез 2d дополнен, решения R-9…R-14 в design.md,
config.yaml переведён на DBML v5.18, формулировка SUPERSEDED выровнена.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 23:35:53 +04:00

96 KiB
Raw Blame History

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

Состояние — arm-core на main от 08.10.2026 (коммит d8bf5b2: после PR #46 «API до сводной ОСВ», #51 «Основной РФ объектом», #52 «КПП ЮЛ», #53 «Схема БД настройкой», #59 «Алиас приоритетнее имени» и #60 «Предупреждение автозамены»). Схема сверена с GET /openapi.json (62 операции на 49 путях), коды ошибок — с api/src/services/* и arm-core/README.md «Ошибки API».

Это точка входа: здесь всё, что нужно, чтобы писать клиент, — подключение, общие правила, каждый маршрут, ошибки, сценарии экранов, известные ограничения и то, что изменится в ближайших 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–09.10.2026)

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

Когда, PR Что изменилось в API Что сделать в клиенте
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 из строки списка con_id / карточки версии consolidation_id), переключатель «Все документы организации» → GET /legal-entities/{id}/files с подписью консолидации у файла; загрузка [40] — новым маршрутом, кнопка недоступна, если версия не «в работе»
09.10, arm-core #85 (#84) Форма ЮЛ одним запросом (макет [51-02], [79]): POST и PATCH /directories/legal-entities принимают aliases: string[] (1–500 символов, ≤ 100), gsk_id, gsz_id и применяют их в той же транзакции, что и поля ЮЛ (ошибка — откат всего). POST: алиасы и членства создаются вместе с ЮЛ. PATCH: не переданное поле не меняется; aliases — итоговый набор (сверка по normalized_alias: совпавшие не трогаются, лишние удаляются — нужно право DIRECTORY_DELETE, иначе 403 строкой; aliases: null — 422), gsk_id/gsz_id — итоговая группа (null — выйти, другая — сменить). Новый код 422 WRONG_GROUP_KIND; дубль в списке — 409 ALIAS_EXISTS. Ответ обоих — LegalEntityDetail (было LegalEntityOut; поля прежние + memberships, aliases) «Добавить»/«Сохранить» в модалках ЮЛ — один запрос со всей формой; в PATCH слать aliases и группы целиком, как они на экране; после ответа брать карточку из него, без GET; удаление тега-алиаса без DIRECTORY_DELETE (/me.permissions) не предлагать — сервер ответит 403
08.10, arm-core #75 (#74) Отмена П-1 (решение владельца 08.10.2026): «несопоставленная» строка свода — нет ЮЛ после резолва и нет ИНН из файла. unmapped_total, GET …/osv?unmapped=true и экспорт с этим фильтром больше не включают строки с ИНН, которого нет в справочнике (unmapped=false — все остальные строки). Новое поле ConsolidatedRowOut.counterparty_entity_inn — ИНН сопоставленного ЮЛ справочника (П-8; counterparty_inn — по-прежнему ИНН из файла) «Указать» — только у строк без counterparty_id и без counterparty_inn; у строки, сопоставленной без ИНН в файле, показывать counterparty_entity_inn с возможностью пересопоставить (макет [32-osv-no-inn], osv-inn-mapped-button; повторный POST …/counterparty разрешён)
08.10, arm-core #60 (#58) Предупреждение автозамены (OQ-154): если новый файл заменил прежний целиком, а части документов прежнего в нём нет (книга «ББ + ОФР» → отдельный ОФР), в job.mapping_warnings нового файла приходит одна строка на заменённый файл: «автозамена: файл «…» (версия N) заменён этим файлом целиком, но в новом файле нет документов прежнего: …; привязки сняты с версий консолидации: …» — с пометкой CONFIRMED у подтверждённых документов. Схемы ответов не менялись; поведение замены прежнее показать предупреждение в карточке нового файла заметно (не только в общем списке подсказок): аналитик должен догрузить выпавшие документы; текст не разбирать
08.10, arm-core #59 (#57) Алиас приоритетнее имени (OQ-128): при ЮЛ-тёзке по нормализованному имени строка с совпавшим алиасом теперь сопоставляется с владельцем алиаса (match_kind = ALIAS; в своде — match_stored = false, если найдено на лету). После ручного сопоставления unmapped_total может уменьшиться больше чем на 1 + propagated после ручного сопоставления перечитывать и страницу, и …/osv/summary (как и раньше), не вычислять «Осталось N» на клиенте
07.10, arm-core #52 (#45) КПП ЮЛ (DBML v5.17, context/05-domain-rules.md §13.5): поле kpp: string | null в LegalEntityCreate, LegalEntityUpdate, LegalEntityOut/Detail, LegalEntityHistoryOut. Формат — 9 символов (4 цифры, 2 цифры или латинские A–Z, 3 цифры; пробелы убираются, буквы — в верхний регистр), иначе 422 INVALID_KPP; у ИП КПП запрещён (422), у ЮЛ необязателен; уникален только ИНН. Поиск q в списке ЮЛ ищет и по КПП; смена типа на ИП у ЮЛ с КПП без очистки КПП — 422 INVALID_KPP; kpp: null в PATCH очищает КПП; в импорте ЕЦП — необязательная колонка «КПП» и код строки INVALID_KPP поле КПП в формах ЮЛ и в карточке; в форме ИП поле скрыть; ошибку INVALID_KPP — на поле «КПП»
07.10, arm-core #53 (#38) Схема БД задаётся настройкой DB_SCHEMA — API не изменился —
07.10, arm-core #51 (#47) «Основной РФ» приходит объектом {id, name, is_head_office} или null: regional_office в LegalEntityOut/Detail/HistoryOut, base_regional_office в CompanyGroupOut/HistoryOut (в т. ч. в ответах POST/PATCH/move/withdraw/archive). Поля *_id остались; неактивный филиал тоже отдаётся; входные схемы прежние (*_id) брать имя филиала из объекта, а не склеивать с GET /directories/regional-offices
07.10, arm-core #50 (#43), #49 (#44) Зависимости под контур (PyJWT 2.8) и настройки из OpenBao — API не изменился —
07.10, arm-core #46 (#42) POST /consolidations принимает необязательный legal_entity_ids (≤ 500): выбранный в модалке [42] периметр. Без поля или null — состав ГСК; [] — явный пустой периметр → 422 PERIMETER_TOO_SMALL; ЮЛ не из ГК → 422 ENTITY_NOT_IN_GROUP модалка [42]: все ЮЛ ГК, отмечены члены ГСК, отправлять отмеченные (§6, строка [42])
07.10, #46 Ячейка чек-листа ChecklistCellOut стала самоописывающей: document_type и account своего правила искать ячейку по document_type/account, а не по индексу
07.10, #46 VersionCard.members[] (MemberOut): новые entity_type ("ЮЛ"/"ИП"/null) и osv_accounts[] — счета ОСВ, загруженные к периоду версии колонки «Тип» и «ОСВ» панели «Организации» [34]
07.10, решения владельца П-1/П-2/П-3 «Осталось N» / «Показать ЮЛ без ИНН» = все несопоставленные строки (unmapped_total), а не только без ИНН (П-1 отменён 08.10, см. arm-core #75); ГСК в версии не хранится; «К отчётности» доступна в любом статусе баннер — по unmapped_total; колонку «ГСК» на [34] не заполнять; кнопку «К отчётности» не прятать по статусу
07.10, решение владельца Фильтра «файлы ЮЛ в контексте версии» нет и не будет (пересмотрено 09.10, см. docs #90 выше) drawer [39] показывает все файлы ЮЛ; подходящесть документа версии — по-прежнему по document_ids ячеек чек-листа
07.10, arm-core #39 (Срез 3b) Новые маршруты: GET/PATCH /documents/{id}/statement, POST …/statement/checks/accept, GET/POST /documents/{id}/comments, GET /files/{id}/content, GET /users/brief, GET …/checklist?format=csv экран [41], комментарии, просмотр первички, имена пользователей, «Скачать чек-лист»
07.10, #39 POST /documents/{id}/confirm для ББ/ОФР — новые 409 PERIOD_REQUIRED и CHECKS_NOT_PASSED (detail.failures[]) обработать оба кода на «Подтвердить данные»
07.10, #39 Новые поля: comments_count у документа (в FileOut.documents[] и DocumentOut), documents_count у ЮЛ бейджи комментариев, колонка «Документы»
07.10, #39 (фиксы ревью) PATCH …/statement: header.inn: null → 422 INVALID_INN (ИНН не очищается); header.units — непустая строка ≤ 50, пробельная → 422; DELETE /files/{id} удаляет и формы ББ/ОФР, и комментарии документов файла; GET /files/{id}/content отдаёт nosniff, Cache-Control: private, no-store, при inline — Content-Security-Policy: sandbox не слать inn: null; после удаления файла не открывать его документы; проверить просмотрщик PDF под CSP sandbox
06.10, arm-core #26 Верхние границы входов: legal_entity_ids ≤ 500 (создание и PUT …/members), StatusIn.comment ≤ 1000, q у сводной ОСВ и экспорта ≤ 500 → иначе 422 pydantic-массивом (без code). Удаление алиаса, который в этот момент переносится сопоставлением, — 404 NOT_FOUND ограничить ввод теми же числами; на 404 удаления алиаса — перечитать алиасы
06.10, arm-core #30 В openapi.json есть схема bearerAuth — в Swagger (/docs) работает кнопка Authorize токен вставлять без слова Bearer
06.10, arm-core #19 + фиксы R-14/R-14a Срез 4 целиком; ОСВ за Q1 подходит версии любого квартала года, ББ/ОФР за Q1 — только версии Q1 не вычислять подходящесть на клиенте — брать из чек-листа
06.10, arm-core #32, #25 make seed-dev создаёт dev-ГК под документы fixtures («Русский Бисквит» и три ГК пакета ДКБ); план счетов 1С — 488 счетов (больше строк ОСВ с account_id) экраны не пустые сразу после сида
05.10, arm-core #10 В проде (DEBUG=false) нет /docs и /openapi.json, CORS — только CORS_ORIGINS; неверный метод — 405 с Allow; даты «не раньше сегодня» — по Москве генерировать типы со стенда разработки
docs #51 Задание распознавания бывает timeout и dlq; поле multipart — files (повторяется), не files[]; MemberOut.entity_type nullable учесть в разборе статусов файла и в форме загрузки

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


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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

3.3 Видимость

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

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

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

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

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

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

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

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

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

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

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

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

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

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


4. Права

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

Область Чтение Изменение Особые
Справочники /directories/* DIRECTORY_VIEW DIRECTORY_EDIT удалить ГСК/ГСЗ и алиас — DIRECTORY_DELETE; импорт ЕЦП — DIRECTORY_ADMIN
Файлы и документы /legal-entities/{id}/files, /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, 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?} 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 /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}. У каждой пары «строка × колонка» есть 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.7 Консолидации

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

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

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

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

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

  • CompanyGroupOut {id, code, crm_id?, name, base_regional_office_id?, base_regional_office? {id, name, is_head_office}, is_archived, valid_from, legal_entities_count, created_at?, updated_at?}.
  • LegalEntityOut {id, inn, kpp?, crm_id?, entity_type?, name, company_group_id?, regional_office_id?, regional_office? {id, name, is_head_office}, close_date?, valid_from, created_at?, updated_at?, documents_count, outside_group, closed}; LegalEntityDetail — + memberships[] {related_group_id, code, kind, name, valid_from}, aliases[] {id, alias, normalized_alias, created_at?}.
  • RelatedGroupOut {id, code, company_group_id, kind, name, created_date?, members_count}; RelatedGroupDetail — + members[] {legal_entity_id, inn, name, valid_from}.
  • FileOut {id, legal_entity_id, original_name?, file_type, status, version_no, size_bytes?, content_hash?, uploaded_at?, uploaded_by?, superseded_by_file_id?, job?, documents[]}; JobBrief {id, status, error_code?, error_message?, mapping_error?, warnings[], mapping_warnings[], finished_at?}; DocumentBrief {id, document_type, osv_account_code?, period_year?, period_quarter?, is_cumulative?, status, period_check?, counterparty_check?, locator?, comments_count}.
  • DocumentOut = поля DocumentBrief + {file_id, legal_entity_id, confirmed_by?, confirmed_at?, group_schema? [{type, name?, has_inn?}], warnings[]}.
  • EntryOut {id, row_number?, level?, parent_entry_id?, row_value?, group_type?, group_name?, data_type?, account_code?, account_root?, account_id?, counterparty_name?, counterparty_inn?, counterparty_id?, match_kind?, opening_debit?, opening_credit?, turnover_debit?, turnover_credit?, closing_debit?, closing_credit?}.
  • VersionBrief {id, version_no, status, scenario, approach, lock?, created_at?, members_count}; LockOut {locked_by, locked_by_name, locked_at, taken_over_from?, takeover_reason?}.
  • VersionCard = VersionBrief + {consolidation_id, year, quarter, reporting_date, company_group {id, name}, initiator {id, name}, initiator_office {id, name}, goodwill_enabled, is_in_work, previous_version_id?, members[], history[]}; MemberOut {id, name, inn, entity_type?, is_head_entity, checklist[] (ChecklistCellOut), osv_accounts[]}; HistoryOut {from_status?, to_status, changed_by, changed_at, comment?}.
  • ChecklistOut {rules[] {document_type, account?, is_mandatory}, rows[] {legal_entity_id, legal_entity_name, legal_entity_inn, cells[]}}; ChecklistCellOut {document_type, account?, status, missing_quarters[], document_ids[]}.
  • ConsolidatedRowOut {entry_ids[], legal_entity_id, legal_entity_name, legal_entity_inn, account_code?, account_root?, period, is_cumulative, row_number?, counterparty_name?, counterparty_inn?, counterparty_id?, counterparty_entity_name?, counterparty_entity_inn?, counterparty_type, match_kind?, match_stored, opening_debit?, opening_credit?, turnover_debit?, turnover_credit?, closing_debit?, closing_credit?, warnings[]}.
  • CandidateOut {id, inn, name, company_group?, gsk?, gsz? ({id, name}), counterparty_type, recommended}.
  • StatementOut — api-contract-slice-3b.md §3.

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

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

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

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

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

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

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

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

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

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

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

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

  • Склеенные квартальные строки свода не содержат null (пустое = "0.00"); строки нарастающего документа — как в файле, могут быть null. Показывайте null как пусто, а не «0,00».
  • FailedCheckOut.expected/actual — строки-суммы; 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} показать нарушения, предложить акцепт
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 загрузка с версией — «ЮЛ не входит в периметр версии»
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

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

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

Задача Что поменяется для фронта Как подготовиться
OQ-54 — SSO РАИСА Меняется только получение токена (вместо dev-формы) держать слой получения токена изолированным (frontend-auth-guide.md §9)

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


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

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