arm-ka-docs/dev/qa-guide.md
Andrey Epifancev 9246a61439 docs(slice-4): правила §2/§3/§9/§10 по факту, OQ, контракт API для фронта, QA-гайд
- 05-domain-rules: §2 (выбор источников, склейка, пробелы, Q1-эквивалентность), §3 (явная привязка, дорезолв, match_kind,
  кандидаты, ручное сопоставление), §9.1 (переходы Среза 4), §9.3, §9.5 (отвязка при автозамене, OQ-110), §10 (политика
  «в работе + своя блокировка»)
- 03-architecture: консолидации, чистый домен свода, порядок блокировок
- 07-open-questions: закрыты OQ-03, OQ-19, OQ-110 (Срез 4), OQ-40 принят; добавлены OQ-118…OQ-129
- 09-delivery-plan, 10-user-journey: Срез 4 по факту, эндпоинты у шагов 2.1–2.6 и 3.1
- dev/api-contract-slice-4.md: маршруты, схемы, коды ошибок, нюансы для фронта
- dev/qa-guide: раздел 7 «Консолидация через API»

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 08:02:50 +04:00

44 KiB
Raw Blame History

Гайд тестировщика: локальный стенд АРМ КА (Срезы 0–2c, 4)

Документ для ручного тестирования текущего main. Путь проверен 05.10.2026 на macOS (Docker 28, compose 2.34, Node 24, uv 0.10): от пустого каталога до экрана администратора — около 15 минут, из них 10 — скачивание образов.

1. Что сейчас есть и чего нет

Есть (Срез 0 + Срез 1 + Срез 2c):

  • Бэкенд arm-core (FastAPI, /api/v1): вход по email в dev-режиме, профиль пользователя, права как данные.
  • API справочников: группы компаний (ГК), юрлица и ИП (ЮЛ) с историей, переводом между ГК и закрытием, алиасы контрагентов, группы связанных компаний и заёмщиков (ГСК/ГСЗ) с составом, филиалы. Видимость по филиалу: головной офис видит всё, филиал — только свои ГК.
  • Импорт справочников из выгрузки ЕЦП CRM (xlsx/csv) с пробным прогоном и отчётом.
  • Администрирование: пользователи, роли, матрица «роль × право», защита последнего администратора.
  • Фронтенд: страница /admin («Пользователи и роли»). Другого UI нет.
  • Журнал аудита в БД (dkb.audit_event).
  • Загрузка файлов ОСВ через API (POST /legal-entities/{id}/files), распознавание реальным tabular-parser, разбор результата в документы и строки ОСВ, статусы файла и документа, автозамена версий, подтверждение документа, удаление файла (раздел 6.6). Всё — через Swagger/curl, UI «Первичные документы» — подсрез 2d.
  • Консолидация через API (Срез 4): версии за период, периметр, блокировка «взять в работу», статусы, привязка подтверждённых документов, чек-лист, сводная ОСВ, ручное сопоставление контрагента, экспорт xlsx (раздел 7). UI консолидации — другой исполнитель.

Нет (это не баги):

  • Экранов справочников ГК/ЮЛ/ГСК — проверяются только через Swagger (/docs) или curl.
  • Экрана загрузки и таблицы ОСВ (подсрез 2d), экранов консолидации (API есть, раздел 7), ББ/ОФР, распознавания PDF, шахматки, КФО — следующие срезы.
  • Настоящего входа через SSO банка. Вход — dev-форма по email без пароля.

2. Что установить и проверить из ПО

Что Версия Проверка Если нет
ОС macOS или Linux. Windows — только через WSL2 (Ubuntu); нативно не проверялось — —
Git + SSH-ключ в Gitea любая свежая ssh -T -p 2222 git@git.aepif.ru отвечает приветствием добавить публичный ключ в профиль Gitea git.aepif.ru
Docker + compose v2 Docker ≥ 24, compose ≥ 2.20 (проверено 28.0 / 2.34) docker compose version Docker Desktop (macOS) или docker-ce + docker-compose-plugin (Linux)
uv ≥ 0.4 (проверено 0.10) uv --version curl -LsSf https://astral.sh/uv/install.sh | sh; Python 3.11 uv скачает сам
Node.js + npm Node ≥ 20 LTS (проверено 24.7) node --version && npm --version nodejs.org или nvm install --lts
make, curl любые make --version, curl --version macOS: Xcode Command Line Tools (xcode-select --install); Linux: build-essential
Свободные порты 5432, 4222, 8222, 9000, 9001 (compose), 8000 (бэкенд), 3000 (фронт) lsof -i :5432 и т. д. пусто закрыть локальный PostgreSQL/MinIO или сменить порты в docs/dev/.env и arm-core/api/.env
Ресурсы Docker: ≥ 4 ГБ RAM, ≥ 3 ГБ диска под образы и тома — —
Доступ в интернет нужен один раз: образы Docker Hub, PyPI, npm — Nexus банка не требуется

