# Журнал: события отклонённого удаления (изменение 008) ## Context Когда удаление запрещено бизнес-правилом (например, у организации есть префиксы/устройства/операторы), API отвечает 409 через `raise HTTPException`, а запись в журнал не пишется: `audit()` вызывается только на успешном пути, сессия закрывается без commit (`app/db.py:get_db`). Нужно фиксировать такие попытки как предупреждения и показывать в блоке «Данные» окна записи журнала, какие именно связанные сущности мешают удалению. ## Решения - **Тип события:** `.delete_blocked` (например `organization.delete_blocked`) — вписывается в существующую схему `entity_type` + `action`, фильтры и facets журнала подхватывают его без изменений (`app/api/v1/journal.py` разбирает `event_type` по точке). - **Охват:** все отказы в удалении по бизнес-правилам (409): организация, VRF, тип устройства (в т.ч. «по умолчанию»), префикс с адресами, пользователь (свой аккаунт / последний администратор). Прочие 409/422 (дубли, валидация) не журналируются. - **Содержимое «Данных»** — уже существующее поле `audit_log.diff` (JSONB), миграция БД не нужна: ```json {"reason": "у организации есть префиксы, устройства или операторы", "blocked_by": {"prefixes": {"total": 13, "items": ["10.0.0.0/8 (default)", "…"]}, "devices": {"total": 2, "items": ["db-master.internal", "…"]}, "isps": {"total": 1, "items": ["Ростелеком"]}}} ``` В списке не более 20 элементов на группу, `total` — полное число. Пустые группы не выводятся. Для отказов без связанных объектов (свой аккаунт, тип по умолчанию, последний админ) — только `reason`. - **Сообщение:** «Организация ООО «X»: удаление отклонено — есть связанные объекты (префиксы: 13, устройства: 2, операторы: 1)». ## Реализация 1. `app/services.py`: хелпер `refuse_delete(db, user, entity_type, entity, label, reason, blocked_by=None)` — пишет `audit(..., "delete_blocked", label, diff, message=...)`, делает `commit(db)` (в транзакции только запись журнала, других изменений к этому моменту нет) и поднимает `HTTPException(409, reason_text)`. Текст ответа API остаётся прежним. Хелпер `_blockers(db, stmt, label_col, limit=20)` — `{"total", "items"}` по запросу (переиспользуем `count()`). 2. Вызовы вместо прямых `raise HTTPException(409, …)` в: - `app/api/v1/refs.py`: `delete_org` (префиксы: `prefix (vrf)`, устройства: `name`, операторы: `name`), `delete_vrf` (префиксы), `delete_type` (устройства + случай `is_default`); - `app/api/v1/prefixes.py`: `delete_prefix` (адреса, до 20 шт.); - `app/api/v1/users.py`: `delete_user` (два отказа: свой аккаунт, единственный активный админ). Для организации проверка «занято» переписывается с `or`-цепочки на три подсчёта, чтобы собрать все группы сразу. 3. `web/app.js`: `eventBadge` — цвет `amber` для `delete_blocked`; в `actionBadge` обзора — `delete_blocked: ["amber", "отклонено"]`. Блок «Данные» (`entryDialog`) уже выводит `diff` как JSON — отдельная вёрстка не нужна. 4. `_VERBS` в `make_message` не трогаем: сообщение передаётся явно. ## Артефакты (правила проекта) `docs/changes/008-blocked-delete-audit/PLAN.md` (копия этого плана) и `SUMMARY.md`; `README.md` — в раздел «Журнал»: событие `*.delete_blocked`, состав «Данных». ## Тест (минимум) Расширить `test_delete_organization` в `tests/test_api.py`: после 409 на организации с префиксом найти в `/audit?event_type=organization.delete_blocked&q=<имя>` запись, проверить `diff.blocked_by.prefixes.total == 1` и наличие CIDR в `items`. ## Проверка 1. `docker compose -p ipam_control_006 up -d --build app` (БД и прежняя поставка не затрагиваются); `venv/bin/python -m pytest -q`. 2. В UI (http://192.168.5.9:8088): «Организации» → «Удалить» у «ООО «Технологии связи»» → 409; в «Журнале» появилась запись `organization.delete_blocked` (жёлтый бейдж), в окне записи блок «Данные» показывает списки префиксов/устройств/операторов; фильтр по типу события находит её. 3. Отказ на удаление VRF с префиксами и своего аккаунта — аналогичные записи.