# Дыра #2 — план реализации partitioning 7 audit-таблиц

**Spec:** `2026-05-23-hole-2-audit-partitioning-design.md` v1.0
**Решения заказчика (23.05 вечер):**

- Scope: все 7 таблиц
- FK: W1 (удалить FK на webhook_log от failed_webhook_jobs/rejected_deals_log)
- Retention: предложенные defaults (auth:24м, activity:36м, tenant_ops:24м, webhook:3м, balance:84м, pd:36м, saas_admin:84м) + cron Sundays 03:00 МСК
- Hash-chain: per-partition (совместимо с hole #1 fix)

---

## Phase A — Разработка (dev)

### A.1. Расширить whitelist

**Файл:** `app/app/Services/MonthlyPartitionManager.php`
**Изменение:** добавить 7 строк в `PARTITIONED_TABLES`. Map имя→partition-key:

```php
public const PARTITIONED_TABLES = [
    'deals' => 'received_at',
    'supplier_lead_costs' => 'received_at',
    'auth_log' => 'created_at',
    'activity_log' => 'created_at',
    'tenant_operations_log' => 'created_at',
    'webhook_log' => 'received_at',  // в этой таблице ключ называется received_at
    'balance_transactions' => 'created_at',
    'pd_processing_log' => 'created_at',
    'saas_admin_audit_log' => 'created_at',
];
```

Refactor `ensureMonth()` чтобы использовал key из map (сейчас hardcoded на `received_at`).

**Test:** `MonthlyPartitionManagerTest` — добавить positive case на каждую новую таблицу + проверить что old test (throw на 'orders') продолжает работать.

### A.2. Миграция rewrite

**Файл:** `app/database/migrations/2026_05_23_000002_partition_audit_tables.php`
**Логика для каждой таблицы** (в одной транзакции BEGIN/COMMIT):

1. `ALTER TABLE auth_log RENAME TO auth_log_old`
2. `CREATE TABLE auth_log (...same columns...) PARTITION BY RANGE (created_at)` с PK=`(id, created_at)`
3. Создать 6 партиций (3 прошлых + текущий + 2 будущих) — диапазоны `[Y_m_01, Y_m+1_01)`
4. Восстановить все индексы (как локальные через `CREATE INDEX ... ON auth_log (...)` — автонаследуются партиции)
5. Восстановить RLS-политики
6. Восстановить триггеры (особенно `audit_chain_hash` через `BEFORE INSERT`)
7. `INSERT INTO auth_log SELECT * FROM auth_log_old`
8. `DROP TABLE auth_log_old CASCADE`

**FK удаление (только для webhook_log):**

- `ALTER TABLE failed_webhook_jobs DROP CONSTRAINT failed_webhook_jobs_webhook_log_id_fkey`
- `ALTER TABLE rejected_deals_log DROP CONSTRAINT rejected_deals_log_webhook_log_id_fkey`
- Имена FK выяснить через `\d` или `pg_constraint`

**ВАЖНО:** миграция через `DB::unprepared()` (raw SQL) — Laravel migrate уже паттерн на проде через postgres-role (см. hole #6 deploy lessons).

### A.3. Обновить `db/schema.sql`

- Заменить 7 объявлений `CREATE TABLE` на партиционированные версии
- PK `(id)` → `(id, created_at)` (или `received_at`)
- Bump schema header: v8.30 → v8.31
- Запись в `db/CHANGELOG_schema.md`

### A.4. Adapt `VerifyAuditChains` (per-partition scan)

**Файл:** `app/app/Console/Commands/VerifyAuditChains.php`
**Изменение:** для каждой из 6 hash-chain таблиц (`auth_log`, `activity_log`, `tenant_operations_log`, `balance_transactions`, `pd_processing_log`, `saas_admin_audit_log`):

- Получить список партиций через `pg_inherits` JOIN `pg_class`
- Для каждой партиции — проверить hash-chain отдельно
- Несоответствие в любой партиции → инцидент с указанием partition_name в summary

**Test:** обновить `VerifyAuditChainsTest` — добавить кейс «разрыв в одной партиции, остальные intact → 1 инцидент с partition_name».

### A.5. Новая команда `partitions:drop-expired`

**Файл:** `app/app/Console/Commands/PartitionsDropExpired.php`
**Логика:**

- Получить retention настройки из `system_settings` (defaults в коде, override через DB):
  - `auth_log_retention_months`, `activity_log_retention_months`, etc. (7 ключей)
- Для каждой таблицы из `PARTITIONED_TABLES`:
  - Найти партиции старше `NOW() - INTERVAL '{retention} months'`
  - `DROP TABLE IF EXISTS {partition_name}` (атомарно)
  - Лог: сколько дропнуто, какой freed space
- Если retention=0 (или не задан) — НЕ дропать (защита от случайного `0`)

**Cron в `routes/console.php`:** Sundays 03:00 МСК + heartbeat-обёртка (как hole #6 паттерн).

**Mailable:** опционально — `PartitionsDroppedReport` ежемесячно? Решим в коде — для МИНИМУМА просто info-лог.

**Test:** `PartitionsDropExpiredTest`:

- настройка retention=2 → дропает партиции старше 2 мес
- защита от 0/null → no-op
- идемпотентность (повторный run — 0 дропов)

### A.6. seed retention defaults в system_settings

**Файл:** `db/schema.sql` (раздел `system_settings` seed) + миграция inserts:

```sql
INSERT INTO system_settings (key, value, description) VALUES
  ('auth_log_retention_months', '24', 'Retention auth_log в месяцах (hole #2)'),
  ...7 строк...
ON CONFLICT (key) DO NOTHING;
```

### A.7. Регрессия

- `php artisan test` — все green (особенно: Auth*, Pd*, BalanceTransactions*, ImpersonationAudit*, PartitionsCreateMonths*, VerifyAuditChains*)
- `pint --test`
- `cspell` + `markdownlint`

### A.8. Локальный smoke

- `php artisan migrate:fresh --env=testing` — применит миграцию, схема корректна
- `php artisan partitions:create-months` — создаст партиции для всех 9 таблиц (2 старые + 7 новые)
- `php artisan partitions:drop-expired` — no-op (нет старых партиций)
- `php artisan audit:verify-chains` — должен пройти per-partition (empty chains = OK)

---

## Phase B — Прод-выкатка (с явным approve каждого критического шага)

### B.1. Pre-flight

- `git fetch && git log HEAD..origin/main --oneline` — pre-flight sync
- Push всех A-коммитов на origin/main

### B.2. Backup БД

```bash
ssh ubuntu@111.88.246.137
TS=$(date +%Y%m%d-%H%M%S)
sudo -u postgres pg_dump -Fc liderra > /home/ubuntu/deploy-backups/pre-partitioning-$TS.dump
# Verify
ls -lh /home/ubuntu/deploy-backups/pre-partitioning-$TS.dump
sudo -u postgres pg_restore --list /home/ubuntu/deploy-backups/pre-partitioning-$TS.dump | wc -l
```

### B.3. Pre-migration snapshot

```sql
SELECT 'auth_log', COUNT(*) FROM auth_log
UNION ALL SELECT 'activity_log', COUNT(*) FROM activity_log
... 7 таблиц ...;
```

### B.4. Deploy code

scp нескольких файлов: `MonthlyPartitionManager.php`, `VerifyAuditChains.php`, `PartitionsDropExpired.php`, `routes/console.php`. Install, optimize.

### B.5. Apply migration

Через `sudo -u postgres psql -d liderra -f /tmp/migration.sql` (НЕ через artisan migrate — права crm_app_user не хватит на DDL, повторяет hole #6 lesson). После — INSERT в `migrations` для учёта Laravel.

### B.6. Post-migration verify

- row counts соответствуют B.3 snapshot
- `\d+ auth_log` показывает PARTITION BY и 6 партиций
- `audit:verify-chains` rc=0 без инцидентов

### B.7. Smoke 1 час

Watch incidents_log, scheduler_heartbeats, queue activity.

---

## Phase C — Documentation

- ПИЛОТ.md §6 п.11 — добавить под-пункт #2
- ЭТАЛОН.md — schema v8.31, metrics обновить
- `memory/project_7holes_audit_followup` — закрыть #2
- `docs/superpowers/plans/2026-05-23-7-holes-overview.md` — tracker обновить (все 7 ✅)

---

## Tracker

- [ ] A.1. Whitelist + map в `MonthlyPartitionManager`
- [ ] A.2. Миграция rewrite (7 таблиц + FK drop)
- [ ] A.3. Update `db/schema.sql` + CHANGELOG
- [ ] A.4. Adapt `VerifyAuditChains` per-partition
- [ ] A.5. `PartitionsDropExpired` команда + cron
- [ ] A.6. Seed retention defaults
- [ ] A.7. Regression
- [ ] A.8. Локальный smoke
- [ ] B.1. Pre-flight
- [ ] B.2. Backup
- [ ] B.3. Snapshot
- [ ] B.4. Deploy code
- [ ] B.5. Apply migration
- [ ] B.6. Verify
- [ ] B.7. Smoke 1h
- [ ] C.1-C.4. Docs