Необязательно, но удобно: клиент PostgreSQL (DBeaver или psql) для просмотра аудита; Postman или аналог — Swagger на /docs достаточно.

Браузер — любой современный (Chrome, Firefox, Safari, Edge).

3. Клонирование

Три репозитория должны лежать рядом, в одном каталоге, под именами docs, arm-core, fixtures (в коде пути вида ../docs/...):

mkdir arm-ka && cd arm-ka
git clone ssh://git@git.aepif.ru:2222/RSHB/arm-ka-docs.git docs
git clone ssh://git@git.aepif.ru:2222/RSHB/arm-core.git arm-core
git clone ssh://git@git.aepif.ru:2222/RSHB/arm-ka-fixtures.git fixtures

Без SSH-ключа можно клонировать по HTTPS с логином и паролем Gitea: https://git.aepif.ru/RSHB/arm-core.git и т. д.

fixtures содержит реальные данные (внутренний доступ) — нужен для файла импорта. Если доступа нет, пункт 6.3 чек-листа можно выполнить на полной выгрузке из docs/source/biq/.

4. Запуск стенда

Все команды — из каталога arm-core, если не сказано иное.

4.1 Инфраструктура (PostgreSQL, NATS, MinIO)

cp ../docs/dev/.env.example ../docs/dev/.env
make up
docker compose -f ../docs/dev/docker-compose.yml up -d --wait postgres nats minio
docker compose -f ../docs/dev/docker-compose.yml ps

Ожидание: postgres, nats, minio — Up (healthy); minio-init — Exited (0) (это нормально: он создаёт бакет и завершается). Первый запуск скачивает образы, это самая долгая часть.

4.2 Бэкенд

make venv                      # api/.venv на Python 3.11, зависимости из PyPI
cp api/.env.example api/.env   # dev-настройки: DEBUG=true, AUTH_DEV_ISSUER=true
make migrate                   # схема БД
make seed-dev                  # справочники + 7 dev-пользователей
cd api && .venv/bin/python run_server.py

Ожидание: в логе нет трейсбэков; curl -s localhost:8000/healthcheck отвечает {"uptime": ..., "nats": "ok"}. Swagger: http://localhost:8000/docs. Сервер слушает только 127.0.0.1, так и оставьте.

Для сценариев загрузки ОСВ (6.6) нужны ещё NATS_ENABLED=true (так в api/.env.example) и tabular-parser; поднимайте только его, без второго экземпляра ядра (он забирает часть сообщений recognition.result):

docker compose -f ../docs/dev/docker-compose.yml --profile services up -d --build --no-deps tabular-parser
curl -s localhost:8001/healthcheck      # nats: ok

4.3 Фронтенд (второй терминал)

cd arm-core/web
npm install
REACT_APP_API_URL=http://localhost:8000 REACT_APP_ROOT_PATH=/ npm run start

Откроется http://localhost:3000/ с формой «Вход для разработки». Две переменные окружения обязательны: без них фронт ищет API на своём порту и путь приложения как на платформе банка.

Не используйте npm run build для тестирования: в production-сборке dev-формы входа нет, страница показывает «Вход выполняется через РАИСА».

Настройки из api/.env.example (DEBUG=true, AUTH_DEV_ISSUER=true, CORS_ORIGINS=, FORWARDED_ALLOW_IPS=127.0.0.1) для локального стенда менять не нужно: DEBUG=true включает документацию API на /docs, dev-вход и разрешает make seed-dev.

4.4 Вход

В форме введите email dev-пользователя (не имя и не логин) и нажмите «Войти (dev)». Пароля нет. Это единственная форма входа АРМ: страница с полями «Имя пользователя / пароль» — это Gitea, а не стенд. Токен живёт в текущей вкладке (sessionStorage), новая вкладка — новый вход. Токен действует 8 часов.

Email Роль Филиал Что видит
admin@armka.local ADMIN Головной офис всё; единственный с правом администрирования и импорта
ka_go@armka.local KA_GO Головной офис все ГК и ЮЛ, может создавать и менять
curator_go@armka.local CURATOR_GO Головной офис все ГК и ЮЛ, может создавать и менять
expert@armka.local EXPERT Головной офис всё, только чтение
ka_rf@armka.local KA_RF Ростовский РФ только ГК своего филиала, может создавать и менять
rka@armka.local RKA Ростовский РФ только ГК своего филиала
client_manager@armka.local CLIENT_MANAGER Краснодарский РФ только ГК своего филиала, чтение

4.5 Токен для Swagger и curl

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 sys,json;print(json.load(sys.stdin)["access_token"])')
curl -s localhost:8000/api/v1/me -H "Authorization: Bearer $TOKEN"

В Swagger: кнопка Authorize → вставить токен (без слова Bearer) → все запросы пойдут от этого пользователя. Чтобы проверить видимость разными ролями, получите второй токен другим email и переавторизуйтесь.

