Срез 1: справочники (API), импорт из ЕЦП CRM, policy видимости, администрирование #6

Merged
aep merged 51 commits from feat/5-slice-1-directories into main 2026-10-05 10:00:05 +03:00
Owner

Closes #5. Спека и реализация Среза 1 по решениям владельца 04.10.2026 (без UI справочника, минимальный экран администратора).

Что в PR

  • DBML v5.14: related_group_member.kind + partial unique (legal_entity_id, kind) WHERE is_current, audit_action + LEGAL_ENTITY_CHANGE/USER_CHANGE; миграция 0002_dbml_v5_14 (пересоздаёт related_group_member ради порядка колонок DBML; downgrade пересоздаёт enum); make dbml-check на v5.14 — пусто.
  • Инфраструктура запросов: одна транзакция на запрос (SessionDep, commit до ответа), RequestAudit, доменные ошибки {"detail": {"code", "message"}} 404/409/422, маппинг гонок уникальных ограничений в те же 409-коды, Page.
  • Policy src/domain/policy.py (§10.1): ГО видит всё, филиал — свои ГК/ЮЛ/ГСК и монозаёмщиков; чужое → 404.
  • API справочников /api/v1/directories/*: филиалы, ГК (история SCD4, архив), ЮЛ/ИП (ИНН 10/12, перевод, вывод из состава, история), алиасы (§3), ГСК/ГСЗ и состав (SCD2, одна группа каждого вида на ЮЛ).
  • Импорт из ЕЦП CRM POST /directories/import (§13): xlsx/csv, dry_run, on_existing=skip|update, коды отчёта §13.2, audit_event(DIRECTORY_IMPORT), файл в S3; полная выгрузка 8400 строк — dry_run 1,14 с.
  • Администрирование /api/v1/admin/*: пользователи, роли, матрица прав, LAST_ADMIN; фронт: API-клиент с Bearer, dev-вход, страница /admin.
  • README, CLAUDE.md, тест маршрутов OpenAPI, ledger SDD docs/superpowers/plans/2026-10-04-slice-1-directories-ledger.md.

make check-full (HEAD 7f891dd, EXIT 0)

lint: ruff/format/mypy чисто (86 файлов) · unit: 776 passed · dbml-check: пусто · check-infra: ок · integration: 277 passed, 0 skipped (fixtures-тесты выполнены: 6) · docker-build: ок · web-check: biome/tsc/rsbuild чисто.

Финальное ревью ветки (opus): Critical — нет; Important I-1 закрыт рулингом R-23; Minor M-1…M-8: 6 исправлено, M-3/M-5 отложены.

Rulings I made (design.md №12–35)

  • R-1: транзакция в get_async_session, не отдельная зависимость. R-6: SessionDep со scope="function" — commit до ответа (FastAPI 0.135 закрывает yield-зависимости после ответа).
  • R-5: миграция 0002 пересоздаёт related_group_member (порядок колонок как в DBML). Если платформа выдаёт GRANT вручную — повторить после миграции.
  • R-7: legal_entities_visible через подзапрос IN; ГК без филиала видна только ГО.
  • R-8/R-9: нормализация crm_id/name; office_for_create и для PATCH; новые 409 ALREADY_CLOSED, NOT_IN_COMPANY_GROUP, LEGAL_ENTITY_CLOSED; «сегодня» для effective_date — по UTC; move/withdraw применяю��ся сразу, effective_date только в истории.
  • R-10: ALIAS_TAKEN отдаёт только legal_entity_id владельца; DELETE алиаса требует DIRECTORY_DELETE (по seed — только ADMIN).
  • R-11/R-12: список ГСК/ГСЗ ГК — массив, не Page; DELETE члена — DIRECTORY_EDIT.
  • R-13: один RegionalOfficeOut (иерархия RegionalOfficeBrief → RegionalOfficeOut).
  • R-15: на фикстуре РБ 20 ЮЛ/ИП (18 ЮЛ + 2 ИП), DUPLICATE_IN_FILE не возникает; на полной выгрузке groups_created=344, RF_CREATED=53.
  • R-16/R-17: ГСК создаётся по флагу буквально по §13.2; импорт при update пишет собственные GROUP_CHANGE/LEGAL_ENTITY_CHANGE; s3_object + flush до put_object; архивные ГК в снимке, не разархивируются.
  • R-19: OFFICE_NOT_FOUND — 422; FOR NO KEY UPDATE на строке права USERS_ADMIN.
  • R-20/R-21: dev-вход только при NODE_ENV !== 'production'; токен в sessionStorage; /admin без Redux, PUT матрицы последовательно, показываются первые 200 пользователей.
  • R-22: SQLSTATE 23505 → кодированный 409 по реестру src/common/constraint_conflicts.py (7 ограничений).
  • R-23: «не снимать с себя USERS_ADMIN» — системная трактовка: 409 LAST_ADMIN только пока ты единственный активный обладатель права; спека приведена к коду.
  • R-24: смена имени и филиала ГК одним PATCH — одно событие ASSIGNMENT_CHANGE.

Решения человека

  1. DIRECTORY_DELETE по seed только у ADMIN → удалять алиасы и ГСК/ГСЗ может только он. Расширять?
  2. effective_date: граница «сегодня» по UTC или по МСК?
  3. R-23: системная трактовка LAST_ADMIN устраивает?
  4. R-5: GRANT платформы на related_group_member после миграции 0002.
  5. Повторный seed возвращает права, снятые администратором (M-8 Среза 0).

Deferred (в follow-up issue при закрытии)

Фаза 1: T1.1–T1.5, T2.1, T2.2, T2.4, T3.2–T3.4, T4.2, T4.3, T5.2–T5.7, T6.2, T6.3, T6.4/T12.6, T6.6, T8.1, T8.4.
Фаза 2: T9.3, T9.5–T9.11, T10.5, T10.7–T10.9, T11.1, T11.2, T11.4, T11.5, T12.2, T12.5, T12.7 (остаток), T13.4, T14.7, O-7, O-9, O-10, O-11, M-3, M-4 (остаток GET /admin/regional-offices), M-5, зачистка S3 при сбое commit.
Полные формулировки — в ledger.

Зависит от: RSHB/arm-ka-docs#7 (DBML v5.14, §13, §10.1), RSHB/arm-ka-fixtures#3.

🤖 Generated with Claude Code

https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2

Closes #5. Спека и реализация Среза 1 по решениям владельца 04.10.2026 (без UI справочника, минимальный экран администратора). ## Что в PR - **DBML v5.14**: `related_group_member.kind` + partial unique `(legal_entity_id, kind) WHERE is_current`, `audit_action` + `LEGAL_ENTITY_CHANGE`/`USER_CHANGE`; миграция `0002_dbml_v5_14` (пересоздаёт `related_group_member` ради порядка колонок DBML; downgrade пересоздаёт enum); `make dbml-check` на v5.14 — пусто. - **Инфраструктура запросов**: одна транзакция на запрос (`SessionDep`, commit до ответа), `RequestAudit`, доменные ошибки `{"detail": {"code", "message"}}` 404/409/422, маппинг гонок уникальных ограничений в те же 409-коды, `Page`. - **Policy** `src/domain/policy.py` (§10.1): ГО видит всё, филиал — свои ГК/ЮЛ/ГСК и монозаёмщиков; чужое → 404. - **API справочников** `/api/v1/directories/*`: филиалы, ГК (история SCD4, архив), ЮЛ/ИП (ИНН 10/12, перевод, вывод из состава, история), алиасы (§3), ГСК/ГСЗ и состав (SCD2, одна группа каждого вида на ЮЛ). - **Импорт из ЕЦП CRM** `POST /directories/import` (§13): xlsx/csv, `dry_run`, `on_existing=skip|update`, коды отчёта §13.2, `audit_event(DIRECTORY_IMPORT)`, файл в S3; полная выгрузка 8400 строк — dry_run 1,14 с. - **Администрирование** `/api/v1/admin/*`: пользователи, роли, матрица прав, `LAST_ADMIN`; **фронт**: API-клиент с Bearer, dev-вход, страница `/admin`. - README, CLAUDE.md, тест маршрутов OpenAPI, ledger SDD `docs/superpowers/plans/2026-10-04-slice-1-directories-ledger.md`. ## make check-full (HEAD 7f891dd, EXIT 0) lint: ruff/format/mypy чисто (86 файлов) · unit: 776 passed · dbml-check: пусто · check-infra: ок · integration: 277 passed, 0 skipped (fixtures-тесты выполнены: 6) · docker-build: ок · web-check: biome/tsc/rsbuild чисто. Финальное ревью ветки (opus): Critical — нет; Important I-1 закрыт рулингом R-23; Minor M-1…M-8: 6 исправлено, M-3/M-5 отложены. ## Rulings I made (design.md №12–35) - R-1: транзакция в `get_async_session`, не отдельная зависимость. R-6: `SessionDep` со `scope="function"` — commit до ответа (FastAPI 0.135 закрывает yield-зависимости после ответа). - R-5: миграция 0002 пересоздаёт `related_group_member` (порядок колонок как в DBML). **Если платформа выдаёт GRANT вручную — повторить после миграции.** - R-7: `legal_entities_visible` через подзапрос `IN`; ГК без филиала видна только ГО. - R-8/R-9: нормализация `crm_id`/`name`; `office_for_create` и для PATCH; новые 409 `ALREADY_CLOSED`, `NOT_IN_COMPANY_GROUP`, `LEGAL_ENTITY_CLOSED`; «сегодня» для `effective_date` — по UTC; `move`/`withdraw` применяю��ся сразу, `effective_date` только в истории. - R-10: `ALIAS_TAKEN` отдаёт только `legal_entity_id` владельца; `DELETE` алиаса требует `DIRECTORY_DELETE` (по seed — только ADMIN). - R-11/R-12: список ГСК/ГСЗ ГК — массив, не `Page`; `DELETE` члена — `DIRECTORY_EDIT`. - R-13: один `RegionalOfficeOut` (иерархия `RegionalOfficeBrief` → `RegionalOfficeOut`). - R-15: на фикстуре РБ 20 ЮЛ/ИП (18 ЮЛ + 2 ИП), `DUPLICATE_IN_FILE` не возникает; на полной выгрузке `groups_created=344`, `RF_CREATED=53`. - R-16/R-17: ГСК создаётся по флагу буквально по §13.2; импорт при `update` пишет собственные `GROUP_CHANGE`/`LEGAL_ENTITY_CHANGE`; `s3_object` + flush до `put_object`; архивные ГК в снимке, не разархивируются. - R-19: `OFFICE_NOT_FOUND` — 422; `FOR NO KEY UPDATE` на строке права `USERS_ADMIN`. - R-20/R-21: dev-вход только при `NODE_ENV !== 'production'`; токен в `sessionStorage`; `/admin` без Redux, PUT матрицы последовательно, показываются первые 200 пользователей. - R-22: SQLSTATE 23505 → кодированный 409 по реестру `src/common/constraint_conflicts.py` (7 ограничений). - R-23: «не снимать с себя `USERS_ADMIN`» — системная трактовка: 409 `LAST_ADMIN` только пока ты единственный активный обладатель права; спека приведена к коду. - R-24: смена имени и филиала ГК одним PATCH — одно событие `ASSIGNMENT_CHANGE`. ## Решения человека 1. `DIRECTORY_DELETE` по seed только у ADMIN → удалять алиасы и ГСК/ГСЗ может только он. Расширять? 2. `effective_date`: граница «сегодня» по UTC или по МСК? 3. R-23: системная трактовка `LAST_ADMIN` устраивает? 4. R-5: GRANT платформы на `related_group_member` после миграции 0002. 5. Повторный seed возвращает права, снятые администратором (M-8 Среза 0). ## Deferred (в follow-up issue при закрытии) Фаза 1: T1.1–T1.5, T2.1, T2.2, T2.4, T3.2–T3.4, T4.2, T4.3, T5.2–T5.7, T6.2, T6.3, T6.4/T12.6, T6.6, T8.1, T8.4. Фаза 2: T9.3, T9.5–T9.11, T10.5, T10.7–T10.9, T11.1, T11.2, T11.4, T11.5, T12.2, T12.5, T12.7 (остаток), T13.4, T14.7, O-7, O-9, O-10, O-11, M-3, M-4 (остаток `GET /admin/regional-offices`), M-5, зачистка S3 при сбое commit. Полные формулировки — в ledger. Зависит от: RSHB/arm-ka-docs#7 (DBML v5.14, §13, §10.1), RSHB/arm-ka-fixtures#3. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
aep added 1 commit 2026-10-04 19:24:00 +03:00
proposal, design, tasks и дельты спек: directories-api, directory-import, directory-policy, users-admin (новые),
data-model (v5.14: related_group_member.kind, audit_action), audit-trail (события справочников и администрирования).

Issue: RSHB/arm-core#5

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
aep added 1 commit 2026-10-04 19:38:13 +03:00
docs/superpowers/plans/2026-10-04-slice-1-directories-phase{1,2}.md — 15 задач из tasks.md change'а
slice-1-directories: модель v5.14 и инфраструктура запросов, policy, API справочников (фаза 1); импорт ЕЦП,
администрирование, экран /admin, документы и check-full (фаза 2).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
aep added 22 commits 2026-10-04 21:19:10 +03:00
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- RelatedGroupMember.kind (related_group_kind, NOT NULL) между legal_entity_id и valid_from, как в DBML;
  частичный уникальный индекс uq_related_group_member_entity_kind_current (legal_entity_id, kind) WHERE is_current —
  ЮЛ одновременно в одной ГСК и одной ГСЗ.
- AuditAction: LEGAL_ENTITY_CHANGE (после GROUP_CHANGE), USER_CHANGE (после USER_ROLE_CHANGE).
- Последовательности company_group_code_seq / related_group_code_seq в metadata (design D-5).
- Миграция 0002_dbml_v5_14: related_group_member пересоздаётся с переносом строк (kind из группы), чтобы порядок
  колонок совпал с DBML и create_all; ALTER TYPE ADD VALUE ×2; downgrade пересоздаёт audit_action v5.13.
- DBML_VERSION = 5.14, пути к data-model-v5.14.dbml (Makefile, dbml_parity), тесты: откат на шаг, перенос строк,
  уникальность gsk+gsz, офлайн-SQL до head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- get_async_session: одна транзакция на запрос (commit при успехе, rollback при исключении), R-1;
  объявляется только через SessionDep (scope="function") — commit до отправки ответа, одна сессия
  на все зависимости запроса; current_user и dev-эмитент переведены на SessionDep.
- DomainError/NotFoundError/ConflictError/DomainValidationError + обработчик
  {"detail": {"code", "message", ...}}; ответы HTTPException и ошибок БД не меняются.
- RequestAudit/get_request_audit: AuditService.record с user_id, ip_address, user_agent запроса.
- schemas/common.py: Page[T], PageParams (limit 1..200, по умолчанию 50; offset >= 0), ErrorDetail.
- tests/test_uow_and_errors.py: формат ошибок, пагинация, аудит, scope сессии во всех маршрутах,
  настоящий commit/rollback на временной БД, commit до http.response.start, ошибка commit -> 409.
- План: Task 2 отмечена; tasks.md: 1.2 отмечен.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Фабрики ГК/ЮЛ/ГСК/членства/пользователя (flush, без commit), фикстуры seeded/app/auth_client
в conftest (override сессии через begin_nested), test_me_and_dev_token переведён на общие
фикстуры (сочетания флагов остались на локальном _build_app), test_factories.py проверяет фабрики.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Первый модуль src/domain/ (чистые функции, без I/O, mypy strict): VisibilityScope,
scope_for/scope_of, SQL-фильтры company_groups_visible/legal_entities_visible,
проверки company_group_is_visible/legal_entity_is_visible, office_for_create
(чужой филиал → 422 OFFICE_NOT_ALLOWED). Код роли не читается; business_segment
не фильтрует (ASSUMPTION(OQ-86)). Принадлежность ЮЛ ГК филиала — подзапросом IN,
а не JOIN (число строк не меняется, нет конфликта с JOIN вызывающего).

Тесты: unit без БД (компиляция Select под PostgreSQL, все ветки) + один
integration-тест семантики фильтров и их согласованности с проверками объектов.
Отмечены шаги Task 4 в плане и 2.1 в tasks.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Закрывает T3.1: предупреждение «Включён dev-эмитент…» из src.core.auth.logger
подавляется через monkeypatch в фикстуре app. Локальный autouse в
test_me_and_dev_token.py оставлен: его _build_app вызывает build_verifier без
фикстуры app.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Репозиторий, сервис и эндпоинты /api/v1/directories/{regional-offices,company-groups}:
код GK-NNNNNN из последовательности, снимки в company_group_history, no-op PATCH
без истории и аудита, поиск q по name/crm_id/code с экранированием LIKE, видимость
по филиалу (чужая ГК = 404 как несуществующая), аудит GROUP_CHANGE/ASSIGNMENT_CHANGE.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- domain/inn.py: нормализация и проверка ИНН (ЮЛ — 10, ИП — 12 цифр 0–9), ASSUMPTION(OQ-88)
- репозиторий: список ЮЛ с видимостью и фильтрами (company_group_id, outside_group, inn точно, q по name/inn),
  карточка, история (по убыванию valid_from), текущие членства, close_memberships (SCD2), алиасы
- LegalEntityService: create (INN_EXISTS, CRM_ID_EXISTS, ГК через scope → 404, филиал по умолчанию — филиал ГК),
  update (no-op без истории; снимок UPDATE), move (MOVE_TO_GROUP, закрытие членств, MEMBERSHIP_CHANGE),
  withdraw (CLOSE / REMOVE_FROM_GROUP), effective_date ≥ сегодня — ASSUMPTION(OQ-21)
- эндпоинты /directories/legal-entities: GET/POST, GET/PATCH /{id}, /{id}/move, /{id}/withdraw, /{id}/history
- тесты: unit ИНН, интеграционные сценарии спеки, Review Focus 1–3, откат мутации

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
`office_for_create` применяется к `regional_office_id` только при фактической смене филиала: тело с тем же
значением — no-op (Review Focus 1), правка `name` в том же запросе проходит. Тест на ЮЛ в ГК своего филиала
с чужим `regional_office_id`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
normalize_alias (lower, ё→е, без пунктуации и ОПФ), POST/DELETE
/directories/legal-entities/{id}/aliases, конфликты ALIAS_EXISTS /
ALIAS_TAKEN (с legal_entity_id владельца), EMPTY_ALIAS, аудит
LEGAL_ENTITY_CHANGE по legal_entity_alias.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Создание с кодом GSK-/GSZ-NNNNNN, изменение name/created_date, удаление только пустой группы
(ASSUMPTION(OQ-34), закрытые строки состава удаляются вместе с группой), добавление члена
(та же ГК, kind группы, MEMBER_OF_OTHER_GROUP / MEMBER_EXISTS), исключение SCD2. Видимость
группы — через явный JOIN ГК и company_groups_visible (T4.1). Аудит MEMBERSHIP_CHANGE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
aep added 27 commits 2026-10-05 01:42:09 +03:00
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- domain/directory_import.py: read_rows (xlsx первый лист / csv UTF-8 `;`/`,`,
  заголовки по нормализованному тексту, MISSING_COLUMNS / UNSUPPORTED_FILE),
  plan_import — чистая функция: операции плана и отчёт с кодами §13.2,
  ASSUMPTION(OQ-87..91); ИНН — через domain/inn.py.
- openpyxl==3.1.5 (+ et_xmlfile==2.0.0) в requirements.txt,
  types-openpyxl в requirements-dev.txt.
- Тесты без БД: нормализация, чтение csv/xlsx (Review Focus 1–3), каждый код
  §13.2, skip/update, фикстура «Русский Бисквит» (ФЛ в строке 12 → UNSUPPORTED_TYPE).
- План фазы 2: Task 9 отмечена; tasks.md 3.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- T9.1: ошибки разбора xlsx (XML, ссылки ячеек, книга без листов, zlib) и при
  ленивой итерации листа → UNSUPPORTED_FILE «файл xlsx не читается».
- T9.2: строки читаются лениво; MAX_IMPORT_ROWS = 100 000 сырых строк (с пустыми)
  и MAX_UNCOMPRESSED_BYTES = 200 MiB распакованного zip → UNSUPPORTED_FILE.
- T9.4: номер строки csv — строка файла, с которой начинается запись.
- Тесты: перепакованные xlsx с повреждёнными частями, r="2000000", zip-бомба,
  предел строк csv, многострочное поле csv.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Логика изменения со снимком истории и аудитом вынесена из CompanyGroupService.update и
LegalEntityService.update в функции модуля, чтобы импорт ЕЦП (Task 10) применял UpdateGroup/
UpdateEntity той же функцией. Снимок и «новая версия» ЮЛ — snapshot_entity/touch_entity
(используются также в move/withdraw). Поведение PATCH не меняется.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
POST /api/v1/directories/import (DIRECTORY_ADMIN, multipart: file, dry_run=true, on_existing=skip):
снимок справочника тремя запросами (load_snapshot), план ядра §13.2, применение пакетами по типу
операции; UpdateGroup/UpdateEntity — apply_group_update/apply_entity_update (R-14) с description
«импорт ЕЦП». dry_run — явный session.rollback(), без аудита и S3. Боевой прогон —
audit_event(DIRECTORY_IMPORT, after_value = отчёт) и файл uploads/imports/{event_id}/{имя} +
s3_object(uploads). IMPORT_MAX_BYTES (20 МБ) → 413; ошибки файла → 422 MISSING_COLUMNS/UNSUPPORTED_FILE.
Тест dry_run — на отдельной БД с сессией запроса как в проде (SAVEPOINT-обёртка db_session
при rollback снимает и seed теста).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- T10.6: committed_sessionmaker/committed_app/committed_client/committed_storage и FakeStorage — в conftest.py,
  приложение собирается общей _build_application (как и app).
- T10.1: dry_run не пишет в S3 (objects == {}), боевой прогон — ровно один объект.
- T10.2: dry_run на реальном xlsx РБ (21 строка, таблицы не изменились).
- T10.3: dry_run с on_existing=update не оставляет истории/аудита, имена прежние.
- T10.4: safe_filename отбрасывает управляющие символы и ограничивает длину (200) с сохранением расширения.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
RegionalOfficeOut больше не дублируется в schemas/me.py: в schemas/directories.py
определены RegionalOfficeBrief (id, name, is_head_office — форма /me Среза 0) и
RegionalOfficeOut(RegionalOfficeBrief) с is_active. /me импортирует RegionalOfficeBrief,
тело ответа /me не меняется; в OpenAPI нет двух одноимённых компонентов.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
/api/v1/admin/* под правом USERS_ADMIN (403 на всё без него): список пользователей
(поиск по email/ФИО, is_active, Page), создание (email уникален без учёта регистра →
409 EMAIL_EXISTS, роли — коды активных ролей → 422 UNKNOWN_ROLE), PATCH полей и
деактивация, PUT ролей (замена набора с diff), GET ролей с правами, GET справочника
прав (16 кодов), PUT прав роли (422 UNKNOWN_PERMISSION, granted_by/granted_at).
Аудит USER_CHANGE / USER_ROLE_CHANGE (USERS) и ROLE_PERMISSION_CHANGE (ACCESS);
повтор того же состояния аудит не пишет.

LAST_ADMIN: деактивация (в том числе себя), замена ролей и снятие USERS_ADMIN с роли
запрещены, если у активных пользователей через активные роли не останется USERS_ADMIN.
Гонки: строка пользователя/роли и строка права USERS_ADMIN блокируются FOR NO KEY
UPDATE (FOR UPDATE давал deadlock на FK-проверке вставки audit_event); тест двух
параллельных транзакций с барьером.

Задача плана Task 12, tasks.md 4.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Токен хранится в sessionStorage (design D-9; без Redux): живёт до закрытия вкладки, все обращения к хранилищу в try/catch с запасным токеном в памяти.
apiFetch подставляет Bearer, на 401 сбрасывает токен и шлёт событие armka:unauthorized, ошибки разбирает по трём формам detail (строка / {code,message} / массив 422).
useMe не ходит в /me без токена и отбрасывает устаревшие ответы (флаг cancelled), AuthGate показывает вход, пока профиля нет.
Форма DevLogin вырезается из production-сборки по литералу process.env.NODE_ENV; через Config это не работает: process.env там заменён объектом REACT_APP_*.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
apiFetch оборачивает сбой сети в ApiError(0, NETWORK) и не-JSON тело 2xx в ApiError(BAD_RESPONSE); AbortError пробрасывается как есть, пустое тело даёт undefined.
В production-ветке входа показываются ошибка загрузки профиля и кнопка «Повторить», форма dev-входа по-прежнему вырезается из бандла литералом process.env.NODE_ENV.
Мелочи: «Сессия истекла» при 401, trim и whitespace-правило для email, центрирование по 100svh, union-типы прав, срез хвостовых слэшей в API_BASE.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Маршрут /admin (вкладки «Пользователи» и «Матрица прав») и пункт «Система → Пользователи и роли» в меню;
без USERS_ADMIN в /me — экран «Нет доступа», таблицы не монтируются и запросов к /admin/* нет, пункт меню скрыт.

Почему без Redux: данные экрана нужны только ему, после каждой правки перечитываются с сервера — хватает
локального state и маленького хука useAsyncData (отбрасывает устаревшие ответы и ответы после ухода со страницы).
Клиент один — api/client.ts (Bearer из sessionStorage, ошибки сервера -> ApiError с кодом и текстом); ошибки
показываются через App.useApp().message (корень обёрнут в antd App). Экран показывает только серверное состояние:
при отказе (409 LAST_ADMIN и др.) — уведомление и перечитывание, матрица сохраняет роли по очереди.
РАИСА появится в auth/: токен даст SSO вместо dev-входа; этот экран и клиент не меняются.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
T14.1: неактивная роль, уже назначенная пользователю, больше не disabled в его Select — её можно снять
(новое назначение неактивной роли по-прежнему запрещено). T14.2: матрица подставляет роли из ответов
успешных PUT до перечитывания — при неудачном перечитывании на экране сохранённое, а не прежнее.
T14.3: исправлен комментарий useAsyncData (неудачный reload оставляет прежние данные). T14.4: ref-замок
от двойного POST в окне создания. T14.5: черновик матрицы — Map вместо объекта (ключи прототипа).
T14.6: группы меню в useMemo, чтобы memo(Section) работал.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Реестр имя ограничения PostgreSQL -> (код, постоянный текст) в common/constraint_conflicts.py;
integrity_error_handler для unique_violation (23505) по известному ограничению отдаёт
{"detail": {"code", "message"}}, иначе прежняя общая строка. Имя берётся из полей драйвера
(asyncpg __cause__.constraint_name, psycopg diag), не из текста ошибки; resolved_code пишется в лог.
Тесты: unit обработчика/реестра (ключи есть в Base.metadata), интеграционные гонки (ГК/ЮЛ crm_id,
ИНН, алиас, email, членство, откат и следующий запрос, FK остаётся общим 409).
T15.3: README шаг 4 импорта; T15.4: test_components_have_no_duplicated_models.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
- M-8: CompanyGroupCreate и LegalEntityCreate отвергают неизвестные поля (422, объект не создаётся); тесты.
- T11.3: safe_filename отбрасывает и категорию Cf (U+202E, U+200B) до обрезки длины; тесты.
- M-2: README «Ошибки API» — пример с ALIAS_TAKEN, INN_EXISTS без дополнительных полей.
- M-1: README — требование к ingress по размеру тела для /directories/import.
- M-4: README — зависимость формы пользователя от DIRECTORY_VIEW, /me не перечитывается, ASSIGNMENT_CHANGE (№35).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
aep changed title from OpenSpec: slice-1-directories — справочники, импорт ЕЦП, policy, администрирование (спека) to Срез 1: справочники (API), импорт из ЕЦП CRM, policy видимости, администрирование 2026-10-05 01:43:11 +03:00
aep merged commit 9fd76a2ece into main 2026-10-05 10:00:05 +03:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RSHB/arm-core#6
No description provided.