README: раздел «Консолидация (Срез 4)» (эндпоинты, статусы, блокировка, привязка, чек-лист, свод, сопоставление, экспорт, порядок блокировок), коды ошибок Среза 4 в «Ошибки API», раскладка. CLAUDE.md: модули консолидации, правила (`transition`, порядок блокировок «файл → документ → advisory ГК → версии», FOR NO KEY UPDATE, Q1-эквивалентность). design.md: решения №82–85; tasks.md: 5.2 выполнена. Co-Authored-By: Claude Sonnet <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
138 lines
19 KiB
Markdown
138 lines
19 KiB
Markdown
# CLAUDE.md — arm-core (ядро АРМ КА)
|
||
|
||
Общий контекст системы — `../CLAUDE.md` и `../docs/` (workspace). Этот файл — только про этот репозиторий.
|
||
|
||
## Что это
|
||
Ядро: справочники, файлы/документы, маппинг результатов распознавания, вычисления консолидации, маршрут согласования,
|
||
ЕХ, аудит, права. **Единственный писатель в PostgreSQL.** Монорепо `api/` (FastAPI) + `web/` (React/TS) по шаблону
|
||
команды «Шаблон FastAPI» (нулевой коммит `RSHB/DKB_ARMKA@ba172f8`; импорт — `ba92222`, очистка от домена — `8e1f423`).
|
||
|
||
## Соглашения шаблона
|
||
Полный текст — `../docs/context/03-architecture.md`, раздел «Соглашения шаблона команды». Главное для этого репозитория:
|
||
- **Команды** — `make` из корня репозитория (Makefile в корне, backend-цели внутри делают `cd api`, venv — `api/.venv`):
|
||
`venv` (uv, Python 3.11, без `raisa_*`), `lint` (ruff + mypy), `test` (без БД), `test-integration` (PostgreSQL/NATS
|
||
из compose, включает цикл миграций), `check` (= lint + test, Docker не нужен), `up` (compose из `../docs/dev/`),
|
||
`migrate`, `seed` / `seed-dev` (справочники / + dev-пользователи), `dbml-check` (сверка ORM↔DBML), `migration-cycle`
|
||
(только цикл миграций), `e2e` (сквозной прогон загрузки ОСВ с реальным tabular-parser, без профиля `services` — skip),
|
||
`check-infra`, `docker-build`, `web-check` (biome + tsc + build), `check-full` (check + dbml-check + check-infra +
|
||
test-integration + e2e + docker-build + web-check; **перед PR**, вывод — в описание PR).
|
||
Вручную — из `api/`: `python run_server.py`, `alembic upgrade head`, `pytest`; из `web/`: `npm run start|build`.
|
||
Бутстрап в банке — `setup/setup.sh` (pip/npm через Nexus).
|
||
- **Раскладка `api/src/`**: `src/api/endpoints` → `services` → `repositories` → `models`; DTO — `schemas/`; инфраструктура —
|
||
`core/` (config, clients, nats, logger, exception_handlers, auth); `db/` — engine и сессия. Наши добавления:
|
||
`domain/` (чистые функции по §§ правил, без БД и сессий: `policy.py` — видимость справочников §10.1,
|
||
`directory_import.py` — чтение xlsx/csv и план импорта §13, `aliases.py` — нормализация алиаса §3, `inn.py` — формат
|
||
ИНН, `file_types.py` — тип файла по расширению и сигнатуре, `osv_mapping.py` — результат парсера → документ/строки
|
||
ОСВ (§1.3, §1.5, §9.3); Срез 4 — `consolidation_periods.py` (выбор источников и приведение периодов, §2, Q1-эквивалентность ОСВ,
|
||
R-14), `consolidated_osv.py` (чистая сборка свода: склейка кварталов, пробелы, дорезолв, фильтры, §2–§3), `checklist.py`
|
||
(чек-лист комплекта, ячейки `CellStatus`); `policy.py` дополнен `IN_WORK_STATUSES`/`ensure_version_editable`, §10; для шахматки
|
||
и КФО — Срезы 5–6), `contract/` (модели контракта обмена), `api/scripts/` (`check_dbml_vs_orm.py`,
|
||
`seed.py` + `seed_lib/`, `check_infra.py`), данные seed — `api/seed/`.
|
||
- **DI** — ручные фабрики `Depends` в `src/api/dependencies/`; `storage`/`nats` — из `request.app.state`. **Сессия** —
|
||
только `session: SessionDep` (`src/api/dependencies/db.py` = `Depends(get_async_session, scope="function")`): одна
|
||
транзакция на запрос (`get_async_session` открывает `session.begin()`), commit **до** отправки ответа, rollback при
|
||
любом исключении; объявление через голый `Depends(get_async_session)` даёт вторую сессию и ловится тестом
|
||
`test_api_routes_declare_session_with_function_scope`. Сервисы не коммитят сами.
|
||
- **Настройки** — pydantic-settings, `api/.env`, вложенность `__`, инфраструктура под `OPENBAO__*`. Флаги разработки
|
||
(`AUTH_DEV_ISSUER`, `DEBUG`) и обмена (`NATS_ENABLED`, по умолчанию выключен) — отдельные поля `Settings`. NATS
|
||
управляется только `NATS_ENABLED` (от `DEBUG` не зависит); при `DEBUG=false` шаблонный импорт
|
||
`raisa_fastapi_protected_api` в `app.py` условный (пакета нет вне банка — не ошибка); middleware РАИСА в Срезе 0
|
||
не подключается.
|
||
- **БД** — схема **`dkb`** (MetaData, `search_path`, Alembic); PK `uuid` через `UUIDPrimaryMixin`; ревизии в
|
||
`api/alembic/versions/`; автогенерация — черновик, финал сверяется с DBML скриптом.
|
||
- **Ошибки** — `common/exceptions.py` → `core/exception_handlers.py`. Доменные ошибки (`DomainError`: `NotFoundError` 404
|
||
`NOT_FOUND`, `ConflictError` 409, `DomainValidationError` 422; код и доп. поля задаёт вызывающий) отдаются как
|
||
`{"detail": {"code", "message", **extra}}` (message по-русски, `code`/`message` в `extra` запрещены); `HTTPException`
|
||
(401/403/413) и ошибки БД — `{"detail": "<строка>"}`, 422 валидации тела — массив. Объект вне видимости пользователя —
|
||
404 с тем же телом, что у несуществующего (не 403). Каталог кодов — README, «Ошибки API». **Логи** — loguru.
|
||
- **Тесты** — pytest-asyncio (auto, session loop), `APP_ENV=test`, `ENV_FILE=.env.test`; маркеры `integration` (нужны
|
||
PostgreSQL/NATS), `fixtures` (реальные данные из `../fixtures`) и `e2e` (поверх `integration`: нужен tabular-parser из
|
||
профиля `services`, иначе skip; `make test-integration` его исключает, гонять — `make e2e`); БД сессии — свежая `armcore_pytest_<hex>` с миграциями,
|
||
удаляется в конце; `domain/` — без БД.
|
||
- **Линтеры** — backend `ruff` + `mypy` (`api/pyproject.toml`); frontend `biome` (ширина 140, одинарные кавычки) + `tsc`.
|
||
- **Frontend** — React 18 + TS strict, rsbuild, antd 6 (основной UI-кит), react-router 7; `layouts/` — страницы
|
||
(`Main`, `Admin`), `widgets/` — блоки (`Sidebar`, `Admin/*`), `components/` — примитивы; env только `REACT_APP_*` через
|
||
`conf/config.ts`; `REACT_APP_ROOT_PATH` = `ROOT_PATH` бэка. Redux Toolkit из шаблона подключён (`store/store.ts`,
|
||
`Provider` в `App.tsx`), но стор пуст и `store/slices/` без слайсов: состояние экранов `/admin` — локальные хуки
|
||
(`widgets/Admin/hooks.ts`), профиль — контекст. `api/client.ts` — единый клиент (`apiFetch`, `api.get|post|put|patch`,
|
||
префикс `${Config.API_URL}/api/v1`, Bearer-токен, `ApiError {status, code, message}` по трём формам `detail`, событие
|
||
`armka:unauthorized` на 401). `auth/` — `AuthGate` (профиль `/me` → `MeContext`/`useCurrentUser`), `DevLogin` (только не
|
||
в production-сборке: `POST /dev/token`), `token.ts` (sessionStorage с запасом в памяти), `permissions.ts` (коды прав
|
||
для скрытия пунктов меню; доступ решает сервер). `/admin` (`layouts/Admin.tsx`, право `USERS_ADMIN`) — пользователи,
|
||
роли, матрица прав.
|
||
- **Сборка** — платформа (Python 3.11) запускает `api/run_server.py` (`SERVER__HOST/PORT`), ресурсы — `raisa_config.yml`;
|
||
SPA: `web/dist` → `api/web/`. `Dockerfile` (`python:3.11-slim`) в корне — для dev-compose и `make check-full`
|
||
(`docker build`; убран из `.gitignore` шаблона). CI в репозитории нет: перед PR — `make check-full`. Без Nexus:
|
||
`make venv` ставит зависимости без `raisa_*`, npm-пакеты `raisa-*` — `optionalDependencies`.
|
||
- **NATS без JetStream** (`NATS_ENABLED=true`): `core/nats.py` — подключение в фоне (supervisor повторяет попытки),
|
||
подписка queue group `arm-core` на `recognition.result`. Обмен — `services/recognition_exchange.py`: `publish_pending`
|
||
(outbox), `expire_deadlines` (вахтёр сроков), `handle_message` (inbox: ack — конверт `processing`, результат);
|
||
публикатор и вахтёр — фоновые задачи lifespan. Запрос без ack повторно публикуется (`ACK_TIMEOUT_SECONDS`=30 ×
|
||
`OUTBOX_MAX_REPUBLISH`=3, счётчик — `recognition_job.metrics.republish_count`) → `dlq`; дедуп — `request_msg_id` /
|
||
`result_msg_id`. Статусы задания в Срезе 0 `audit_event` не пишут.
|
||
- **Первичные документы (Срез 2c)** — `services/primary_files.py` (загрузка `upload_one`: дедуп по `(ЮЛ, sha256)` под
|
||
advisory-lock, SAVEPOINT на файл, `delete`), `services/file_status.py` (единственное место смены `stored_file.status`
|
||
по ТЕКУЩЕМУ заданию; `PROCESSED`/`SUPERSEDED` обменом не перезаписываются), `services/result_mapping.py` (цикл маппинга:
|
||
`pick_candidate` → проверка ключа результата (OQ-70) → чтение S3 вне транзакции → `map_file` в одной транзакции →
|
||
автозамена `FILE_REPLACE`), `services/documents.py` (карточки, `confirm`), `domain/osv_mapping.py` (чистые функции:
|
||
`normalize_account_code`, `build_entries`, `counterparty_check`, `document_status`), `repositories/counterparties.py`
|
||
(`CounterpartyIndex` — резолв контрагента без policy). **Фоновых циклов три** (запускаются в lifespan при
|
||
`NATS_ENABLED`; цикл маппинга — ещё и при подключённом S3 (storage)): публикатор outbox, вахтёр сроков и цикл маппинга
|
||
(`MAPPING_POLL_SECONDS`, `MAPPING_MAX_ATTEMPTS`); inbox результат не читает и документов не создаёт. **Порядок блокировок: задание → файл → документы** (inbox, вахтёр, публикатор, маппер,
|
||
`confirm`, `delete`; с Среза 4 далее — advisory-lock ГК → версии консолидации, см. «Консолидации») — иначе
|
||
взаимоблокировки; автозамена дополнительно берёт advisory-lock по ЮЛ. Аудит: `FILE_UPLOAD`,
|
||
`FILE_REPLACE` (системное, `user_id` пуст), `FILE_DELETE`, `DOCUMENT_CONFIRM`; смена статусов обмена и маппинга не аудируется
|
||
(OQ-55). `osv_entry` пишет только маппер, один раз на файл (очистка перед вставкой); `TOTAL` не хранится.
|
||
- **Консолидации (Срез 4)** — `api/endpoints/consolidations/` (periods, versions, locks, status, members, checklist, osv, export,
|
||
mapping), сервисы: `consolidations` (создание, список, карточка, периметр `PUT …/members`), `consolidation_guard`
|
||
(`lock_visible_version`/`lock_editable_version`: версия `FOR UPDATE` → видимость 404 → статус «в работе» → своя блокировка),
|
||
`consolidation_locks` (взять/перехватить/снять; без истечения, OQ-119), `consolidation_status` (`transition` + ручной
|
||
`POST …/status`), `consolidation_documents` (`fits`, `bind_confirmed`, `unbind_entity`, хуки `on_document_confirmed` /
|
||
`on_documents_superseded` / `on_documents_mapped`), `consolidated_osv` (единая сборка `_build`: страница, сводка, кандидаты,
|
||
экспорт), `counterparty_mapping` (ручное сопоставление, алиас, автоприменение), `osv_export` (openpyxl, лист «Пробелы»).
|
||
**Правила:** (1) статус версии меняет ТОЛЬКО `consolidation_status.transition`; версия к вызову уже под `FOR UPDATE` у
|
||
вызывающего (иначе параллельный переход или перехват читает устаревший статус); (2) **порядок блокировок: файл → документ →
|
||
advisory-lock ГК → версии (по возрастанию `id`)**; правка версии начинается с `consolidation_version FOR UPDATE`;
|
||
`lock_versions_for_entity` блокирует надмножество версий «в работе» (ЮЛ — член ИЛИ та же ГК), создание версии берёт
|
||
per-ГК `pg_advisory_xact_lock` (R-2); (3) `lock_document`/`lock_file_documents` — `FOR NO KEY UPDATE` (R-3: вставка
|
||
`consolidation_document` берёт `FOR KEY SHARE` на документ и взаимоблокировалась бы с `FOR UPDATE`); (4) сводная ОСВ,
|
||
чек-лист и признак контрагента — чистые функции, в БД не пишутся; единственное хранимое — ручное сопоставление
|
||
(`counterparty_id` + `counterparty_match_kind` + алиас); (5) ключ замены привязки — «тип, счёт, квартал, нарастающий»
|
||
(R-1); ОСВ за Q1 равнозначна нарастающей и квартальной (R-14, Q1-эквивалентность — только для ОСВ, ББ/ОФР за Q1 версиям Q2–Q4
|
||
не подходят, R-14a); (6) «строка версии» для ручного сопоставления/автоприменения — только строки свода (R-11); (7) 404 —
|
||
код `NOT_FOUND`, различие по тексту; правки — `ensure_version_editable` (`VERSION_NOT_EDITABLE`/`LOCK_REQUIRED`/
|
||
`LOCKED_BY_OTHER` без `locked_by_name`). Деньги в ответах — строки с двумя знаками. Аудит: `CONSOLIDATION_CREATE`,
|
||
`CONSOLIDATION_STATUS_CHANGE`, `CONSOLIDATION_LOCK`, `CONSOLIDATION_LOCK_TAKEOVER`, `MEMBERSHIP_CHANGE`,
|
||
`OSV_COUNTERPARTY_MAP`, `LEGAL_ENTITY_CHANGE`, `EXPORT`; `FILE_REPLACE.unbound_version_ids`. Контракт для фронта —
|
||
`../docs/dev/api-contract-slice-4.md`; решения — `openspec/changes/slice-4-consolidation/design.md`.
|
||
- **Аудит** — `services/audit.py`, единственная точка записи `audit_event` (append-only: только `record`, в текущей транзакции;
|
||
тест `test_audit_service.py` сканирует `src/` на UPDATE/DELETE по `AuditEvent`). В маршрутах — `RequestAudit`
|
||
(`get_request_audit`: пользователь, IP, user-agent).
|
||
- **Справочники и права (Срез 1)** — видимость только через `domain/policy.py` (`VisibilityScope`: ГО — всё, филиал — свои
|
||
ГК/ЮЛ/ГСК/ГСЗ и монозаёмщики), код роли в логике не читается; `domain/inn.py` — ИНН строкой (ЮЛ 10, ИП 12 цифр,
|
||
без контрольных разрядов, OQ-88); `domain/aliases.py` — `normalized_alias` по §3; `domain/directory_import.py` — импорт
|
||
ЕЦП (§13, зависимость `openpyxl` для xlsx; допущения `ASSUMPTION(OQ-87…91)`). Администрирование — `services/users_admin.py`
|
||
(`LAST_ADMIN` с блокировкой строки права). Порядок: маршрут (`require(code)`) → policy → сервис → аудит.
|
||
- **Аутентификация** — `core/auth.py` (проверка JWT), `src/api/dependencies/auth.py` (`current_user`, `require(code)`);
|
||
dev-эмитент (`POST /api/v1/dev/token`) существует только при `AUTH_DEV_ISSUER=true` **и** `DEBUG=true`.
|
||
Аудит входа — `services/session_audit.py` (`app.state.session_audit`): `LOGIN` (dev-токен; первый запрос после старта),
|
||
`LOGIN_FAILED` (`AuthenticationFailedError` 401/403 → обработчик пишет в отдельной транзакции `session_transaction`);
|
||
нет заголовка `Authorization` или схема не `Bearer`, 403 «недостаточно прав» — не события входа.
|
||
- **Seed** — `python -m scripts.seed [--dev]` (`make seed|seed-dev`), идемпотентен (матрица `role_permission` — целиком только в
|
||
пустую таблицу, в заполненную — лишь пары впервые созданных ролей/прав); `--dev` — только для разработки (CLI отказывает с кодом 2 при `DEBUG=false`).
|
||
**Сверка ORM↔DBML** — `scripts/check_dbml_vs_orm.py` (`make dbml-check`).
|
||
|
||
## Зависимости от соседей
|
||
- Контракты обмена: `../docs/contracts/recognition-request.schema.json`, `recognition-result-message.schema.json`,
|
||
`recognition-result.schema.json`; в репозитории — байт-идентичные копии в `api/contracts/` (сверяются тестами,
|
||
`tests/test_contract.py`). `ex-payload-v1.schema.json` (payload ЕХ) появится в Срезе 7 (`../docs/context/09-delivery-plan.md`).
|
||
- Фикстуры: `../fixtures/` (env `ARM_KA_FIXTURES`); при отсутствии тесты с маркером `fixtures` пропускаются с явной причиной (skip reason).
|
||
- NATS subjects и схема ключей S3 — `../docs/context/03-architecture.md`.
|
||
|
||
## Правила репозитория
|
||
- ORM 1:1 с `../docs/model/data-model-v5.16.dbml` (текущая версия; миграции `0001_dbml_v5_13` + `0002_dbml_v5_14` + `0003_dbml_v5_15` + `0004_dbml_v5_16`); `api/scripts/check_dbml_vs_orm.py` (`make dbml-check`, входит в `check-full`) — обязательная проверка.
|
||
- Алгоритмы — только из `../docs/context/05-domain-rules.md`; номер § — в docstring.
|
||
- Вычисляемые объекты консолидации — `api/src/domain/`, без ORM-таблиц.
|
||
- Фронт: пользователь слаб во фронте — для TS/React изменений делай ревью сам и объясняй решения в PR.
|
||
- OpenSpec: `openspec/` здесь; сквозные изменения — change в `../docs`.
|