5. Как читать ответы

  • Успех: 200/201, тело JSON. Удаление — 204 без тела.
  • Нет токена или он плохой — 401. Нет права — 403 с текстом недостаточно прав: требуется <КОД>.
  • Доменные ошибки — объект {"detail": {"code": "...", "message": "..."}}: 404 NOT_FOUND (в том числе для чужой записи — так задумано, чужое неотличимо от несуществующего), 409 конфликты (INN_EXISTS, LAST_ADMIN, …), 422 валидация (INVALID_INN, OFFICE_NOT_ALLOWED, …).
  • Ошибки валидации тела (не тот тип поля) — 422 со списком от pydantic.
  • Списки — {"items": [...], "total": N, "limit": 50, "offset": 0}, limit ≤ 200.

Полный каталог кодов — arm-core/README.md, раздел «Ошибки API».

6. Чек-лист сценариев

Отмечайте: ✅ ожидание совпало / ❌ баг (с номером issue) / ⚠️ вопрос. Пустые поля ожиданий — проверьте сами и зафиксируйте результат.

6.1 Администрирование (UI /admin, вход admin@armka.local)

№ Шаги Ожидание
A1 Открыть «Система → Пользователи и роли» таблица из 7 пользователей, вкладки «Пользователи» и «Матрица прав»
A2 «Создать пользователя»: email qa1@example.com, ФИО, филиал, роль KA_RF пользователь появился; войти им в новой вкладке — получится, роли KA_RF
A3 Создать пользователя с email qa1@EXAMPLE.com ошибка 409 EMAIL_EXISTS уведомлением
A4 Создать пользователя с email x@corp.local ошибка 422 (домен .local отклоняется — известное ограничение)
A5 У qa1@example.com сменить роли на KA_GO + EXPERT сохранилось; /me этого пользователя (новая вкладка) показывает обе роли
A6 Снять роль ADMIN у admin@armka.local (у себя), пока он единственный админ 409 LAST_ADMIN, роли не изменились
A7 Выдать роль ADMIN пользователю qa1@example.com, затем снять ADMIN у себя теперь разрешено
A8 Деактивировать единственного активного админа 409 LAST_ADMIN
A9 Матрица прав: роли EXPERT добавить EXPORT, сохранить /me эксперта содержит EXPORT
A10 Матрица прав: снять USERS_ADMIN у ADMIN, когда других админов нет 409 LAST_ADMIN
A11 Войти expert@armka.local, открыть /admin напрямую «Нет доступа», пункт меню скрыт, запросов к /admin/* нет (вкладка Network)
A12 После правки своих ролей обновить страницу профиль в шапке обновился только после перезагрузки (известное ограничение)

6.2 Справочники (Swagger /docs)

№ Пользователь Шаги Ожидание
D1 ka_go POST /directories/company-groups {"name":"ГК Тест ГО"} 201, code вида GK-000001, base_regional_office_id пустой
D2 ka_rf POST /directories/company-groups {"name":"ГК Тест РФ"} без филиала 201, филиал подставлен Ростовский
D3 ka_rf то же с base_regional_office_id Краснодарского РФ (id из GET /directories/regional-offices) 422 OFFICE_NOT_ALLOWED
D4 ka_rf GET /directories/company-groups только «ГК Тест РФ», total = 1 (ГК ГО не видна)
D5 ka_rf GET /directories/company-groups/{id ГК Тест ГО} 404, тело такое же, как для случайного uuid
D6 ka_go GET /directories/company-groups обе ГК
D7 ka_go PATCH ГК: сменить имя 200; GET .../history — одна запись с прежним именем и valid_to
D8 ka_go POST /directories/legal-entities ИП с ИНН из 10 цифр 422 INVALID_INN
D9 ka_go ЮЛ {"inn":"7700000001","entity_type":"ЮЛ","name":"ООО Альфа","company_group_id":<ГК ГО>} 201
D10 ka_go повторно тот же ИНН 409 INN_EXISTS
D11 ka_go ЮЛ без company_group_id 201, в списке outside_group = true
D12 ka_go POST .../legal-entities/{id}/aliases {"alias":"ООО «Альфа»"} 201, normalized_alias = альфа
D13 ka_go тот же алиас другому ЮЛ 409 ALIAS_TAKEN с legal_entity_id владельца
D14 ka_go POST /directories/company-groups/{ГК}/related-groups kind gsk, затем gsz 201 оба, коды GSK-…, GSZ-…
D15 ka_go добавить ЮЛ «Альфа» в ГСК и в ГСЗ той же ГК оба 201
D16 ka_go создать вторую ГСК в той же ГК и добавить туда «Альфа» 409 MEMBER_OF_OTHER_GROUP
D17 ka_go добавить в ГСК ЮЛ из другой ГК 422 DIFFERENT_COMPANY_GROUP
D18 ka_go POST .../legal-entities/{Альфа}/move в другую ГК, effective_date сегодня 200; членства закрыты (GET .../legal-entities/{id} — memberships пуст); история MOVE_TO_GROUP
D19 ka_go move с effective_date вчера 422
D20 ka_go POST .../withdraw {"event":"CLOSE","effective_date":сегодня} close_date заполнен, company_group_id пуст
D21 admin DELETE /directories/related-groups/{id непустой} 409 GROUP_NOT_EMPTY; пустой — 204
D22 ka_go DELETE /directories/related-groups/{id пустой} 403 (право DIRECTORY_DELETE только у ADMIN — так задумано)
D23 expert любой POST в справочники 403
D24 любой GET /directories/company-groups?limit=201 422

6.3 Импорт из ЕЦП CRM (пользователь admin, curl или Swagger)

Файл: fixtures/directories/ecp-export-russkiy-biskvit.xlsx (21 строка). Выполняйте на свежем стенде или после сброса (раздел 9), иначе счётчики будут другими.

№ Шаги Ожидание
I1 POST /directories/import с dry_run=true 200; rows 21, groups_created 1, offices_created 1, entities_created 20, memberships_created 2, errors 1 (строка типа ФЛ → UNSUPPORTED_TYPE); в БД и справочниках ничего не появилось
I2 тот же файл dry_run=false 200, тот же отчёт; появились ГК «Русский Бисквит», филиал «Санкт-Петербургский РФ», 20 ЮЛ/ИП, одна ГСК с двумя членами
I3 ещё раз dry_run=false, on_existing=skip все строки LE_EXISTS/GK_EXISTS, число записей не изменилось
I4 csv без колонки «Тип» 422 MISSING_COLUMNS, в ответе список недостающих колонок
I5 файл в кодировке cp1251 (сохранить csv из Excel «CSV (разделители — запятые)») 422 UNSUPPORTED_FILE
I6 пользователем ka_go 403
I7 полная выгрузка docs/source/biq/ecp-export-lagal-entity.xlsx, dry_run=true 200 за несколько секунд; rows 8400, offices_created 53; есть коды UNSUPPORTED_TYPE, INVALID_INN, DUPLICATE_IN_FILE
ADMIN=$(curl -s -X POST localhost:8000/api/v1/dev/token -H 'Content-Type: application/json' \
  -d '{"email":"admin@armka.local"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
curl -s -X POST localhost:8000/api/v1/directories/import -H "Authorization: Bearer $ADMIN" \
  -F file=@../fixtures/directories/ecp-export-russkiy-biskvit.xlsx -F dry_run=true | python3 -m json.tool | head -40

6.4 Аудит (БД)

docker compose -f ../docs/dev/docker-compose.yml exec postgres \
  psql -U armka -d armka -c "select occurred_at, action, section, object_type, changed_field from dkb.audit_event order by occurred_at desc limit 20"
№ После чего Ожидание
L1 A2, A5, A9 записи USER_CHANGE, USER_ROLE_CHANGE, ROLE_PERMISSION_CHANGE с before_value/after_value
L2 D1, D7 GROUP_CHANGE; смена филиала ГК — ASSIGNMENT_CHANGE
L3 D9, D12, D18 LEGAL_ENTITY_CHANGE, членства — MEMBERSHIP_CHANGE
L4 I2 одна запись DIRECTORY_IMPORT с отчётом в after_value; после I1 (dry_run) записи нет
L5 A6, D10 (отказы) записей аудита нет

6.5 Негатив и границы

№ Шаги Ожидание
N1 запрос без заголовка Authorization 401
N2 токен для несуществующего email (nobody@armka.local) через POST /dev/token 404 пользователь не найден или отключён, токен не выдан
N3 тело PATCH ГК с неизвестным полем 422 (поля вне схемы запрещены)
N4 импорт файла больше 20 МБ 413

6.6 Загрузка ОСВ и просмотр строк через API (Срез 2c)

Предусловия: раздел 4.2 (ядро с NATS_ENABLED=true, tabular-parser на :8001), файлы из ../fixtures/osv/. Выполняйте на свежем стенде (раздел 9) или с другим ЮЛ: дедуп и автозамена зависят от уже загруженного. Ожидаемое время от загрузки до PROCESSED — до 15 секунд (опрос публикатора и цикла маппинга по 5 с, парсер — доли секунды).

Подготовка: пользователь ka_go (права DOCUMENT_EDIT, DOCUMENT_VIEW, видит все ЮЛ). ЮЛ должно называться как в шапке ОСВ — АО "Русский бисквит", ИНН 3528052718 (если вы делали импорт I2 — найдите его в GET /directories/legal-entities?q=бисквит и используйте существующее, иначе создайте как в D9). Для проверки «чужого филиала» понадобится ГК с филиалом.

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 sys,json;print(json.load(sys.stdin)["access_token"])')
H="Authorization: Bearer $TOKEN"
# ЮЛ (пропустите, если уже есть)
LE=$(curl -s -X POST localhost:8000/api/v1/directories/legal-entities -H "$H" -H 'Content-Type: application/json' \
  -d '{"inn":"3528052718","entity_type":"ЮЛ","name":"АО \"Русский бисквит\""}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
# загрузка (U1)
curl -s -X POST "localhost:8000/api/v1/legal-entities/$LE/files" -H "$H" -F 'files=@../fixtures/osv/rb/2024-FY/ОСВ_66 за 2024.xls'
FILE=<file_id из ответа>
# опрос карточки файла (U2): повторяйте до status = PROCESSED
curl -s "localhost:8000/api/v1/files/$FILE" -H "$H" | python3 -m json.tool
DOC=<documents[0].id из карточки>
curl -s "localhost:8000/api/v1/documents/$DOC" -H "$H" | python3 -m json.tool
curl -s "localhost:8000/api/v1/documents/$DOC/entries" -H "$H" | python3 -m json.tool
curl -s -X POST "localhost:8000/api/v1/documents/$DOC/confirm" -H "$H"
curl -s -X DELETE "localhost:8000/api/v1/files/$FILE" -H "$H" -w '%{http_code}\n'
№ Пользователь Шаги Ожидание
U1 ka_go загрузить ОСВ_66 за 2024.xls для ЮЛ «Русский бисквит» 201, accepted из одного элемента (file_id, job_id, name), rejected пуст; сразу после загрузки GET /files/{id} — status UPLOADED или PROCESSING
U2 ka_go опрашивать GET /files/{file_id} (раз в 2–3 с) не позднее чем через ~15 с status = PROCESSED, job.status = done, job.error_code пуст; documents — один: document_type = OSV, osv_account_code = "66", period_year = 2024, period_quarter = Q4, is_cumulative = true, status = RECOGNIZED, counterparty_check = MATCH, period_check = UNKNOWN
U3 ka_go GET /documents/{id} locator = sheet:Лист_1, file_id = файл из U1, group_schema с группировками листа, warnings пуст, confirmed_by пуст
U4 ka_go GET /documents/{id}/entries total = 9; первая строка — account_code = "66", level = 0, parent_entry_id пуст; строка «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» под 66.03 имеет parent_entry_id = id строки 66.03; строки «Итого» нет; суммы — десятичные строки как в Excel (сальдо на конец счёта 66: кредит 54.64)
U5 ka_go загрузить тот же файл ещё раз 422, rejected[0].code = DUPLICATE_FILE, rejected[0].file_id — файл из U1; новых файлов нет
U6 ka_go загрузить .docx (или .xlsx, переименованный из .xls) 422, rejected[0].code = UNSUPPORTED_FILE
U7 ka_go загрузить ОСВ_67 за 2024.xls и .docx одним запросом (-F 'files=@…' -F 'files=@…') 201, один в accepted, один в rejected с UNSUPPORTED_FILE
U8 ka_go POST /documents/{id}/confirm 200, status = CONFIRMED, confirmed_by/confirmed_at заполнены; повторный вызов — 409 ALREADY_CONFIRMED
U9 ka_go DELETE /files/{file_id} файла с подтверждённым документом 409 FILE_HAS_CONFIRMED_DOCUMENTS, файл и строки на месте
U10 ka_go для того же ЮЛ загрузить ОСВ_60 за 2024.xls (rb) и дождаться PROCESSED; затем ../fixtures/osv/schastl/2024-FY/Оборотносальдовая ведомость по счету 60 за 2024 г..xlsx второй файл — PROCESSED, version_no = 2 (если оба документа 60 / 2024 / Q4); первый — SUPERSEDED, superseded_by_file_id = второй; у документа второго файла counterparty_check = MISMATCH, status = NEEDS_CHECK (шапка — другая организация)
U11 ka_go GET /legal-entities/{id}/files, затем с include_superseded=true в первом — только актуальные файлы; во втором — и заменённый SUPERSEDED
U12 ka_go DELETE второго файла из U10 (заменил первый) 409 FILE_SUPERSEDES_OTHERS; DELETE первого (SUPERSEDED) — 204, далее GET /files/{id} — 404
U13 ka_go POST /documents/{id}/confirm для документа первого файла из U10 (SUPERSEDED), до удаления в U12 409 FILE_SUPERSEDED
U14 expert POST .../files 403 (нет DOCUMENT_EDIT); GET /files/{id} — 200 (есть DOCUMENT_VIEW)
U15 client_manager GET /files/{id} файла ЮЛ другого филиала 404, тело как у случайного uuid
U16 ka_go остановить tabular-parser, загрузить новый файл status остаётся UPLOADED (ack нет — в PROCESSING файл не переходит) ~2 мин (3 повтора по 30 с), затем ERROR, job.error_code заполнен; DELETE такого файла — 204

Аудит (6.4 — запрос из psql): после U1 — FILE_UPLOAD; U8 — DOCUMENT_CONFIRM; U10 — FILE_REPLACE с пустым user_id (системное событие) и description, где перечислены документы заменённого файла; U12 — FILE_DELETE с карточкой файла в before_value. Смены статусов обмена (PROCESSING, PROCESSED, ERROR) аудитом не фиксируются — так задумано.

7. Консолидация через API (Срез 4)

Предусловия: ядро запущено (раздел 4.2); для сценариев с файлами — ещё NATS_ENABLED=true и tabular-parser на :8001 (раздел 6.6); данные — в ../fixtures/osv/. Токены: ka_go (CONSOLIDATION_VIEW/EDIT, видит всё), ka_rf (CONSOLIDATION_VIEW/EDIT, только ГК Ростовского РФ), expert (только CONSOLIDATION_VIEW), client_manager (CONSOLIDATION_VIEW, Краснодарский РФ), admin (прав на консолидации нет). Подробные схемы запросов и ответов, коды ошибок и нюансы для фронта — dev/api-contract-slice-4.md; Swagger — /docs.

Подготовка. Нужна ГК с ГСК не менее чем из двух ЮЛ (ГК — в Ростовском РФ, чтобы её видел ka_rf): создайте через API справочников (раздел 6.2; ГСК — POST /directories/company-groups/{id}/related-groups, участники — POST /directories/related-groups/{id}/members) либо импортом ЕЦП (I2). Для данных РБ: ЮЛ «АО "Русский бисквит"» (ИНН 3528052718) и «ООО "Пекарня Счастливочная"» (в ГСК обе). Ниже V — id версии, G — id ГК, S — id ГСК.

curl -s -X POST localhost:8000/api/v1/consolidations -H "$H" -H 'Content-Type: application/json' \
  -d '{"company_group_id":"'$G'","related_group_id":"'$S'","year":2024,"quarter":"Q3"}'
# 201: VersionCard; V = ответ.id, status = INITIATED, reporting_date = 2024-10-01
curl -s -X POST localhost:8000/api/v1/consolidations/versions/$V/lock -H "$H"                       # 201 LockOut
curl -s -X POST "localhost:8000/api/v1/legal-entities/$LE/files?consolidation_version_id=$V" -H "$H" -F 'files=@…'
curl -s localhost:8000/api/v1/consolidations/versions/$V/checklist -H "$H"
curl -s "localhost:8000/api/v1/consolidations/versions/$V/osv?limit=50" -H "$H"
curl -s localhost:8000/api/v1/consolidations/versions/$V/osv/summary -H "$H"
curl -s -o svod.xlsx -D - "localhost:8000/api/v1/consolidations/versions/$V/osv/export" -H "$H"   # Content-Disposition
№ Пользователь Шаги Ожидание
K1 ka_go создать консолидацию ГК/ГСК за 2024 Q3 (команда выше) 201, status = INITIATED, version_no = 1, scenario = CREATE, approach = BASIC, goodwill_enabled = false, members = текущие члены ГСК, lock = null; повтор — 409 CONSOLIDATION_EXISTS
K2 ka_go то же с year = 1999; с ГСК другой ГК; с ГСК из одного ЮЛ; с чужим/несуществующим id ГК 422 (валидация года); 422 RELATED_GROUP_MISMATCH; 422 PERIMETER_TOO_SMALL; 404 NOT_FOUND
K3 ka_go GET /company-groups/{G}/consolidations, затем с ?in_work_only=true; GET …/versions/{V} период с версией; карточка с members[] (у каждого checklist[]), history[0].from_status = null; ka_rf для ГК вне своего филиала — 404
K4 ka_go POST …/lock; повтор 201 LockOut (locked_by, locked_by_name); повтор — 200, блокировка та же
K5 ka_rf (ГК его филиала) POST …/lock на версию, занятую ka_go 409 LOCKED_BY_OTHER (locked_by, locked_at, locked_by_name); с телом {"takeover_reason":"…"} — 201, taken_over_from = id ka_go, ka_go теряет блокировку; в аудите CONSOLIDATION_LOCK_TAKEOVER
K6 ka_go после K5 PUT …/members 409 LOCKED_BY_OTHER (чужая блокировка), без блокировки вообще — 409 LOCK_REQUIRED
K7 владелец блокировки PUT …/members с составом ≤ 1 ЮЛ, с ЮЛ другой ГК, с тем же составом 422 PERIMETER_TOO_SMALL; 422 ENTITY_NOT_IN_GROUP (legal_entity_ids); 200 без изменений и без события MEMBERSHIP_CHANGE
K8 владелец блокировки загрузить ОСВ ЮЛ периметра с ?consolidation_version_id=V (раздел 6.6) 201; версия INITIATED → DOCUMENTS_PROCESSING; ЮЛ вне периметра — 422 ENTITY_NOT_IN_PERIMETER; несуществующая версия — 404
K9 ka_go после PROCESSED и NEEDS_CHECK документа — GET …/versions/{V}; затем POST /documents/{id}/confirm NEEDS_CHECK → версия DATA_CORRECTION (если файл загружал пользователь); подтверждение переводит INITIATED/DOCUMENTS_PROCESSING → DATA_CORRECTION; документ привязан к версии (виден в чек-листе: ячейка CONFIRMED)
K10 ka_go GET …/checklist до и после подтверждения; для квартальных ОСВ — только Q3 ячейка ОСВ: MISSING → NEEDS_CHECK/RECOGNIZED → CONFIRMED; при неполных кварталах — missing_quarters; ОСВ по субсчёту (58.03) не закрывает «ОСВ 58»
K11 ka_go GET …/osv, …/osv/summary по данным РБ (раздел «Данные для K11») строки свода по парам, где покрыты все кварталы Q1..Qn; пары без квартала — в gaps[] с missing_quarters; деньги — строки с двумя знаками; unmapped_total > 0
K12 ka_go фильтры: legal_entity_id, account_root=60, counterparty_type=EXTERNAL, unmapped=true, match_kind=MANUAL&match_kind=PROPAGATED, q=бисквит, limit=2001 отфильтрованные страницы с total; limit=2001 — 422
K13 ka_go GET …/counterparty-candidates?q=счастл&entry_id=<entry_ids[0]>; scope=all&q=ab список ЮЛ с counterparty_type; recommended — только при точном совпадении нормализованного имени; scope=all с q короче 3 — 422 QUERY_TOO_SHORT
K14 владелец блокировки POST …/osv/entries/{entry_id}/counterparty {legal_entity_id} для строки «Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ» 200: row.match_kind = MANUAL (слать последнюю entry_id строки свода), propagated ≥ 0, alias_created = true; в сводке unmapped_total уменьшился, у других строк с тем же именем — PROPAGATED; в аудите OSV_COUNTERPARTY_MAP и LEGAL_ENTITY_CHANGE
K15 владелец блокировки сопоставить другое имя с алиасом, уже закреплённым за другим ЮЛ; затем с "move_alias": true 409 ALIAS_TAKEN (owners[]); с move_alias — 200, alias_moved_from = прежний владелец
K16 ka_go entry_id строки NU/ACCOUNT или чужой версии 404 NOT_FOUND; без блокировки — 409 LOCK_REQUIRED
K17 ka_go GET …/osv/export (в т. ч. с фильтрами) 200, Content-Disposition: attachment; filename="svod-osv.xlsx"; filename*=UTF-8''…; файл открывается в Excel: лист «Сводная ОСВ» (суммы — числа, формат #,##0.00, число строк как в API) и лист «Пробелы» (от фильтров не зависит); в аудите EXPORT
K18 владелец блокировки POST …/status {"to_status":"CANCELLED"} без комментария; с комментарием 422 COMMENT_REQUIRED; 200, status = CANCELLED, блокировка снята (lock = null), history пополнена
K19 ka_go после K18: PUT …/members, POST …/lock, POST …/status, сопоставление 409 VERSION_NOT_EDITABLE; чтение (GET …/osv, экспорт, чек-лист) — 200
K20 владелец блокировки POST …/status {"to_status":"INITIATED"} из DATA_CORRECTION; затем {"to_status":"DATA_CORRECTION"} и {"to_status":"VERIFIED"} 200, привязки документов сохраняются; ручные DATA_CORRECTION/VERIFIED — 409 TRANSITION_NOT_ALLOWED (from_status, to_status)
K21 expert, client_manager GET …/osv, …/checklist; POST …/lock, PUT …/members чтение — 200 (client_manager — только ГК своего филиала, иначе 404); правки — 403 недостаточно прав: требуется CONSOLIDATION_EDIT
K22 ka_go заменить файл (загрузить исправленную ОСВ того же счёта и периода) после подтверждения документа старый файл SUPERSEDED, привязка его документов к версии снята (FILE_REPLACE.after_value.unbound_version_ids содержит V); новый документ привязывается только после подтверждения

Данные для K11 (РБ 2024, как в сквозном тесте test_consolidation_fixtures): ОСВ 60 и 62 РБ и Счастливочной, ОСВ 76 Счастливочной за Q1, Q2 и Q3 2024 (../fixtures/osv/…, подтвердить документы), плюс по одному документу за Q3 (РБ: 01, 58.03, 66, 67, 76; Счастливочная: 01, 02). Для версии Q3 пары с одним Q3 попадают в gaps (пропущены Q1–Q2); ОСВ за Q1 (нарастающая по признаку парсера) подходит версии любого квартала (R-14).

Аудит (6.4): K1 — CONSOLIDATION_CREATE; K4 — CONSOLIDATION_LOCK; K5 — CONSOLIDATION_LOCK_TAKEOVER; K7 — MEMBERSHIP_CHANGE (только при изменении состава); переходы статуса — CONSOLIDATION_STATUS_CHANGE; K14 — OSV_COUNTERPARTY_MAP и LEGAL_ENTITY_CHANGE; K17 — EXPORT; во всех — consolidation_version_id. Блокировка не истекает по времени — освободить её можно только DELETE …/lock, перехватом или аннулированием версии (OQ-119).

8. Известные ограничения (не заводить как баги)

  • UI только для администрирования; справочники — через API.
  • Вход без пароля, открытый /docs, CORS * — dev-режим.
  • Email *.local через API/UI отклоняется, при этом dev-пользователи именно такие (созданы seed напрямую).
  • Удалять алиасы и пустые ГСК/ГСЗ может только ADMIN (право DIRECTORY_DELETE).
  • Граница «дата перевода не раньше сегодня» считается по UTC: до 03:00 МСК «сегодня» ещё вчера. Будет исправлено.
  • Повторный make seed-dev возвращает права, снятые администратором в матрице.
  • Профиль в шапке не перечитывается после правки собственных ролей.
  • В консоли браузера сообщение Store does not have a valid reducer — каркас, не влияет.
  • Таблица пользователей показывает первые 200.
  • При импорте ИНН, записанный в Excel числом с ведущим нулём, теряет ноль и даёт INVALID_INN.
  • Консолидация (Срез 4): только API; маршрут согласования (CONSOLIDATION_RF и далее), «Добавить версию», гудвилл, шахматка — следующие срезы; ББ/ОФР в своде не участвуют; блокировка версии не истекает по времени; period_check документов остаётся UNKNOWN; PROCESSING в чек-листе ставится по всем недостающим столбцам ЮЛ, пока файл не смаплен (OQ-119, OQ-120, OQ-122).
  • Ручное сопоставление контрагента меняет строку ОСВ для всех версий, куда входит документ (OQ-123); рекомендация кандидата — только при точном совпадении нормализованного имени (OQ-127).

Полный список отложенных находок — Gitea RSHB/arm-core, issue «Follow-ups from #5».

9. Сброс стенда

docker compose -f ../docs/dev/docker-compose.yml down -v    # БД и бакет с нуля
make up && docker compose -f ../docs/dev/docker-compose.yml up -d --wait postgres nats minio
make migrate && make seed-dev

Бэкенд и фронт перезапускать не нужно, но сессии в браузере лучше начать заново.

Файлы, загруженные в 6.6, удаляются из БД командой DELETE /files/{id}, но объекты в MinIO остаются (им ставится только expires_at) — down -v стирает и их.

10. Как заводить баги

Gitea, репозиторий RSHB/arm-core (для документации — RSHB/arm-ka-docs). Один дефект — одна issue.

Шаблон:

Срез: 1 / Сценарий: D16 (номер из гайда, если есть)
Пользователь: ka_go@armka.local (роль KA_GO)
Шаги: 1. … 2. …
Ожидание: 409 MEMBER_OF_OTHER_GROUP
Факт: 500 (тело ответа ниже)
Запрос: метод, путь, тело (без токена)
Ответ: статус и тело
Лог бэкенда: строки с трейсбэком, если есть
Окружение: ОС, версии docker/node, коммит arm-core (`git rev-parse --short HEAD`)

Вопросы по ожидаемому поведению (не ясно, баг ли) — тоже issue, с пометкой «вопрос» в заголовке.

11. Типичные проблемы при запуске

Симптом Причина и решение
make up → порт занят локальный PostgreSQL/MinIO; остановить или сменить *_PORT в docs/dev/.env и те же порты в arm-core/api/.env
docker compose up --wait без списка сервисов возвращает 1 нормально из-за minio-init; используйте команду со списком из 4.1
make migrate → connection refused PostgreSQL ещё не healthy; подождать и повторить
make venv не находит Python 3.11 uv скачает его сам при наличии интернета; проверьте uv python list
Фронт открылся, но после входа пусто или ошибки сети забыты REACT_APP_API_URL/REACT_APP_ROOT_PATH при npm run start
Страница «Вход выполняется через РАИСА» это production-сборка; запускайте npm run start, а не build
Импорт падает на записи файла в S3 MinIO не поднят или не создан бакет: docker compose logs minio-init
/healthcheck показывает nats: down NATS не поднят; на сценарии Среза 1 не влияет, но загрузка ОСВ (6.6) без NATS не дойдёт до парсера
Файл завис в UPLOADED/PROCESSING не запущен tabular-parser (curl localhost:8001/healthcheck) или ядро запущено без NATS_ENABLED=true; без ack файл через ~2 мин станет ERROR

Подробнее об инфраструктуре — docs/dev/README.md, о бэкенде — arm-core/README.md.