# Приёмка liderra.ru — ПУНКТ №12: Денежный аудит (цепочка цела, правки задним числом заблокированы)

> **Для исполнителя:** формат PR1 (одобрен 20.06). Каждый шаг = `🔧 Код-факт (file:line)` · `🎬 Действие` · `📋 ОТЧЁТ (было→ожидали→стало + 📸/📝)` · `❌ Если не так`. Техника «под капотом»; владельцу — человеческие карточки.

**Цель:** показать, что денежные и важные записи **нельзя подделать задним числом**. Списания и журналы устроены как «несгораемая лента»: их можно только дописывать, но **нельзя ни изменить, ни удалить**. А если кто-то попытается влезть в базу в обход правил — портал это **обнаружит** (записи связаны в защищённую цепочку, разрыв сразу виден, приходит сигнал-тревога).

**Что снимаем глазами:** это **внутренняя защита базы** — экранов в кабинете нет. Доказываем **текстом** (📝): вывод проверки целостности + отказ при попытке правки. (Отдельного 📸 портала здесь нет — честно помечаем.)

**Источник сценария:** свод проверок №12 (M-AUDIT); R3 «движок».

---

## 🔧 Код-факты (подтверждено чтением 20.06)

- **Запрет правок (append-only) — реестр списаний:** `lead_charges` — только SELECT+INSERT для приложения, **UPDATE/DELETE недопустимы** (гарантия для биллинга/аудита) — [db/schema.sql:1175-1176](../../../db/schema.sql#L1175-L1176).
- ⚠️ **append-only (важно для прогона):** защита `lead_charges` (и `supplier_lead_costs`) — это **GRANT без UPDATE/DELETE** под ролью `crm_app_user`, и он **закомментирован в schema.sql** — применяется только на проде через `db/02_grants.sql`. **Триггера-стража у них НЕТ** (триггеры — только у 6 audit-таблиц/`balance_transactions`). Поэтому на **dev (postgres-superuser, BYPASSRLS)** правка строки `lead_charges` **ПРОЙДЁТ** → шаг 12-2 для списаний даст **ложный GREEN на локалке**. Этот суб-чек гонять **на проде под `crm_app_user`**. Журналы (триггеры) блокируются на любой роли — их можно проверять и на dev.
- **Запрет правок — журналы (триггеры):** 6 audit-таблиц + `balance_transactions` имеют пары триггеров: hash-fill при вставке + **block-mutation BEFORE UPDATE/DELETE → RAISE EXCEPTION** (запись становится неизменяемой) — [db/schema.sql:3301-3339](../../../db/schema.sql#L3301-L3339); функция-страж `audit_block_mutation()` — [:246](../../../db/schema.sql#L246), [:3247](../../../db/schema.sql#L3247).
- **Защищённая цепочка (hash-chain):** каждая запись хранит SHA-256 от «предыдущая запись + эта запись»; разорвать цепочку незаметно нельзя — [db/schema.sql:81-86](../../../db/schema.sql#L81-L86).
- **Обнаружение подмены:** `audit:verify-chains` пересчитывает hash-chain во всех 6 таблицах (по партициям, на стороне PostgreSQL); при разрыве → инцидент + письмо на kdv1@bk.ru + код возврата FAILURE — [VerifyAuditChains.php:87-135](../../../app/app/Console/Commands/VerifyAuditChains.php#L87-L135). Запускается ежедневно 04:00.
- Роль `crm_audit_writer` — только INSERT, REVOKE UPDATE/DELETE даже у неё — [db/schema.sql:86](../../../db/schema.sql#L86).

---

## Шаг 12-1 — Проверка целостности: «всё цело»

**🔧 Код:** [VerifyAuditChains :87-135](../../../app/app/Console/Commands/VerifyAuditChains.php#L87-L135).
**🎬 Действие (лок):** запустить `php artisan audit:verify-chains`.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Результат проверки | — | «All audit chains intact» (все цепочки целы) | _(слот прогона)_ | 📝 вывод команды (текст) |
| Код возврата | — | успех (0) | _(слот)_ | 📝 exit code 0 |

**💡 Что внутри:** портал каждую ночь сам проверяет, что ни одна денежная или важная запись не была подделана. Если всё в порядке — отвечает «всё цело». Это как ежедневная инвентаризация, которая ловит любое незаконное вмешательство.

**❌ Если не так:** проверка нашла разрыв на чистых данных → разбираться (реальное вмешательство или баг проверки).

---

## Шаг 12-2 — Правку задним числом не дают сделать (запрет)

**🔧 Код:** триггеры block-mutation — [db/schema.sql:3301-3339](../../../db/schema.sql#L3301-L3339); запрет на `lead_charges` — [:1175-1176](../../../db/schema.sql#L1175-L1176).
**🎬 Действие (лок, под обычной ролью приложения):** попробовать изменить или удалить строку списания / журнала.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Попытка изменить списание | — | **отказ** (нет права / исключение) | _(слот)_ | 📝 ошибка БД (UPDATE/DELETE запрещён) |
| Попытка удалить запись журнала | — | **отказ** (RAISE EXCEPTION) | _(слот)_ | 📝 ошибка «mutation blocked» |

**💡 Что внутри:** даже сам портал (под обычными правами) не может переписать или стереть уже сделанную денежную запись. База физически запрещает это: запись можно только добавить. Поэтому «подправить» сумму списания или замести след задним числом невозможно.

**❌ Если не так:** правка/удаление прошли → денежный/аудиторский дефект (СТОП).

---

## Шаг 12-3 — Если влезть в обход правил — проверка это ловит (обнаружение)

**🔧 Код:** [VerifyAuditChains :101-135](../../../app/app/Console/Commands/VerifyAuditChains.php#L101-L135).
**🎬 Действие (лок, ТОЛЬКО на тестовой базе):** под суперправами подменить одну строку (имитация вмешательства в обход триггеров) → снова запустить `audit:verify-chains`.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Проверка после подмены | «всё цело» | **разрыв обнаружен** (указан id и таблица) | _(слот)_ | 📝 вывод «mismatch, first broken id=…» |
| Реакция | — | инцидент записан + письмо-тревога + FAILURE | _(слот)_ | 📝 incidents_log + письмо (внутр.) |

**💡 Что внутри:** допустим, кто-то с прямым доступом к базе всё же изменил запись в обход защиты. Записи связаны в цепочку, как звенья: тронул одно — следующее перестаёт сходиться. Ночная проверка это сразу видит, поднимает тревогу (письмо владельцу) и фиксирует инцидент. Скрыть подмену невозможно.

**❌ Если не так:** проверка НЕ заметила подмену → дефект защиты целостности (СТОП).

> ⚠️ Шаг 12-3 выполнять **только на тестовой базе** — это намеренная порча данных для проверки сигнализации. На проде НЕ делать.

---

## Verification №12 (выход из пункта)
- [ ] 📝 `audit:verify-chains` на чистых данных → «all intact», exit 0.
- [ ] 📝 Попытка UPDATE/DELETE списания и журнала → отказ (право/триггер).
- [ ] 📝 (тест-база) Подмена в обход → проверка ловит разрыв + инцидент + письмо.
- [ ] 📝 6 audit-таблиц + lead_charges + balance_transactions под защитой.

## 📸 vs 📝 в этом пункте
- **📝 Текст (весь пункт):** вывод `audit:verify-chains`, отказ на правку, обнаружение разрыва, инцидент — это внутренняя защита базы, **экранов в кабинете нет**.
- **📸 Скриншот:** не применим (нет портального экрана). При желании — снимок окна с выводом команды (опц.).

## Грабли
- Шаг 12-3 (намеренная порча) — **только тест-база**, на проде запрещено.
- Это «под капотом» — владельцу объясняем словами (несгораемая лента + ночная инвентаризация), без скриншотов портала.
- Связка с №7 (тройная запись) и №9 (откат) — защищаются те же денежные таблицы.
