# Tasks: Срез 1 — arm-core Каждая задача ≤ половины дня, начинается с тестов/фикстур, заканчивается зелёным `make check` (интеграционные — `make test-integration`). Пути — от корня репозитория. Допущения в коде помечаются `# ASSUMPTION(OQ-nn)`. ## 1. Модель v5.14 и инфраструктура запросов - [x] 1.1 **ORM и миграция 0002.** `api/src/models/directories.py`: `RelatedGroupMember.kind` (enum `related_group_kind`, NOT NULL), индекс `uq_related_group_member_entity_kind_current (legal_entity_id, kind) WHERE is_current` вместо `…_entity_current`; `enums.py`: `AuditAction.LEGAL_ENTITY_CHANGE` (после `GROUP_CHANGE`), `USER_CHANGE` (после `USER_ROLE_CHANGE`); `alembic/versions/0002_dbml_v5_14.py` (явный DDL: колонка с заполнением из `related_group`, индексы, `ALTER TYPE … ADD VALUE`, последовательности `company_group_code_seq`, `related_group_code_seq`; downgrade пересоздаёт enum); `DBML_VERSION = "5.14"`, `Makefile DBML`, `dbml_parity.DBML_FILE_NAME`, `tests/test_migrations.HEAD`, `test_enums`, `test_check_dbml_script` (фиктивная версия 5.15), `_check_related_group_member_uniqueness`. Проверка: `make dbml-check` пуст; `make migration-cycle` зелёный; тест «ЮЛ в ГСК и ГСЗ одновременно, вторая ГСК — IntegrityError». - [x] 1.2 **Единица работы, аудит, исключения, пагинация.** *(Фактически: R-1/R-6, design #12/#17 — `get_async_session` коммитит, зависимость `SessionDep` (`dependencies/db.py`), `get_request_audit`; `get_uow_session`/`get_audit_service` не создавались.)* `api/src/api/dependencies/db.py:get_uow_session` (commit при успехе, rollback при исключении); `dependencies/audit.py:get_audit_service` (ip/user-agent/user_id); `common/exceptions.py`: `NotFoundError`, `ConflictError(code, message, extra)`, `DomainValidationError(code, message)` + обработчики 404/409/422 в `core/exception_handlers.py` (формат `{"detail": {"code", "message", …}}`); `schemas/common.py`: `Page[T]` `{items,total,limit,offset}`, параметры `limit ≤ 200`. Проверка: unit-тесты обработчиков; интеграционный тест через `scratch_db_url`: эндпоинт-заглушка с `get_uow_session` коммитит при 2xx и откатывает при исключении. - [x] 1.3 **Фабрики и клиент тестов.** `tests/factories.py`: `make_regional_office`, `make_company_group`, `make_legal_entity`, `make_related_group`, `make_membership`, `make_user(email, office, roles)`; `tests/conftest.py`: `seeded`, `app`, `auth_client(email) -> httpx.AsyncClient` с dev-токеном. Переписать `test_me_and_dev_token.py` на новые фикстуры. Проверка: существующие тесты зелёные. ## 2. Policy и справочники - [x] 2.1 **Policy.** *(Фактические имена: `company_groups_visible`, `legal_entities_visible`, `office_for_create`, `scope_for`/`scope_of`; R-7, design #18.)* `api/src/domain/policy.py` (strict mypy): `VisibilityScope`, `scope_for(principal)`, `apply_scope_company_groups(stmt, scope)`, `apply_scope_legal_entities(stmt, scope)`, `assert_office_allowed(scope, office_id)`; unit-тесты без БД на все ветки (ГО, филиал, монозаёмщик, чужой филиал → `OFFICE_NOT_ALLOWED`). - [x] 2.2 **Филиалы и ГК.** `repositories/directories.py` (запросы с scope), `services/company_groups.py` (создание с `code` из последовательности, изменение со снимком `company_group_history`, архив, аудит `GROUP_CHANGE`/`ASSIGNMENT_CHANGE`), `schemas/directories.py`, `api/endpoints/directories/regional_offices.py`, `company_groups.py`. Интеграционные тесты: сценарии спеки `directories-api` (ГК) и `directory-policy` (видимость филиала/ГО, 404 вне scope, создание из филиала). - [x] 2.3 **ЮЛ/ИП: CRUD, перевод, вывод, история.** `domain/inn.py` (длина 10/12 по типу; `ASSUMPTION(OQ-88)`), `services/legal_entities.py` (создание, изменение со снимком `legal_entity_history(UPDATE)`, `move` (MOVE_TO_GROUP, закрытие членств), `withdraw` (CLOSE/REMOVE_FROM_GROUP), `effective_date ≥ today`), эндпоинты `legal_entities.py`, аудит `LEGAL_ENTITY_CHANGE` + `MEMBERSHIP_CHANGE`. Тесты: сценарии спеки (ИНН, монозаёмщик, перевод закрывает членства, закрытие, откат). - [x] 2.4 **Алиасы.** `domain/aliases.py:normalize_alias` по §3 (unit-тесты на кавычки, ОПФ, пробелы, ё), сервис + эндпоинты `POST/DELETE …/aliases`, конфликты `ALIAS_EXISTS` / `ALIAS_TAKEN` (с владельцем). Тесты: сценарий «Нормализация». - [x] 2.5 **ГСК/ГСЗ и состав.** `services/related_groups.py` (создание с `code` `GSK-/GSZ-`, изменение, удаление пустой `ASSUMPTION(OQ-34)`, добавление члена: та же ГК, `kind` группы, одна группа каждого вида → `MEMBER_OF_OTHER_GROUP`, повтор → `MEMBER_EXISTS`; исключение — SCD2), эндпоинты `related_groups.py`, аудит `MEMBERSHIP_CHANGE`. Тесты: сценарии спеки (ГСК+ГСЗ одновременно, вторая ГСК, повторное включение, непустая группа). ## 3. Импорт из ЕЦП CRM - [x] 3.1 **Чтение файла и ядро решений.** `openpyxl` в `api/requirements.txt` (пин); `domain/directory_import.py`: `read_rows(file) -> (headers, rows)` для xlsx/csv, нормализация заголовков (`MISSING_COLUMNS`), `ImportCode` (StrEnum = §13.2), `plan_import(rows, snapshot, on_existing) -> ImportPlan(operations, report)` — чистая функция; `ASSUMPTION(OQ-87..91)` в коде. Unit-тесты без БД: синтетический csv, покрывающий каждый код §13.2 ровно один раз; xlsx-фикстура `../fixtures/directories/ecp-export-russkiy-biskvit.xlsx` → ожидаемый план (1 ГК, 1 РФ, 18 ЮЛ/ИП, 1 ГСК с 2 членами, коды ФЛ/дубля по фактическому порядку строк). - [ ] 3.2 **Сервис и эндпоинт.** `services/directory_import.py` (снимок справочника одним запросом на таблицу, применение плана, `dry_run` → rollback, `audit_event(DIRECTORY_IMPORT, after_value = отчёт)`, файл в S3 `uploads/imports/{audit_event_id}/{name}` + `s3_object`), `POST /api/v1/directories/import` (multipart, `IMPORT_MAX_BYTES` = 20 МБ → 413, `DIRECTORY_ADMIN`), `schemas/directory_import.py`. Интеграционные тесты: сценарии спеки `directory-import` (срез РБ, идемпотентность, `update`, `LE_IN_OTHER_GROUP`, dry_run без следов, `MISSING_COLUMNS`); S3 — через `app.state.storage` с заглушкой в тестах. - [ ] 3.3 **Полная выгрузка (fixtures-тест).** Тест с маркером `fixtures`: `ARM_KA_DOCS/source/biq/ecp-export-lagal-entity.xlsx`, `dry_run`, `rows = 8400`, `RF_CREATED = 53`, присутствие кодов, время ≤ 60 с; пропуск с причиной, если файла нет. Профилирование: при превышении — пакетные вставки (`insert().values([...])`). ## 4. Администрирование - [ ] 4.1 **Пользователи и роли (API).** `services/users_admin.py` (создание с проверкой `lower(email)`, изменение, деактивация, `PUT roles` с diff и `LAST_ADMIN`), `repositories/users.py` (+ запросы списка), эндпоинты `api/endpoints/admin/users.py`, `roles.py` (`GET roles`, `GET permissions`, `PUT roles/{code}/permissions` с `LAST_ADMIN` и защитой `is_system`), аудит `USER_CHANGE` / `USER_ROLE_CHANGE` / `ROLE_PERMISSION_CHANGE`. Тесты: сценарии спеки `users-admin` (создание с ролью → `/me`, email в другом регистре, замена набора ролей, последний администратор, матрица → `/me` видит новое право). - [ ] 4.2 **Фронт: API-клиент и dev-вход.** `web/src/api/client.ts` (`apiFetch(path, init)`: базовый URL, Bearer из `sessionStorage`, 401 → очистка токена и переход на вход, ошибка `{detail}` → `Error` с текстом), `web/src/api/types.ts` (DTO `/me`, `/admin/*`), `web/src/auth/DevLogin.tsx` (поле email → `POST /dev/token`, только при `process.env.NODE_ENV !== 'production'`), `web/src/auth/useMe.ts` (загрузка `/me`, права). Проверка: `make web-check`; ручной сценарий в README. - [ ] 4.3 **Фронт: страница администратора.** `web/src/layouts/Admin.tsx`: antd `Table` пользователей (email, ФИО, филиал, роли — `Select mode="multiple"` с сохранением по `PUT roles`, переключатель `is_active`), `Modal` создания (email, ФИО, филиал из `/directories/regional-offices`, роли), таблица «роль × право» с `Checkbox` и кнопкой «Сохранить» (`PUT roles/{code}/permissions`), `message.error` на ошибки API, экран «нет доступа» без `USERS_ADMIN`; маршрут `/admin` в `App.tsx`, пункт «Система → Пользователи и роли» в `Sidebar`. Проверка: `make web-check`; самостоятельное ревью TS/React с объяснением решений в PR. ## 5. Завершение - [ ] 5.1 **Документы и проверка.** `README.md` (разделы: API справочников, импорт из ЕЦП — пошагово с `dry_run`, администрирование и экран `/admin`, ограничение «повторный seed не отзывает права»), `CLAUDE.md` (DBML v5.14, `domain/policy.py`, `get_uow_session`, формат ошибок, `openpyxl`), `api/.env.example` (`IMPORT_MAX_BYTES`), спеки `openspec/specs/*` после merge (archive); `make check-full` зелёный, вывод — в описание PR. Проверка: OpenAPI (`/docs`) содержит все маршруты спек.