Решения владельца 08.10.2026 (arm-core #74): несопоставленная строка свода — без ЮЛ и без ИНН из файла; `ConsolidatedRowOut.counterparty_entity_inn` (П-8). Обновлены frontend-api-guide (таблица изменений, п. 10, схема, пробелы API), api-contract-slice-4, 10-user-journey 3.1, qa-guide (контрольные цифры 1381 → 1260). Новый открытый вопрос OQ-158: строки аналитики, не являющиеся организациями, в «Осталось N». Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qoydUVv8g1qHxc123u2RR
662 lines
88 KiB
Markdown
662 lines
88 KiB
Markdown
# Гайд фронтенд-разработчика: 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`](frontend-auth-guide.md) | вход, токен, `/me`, 401/403, эталонный клиент `web/src/{api,auth}` |
|
||
| [`api-contract-files-directories.md`](api-contract-files-directories.md) | файлы и документы ЮЛ, чтение справочников, сборка строки drawer [39] |
|
||
| [`api-contract-slice-3b.md`](api-contract-slice-3b.md) | формы ББ/ОФР, комментарии, исходный файл, `users/brief`, CSV чек-листа |
|
||
| [`api-contract-slice-4.md`](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 | Что сделать в клиенте |
|
||
|---|---|---|
|
||
| 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, решение владельца | Фильтра «файлы ЮЛ в контексте версии» нет и не будет | 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).
|
||
|
||
```ts
|
||
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?}`; без `company_group_id` — монозаёмщик | 201 `LegalEntityOut` | 422 `INVALID_INN` (ЮЛ — 10 цифр, ИП — 12; пробелы внутри убираются), 422 `INVALID_KPP` (формат или КПП у ИП), 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?, kpp?}`; `kpp: null` очищает КПП; ИНН и ГК так не меняются | `LegalEntityOut` | 422 `INVALID_INN` (ИНН не подходит новому типу), 422 `INVALID_KPP` (формат; КПП у ИП, в т. ч. смена типа на ИП без очистки КПП), 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` | документы созданы | статус — по документу: `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, 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):
|
||
|
||
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 |
|
||
| [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`).
|
||
|
||
```ts
|
||
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 | член ГСК/ГСЗ из другой ГК | — | сообщение |
|
||
| `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 | удаление файла | — | скрывать «Удалить» у таких файлов (есть документ, подтверждённый пользователем, или автоподтверждённый документ в версии вне корректировки данных (не `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 с.
|