Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
arm-core — ядро АРМ КА
Ядро автоматизированного рабочего места кредитного аналитика Департамента крупного бизнеса (АРМ КА, АО «Россельхозбанк»):
справочники, файлы и документы, приём результатов распознавания, вычисления консолидации (КФО ГСК), маршрут
согласования, Единое хранилище (ЕХ), аудит и права. Единственный писатель в PostgreSQL: сервисы распознавания
(tabular-parser, scan-gateway) видят только NATS и S3.
Монорепозиторий по шаблону команды «Шаблон FastAPI»: api/ — бэкенд (FastAPI, SQLAlchemy 2 async, Alembic,
PostgreSQL 16, core NATS без JetStream), web/ — фронтенд (React + TypeScript, rsbuild).
Этот репозиторий — часть workspace АРМ КА. Общий контекст, модель данных, правила предметной области и контракты — в соседних репозиториях:
../CLAUDE.md— инварианты и процесс работы всего workspace;../docs/— контекст (context/), модель данных (model/), контракты (contracts/), окружение (dev/);CLAUDE.md— соглашения этого репозитория.
Срез 0 («скелет ядра»): схема БД по DBML, справочники (seed), аутентификация и права, GET /api/v1/me,
/healthcheck. Спецификация — openspec/changes/slice-0-skeleton/.
Срез 1 («справочники»): схема по DBML v5.14, API ГК / ЮЛ / ГСК/ГСЗ с политикой видимости, импорт из ЕЦП CRM,
администрирование пользователей и ролей, экран /admin, аудит. Спецификация — openspec/changes/slice-1-directories/;
разделы ниже: «API справочников», «Импорт из ЕЦП CRM», «Администрирование и экран /admin», «Ошибки API».
Предварительные требования
- Python 3.11 (версия платформы) и
uv—make venvсоздаёт окружение и ставит зависимости черезuv; без него цель не работает. Системныйpython3другой версии не подойдёт:uvсам подберёт интерпретатор 3.11. - Docker и compose v2 — для PostgreSQL, NATS и MinIO (compose лежит в
../docs/dev/). - Node.js и
npm— только дляweb/. Для запуска бэкенда не нужны. make,curl.
Репозитории workspace должны лежать рядом: arm-core/ и docs/ — соседние каталоги (так же устроено зеркало
.wt/<срез>/). Пути ../docs/... ниже и в Makefile рассчитаны на это.
Быстрый старт
Путь «с нуля до зелёного /api/v1/me». Команды make выполняются из корня репозитория.
1. Виртуальное окружение
make venv
Создаёт api/.venv (Python 3.11) и ставит api/requirements.txt и api/requirements-dev.txt. Пакеты raisa_*
из Nexus банка вне банка недоступны, поэтому цель пропускает строки raisa_* (подробнее — «Работа без Nexus»).
Повторный запуск безопасен.
2. Инфраструктура (PostgreSQL, NATS, MinIO)
Файл .env compose — отдельный от api/.env; он создаётся один раз и в git не попадает:
cp ../docs/dev/.env.example ../docs/dev/.env
make up # docker compose -f ../docs/dev/docker-compose.yml up -d
make up поднимает postgres (5432), nats (4222, мониторинг 8222), minio (9000, консоль 9001) и одноразовый
minio-init, который создаёт бакет arm-ka и завершается с кодом 0. Команда возвращается сразу, а PostgreSQL становится
готовым примерно через 6 секунд: дождитесь healthy у трёх долгоживущих сервисов, прежде чем выполнять make migrate:
docker compose -f ../docs/dev/docker-compose.yml up -d --wait postgres nats minio # возвращается, когда все healthy
docker compose -f ../docs/dev/docker-compose.yml ps # проверка статуса
(голый --wait без списка сервисов возвращает код 1 из-за завершающегося minio-init; поэтому make up для бакета
и --wait со списком — вместе). Подробности, смена портов, сброс — ../docs/dev/README.md.
3. Настройки приложения
cp api/.env.example api/.env
Значения из api/.env.example уже совпадают с ../docs/dev/.env.example (стандартные порты и учётные данные compose),
менять ничего не нужно. Менять api/.env придётся, только если вы изменили порты или пароли в ../docs/dev/.env
(правьте обе части). api/.env в git не попадает.
4. Миграции и справочники
make migrate # alembic upgrade head: схема dkb, 27 enum-типов, 43 таблицы
make seed-dev # справочники продукта + dev-пользователи (по одному на каждую роль)
make seed загружает только справочники (роли, права, филиалы, план счетов, правила элиминации, чек-лист), без
dev-пользователей. Seed идемпотентен: повторный запуск обновляет данные и ничего не дублирует.
5. Запуск
cd api && .venv/bin/python run_server.py
Сервер слушает 127.0.0.1:8000 (SERVER__HOST, SERVER__PORT из api/.env). Для dev-запуска оставляйте loopback:
при DEBUG=true и AUTH_DEV_ISSUER=true эндпоинт dev-токенов выдаёт токен любому, кто знает email пользователя, —
не открывайте его в сеть. Дальнейшие команды — из другого терминала.
6. Проверка
curl -s localhost:8000/healthcheck
{"uptime":"2026-10-03T18:37:15.121394+00:00","version":"1.0.0","nats":"ok"}
nats: ok — подключён, down — включён (NATS_ENABLED=true), но связи нет, off — выключен.
Dev-токен по email любого dev-пользователя и профиль текущего пользователя:
curl -s -X POST localhost:8000/api/v1/dev/token \
-H 'Content-Type: application/json' \
-d '{"email":"ka_rf@armka.local"}'
# {"access_token":"<JWT>","token_type":"bearer"}
curl -s localhost:8000/api/v1/me -H "Authorization: Bearer <JWT>"
{
"id": "b8d7595a-787f-4bcb-8e93-7612cc8afba3",
"email": "ka_rf@armka.local",
"full_name": "Dev: Аналитик РФ",
"regional_office": {
"id": "3b38e823-e09a-47ce-a296-061948a200b0",
"name": "Ростовский региональный филиал",
"is_head_office": false
},
"business_segment": "DKB",
"roles": ["KA_RF"],
"permissions": [
"CONSOLIDATION_EDIT", "CONSOLIDATION_VIEW", "DIRECTORY_EDIT", "DIRECTORY_VIEW", "DOCUMENT_EDIT",
"DOCUMENT_VIEW", "EXPORT", "EX_VIEW", "KFO_EXPORT", "KFO_VIEW"
]
}
(идентификаторы в вашей БД будут другими). Токен действует 8 часов. Интерактивная документация API — /docs
(Swagger UI), схема — /openapi.json.
Dev-пользователи (api/seed/dev/users.yaml, создаются make seed-dev):
| Роль | Филиал | |
|---|---|---|
ka_rf@armka.local |
KA_RF |
Ростовский региональный филиал |
rka@armka.local |
RKA |
Ростовский региональный филиал |
client_manager@armka.local |
CLIENT_MANAGER |
Краснодарский региональный филиал |
ka_go@armka.local |
KA_GO |
Головной офис |
curator_go@armka.local |
CURATOR_GO |
Головной офис |
expert@armka.local |
EXPERT |
Головной офис |
admin@armka.local |
ADMIN |
Головной офис |
Права не зашиты в код роли: effective_permissions(user) — объединение прав ролей пользователя (таблицы permission,
role_permission).
Что вы увидите в логе при запуске
Старт занимает около секунды; трейсбэков в логе нет. SMB (горячие папки ИС Сканирование из шаблона) необязателен и по
умолчанию выключен: в .env.example OPENBAO__SMB__SMB_HOST пуст, сессия SMB не регистрируется (сообщение об этом —
только на уровне DEBUG). Если SMB_HOST задан, регистрация выполняется в отдельном потоке (цикл событий не
блокируется; старт ждёт её завершения не дольше тайм-аута smbclient, 60 с); ошибка подключения логируется трейсбэком
Ошибка при регистрации сессии SMB, приложение продолжает работу.
Строки Бакет проверен, Подключение к NATS установлено и Подписка NATS активна. subject=recognition.result, queue=arm-core означают, что S3 и NATS из compose подключены (MinIO из compose слушает http, поэтому в .env
задано OPENBAO__MINIO__MINIO_SECURE=false).
Недоступность S3 или NATS при старте приложение не роняет и не задерживает (недоступный SMB, если он настроен, — не
роняет): ошибка логируется, состояние NATS видно в /healthcheck. К NATS ядро подключается в фоне и повторяет попытки
само (nats=down, пока связи нет; warning в логе — на первую ошибку и дальше не чаще раза в минуту); после разрыва
nats-py переподключается и восстанавливает подписку.
При остановке ресурсы закрываются по шагам: фоновые задачи обмена → NATS → S3 → SMB → пул соединений БД. Ошибка
одного шага логируется (Ошибка при остановке приложения. step=...) и не отменяет следующие.
Предупреждение «Включён dev-эмитент токенов … только для разработки» появляется не при старте, а при первом
запросе к защищённому эндпоинту (/api/v1/me): проверяющий токены создаётся лениво (штатно, см. «Безопасность
dev-режима»).
Остановка и сброс
docker compose -f ../docs/dev/docker-compose.yml down # остановить, данные (тома) сохраняются
docker compose -f ../docs/dev/docker-compose.yml down -v # полный сброс: БД и бакет с нуля (затем снова migrate и seed-dev)
Имя проекта compose фиксировано (arm-ka), тома общие для всех worktree.
API справочников
Срез 1: справочники и их политика видимости. Все маршруты — под /api/v1, нужен токен (Authorization: Bearer …),
схема — /openapi.json (интерактивно /docs); состав маршрутов проверяет api/tests/test_openapi_routes.py.
Права — данные (permission × role_permission), код роли в логике не проверяется. Видимость записей решает
src/domain/policy.py: пользователь головного офиса видит всё, пользователь филиала — ГК своего филиала, их ЮЛ и
ГСК/ГСЗ и монозаёмщиков своего филиала. Объект вне видимости — 404 с тем же телом, что у несуществующего (не 403).
| Метод и путь | Право | Что делает |
|---|---|---|
GET /directories/regional-offices |
DIRECTORY_VIEW |
филиалы (включая Головной офис) |
GET /directories/company-groups |
DIRECTORY_VIEW |
видимые ГК: q (имя/crm_id/код), is_archived, limit/offset |
POST /directories/company-groups |
DIRECTORY_EDIT |
создать ГК (пользователь филиала закрепляет только за своим филиалом, иначе OFFICE_NOT_ALLOWED) |
GET /directories/company-groups/{group_id} |
DIRECTORY_VIEW |
карточка ГК |
PATCH /directories/company-groups/{group_id} |
DIRECTORY_EDIT |
изменить ГК (в т.ч. перезакрепление за филиалом) |
POST /directories/company-groups/{group_id}/archive, /unarchive |
DIRECTORY_EDIT |
архивировать / вернуть из архива |
GET /directories/company-groups/{group_id}/history |
DIRECTORY_VIEW |
история ГК, новые первыми |
GET, POST /directories/company-groups/{company_group_id}/related-groups |
DIRECTORY_VIEW / DIRECTORY_EDIT |
ГСК/ГСЗ группы компаний (простой массив) / создать |
GET /directories/related-groups/{group_id} |
DIRECTORY_VIEW |
карточка ГСК/ГСЗ с текущими членами |
PATCH /directories/related-groups/{group_id} |
DIRECTORY_EDIT |
изменить ГСК/ГСЗ |
DELETE /directories/related-groups/{group_id} |
DIRECTORY_DELETE |
удалить пустую ГСК/ГСЗ (GROUP_NOT_EMPTY) |
POST /directories/related-groups/{group_id}/members |
DIRECTORY_EDIT |
добавить ЮЛ (только из той же ГК) |
DELETE /directories/related-groups/{group_id}/members/{legal_entity_id} |
DIRECTORY_EDIT |
исключить ЮЛ (членство закрывается, не удаляется) |
GET /directories/legal-entities |
DIRECTORY_VIEW |
видимые ЮЛ/ИП: company_group_id, outside_group, inn, q, closed, limit/offset |
POST /directories/legal-entities |
DIRECTORY_EDIT |
создать ЮЛ/ИП (ИНН: ЮЛ — 10 цифр, ИП — 12) |
GET, PATCH /directories/legal-entities/{entity_id} |
DIRECTORY_VIEW / DIRECTORY_EDIT |
карточка (с членствами и алиасами) / изменить |
POST /directories/legal-entities/{entity_id}/move |
DIRECTORY_EDIT |
перевести в другую ГК с датой effective_date ≥ сегодня; членства ГСК/ГСЗ закрываются |
POST /directories/legal-entities/{entity_id}/withdraw |
DIRECTORY_EDIT |
вывести из ГК: CLOSE (с close_date) или REMOVE_FROM_GROUP |
GET /directories/legal-entities/{entity_id}/history |
DIRECTORY_VIEW |
история ЮЛ/ИП, новые первыми |
POST /directories/legal-entities/{entity_id}/aliases |
DIRECTORY_EDIT |
добавить алиас (normalized_alias по §3) |
DELETE /directories/legal-entities/{entity_id}/aliases/{alias_id} |
DIRECTORY_DELETE |
удалить алиас |
POST /directories/import |
DIRECTORY_ADMIN |
импорт выгрузки ЕЦП CRM (см. ниже) |
GET /admin/users, POST /admin/users, PATCH /admin/users/{user_id}, PUT /admin/users/{user_id}/roles |
USERS_ADMIN |
пользователи и их роли (см. «Администрирование») |
GET /admin/roles, GET /admin/permissions, PUT /admin/roles/{code}/permissions |
USERS_ADMIN |
матрица «роль × право» |
Списки верхнего уровня (ГК, ЮЛ, пользователи) отдают страницу {items, total, limit, offset} (limit 1–200, по
умолчанию 50); список ГСК/ГСЗ одной ГК — простой массив. Каждая мутация пишет audit_event в той же транзакции, что и
сама правка; повтор того же состояния ничего не пишет. Примеры:
TOKEN=$(curl -s -X POST localhost:8000/api/v1/dev/token -H 'Content-Type: application/json' \
-d '{"email":"ka_go@armka.local"}' | python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])')
curl -s 'localhost:8000/api/v1/directories/company-groups?q=бисквит' -H "Authorization: Bearer $TOKEN"
Импорт из ЕЦП CRM
POST /api/v1/directories/import (multipart) загружает справочник из выгрузки ЕЦП CRM: ГК, ЮЛ/ИП, филиалы и членство
в ГСК/ГСЗ. Нужно право DIRECTORY_ADMIN (seed: ADMIN). Алгоритм — ../docs/context/05-domain-rules.md §13
(§13.1 формат файла, §13.2 «строка → решение», §13.3 транзакция и отчёт), чистое ядро — src/domain/directory_import.py.
Порядок работы.
- Сначала пробный прогон:
dry_run=true(это значение по умолчанию). Прогон идёт тем же кодом, но транзакция откатывается; в БД, аудит и S3 ничего не попадает, отчёт тот же, что будет при записи. - Прочитайте отчёт: счётчики и построчный список
lines(row,inn,code,message). Пропущенные строки (errors) исправляются в файле и прогоняются заново. - Когда отчёт устраивает — тот же файл с
dry_run=false. Один импорт — одна транзакция; результат — отчёт в ответе,audit_event(DIRECTORY_IMPORT)с отчётом вafter_valueи файл в S3 (uploads/imports/{audit_event_id}/{имя}). - Повторный импорт того же файла при
on_existing=skipидемпотентен: все ранее принятые строки получают коды*_EXISTS; ранее пропущенные (ошибочные) строки остаются ошибками.
# шаг 1: пробный прогон (dry_run по умолчанию true, on_existing по умолчанию skip)
curl -s -X POST localhost:8000/api/v1/directories/import -H "Authorization: Bearer $ADMIN_TOKEN" \
-F file=@ecp-export.xlsx -F dry_run=true -F on_existing=skip
# шаг 3: запись
curl -s -X POST localhost:8000/api/v1/directories/import -H "Authorization: Bearer $ADMIN_TOKEN" \
-F file=@ecp-export.xlsx -F dry_run=false -F on_existing=update
Формат файла (§13.1). xlsx (первый лист) или csv (UTF-8, разделитель ; или ,); формат выбирается по расширению
имени. Заголовки — первая непустая строка, сопоставляются без регистра и пробелов, ё = е; порядок колонок не важен,
лишние игнорируются.
| Колонка | Обязательна | Смысл |
|---|---|---|
ИНН |
да | пробелы убираются; ЮЛ — 10 цифр, ИП — 12 |
iD ГК |
да | company_group.crm_id |
Группа компаний |
да | имя ГК |
Организация |
да | имя ЮЛ/ИП |
Тип |
да | ЮЛ или ИП (без регистра) |
Базовый РФ ГК в CRM |
нет | филиал по точному имени |
ГСК, ГСЗ |
нет | флаги «входит в ГСК» / «входит в ГСЗ» |
Нет любой обязательной колонки — файл отвергается целиком (422 MISSING_COLUMNS с перечнем в поле missing).
Параметр on_existing. skip (по умолчанию) — существующие ГК/ЮЛ не меняются; update — обновляются имя и
филиал ГК и имя/тип ЮЛ (со снимками в историю и аудитом).
Коды отчёта (§13.2). У каждой строки ровно один итоговый код, плюс сопутствующие события той же строки.
Пропуск строки (учитывается в errors): UNSUPPORTED_TYPE, INVALID_INN, DUPLICATE_IN_FILE, LE_IN_OTHER_GROUP,
GK_NAME_CONFLICT (разные имена одной ГК в файле или пустой iD ГК). Итоговые: LE_CREATED, LE_UPDATED,
LE_EXISTS. Сопутствующие: RF_CREATED, GK_CREATED, GK_UPDATED, GK_EXISTS, GSK_CREATED, GSZ_CREATED,
MEMBER_ADDED, UNKNOWN_FLAG.
Допущения (открытые вопросы, в коде помечены ASSUMPTION(OQ-…), список — ../docs/context/07-open-questions.md).
- OQ-87: типы вне {ЮЛ, ИП} (
нерез.,ФЛ,нет инн, пусто) пропускаются —UNSUPPORTED_TYPE. - OQ-88: ИНН проверяется только по длине и составу (ASCII-цифры), контрольные разряды не считаются; нечисловые
значения (
LV…,РНН…) —INVALID_INN. - OQ-89: филиала из файла нет в справочнике — он создаётся как обычный (
is_head_office=false), в том числе «ЦКБ» и «Головная организация» (RF_CREATED). - OQ-90: ЮЛ, которое уже состоит в другой ГК (или вне ГК), импорт не переносит:
LE_IN_OTHER_GROUP, перевод — толькоPOST …/move. Сообщение называет имя этого ЮЛ из другой группы — импорт доступен толькоDIRECTORY_ADMIN, который видит весь справочник. - OQ-91: флаги ГСК/ГСЗ создают не более одной ГСК и одной ГСЗ на ГК с именем ГК; отсутствие флага членство не закрывает.
Особенности, о которых стоит знать.
- ИНН — только текстом. Число из ячейки xlsx теряет ведущий ноль (
0123456789→123456789) и получаетINVALID_INN; в Excel задавайте текстовый формат колонки (или используйте csv). - Пустые «Организация» или «Группа компаний» при непустом
iD ГКне отвергаются: создаётся запись с пустым именем (допущение, правила на пустое имя в §13 нет). - У существующей ГК при
skipновый филиал из файла всё равно создаётся (RF_CREATED), но ГК не меняется. - Архивные ГК входят в снимок справочника;
updateне возвращает ГК из архива. - Пустые строки пропускаются, но входят в счётчик строк
MAX_IMPORT_ROWS.
Пределы.
| Предел | Значение | Результат при превышении |
|---|---|---|
IMPORT_MAX_BYTES (настройка) |
20 МиБ | 413, файл не читается в память целиком |
MAX_IMPORT_ROWS |
100 000 строк листа/записей csv, включая пустые | 422 UNSUPPORTED_FILE |
MAX_UNCOMPRESSED_BYTES |
200 МиБ суммарно по частям xlsx (zip), проверяется до разбора | 422 UNSUPPORTED_FILE |
Остальные причины UNSUPPORTED_FILE: расширение не xlsx/csv, повреждённый xlsx, csv не в UTF-8.
Администрирование и экран /admin
Бэкенд — /api/v1/admin/*, все маршруты требуют USERS_ADMIN (без права — 403 до любых проверок тела и существования
объектов). Фронтенд — страница /admin («Пользователи и роли» в боковом меню; видна только с USERS_ADMIN): таблица
пользователей (создание, смена ролей и активности) и вкладка «Матрица прав» (роль × право). Без права экран показывает
«Нет доступа» и запросов к /admin/* не делает (настоящую защиту обеспечивает сервер).
Как запустить.
make migrate && make seed-dev # схема, справочники и dev-пользователи
make up # если инфраструктура ещё не поднята
cd api && .venv/bin/python run_server.py # бэкенд, :8000 (в api/.env — DEBUG=true и AUTH_DEV_ISSUER=true)
cd web && npm install && npm run start # фронтенд (rsbuild dev), откроется браузер
Вход — dev-форма «Вход для разработки» (web/src/auth/DevLogin.tsx): введите admin@armka.local; форма вызывает
POST /api/v1/dev/token, токен хранится в sessionStorage вкладки. В production-сборке формы нет — вход через SSO
РАИСА (параметры JWT — открытый вопрос).
Что нужно знать.
- Роли и права — данные. Права роли правятся матрицей (
PUT /admin/roles/{code}/permissions), набор ролей пользователя —PUT /admin/users/{user_id}/roles; код роли нигде в логике не проверяется. Новых прав и ролей через API создать нельзя: неизвестный код — 422UNKNOWN_PERMISSION/UNKNOWN_ROLE(неактивная роль не назначается). LAST_ADMIN(409). После операции хотя бы у одного активного пользователя должно остаться правоUSERS_ADMINчерез активную роль. Защищены: деактивация пользователя (в том числе себя), замена его ролей и снятие права с роли; параллельные запросы двух администраторов идут по очереди (блокировка строки права).- Email. В теле запроса —
EmailStr, поэтому адрес с зарезервированным доменом*.local(x@corp.local) через API и UI отклоняется (422); dev-пользователи*@armka.localсоздаётmake seed-devнапрямую в БД. Для ручной проверки заводите пользователей сexample.com. Email уникален без учёта регистра (409EMAIL_EXISTS), хранится как введён. - Повторный seed возвращает снятые права.
make seed/make seed-devидемпотентны и только добавляют недостающие связи «роль × право» изapi/seed/core/role_permissions.yaml: право, которое администратор снял с роли в матрице, следующий запуск seed выдаст снова (granted_by— пусто); права, добавленные администратором сверх seed, seed не отзывает. На стенде с правками матрицы seed повторно не запускайте (follow-up к M-8 Среза 0). - Один выбор роли — один запрос. Мульти-выбор ролей в строке таблицы отправляет по одному
PUT …/rolesна каждое нажатие, поэтому каждое нажатие — отдельное событиеaudit_event(USER_ROLE_CHANGE). - Показываются первые 200 пользователей (
limit=200, максимум страницы); постраничного просмотра в UI пока нет — остальных ищетGET /admin/users?q=…(по email/ФИО). - Аудит.
USER_CHANGE(создание, правка полей, деактивация) иUSER_ROLE_CHANGE— разделUSERS;ROLE_PERMISSION_CHANGE— разделACCESS;before_value/after_value— прежнее и новое состояние.
Ошибки API
Доменные ошибки (src/common/exceptions.py, обработчик — src/core/exception_handlers.py) отдаются объектом:
{"detail": {"code": "INN_EXISTS", "message": "ЮЛ с таким ИНН уже существует", "legal_entity_id": "…"}}
code — машинный код (по нему работает клиент), message — текст по-русски, дополнительные поля зависят от ошибки
(например, legal_entity_id, related_group_id, permissions, roles, missing). Остальные ответы — {"detail": …}
другой формы: 401/403/413 — строка, 422 валидации тела — массив ошибок pydantic, 409 нарушения целостности БД — строка.
Объект вне видимости — 404 NOT_FOUND с тем же телом, что у несуществующего.
Гонки уникальных ограничений. Если параллельный запрос успел вставить запись между проверкой сервиса и INSERT,
БД отклоняет вставку (unique_violation), и ответ тот же 409 с тем же кодом, что у проверки (реестр —
src/common/constraint_conflicts.py): CRM_ID_EXISTS (ГК и ЮЛ), INN_EXISTS, ALIAS_EXISTS, EMAIL_EXISTS,
MEMBER_EXISTS/MEMBER_OF_OTHER_GROUP. В теле гонки — только код и постоянный текст, без дополнительных полей
(legal_entity_id, related_group_id) и без значений ключей. Особенности: повторное добавление в ту же группу
отдаёт MEMBER_OF_OTHER_GROUP с нейтральным текстом «ЮЛ уже входит в группу этого вида» (его раньше срабатывает индекс
«ЮЛ + вид»); гонка за один алиас у разных ЮЛ (ALIAS_TAKEN) в БД не защищена (уникален только (ЮЛ, алиас));
email без учёта регистра проверяет только сервис, поэтому гонка с другим регистром остаётся открытым ограничением.
Любое иное нарушение целостности (FK, check, неизвестное unique) — прежняя строка нарушение целостности данных в БД.
| Код | HTTP | Где |
|---|---|---|
NOT_FOUND |
404 | объекта нет или он вне видимости |
OFFICE_NOT_FOUND |
422 | филиал из тела запроса не существует (ГК, ЮЛ, пользователь) |
OFFICE_NOT_ALLOWED |
422 | ГК можно закрепить только за своим филиалом |
INVALID_INN |
422 | ИНН не из 10 (ЮЛ) / 12 (ИП) цифр |
EFFECTIVE_DATE_IN_PAST |
422 | дата перевода/вывода раньше сегодняшней |
EMPTY_ALIAS |
422 | алиас пуст после нормализации (одни ОПФ и знаки препинания) |
DIFFERENT_COMPANY_GROUP |
422 | ЮЛ не входит в ГК этой ГСК/ГСЗ |
UNKNOWN_ROLE / UNKNOWN_PERMISSION |
422 | неизвестная или неактивная роль / неизвестный код права (поля roles / permissions) |
MISSING_COLUMNS / UNSUPPORTED_FILE |
422 | файл импорта целиком (missing — перечень колонок) |
CRM_ID_EXISTS |
409 | ГК или ЮЛ с таким crm_id уже есть |
INN_EXISTS |
409 | ЮЛ с таким ИНН уже есть (legal_entity_id, если видно) |
LEGAL_ENTITY_CLOSED |
409 | перевод закрытого ЮЛ |
SAME_COMPANY_GROUP |
409 | перевод в ту же ГК |
ALREADY_CLOSED |
409 | ЮЛ уже закрыто |
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) |
EMAIL_EXISTS |
409 | пользователь с таким email уже есть |
LAST_ADMIN |
409 | операция оставила бы систему без активного USERS_ADMIN |
Файл импорта больше IMPORT_MAX_BYTES — 413 со строкой в detail. Недостаточно прав — 403 строкой
(недостаточно прав: требуется <ПРАВО>), нет токена или он неверен — 401.
Конфигурация
Настройки — pydantic-settings; читаются из переменных окружения и файла api/.env. Вложенные настройки
разделяются __. Файл окружения выбирается переменной ENV_FILE (по умолчанию .env, путь относительно текущего
каталога — api/; значение читается при импорте). Тесты используют api/.env.test (pytest.ini).
Полный пример — api/.env.example.
| Переменная | По умолчанию | Назначение |
|---|---|---|
ENV_FILE |
.env |
файл окружения (относительно api/) |
DEBUG |
false |
режим разработки. true необходим dev-эмитенту токенов; при false пакеты РАИСА подгружаются условно |
SERVER__HOST, SERVER__PORT |
0.0.0.0, 8000 |
адрес и порт сервера (run_server.py); в .env.example — 127.0.0.1 (dev-запуск только на loopback) |
ROOT_PATH |
/ |
корневой путь за прокси (должен совпадать с REACT_APP_ROOT_PATH фронта) |
VERSION |
1.0.0 |
версия в /healthcheck и OpenAPI |
OPENBAO__DATABASE__DATABASE_URL |
— (обязательно) | PostgreSQL, драйвер asyncpg: postgresql+asyncpg://armka:armka@localhost:5432/armka |
OPENBAO__MINIO__MINIO_URL |
— (обязательно) | адрес MinIO без схемы (host:port, так ждёт miniopy-async) |
OPENBAO__MINIO__MINIO_ADMIN_LOGIN |
— (обязательно) | логин MinIO |
OPENBAO__MINIO__MINIO_ADMIN_PASSWORD |
— (обязательно) | пароль MinIO |
OPENBAO__MINIO__MINIO_BUCKET |
— (обязательно) | бакет MinIO (arm-ka) |
OPENBAO__MINIO__MINIO_SECURE |
true |
true — MinIO по https (платформа), false — по http (MinIO из compose; так в .env.example) |
OPENBAO__NATS__NATS_URL |
— (обязательно) | адрес NATS (nats://localhost:4222) |
OPENBAO__NATS__NATS_SEED |
пусто | nkey seed; пусто — подключение без учётных данных (compose, тесты). Задаётся вместе с NATS_JWT или не задаётся вовсе |
OPENBAO__NATS__NATS_JWT |
пусто | JWT NATS; см. NATS_SEED |
OPENBAO__SMB__SMB_HOST |
пусто | SMB из шаблона, необязателен (ядру в Срезе 0 не нужен). Пусто — сессия SMB не регистрируется; задан — регистрация при старте (в отдельном потоке) и сброс кэша соединений при остановке |
OPENBAO__SMB__SMB_PATH |
пусто | путь SMB (в Срезе 0 не используется) |
OPENBAO__SMB__SMB_LOGIN |
пусто | логин SMB |
OPENBAO__SMB__SMB_PASSWORD |
пусто | пароль SMB |
NATS_ENABLED |
false |
подключение к NATS и обмен через recognition_job: фоновые outbox-публикатор и вахтёр сроков ответа (период 30 с, processing/published с прошедшим reply_deadline → timeout); без него фоновых задач нет, /healthcheck показывает nats=off |
OUTBOX_POLL_SECONDS |
5 |
период опроса outbox (queued → публикация → published) |
ACK_TIMEOUT_SECONDS |
30 |
ожидание ack сервиса распознавания до повторной публикации |
OUTBOX_MAX_REPUBLISH |
3 |
число повторных публикаций до перевода в dlq |
IMPORT_MAX_BYTES |
20971520 (20 МиБ) |
предельный размер файла импорта справочников; больше — 413 |
AUTH_DEV_ISSUER |
false |
dev-эмитент токенов: работает только при AUTH_DEV_ISSUER=true и DEBUG=true |
AUTH_DEV_SECRET |
пусто | секрет HS256 dev-эмитента (не короче 32 байт); только для разработки |
AUTH_JWKS_URL или AUTH_PUBLIC_KEY |
пусто | проверка токенов РАИСА: JWKS по URL либо публичный ключ (PEM) |
AUTH_ISSUER, AUTH_AUDIENCE |
пусто | проверка iss и aud токена РАИСА |
AUTH_EMAIL_CLAIM |
email |
claim токена с email пользователя (сопоставляется с app_user.email) |
Параметры JWT РАИСА (алгоритм, claim, эмитент) пока неизвестны — это открытый вопрос
(../docs/context/07-open-questions.md); AUTH_JWKS_URL/AUTH_PUBLIC_KEY
и связанные поля — каркас под него. Если не настроено ни dev-эмитента, ни ключей РАИСА, токены не принимаются вовсе
(/me отвечает 401).
Порты по умолчанию: приложение 8000; compose — PostgreSQL 5432, NATS 4222 (мониторинг 8222), MinIO 9000 (консоль 9001).
Порты compose меняются в ../docs/dev/.env, тогда же — соответствующие OPENBAO__* в api/.env.
Команды make
Все цели выполняются из корня репозитория (сами переходят в api/).
| Цель | Что делает |
|---|---|
make venv |
api/.venv на Python 3.11 через uv, зависимости без raisa_* |
make up |
поднимает compose из ../docs/dev (PostgreSQL, NATS, MinIO, бакет) |
make migrate |
alembic upgrade head на БД из api/.env |
make seed |
загрузка справочников продукта (идемпотентна) |
make seed-dev |
то же и dev-пользователи/филиалы (--dev) — только для разработки |
make lint |
ruff check, ruff format --check, mypy src scripts |
make test |
юнит-тесты: pytest -m "not integration", Docker не нужен |
make test-integration |
интеграционные тесты: pytest -m integration, нужны PostgreSQL (и NATS) |
make dbml-check |
сверка ORM с ../docs/model/data-model-v5.14.dbml |
make migration-cycle |
миграции на временной БД: upgrade head → downgrade base → upgrade head, пустой autogenerate |
make check |
быстрая проверка без Docker: lint + test |
make docker-build |
сборка образа: docker build -t arm-core . |
make web-check |
фронт: npm install, biome check src, tsc --noEmit, npm run build |
make check-infra |
быстрая проверка, что PostgreSQL и NATS из compose доступны (иначе: make up) |
make check-full |
полная проверка перед PR: check + dbml-check + check-infra + test-integration (в нём цикл миграций) + docker-build + web-check; нужна поднятая инфраструктура (make up) и Docker |
Тесты
- Юнит-тесты (
make test) не обращаются ни к БД, ни к NATS, Docker не нужен. - Интеграционные (
make test-integration) требуют PostgreSQL, тесты обмена через NATS — ещё и NATS. Рабочую БДarmkaони не трогают: на каждую сессию pytest создаётся свежая БДarmcore_pytest_<hex>, мигрируетсяalembicи удаляется в конце. Параметры подключения берутся изapi/.env.test(pytest.iniзадаётENV_FILE=.env.test). Файл указывает на стандартные порты и учётные данные compose из../docs/dev(PostgreSQL5432, NATS4222, MinIO9000), поэтому послеmake upтесты работают «из коробки». То же относится кmake migration-cycleиmake check-full: они тоже читают.env.testчерезpytest.ini. - Реальные данные — репозиторий
../fixtures/(путь можно задать переменнойARM_KA_FIXTURES). Тесты с маркеромfixturesбез каталога пропускаются с понятной причиной.
Проверка ORM и DBML
Модель данных — ../docs/model/data-model-v5.14.dbml. ORM обязана совпадать с ней один в один:
make dbml-check # пустой вывод и код 0 — совпадение; расхождения печатаются построчно
Другой путь к DBML: make dbml-check DBML=<путь относительно api/>.
Миграции
make migrate # применить: alembic upgrade head
cd api && .venv/bin/alembic downgrade base # откатить до пустой БД
make migration-cycle # проверить цикл upgrade -> downgrade -> upgrade на временной БД
Ревизии лежат в api/alembic/versions/; автогенерация — только черновик, финал сверяется с DBML (make dbml-check).
Раскладка репозитория
api/
run_server.py точка входа (uvicorn; хост и порт — из настроек)
alembic/ миграции (0001_dbml_v5_13.py — модель v5.13, 0002_dbml_v5_14.py — v5.14)
seed/core/ справочники продукта (роли, права, филиалы, план счетов, правила элиминации, чек-лист)
seed/dev/ dev-данные: филиалы и пользователи по ролям
scripts/ check_dbml_vs_orm.py (ORM <-> DBML), seed.py и seed_lib/ (загрузчик справочников)
src/
app.py FastAPI-приложение: lifespan (S3, NATS, SMB), /healthcheck, раздача SPA
api/endpoints/ эндпоинты (me.py, dev_auth.py, directories/, admin/); router.py собирает /api/v1
api/dependencies/ зависимости FastAPI (current_user, проверка прав)
core/ config, auth (JWT), clients (S3), nats (подключение, publish, подписка),
logger, exception_handlers
db/ движок и сессия (схема dkb)
models/ ORM-модели, 1:1 с DBML
repositories/ доступ к данным
schemas/ pydantic-схемы запросов и ответов
services/ бизнес-логика
common/ доменные ошибки (`{detail: {code, message}}`), часы
domain/ чистые функции по §§ правил, без БД: policy (видимость), directory_import (§13), aliases (§3), inn
tests/ pytest (юнит и integration/fixtures по маркерам)
web/ фронтенд (React 18 + TypeScript, rsbuild, antd): api/ (клиент), auth/ (вход, профиль), layouts/Admin (/admin); сборка web/dist -> api/web/
setup/ бутстрап шаблона через Nexus банка (pip.conf, setup.sh)
openspec/ спецификации и изменения (OpenSpec)
docs/superpowers/ планы реализации
raisa_config.yml ресурсы для платформы
Dockerfile только для локального compose
Слои: src/api/endpoints → services → repositories → models.
Работа без Nexus
Пакеты raisa_* (Python) и raisa-* (npm) лежат в Nexus банка и вне банка недоступны. Что с этим сделано:
make venvставитrequirements.txtбез строкraisa_*(фильтрgrep -v '^raisa_') и поэтому требуетuv, а неsetup/setup.sh(тот рассчитан наpip.confс Nexus).- Код импортирует
raisa_*условно: пакет middleware РАИСА импортируется только приDEBUG=false, иImportErrorдопустим — приложение работает без него. Аутентификация Среза 0 — собственная проверка JWT (api/src/core/auth.py). - В
web/package.jsonпакетыraisa-*вынесены вoptionalDependencies:npm installне падает, если Nexus недоступен. Файл.npmrcс адресом реестра (см.setup/setup.sh) вне банка не нужен.
Безопасность dev-режима
POST /api/v1/dev/tokenвыдаёт токен любому, кто знает email существующего активного пользователя: пароля нет. Только для локальной разработки и тестовых стендов.- Маршрут существует лишь при
AUTH_DEV_ISSUER=trueиDEBUG=true; при любом другом сочетании его нет вовсе (404), а dev-проверяющий токенов не создаётся даже при заданномAUTH_DEV_SECRET. В продеDEBUG=false, поэтому dev-эмитент там невозможен. AUTH_DEV_SECRETизapi/.env.example— публичный пример, в проде не использовать.make seed-devв прод не запускать.- Порты compose по умолчанию публикуются только на
127.0.0.1(BIND_ADDR); паролиarmka/minioadmin— только dev.
Соседние документы
CLAUDE.md— соглашения этого репозитория;../CLAUDE.md— workspace: инварианты, источники истины, процесс;../docs/dev/README.md— окружение разработки (compose), порты, типичные проблемы;../docs/context/03-architecture.md— архитектура, соглашения шаблона, NATS и S3;../docs/context/04-data-model-guide.mdи../docs/model/data-model-v5.14.dbml— модель данных;../docs/context/05-domain-rules.md— алгоритмы предметной области;../docs/context/07-open-questions.md— открытые вопросы;../docs/context/09-delivery-plan.md— план срезов;../docs/contracts/— JSON-схемы обмена с сервисами распознавания;openspec/changes/slice-0-skeleton/— спецификация и решения Среза 0.