# Legacy Direct Webhook Removal — План реализации

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) или superpowers:executing-plans для исполнения task-by-task. Шаги в формате checkbox (`- [ ]`). **Pravila §15.1** — git-коммит задачи только Sonnet/Opus субагентами (не Haiku); контроллер верифицирует `git rev-parse HEAD` после каждого субагента.

**Goal:** Удалить legacy-кусок `ProcessWebhookJob` + всю его обвязку (контроллер, роут, модель `WebhookDedupKey`, тесты, таблицы БД, колонки `tenants.webhook_token*`), потому что он не часть актуальной архитектуры каналов (основной webhook + резервный CSV — оба уже на always-rub).

**Architecture:** Cleanup-задача, не feature work. Одна последовательность: discovery (impact-checks) → удаление кода/тестов файл-за-файлом с регрессией между шагами → одна DROP-миграция → финальная регрессия → деплой одним PR.

**Tech Stack:** PHP 8.3 / Laravel 13, PostgreSQL 16 (RLS, партиционирование таблиц), Pest 4.

**Спек:** [docs/superpowers/specs/2026-05-24-legacy-direct-webhook-removal-design.md](../specs/2026-05-24-legacy-direct-webhook-removal-design.md)

---

## Контекст для исполнителя (прочитать до начала)

**Что нужно знать про проект:**

- БД-роли: миграции с `DROP TABLE` / `DROP COLUMN` запускать через `DB::connection('pgsql_supplier')` (BYPASSRLS-роль `crm_supplier_worker`), потому что под обычной `crm_app_user` без `app.current_tenant_id` GUC не пройдёт. Этот паттерн закреплён в Спеке B Phase 1 (`546ca30a`).
- Партиционированная таблица (`webhook_log` — 13 партиций) удаляется одной командой `DROP TABLE webhook_log` — Postgres сам каскадит партиции.
- Тесты Pest: `cd app && composer test` или `php artisan test --filter=...`. Параллельный режим `--parallel` — учитывать quirk 72 (Redis `supplier:session` race в subdir, не наш случай — мы удаляем, не добавляем).
- Pre-commit hooks (`lefthook.yml`): gitleaks + markdownlint + cspell + ESLint + Pint + Larastan + squawk (если миграция). Все должны быть зелёные.
- `db/schema.sql` — baseline схемы; после миграции **обязательно** обновляется + запись в `db/CHANGELOG_schema.md`.

**Что говорит спек:**

- §2.1 «Что удаляем» — основной список (включая conditional на impact-check).
- §2.2 «Что НЕ трогаем» — границы (failed_webhook_jobs, SupplierLeadCost, MonthlyPartitionManager — оставляем).
- §3 «Impact-checks» — обязательная discovery перед удалением каждой conditional-сущности.
- §6 «Выкатка» — деплой одним PR.

**Стиль работы:**

- TDD-цикл «red→green» не очень применим — это удаление, не создание. Вместо этого: «удалить → запустить регрессию → ожидаемо GREEN (т.к. удалён только мёртвый код)». Если регрессия RED — расследовать (значит код не мёртвый, нужно лечить иначе).
- Между удалениями — частые мелкие коммиты, чтобы легко делать `git bisect` если что.

---

## Phase 0 — Подготовка рабочего пространства

### Task 0.1: Создать worktree от свежего origin/main

**Files:** нет (git-операция)

- [ ] **Step 1: Pre-flight sync (Pravila §15.2)**

Перед созданием worktree — `git fetch && git log HEAD..origin/main --oneline` обязателен (если параллельная сессия что-то залила в main, надо знать).

Run:

```bash
git fetch origin
git log HEAD..origin/main --oneline
```

Expected: пусто или несколько коммитов — зафиксировать что новое; если ничего критичного — продолжаем.

- [ ] **Step 2: Создать worktree**

REQUIRED SUB-SKILL: `superpowers:using-git-worktrees`.

Run:

```bash
git worktree add -b feat/legacy-webhook-removal .claude/worktrees/legacy-webhook-removal origin/main
```

Expected: создаётся директория `.claude/worktrees/legacy-webhook-removal/` с чек-аутом ветки `feat/legacy-webhook-removal` от `origin/main`.

- [ ] **Step 3: Скопировать gitignored runtime-файлы в worktree** (урок Sprint 4 — свежий worktree без `.env`, `storage/`, бинарей)

Run (из основного чек-аута):

```bash
cp app/.env .claude/worktrees/legacy-webhook-removal/app/.env
cp -r app/storage/framework .claude/worktrees/legacy-webhook-removal/app/storage/
cp app/_ide_helper.php app/_ide_helper_models.php .claude/worktrees/legacy-webhook-removal/app/ 2>/dev/null || true
```

Expected: файлы скопированы; `_ide_helper*.php` опциональны (если есть на dev — нужны для phpstan baseline).

NB для Windows: junction для `app/node_modules/` (`New-Item -ItemType Junction -Path .claude/worktrees/legacy-webhook-removal/app/node_modules -Target $(pwd)/app/node_modules`) — quirk #108 (cmd-link через `cmd /c` ломается на кириллических путях, нужен PowerShell `New-Item`).

- [ ] **Step 4: Smoke composer + migrate на тестовой БД worktree**

Run в worktree:

```bash
cd .claude/worktrees/legacy-webhook-removal/app
composer install --no-interaction
php artisan migrate:fresh --env=testing
php artisan test --filter=Smoke --stop-on-failure
```

Expected: composer ничего не доустанавливает (lock не менялся), `migrate:fresh` проходит за <2с, smoke-тест GREEN. Если RED — worktree не готов, исправить **до** начала Task 1.1.

---

## Phase 1 — Impact-checks (discovery, без правок кода)

**Цель:** для каждой conditional-сущности из спека §3 получить финальный ответ «удалять или оставить». Результаты фиксируются inline в комментариях задач плана (отмечать ✅ удаляем / ⚠️ оставить — есть use в [имя_файла:строка]).

### Task 1.1: Impact-check `WebhookReceiveController`

**Files:** нет (только grep)

- [ ] **Step 1: Найти все use'ы класса**

Run в worktree:

```bash
grep -rn "WebhookReceiveController" app/ docs/
```

Expected: записать список файлов. Ожидается:

- `app/routes/web.php` — регистрация роута (это удаляем).
- `app/tests/Feature/WebhookReceiveTest.php` — тест (удаляем целиком).
- никаких других use'ов.

- [ ] **Step 2: Зафиксировать решение**

Если найдены только роут + тест — отметить ✅ **удаляем целиком** в Task 3.1.
Если найдены use вне роута/теста — отметить ⚠️ **оставить класс, удалить только метод `receive`**.

### Task 1.2: Impact-check `NotificationService::notifyLowBalance` / `notifyZeroBalance`

**Files:** нет (только grep)

- [ ] **Step 1: Найти caller'ы `notifyLowBalance`**

Run:

```bash
grep -rn "notifyLowBalance\b" app/
```

NB: word boundary `\b` опционален (имя уникальное).

Expected: список caller'ов. Ожидается только `app/app/Jobs/ProcessWebhookJob.php:217` (single call site).

- [ ] **Step 2: Найти caller'ы `notifyZeroBalance` (без `Paused`)**

Run:

```bash
grep -rn "notifyZeroBalance\b" app/ | grep -v "notifyZeroBalancePaused"
```

Expected: список caller'ов **БЕЗ** `notifyZeroBalancePaused` (это другой метод, его НЕ трогаем — он живой через шеринг).

Ожидается только `app/app/Jobs/ProcessWebhookJob.php:142` (single call site).

- [ ] **Step 3: Зафиксировать решение**

Если каждый метод имеет ровно одного caller'а (`ProcessWebhookJob`) — отметить ✅ **удаляем оба метода** в Task 3.5.
Если есть другие caller'ы — отметить ⚠️ **оставить методы**, в Task 3.5 не трогать.

### Task 1.3: Impact-check `LowBalanceNotification`, `ZeroBalanceNotification` (Mailable)

**Files:** нет (только grep)

- [ ] **Step 1: Найти use'ы классов**

Run:

```bash
grep -rn "LowBalanceNotification\|ZeroBalanceNotification" app/ | grep -v "ZeroBalancePaused"
```

Expected: список use'ов. Ожидается:

- `app/app/Services/NotificationService.php` — внутри методов `notifyLowBalance`/`notifyZeroBalance` (если они удаляются в Task 1.2 — то и mailable удаляются вместе).
- `app/tests/Feature/Notifications/BalanceNotificationsTest.php` — assertions в тестах (удаляются вместе с тестом).

- [ ] **Step 2: Зафиксировать решение**

Если use'ы только в удаляемых методах NotificationService + тестах — отметить ✅ **удаляем оба Mailable файла** в Task 3.6.
Если есть use в живом коде — отметить ⚠️ **оставить**.

### Task 1.4: Impact-check `RejectedDealsLog`

**Files:** нет (только grep)

- [ ] **Step 1: Найти все use'ы**

Run:

```bash
grep -rn "RejectedDealsLog\|rejected_deals_log" app/ docs/
```

Expected: список. Ожидается:

- `app/app/Models/RejectedDealsLog.php` — сама модель (если есть).
- `app/app/Jobs/ProcessWebhookJob.php:117` — `RejectedDealsLog::create([...])` (удаляется с job'ом).
- Возможно админ-UI / API чтения журнала отказов (если есть).

- [ ] **Step 2: Зафиксировать решение**

Если writers только `ProcessWebhookJob` И reader'ов в UI/API нет — отметить ✅ **удаляем модель + DROP TABLE** в Task 3.7 и Task 4.1.
Если есть reader в UI/API — отметить ⚠️ **оставить таблицу+модель** (после удаления writer'а таблица перестанет расти, но журнал старых записей доступен).

### Task 1.5: Impact-check `webhook_dedup_keys`

**Files:** нет (только grep)

- [ ] **Step 1: Найти все use'ы**

Run:

```bash
grep -rn "webhook_dedup_keys\|WebhookDedupKey" app/ docs/
```

Expected: список. Ожидается только `ProcessWebhookJob` + `app/app/Models/WebhookDedupKey.php` (модель).

- [ ] **Step 2: Зафиксировать решение**

Если использований только в `ProcessWebhookJob` + модель — отметить ✅ **удаляем модель + DROP TABLE** в Task 3.8 и Task 4.1.
Иначе — RED FLAG, расследовать.

### Task 1.6: Impact-check `tenants.webhook_token` / `webhook_token_rotated_at`

**Files:** нет (grep — самая важная проверка, эти колонки могут быть в UI)

- [ ] **Step 1: Найти use'ы в PHP**

Run:

```bash
grep -rn "webhook_token" app/app/ app/database/ app/routes/ app/tests/
```

Expected: список. Ожидается:

- `app/app/Models/Tenant.php` — `$fillable`/`$casts` (удалить из этих массивов).
- `app/app/Http/Controllers/Api/WebhookReceiveController.php` — резолв тенанта по токену (удаляется с контроллером).
- `app/database/factories/TenantFactory.php` — генерация токена (удалить если только тут).
- `app/database/seeders/*Seeder.php` — возможно есть seed (удалить).
- `app/database/migrations/*.php` — старая миграция создания колонок (не трогаем, история).
- `app/tests/Feature/WebhookReceiveTest.php` — токен в тестах (удаляется с тестом).

- [ ] **Step 2: Найти use'ы во frontend**

Run:

```bash
grep -rn "webhook_token\|webhookToken" app/resources/js/
```

Expected: либо ничего (колонки нет в UI) — отметить ✅, либо есть UI «личный кабинет → твой webhook URL» — RED FLAG.

- [ ] **Step 3: Найти use'ы в API resources / OpenAPI**

Run:

```bash
grep -rn "webhook_token" app/app/Http/Resources/ docs/api/ 2>/dev/null
```

Expected: пусто (нет API-ресурса с этим полем) — отметить ✅.

- [ ] **Step 4: Зафиксировать решение**

Если все use'ы — только во `ProcessWebhookJob` + `WebhookReceiveController` + фабрика + сидер + тесты — отметить ✅ **удаляем колонки** в Task 4.1.
Если есть UI/API consumer — отметить ⚠️ **оставить колонки**, добавить TODO «отдельная задача на удаление UI».

### Task 1.7: Impact-check `low_balance_threshold_leads` (system_settings)

**Files:** нет (grep)

- [ ] **Step 1: Найти use'ы**

Run:

```bash
grep -rn "low_balance_threshold_leads" app/ docs/ db/
```

Expected: список. Ожидается:

- `app/app/Jobs/ProcessWebhookJob.php:227-228` — чтение (удаляется с job'ом).
- `db/schema.sql` — возможно seed строки для `system_settings` (удалить).
- `app/database/seeders/*Seeder.php` — возможно seed (удалить).

- [ ] **Step 2: Зафиксировать решение**

Если caller только `ProcessWebhookJob` — отметить ✅ **удаляем seed-строку** в Task 4.1 (если она существует).
Иначе — оставить.

### Task 1.8: Финальный список удаления (заполнено после Phase 1, 2026-05-24)

**Финальный список после impact-checks Phase 1 (2026-05-24, субагент grep):**

| Сущность | Решение | Причина | Куда в плане |
|---|---|---|---|
| `ProcessWebhookJob.php` | ✅ удаляем | Рудимент, нет caller'ов из живого кода | Task 2.1 |
| `ProcessWebhookJobTest.php` | ✅ удаляем | Тест удаляемого job'а | Task 2.1 |
| `WebhookReceiveController.php` | ✅ удаляем целиком | Только use в роуте; в `SupplierWebhookController` и `DealController` — только комментарии | Task 3.1 |
| `WebhookReceiveTest.php` | ✅ удаляем | Тест удаляемого контроллера | Task 3.1 |
| Роут `POST /api/webhook/{token}` | ✅ удаляем | Регистрация удаляемого контроллера | Task 3.2 |
| `WebhookDedupKey.php` (model) | ⚠️ **ОСТАВИТЬ** | **RED FLAG:** `HistoricalImportService` (CSV-канал) активно использует `webhook_dedup_keys` для идемпотентности | — (Task 3.3 ОТМЕНЯЕТСЯ) |
| `NotificationService::notifyLowBalance` | ✅ удаляем | Единственный caller — `ProcessWebhookJob` | Task 3.5 |
| `NotificationService::notifyZeroBalance` | ✅ удаляем | Единственный caller — `ProcessWebhookJob` (не путать с `notifyZeroBalancePaused` — оставляем) | Task 3.5 |
| `LowBalanceNotification` (Mailable) | ✅ удаляем | Используется только в удаляемом `notifyLowBalance` | Task 3.6 |
| `ZeroBalanceNotification` (Mailable) | ✅ удаляем | Используется только в удаляемом `notifyZeroBalance` (не путать с `ZeroBalancePausedMail` — оставляем) | Task 3.6 |
| `RejectedDealsLog` (model + table) | ✅ удаляем | Writer только `ProcessWebhookJob`, reader'ов в UI/API нет | Task 3.7 + Task 4.1 |
| `webhook_log` (partitioned table, 13 партиций) | ✅ DROP | Источник записей — только `ProcessWebhookJob` (0 строк на проде) | Task 4.1 |
| `webhook_dedup_keys` (table) | ⚠️ **НЕ ДРОПАТЬ** | **RED FLAG:** см. `WebhookDedupKey` выше — таблица жива через `HistoricalImportService` | — (Task 4.1 пропускает) |
| `tenants.webhook_token` + `webhook_token_rotated_at` | ✅ удаляем | Нет в UI/API, нет в API resources. **NB:** требуется чистка 7+ тестовых файлов (см. Task 3.0 ниже) | Task 3.0 + Task 4.1 |
| `system_settings.low_balance_threshold_leads` (seed) | ✅ удаляем | Единственный caller — `ProcessWebhookJob` | Task 4.1 |

**Дополнительные правки (открыты impact-check'ом Task 1.6, добавлены в план как Task 3.0):**

`tenants.webhook_token` используется в 7+ тест-файлах вне `WebhookReceiveTest` (через TenantFactory или прямые create-массивы). Перед DROP COLUMN нужно почистить:

- `app/database/factories/TenantFactory.php:25` — `'webhook_token' => Str::random(64)` (убрать из фабрики)
- `app/app/Models/Tenant.php:34,35,64` — `$fillable` и `$casts` (убрать)
- `app/tests/Feature/RlsSmokeTest.php:48,55` — create tenant массивы
- `app/tests/Feature/AdminIncidentShowTest.php:67` — create tenant
- `app/tests/Feature/AdminBillingActionsTest.php:17` — create tenant
- `app/tests/Feature/Admin/AdminPdSubjectRequestsControllerTest.php:167` — create tenant
- `app/tests/Feature/PartitionsCreateMonthsTest.php:103` — create tenant
- `app/tests/Feature/Console/VerifyAuditChainsTest.php:72` — create tenant
- `app/tests/Feature/Plan4/Schema/SchemaDeltaTest.php:22` — прямой `INSERT INTO tenants ... webhook_token` (потребует ручной правки SQL)
- `app/storage/_demo_split_tenants.php:80` — demo-скрипт (не критично, но удалить)

- [ ] **Step 1: Commit обновлённого плана (с финальной таблицей + новой Task 3.0)**

Run в worktree:

```bash
git add docs/superpowers/plans/2026-05-24-legacy-direct-webhook-removal.md
git commit -m "docs(billing): план — заполнены impact-checks, найден RED FLAG webhook_dedup_keys"
```

Expected: коммит проходит, lefthook GREEN.

---

## Phase 2 — Удаление core: ProcessWebhookJob + тест

### Task 2.1: Удалить `ProcessWebhookJob` и `ProcessWebhookJobTest`

**Files:**

- Delete: `app/app/Jobs/ProcessWebhookJob.php`
- Delete: `app/tests/Feature/ProcessWebhookJobTest.php`

- [ ] **Step 1: Зафиксировать baseline регрессии до удаления**

Run в worktree:

```bash
cd .claude/worktrees/legacy-webhook-removal/app
php artisan test --parallel 2>&1 | tail -3
```

Expected: запомнить количество tests / passed / failed. Должно быть ≥1 failed (если есть pre-existing red — задокументировать, не наша проблема).

- [ ] **Step 2: Удалить файлы**

Run:

```bash
rm app/app/Jobs/ProcessWebhookJob.php
rm app/tests/Feature/ProcessWebhookJobTest.php
```

- [ ] **Step 3: Прогнать регрессию — ожидаемо упадут другие тесты**

Run:

```bash
php artisan test --parallel 2>&1 | tail -20
```

Expected: упадут тесты, использующие `App\Jobs\ProcessWebhookJob` (class not found). Список — это test-debt от рудимента, который мы лечим в Task 2.2.

- [ ] **Step 4: Зафиксировать список упавших тестов**

Run:

```bash
php artisan test --parallel 2>&1 | grep -E "FAIL|FAILED" | head -20
```

Expected: список файлов. Это карта работ для Task 2.2 (а также проверка completeness списка из §2.1 спека).

NB: НЕ коммитим в этом шаге — коммит после Task 2.2 (полное удаление + тесты-каскады в одном коммите).

### Task 2.2: Удалить упоминания `ProcessWebhookJob` из остальных тестов

**Files:**

- Modify: `app/tests/Feature/Pd/DealCreatePdLogTest.php`
- Modify: `app/tests/Feature/Notifications/BalanceNotificationsTest.php`
- Modify: `app/tests/Feature/Notifications/NewLeadNotificationTest.php`
- Modify: `app/tests/Feature/Notifications/InAppNotificationTest.php`

NB: список из §2.1 спека (test-debt частичный); если в Task 2.1 Step 4 нашли ещё файлы — добавить сюда.

- [ ] **Step 1: Открыть каждый файл по очереди, найти тесты с `ProcessWebhookJob`**

Run для первого:

```bash
grep -n "ProcessWebhookJob" app/tests/Feature/Pd/DealCreatePdLogTest.php
```

- [ ] **Step 2: В каждом файле — удалить только кейсы (`it('...', ...)`), использующие `ProcessWebhookJob`**

Edit вручную. Если после удаления файл стал пустым (только `<?php` + `use` импорты) — удалить файл целиком.

NB: НЕ удалять кейсы, использующие `RouteSupplierLeadJob` (это шеринг-канал, живой). Кейс может проверять обе модели — оставить часть с шерингом, убрать ассерты по old-prepaid списанию.

Пример (BalanceNotificationsTest.php):

- Кейс «webhook rejection при balance_leads=0» — удалить целиком.
- Кейс «low_balance threshold-cross trigger через ProcessWebhookJob» — удалить целиком.
- Кейс «zero_balance_paused при шеринг + балансе 0» — **оставить** (это `RouteSupplierLeadJob`-флоу, живое поведение).

- [ ] **Step 3: Прогнать суиту**

Run:

```bash
php artisan test --parallel 2>&1 | tail -5
```

Expected: GREEN (нет class-not-found ошибок).

- [ ] **Step 4: Commit (атомарно Task 2.1 + Task 2.2 — иначе bisect ломается)**

Run:

```bash
git add -A
git commit -m "refactor(billing): remove legacy ProcessWebhookJob + cascade test cleanup

Удалён рудимент pre-sharing эпохи: job + 1 dedicated test файл +
кейсы в 4 файлах NotificationService/PdLog тестов.

Закрытие первой части спека docs/superpowers/specs/2026-05-24-legacy-direct-webhook-removal-design.md"
```

Expected: lefthook GREEN, коммит проходит.

---

## Phase 3 — Удаление обвязки

### Task 3.0: Чистка `webhook_token` из живых тестов и фабрики (добавлено после Phase 1 impact-check)

**Files:**

- Modify: `app/database/factories/TenantFactory.php`
- Modify: `app/app/Models/Tenant.php`
- Modify: `app/tests/Feature/RlsSmokeTest.php`
- Modify: `app/tests/Feature/AdminIncidentShowTest.php`
- Modify: `app/tests/Feature/AdminBillingActionsTest.php`
- Modify: `app/tests/Feature/Admin/AdminPdSubjectRequestsControllerTest.php`
- Modify: `app/tests/Feature/PartitionsCreateMonthsTest.php`
- Modify: `app/tests/Feature/Console/VerifyAuditChainsTest.php`
- Modify: `app/tests/Feature/Plan4/Schema/SchemaDeltaTest.php`
- Modify (opt): `app/storage/_demo_split_tenants.php`

**Контекст:** колонки `tenants.webhook_token` и `webhook_token_rotated_at` исчезнут в Task 4.1. Перед DROP COLUMN все живые тесты/фабрики, явно передающие эти поля, должны прекратить их передавать (иначе после миграции INSERT упадёт на «column does not exist»).

- [ ] **Step 1: Убрать `webhook_token` из `Tenant.php` `$fillable` и `$casts`**

Открыть `app/app/Models/Tenant.php`. Найти строки `34, 35, 64` (по результатам impact-check) — удалить `'webhook_token'` и `'webhook_token_rotated_at'` из массивов `$fillable` и `$casts`.

- [ ] **Step 2: Убрать `webhook_token` из `TenantFactory`**

Открыть `app/database/factories/TenantFactory.php:25`. Удалить строку:

```php
'webhook_token' => Str::random(64),
```

(если есть и `webhook_token_rotated_at` в фабрике — тоже удалить).

- [ ] **Step 3: Почистить 7 тест-файлов**

Для каждого файла:

```bash
grep -n "webhook_token" app/tests/Feature/RlsSmokeTest.php
```

Удалить строки `'webhook_token' => ...` и `'webhook_token_rotated_at' => ...` из create-массивов в каждом из 7 файлов. Поскольку фабрика тоже почищена (Step 2), тесты использующие `Tenant::factory()->create()` без явных полей сами начнут работать.

Особый случай — `app/tests/Feature/Plan4/Schema/SchemaDeltaTest.php:22`: там прямой SQL `INSERT INTO tenants (..., webhook_token, ...) VALUES (...)`. Удалить из INSERT и колонку, и соответствующее значение.

- [ ] **Step 4: Опционально — почистить demo-скрипт**

`app/storage/_demo_split_tenants.php:80` — это dev-скрипт для split-теста, не продакшен. Если решено его не трогать (один лишний failure при запуске после Phase 4) — пропустить. Иначе удалить строку `'webhook_token' => Str::random(64)`.

- [ ] **Step 5: Прогнать тесты + verify**

```bash
php artisan test --parallel 2>&1 | tail -5
grep -rn "webhook_token" app/tests/ app/database/factories/ app/app/Models/Tenant.php | head -10
```

Expected: тесты GREEN. Grep — 0 матчей (либо только в комментариях).

NB: тесты пройдут с **существующими** колонками `webhook_token` в БД (мы их пока не дропали — это Task 4.1). После Task 4.1 они продолжат проходить, потому что больше не используют эти поля.

- [ ] **Step 6: Commit**

```bash
git add -A
git commit -m "refactor(billing): cleanup tenants.webhook_token from factories + 7 tests (pre-DROP)"
```

### Task 3.1: Удалить `WebhookReceiveController` (если impact-check ✅) + тест

**Files:**

- Delete (conditional): `app/app/Http/Controllers/Api/WebhookReceiveController.php` (только если Task 1.1 → ✅)
- Delete: `app/tests/Feature/WebhookReceiveTest.php`

- [ ] **Step 1: Удалить файлы**

Run:

```bash
rm app/tests/Feature/WebhookReceiveTest.php
# Только если Task 1.1 → ✅:
rm app/app/Http/Controllers/Api/WebhookReceiveController.php
```

- [ ] **Step 2: Прогнать тесты**

```bash
php artisan test --parallel 2>&1 | tail -5
```

Expected: GREEN. Если RED по `WebhookReceiveController` not found — значит роут ещё ссылается (это нормально, удалим в Task 3.2).

- [ ] **Step 3: Commit (вместе с Task 3.2 — иначе ломается роут на класс)**

Не коммитим, продолжаем в Task 3.2.

### Task 3.2: Удалить роут `POST /api/webhook/{token}`

**Files:**

- Modify: `app/routes/web.php`

- [ ] **Step 1: Открыть `app/routes/web.php` около строки 273-277**

- [ ] **Step 2: Удалить 4 строки**

Удалить блок:

```php
// Receive endpoint для входящих webhook'ов (narrative §5.5).
// Auth — по `tenants.webhook_token` в URL (без middleware, проверка внутри controller).
Route::post('/api/webhook/{token}', 'App\Http\Controllers\Api\WebhookReceiveController@receive')
    ...;
```

NB: оставить новый supplier-роут `POST /api/webhook/supplier/{secret}` (строки 279-283) — это другой канал.

- [ ] **Step 3: Прогнать тесты + Larastan**

```bash
php artisan test --parallel 2>&1 | tail -5
cd app && composer stan 2>&1 | tail -5
```

Expected: GREEN. Если Larastan baseline дрейфит из-за удаления класса — обновить baseline:

```bash
cd app && php vendor/bin/phpstan analyse --generate-baseline phpstan-baseline.neon
```

- [ ] **Step 4: Verify — `route:list` не показывает legacy роут**

```bash
cd app && php artisan route:list 2>&1 | grep webhook
```

Expected: только `POST api/webhook/supplier/{secret}` (новый канал), `POST api/webhooks/test` (исходящий webhook test). НЕ должно быть `POST api/webhook/{token}`.

- [ ] **Step 5: Commit (Task 3.1 + 3.2 атомарно)**

```bash
git add -A
git commit -m "refactor(billing): remove legacy WebhookReceiveController + POST /api/webhook/{token} route

Закрытие публичного эндпоинта прямого приёма (использовался ProcessWebhookJob,
который уже удалён). Шеринг-канал /api/webhook/supplier/{secret} не затронут."
```

### Task 3.3: ~~Удалить модель WebhookDedupKey~~ — **ОТМЕНЕНА** (Phase 1 RED FLAG)

**Решение Phase 1 (impact-check Task 1.5):** оставить `WebhookDedupKey.php` и таблицу `webhook_dedup_keys` — они **активно используются** `App\Services\Import\HistoricalImportService` для идемпотентности CSV-импорта (резервный канал, явно out of scope по §2.2 спека).

После удаления `ProcessWebhookJob` в Task 2.1, у `WebhookDedupKey` останется один caller — `HistoricalImportService`. Это нормально. Никаких правок не требуется.

Перейти к Task 3.4.

### Task 3.4: Удалить `RejectedDealsLog` модель (conditional)

**Files:**

- Delete (conditional): `app/app/Models/RejectedDealsLog.php` (только если Task 1.4 → ✅)

- [ ] **Step 1: Если Task 1.4 → ✅, удалить файл**

```bash
rm app/app/Models/RejectedDealsLog.php
```

Если Task 1.4 → ⚠️ — пропустить эту задачу.

- [ ] **Step 2: Verify нет orphan ссылок**

```bash
grep -rn "RejectedDealsLog" app/
```

Expected: пусто.

- [ ] **Step 3: Прогнать тесты**

```bash
php artisan test --parallel 2>&1 | tail -5
```

Expected: GREEN.

- [ ] **Step 4: Commit (только если что-то удаляли)**

```bash
git add -A
git commit -m "refactor(billing): remove RejectedDealsLog model (legacy, no readers)"
```

### Task 3.5: Удалить методы `notifyLowBalance` / `notifyZeroBalance` (conditional)

**Files:**

- Modify (conditional): `app/app/Services/NotificationService.php` (только если Task 1.2 → ✅)

- [ ] **Step 1: Если Task 1.2 → ✅, удалить методы**

Открыть `NotificationService.php`, удалить методы `notifyLowBalance` и `notifyZeroBalance` целиком (НЕ путать с `notifyZeroBalancePaused` — оставить).

Если есть `use App\Mail\LowBalanceNotification` / `use App\Mail\ZeroBalanceNotification` — удалить эти `use`-импорты (если методы удалены).

- [ ] **Step 2: Verify нет orphan ссылок**

```bash
grep -rn "notifyLowBalance\b\|notifyZeroBalance\b" app/ | grep -v "Paused"
```

Expected: пусто.

- [ ] **Step 3: Прогнать тесты + Larastan**

```bash
php artisan test --parallel 2>&1 | tail -5
cd app && composer stan 2>&1 | tail -5
```

Expected: GREEN.

- [ ] **Step 4: Commit**

```bash
git add -A
git commit -m "refactor(billing): remove NotificationService::notifyLowBalance/notifyZeroBalance (legacy)"
```

### Task 3.6: Удалить Mailable классы `LowBalanceNotification` / `ZeroBalanceNotification` (conditional)

**Files:**

- Delete (conditional): `app/app/Mail/LowBalanceNotification.php` (только если Task 1.3 → ✅)
- Delete (conditional): `app/app/Mail/ZeroBalanceNotification.php` (только если Task 1.3 → ✅)

- [ ] **Step 1: Если Task 1.3 → ✅, удалить файлы**

```bash
rm app/app/Mail/LowBalanceNotification.php
rm app/app/Mail/ZeroBalanceNotification.php
```

NB: `ZeroBalancePausedMail.php` НЕ удаляем (это другой класс, используется шеринг-флоу).

- [ ] **Step 2: Удалить blade-шаблоны (если есть)**

```bash
ls app/resources/views/emails/billing/ 2>&1 | grep -iE "low.balance|zero.balance"
```

Если найдены `low_balance*.blade.php` / `zero_balance*.blade.php` (без `paused`) — удалить.

- [ ] **Step 3: Verify**

```bash
grep -rn "LowBalanceNotification\|ZeroBalanceNotification" app/ | grep -v "Paused"
```

Expected: пусто.

- [ ] **Step 4: Прогнать тесты**

```bash
php artisan test --parallel 2>&1 | tail -5
```

Expected: GREEN.

- [ ] **Step 5: Commit**

```bash
git add -A
git commit -m "refactor(billing): remove LowBalanceNotification/ZeroBalanceNotification mailables (legacy)"
```

### Task 3.7: Финальная регрессия Phase 3

**Files:** нет

- [ ] **Step 1: Полная регрессия (Pest + Larastan + Pint)**

```bash
cd .claude/worktrees/legacy-webhook-removal/app
php artisan test --parallel 2>&1 | tail -5
composer stan 2>&1 | tail -5
composer pint -- --test 2>&1 | tail -5
```

Expected: всё GREEN. Если Larastan ругается — обновить baseline (см. Task 3.2 Step 3).

- [ ] **Step 2: Verify нет orphan ссылок на legacy**

```bash
grep -rn "ProcessWebhookJob\|WebhookReceiveController" app/
```

Expected: пусто.

**NB:** `WebhookDedupKey` ИЗ grep-команды исключён — он остаётся живым (Task 1.5 RED FLAG, использует CSV-канал). Проверять отдельно:

```bash
grep -rn "WebhookDedupKey\|webhook_dedup_keys" app/ | grep -v Import
```

Expected: пусто (все use вне CSV-импорта удалены). Если что-то осталось — добавить inline-задачу.

- [ ] **Step 3: Без коммита (он был в каждом step'е), переходим к Phase 4**

---

## Phase 4 — Миграция БД (одна атомарная)

### Task 4.1: Написать DROP-миграцию

**Files:**

- Create: `app/database/migrations/2026_05_24_140000_drop_legacy_webhook_artefacts.php`

- [ ] **Step 1: Создать файл миграции**

Содержимое (адаптировать `down()` и conditional блоки под результаты impact-checks):

```php
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Удаление legacy-артефактов прямого webhook-канала.
     *
     * Spec: docs/superpowers/specs/2026-05-24-legacy-direct-webhook-removal-design.md
     *
     * Что удаляем (финальный список по результатам Phase 1 impact-checks):
     *   - webhook_log (partitioned, 13 партиций) — пустая на проде, источник = только удалённый ProcessWebhookJob
     *   - rejected_deals_log — writer только ProcessWebhookJob, нет readers
     *   - tenants.webhook_token + tenants.webhook_token_rotated_at — нет в UI/API, тесты почищены в Task 3.0
     *   - system_settings.low_balance_threshold_leads (seed) — только legacy
     *
     * Что НЕ удаляем (Phase 1 RED FLAG):
     *   - webhook_dedup_keys — таблица АКТИВНО используется HistoricalImportService (CSV-канал)
     *     для идемпотентности повторных импортов. Дропать = сломать резервный канал.
     *
     * pgsql_supplier connection — BYPASSRLS-роль crm_supplier_worker (паттерн Спека B):
     * под обычной crm_app_user DROP/ALTER без app.current_tenant_id GUC не пройдёт.
     */
    public function up(): void
    {
        $conn = DB::connection('pgsql_supplier');

        // Partitioned table — DROP TABLE каскадит партиции.
        $conn->statement('DROP TABLE IF EXISTS webhook_log CASCADE');

        // NB: webhook_dedup_keys НЕ дропаем — Phase 1 RED FLAG, живой через HistoricalImportService.

        // RejectedDealsLog (Task 1.4 → ✅ — нет readers):
        $conn->statement('DROP TABLE IF EXISTS rejected_deals_log CASCADE');

        // tenants.webhook_token + webhook_token_rotated_at (Task 1.6 → ✅ — нет UI; тесты почищены в Task 3.0):
        $conn->statement('ALTER TABLE tenants DROP COLUMN IF EXISTS webhook_token, DROP COLUMN IF EXISTS webhook_token_rotated_at');

        // system_settings.low_balance_threshold_leads (Task 1.7 → ✅ — только legacy seed):
        $conn->statement("DELETE FROM system_settings WHERE key = 'low_balance_threshold_leads'");
    }

    /**
     * Откат — пересоздать структуру (бессмысленно для прод-restore: данные восстанавливаются из pg_dump backup).
     * Этот метод нужен только для dev migrate:rollback, чтобы не падать.
     */
    public function down(): void
    {
        $conn = DB::connection('pgsql_supplier');

        // НЕ восстанавливаем структуру — пустая заглушка, чтобы rollback не падал.
        // Прод-restore — из pg_dump backup (см. runbook docs/deploy/test-server-runbook.md).
    }
};
```

- [ ] **Step 2: Применить в worktree (testing БД)**

```bash
cd .claude/worktrees/legacy-webhook-removal/app
php artisan migrate --env=testing
```

Expected: миграция применилась, нет ошибок. Если падает с `permission denied` — проверить, что используется `pgsql_supplier` connection.

- [ ] **Step 3: Verify в БД**

```bash
psql $DATABASE_URL -c "\dt webhook_log* rejected_deals_log" 2>&1
psql $DATABASE_URL -c "\dt webhook_dedup_keys" 2>&1  # должна ОСТАТЬСЯ (RED FLAG)
psql $DATABASE_URL -c "\d tenants" 2>&1 | grep webhook
psql $DATABASE_URL -c "SELECT * FROM system_settings WHERE key LIKE 'low_balance%'" 2>&1
```

Expected:

- `\dt webhook_log*` → empty (нет таблицы и партиций).
- `\dt webhook_dedup_keys` → 1 row (таблица ЖИВА, RED FLAG из Phase 1 — оставлена для CSV-канала).
- `\dt rejected_deals_log` → empty (если Task 1.4 → ✅).
- `\d tenants | grep webhook` → пусто (если Task 1.6 → ✅).
- `low_balance_threshold_leads` → 0 rows (если Task 1.7 → ✅).

- [ ] **Step 4: Прогнать ВСЕ тесты на свежей миграции**

```bash
php artisan migrate:fresh --env=testing
php artisan test --parallel 2>&1 | tail -5
```

Expected: GREEN. Если падает — значит какой-то живой тест ссылается на удалённую таблицу/колонку → расследовать.

- [ ] **Step 5: Commit миграции**

```bash
git add app/database/migrations/2026_05_24_140000_drop_legacy_webhook_artefacts.php
git commit -m "db(billing): миграция — DROP legacy webhook артефактов"
```

### Task 4.2: Обновить `db/schema.sql` baseline

**Files:**

- Modify: `db/schema.sql`

- [ ] **Step 1: Найти удаляемые блоки**

Открыть `db/schema.sql`. Найти и удалить:

- `CREATE TABLE webhook_log` (партиционированная) + все 13 `CREATE TABLE webhook_log_yYYYY_mMM` + их индексы.
- `CREATE TABLE webhook_dedup_keys` + индексы — **НЕ удалять из baseline** (таблица живая, RED FLAG из Phase 1).
- `CREATE TABLE rejected_deals_log` (если Task 1.4 → ✅) + индексы.
- Из `CREATE TABLE tenants` — удалить колонки `webhook_token` и `webhook_token_rotated_at` (если Task 1.6 → ✅).
- Из seed `INSERT INTO system_settings` — удалить строку `low_balance_threshold_leads` (если Task 1.7 → ✅).
- Из `CREATE POLICY` / `CREATE INDEX` — удалить упоминания удалённых таблиц.

- [ ] **Step 2: Verify schema.sql применяется в чистую БД**

```bash
# Создать пустую БД, применить schema.sql, прогнать миграции pending
createdb test_schema_baseline
psql test_schema_baseline < db/schema.sql
cd app && DATABASE_URL=postgres://...test_schema_baseline php artisan migrate --pretend
dropdb test_schema_baseline
```

Expected: `pretend` показывает что pending миграций нет (schema.sql полностью покрывает текущее состояние). Если есть pending — что-то в schema.sql упущено, поправить.

- [ ] **Step 3: Обновить header `db/schema.sql`**

Открыть `db/schema.sql:1-10`, обновить счётчики (минус 2 таблицы основные + 13 партиций + 1 conditional, минус 2 колонки в tenants). Точные числа — после Step 1.

- [ ] **Step 4: Запись в `db/CHANGELOG_schema.md`**

Добавить запись:

```markdown
## v8.XX от 2026-05-24 — Legacy webhook removal

- **DROP**: `webhook_log` (partitioned, 13 партиций), `rejected_deals_log`. **NB:** `webhook_dedup_keys` НЕ удаляется — Phase 1 RED FLAG.
- **ALTER**: `tenants` — DROP COLUMN `webhook_token`, `webhook_token_rotated_at` (conditional).
- **DELETE seed**: `system_settings.low_balance_threshold_leads` (conditional).

См. spec: `docs/superpowers/specs/2026-05-24-legacy-direct-webhook-removal-design.md`.
Миграция: `2026_05_24_140000_drop_legacy_webhook_artefacts.php`.
```

- [ ] **Step 5: Commit**

```bash
git add db/schema.sql db/CHANGELOG_schema.md
git commit -m "db(schema): sync baseline — drop legacy webhook tables/columns"
```

---

## Phase 5 — Финальная регрессия + Code Review

### Task 5.1: Полная регрессия

**Files:** нет

- [ ] **Step 1: Полный прогон — Pest + Larastan + Pint + Vitest + Vite build + lefthook checks**

В worktree:

```bash
cd .claude/worktrees/legacy-webhook-removal/app

# Backend
php artisan test --parallel 2>&1 | tail -5
composer stan 2>&1 | tail -5
composer pint -- --test 2>&1 | tail -5

# Frontend (если задеваем — не должно)
npm run test:vue 2>&1 | tail -5
npm run build 2>&1 | tail -5

cd ..
# Корневые проверки
npm run lint:md 2>&1 | tail -5
npm run links 2>&1 | tail -5
```

Expected: всё GREEN. Любой RED — расследовать, не оставлять.

- [ ] **Step 2: Verify deptrac (architecture fitness)**

```bash
cd .claude/worktrees/legacy-webhook-removal/app
composer deptrac 2>&1 | tail -10
```

Expected: 0 нарушений.

- [ ] **Step 3: Запустить regression skill**

REQUIRED SUB-SKILL: `regression` (full mode).

Run: `/regression full`

Expected: вердикт **GREEN**.

### Task 5.2: Запросить subagent code-review

**Files:** нет

- [ ] **Step 1: REQUIRED SUB-SKILL: `superpowers:requesting-code-review`**

Дать ревьюеру:

- Diff с main: `git diff origin/main..feat/legacy-webhook-removal`
- Спек: `docs/superpowers/specs/2026-05-24-legacy-direct-webhook-removal-design.md`
- План: `docs/superpowers/plans/2026-05-24-legacy-direct-webhook-removal.md`

Фокус ревью:

1. Нет ли удалений, ломающих живой код (шеринг-канал / CSV reconcile / админка)?
2. Все ли impact-checks выполнены и зафиксированы (Phase 1)?
3. Atomic-коммиты — каждый отдельно собирается и тесты GREEN?
4. Миграция — есть ли `IF EXISTS` для идемпотентности?

- [ ] **Step 2: Исправить замечания, если есть**

Каждое замечание — отдельный мелкий fixup-коммит.

---

## Phase 6 — Merge в main + деплой на боевой

### Task 6.1: Merge feat/legacy-webhook-removal → main

**Files:** нет

- [ ] **Step 1: Pre-flight sync (Pravila §15.2)**

В worktree:

```bash
git fetch origin
git log feat/legacy-webhook-removal..origin/main --oneline
```

Expected: пусто (main не ушёл вперёд). Если что-то есть — rebase на свежий main, ре-регрессия.

- [ ] **Step 2: Push ветки**

```bash
git push -u origin feat/legacy-webhook-removal
```

- [ ] **Step 3: FF-merge в main (без PR на GitHub, прямой push)**

```bash
git checkout main
git pull origin main
git merge --ff-only feat/legacy-webhook-removal
git push origin main
```

Expected: FF без конфликтов.

### Task 6.2: Деплой на боевой liderra.ru

**Files:** нет (SSH-операция)

REQUIRED REFERENCE: `docs/deploy/test-server-runbook.md` + memory `ПИЛОТ.md`.

- [ ] **Step 1: Бэкап перед миграцией**

```bash
ssh -i ~/.ssh/liderra_deploy ubuntu@111.88.246.137 \
  'sudo -u postgres pg_dump liderra > /home/ubuntu/deploy-backups/liderra-schema-pre-legacy-webhook-removal-$(date +%Y%m%d-%H%M%S).sql'
```

Expected: файл создан в `/home/ubuntu/deploy-backups/`.

- [ ] **Step 2: Деплой кода (scp + tar + redeploy.sh)**

```bash
# Из локального main (после merge):
git archive feat/legacy-webhook-removal | ssh -i ~/.ssh/liderra_deploy ubuntu@111.88.246.137 \
  'cd /var/www/liderra && sudo tar -xf - && sudo bash redeploy.sh'
```

Expected: composer install no-op, migrate применяет одну миграцию (legacy DROP), cache rebuild, php-fpm reload.

- [ ] **Step 3: Smoke на проде**

```bash
ssh -i ~/.ssh/liderra_deploy ubuntu@111.88.246.137 'sudo -u postgres psql -d liderra <<EOF
SELECT table_name FROM information_schema.tables WHERE table_name IN ('webhook_log', 'rejected_deals_log');
-- ожидается: 0 rows (webhook_dedup_keys НЕ проверяем — она ОСТАЛАСЬ для CSV-канала)
SELECT column_name FROM information_schema.columns WHERE table_name='tenants' AND column_name LIKE 'webhook_%';
EOF'

curl -X POST https://liderra.ru/api/webhook/test-token -d '{}' -i 2>&1 | head -3
curl -X POST https://liderra.ru/api/webhook/supplier/test -d '{}' -i 2>&1 | head -3
```

Expected:

- SQL: 0 строк в обоих запросах (всё удалено).
- `/api/webhook/{token}` → **404**.
- `/api/webhook/supplier/{secret}` → **403/401** (auth fail на тестовом secret, но роут существует) — это OK, шеринг-канал жив.

- [ ] **Step 4: Обновить ПИЛОТ.md**

Добавить entry в ПИЛОТ.md (через `revise-claude-md` или прямой Edit — следуя Pravila §5.10 worktree-эксцепшну).

- [ ] **Step 5: Commit ПИЛОТ.md**

```bash
git add ПИЛОТ.md
git commit -m "docs(pilot): legacy webhook removal — выкачено на боевой $(date +%Y-%m-%d)"
git push origin main
```

### Task 6.3: 7-дневное наблюдение

**Files:** нет

- [ ] **Step 1: Сразу после деплоя — мониторинг 1 час**

Проверять каждые 15 минут:

```bash
ssh -i ~/.ssh/liderra_deploy ubuntu@111.88.246.137 \
  'sudo journalctl -u liderra-queue --since "15 min ago" --no-pager | tail -20'
```

Expected: 0 errors, обычная активность шеринг-канала.

- [ ] **Step 2: 1 день — проверить nginx access log**

```bash
ssh -i ~/.ssh/liderra_deploy ubuntu@111.88.246.137 \
  'sudo grep "POST /api/webhook/" /var/log/nginx/access.log | grep -v "supplier" | head -20'
```

Expected: пусто или единичные 404 от случайных сканеров. Если кто-то реально начнёт долбиться (что маловероятно: 0 за всю историю) — это сигнал к ретроспективе, но не блокер.

- [ ] **Step 3: 7 дней — обновить memory project_billing_v2.md**

После 7-дневного наблюдения без инцидентов — добавить запись в `memory/project_billing_v2.md` о закрытии legacy webhook removal. **Снять блокер для Спека A Phase B** — пометить, что окно открыто.

### Task 6.4: Cleanup worktree

**Files:** нет

- [ ] **Step 1: После наблюдения — удалить worktree**

```bash
git worktree remove .claude/worktrees/legacy-webhook-removal
git branch -D feat/legacy-webhook-removal  # local
git push origin --delete feat/legacy-webhook-removal  # remote
```

Expected: worktree удалён, ветки нет.

---

## Self-Review (заполнено перед commit плана)

**Spec coverage (sweep по §-секциям спека):**

- §1 контекст → объяснён в «Контекст для исполнителя» + Phase 0.
- §2.1 что удаляем → Tasks 2.1, 3.1-3.6, 4.1.
- §2.2 что НЕ трогаем → явные NB в Tasks 2.2 (notifyZeroBalancePaused), 3.6 (ZeroBalancePausedMail), 4.1 (failed_webhook_jobs/SupplierLeadCost/MonthlyPartitionManager не упоминаются).
- §3 impact-checks → Phase 1 целиком (Tasks 1.1-1.8).
- §4.1 главный инвариант → Task 5.1.
- §4.2 что меняется в публичном API → Task 6.2 smoke (404).
- §4.3 откатываемость → Task 6.2 backup + Task 4.1 down() заглушка.
- §4.4 совместимость со Спеком A/B/C → Task 6.3 (снять блокер Phase B).
- §5 тестирование → Phase 5 + Phase 6 smoke.
- §6 выкатка → Phase 6.

**Placeholder scan:** проверено, нет TBD/TODO/«similar to», все шаги конкретные.

**Type consistency:** имена классов (`ProcessWebhookJob`, `WebhookReceiveController`, `WebhookDedupKey`, `RejectedDealsLog`, `LowBalanceNotification`, `ZeroBalanceNotification`, `ZeroBalancePausedMail`) единообразны между задачами.

**Conditional consistency:** все conditional-задачи ссылаются на правильные impact-checks (Task 1.X), решение фиксируется в Task 1.8.

---

## Связано

- Спек: [docs/superpowers/specs/2026-05-24-legacy-direct-webhook-removal-design.md](../specs/2026-05-24-legacy-direct-webhook-removal-design.md)
- Спек A (Phase B blocked by this plan): [docs/superpowers/specs/2026-05-23-billing-v2-spec-a-balance-rub-design.md](../specs/2026-05-23-billing-v2-spec-a-balance-rub-design.md)
- Memory: `project_billing_v2.md` (обновить после Task 6.3).
- Memory: `ПИЛОТ.md` (обновить в Task 6.2 Step 4).
- Runbook: `docs/deploy/test-server-runbook.md` (бэкап + redeploy).
- Pravila §15.1 (subagent git safety) / §15.2 (pre-flight sync).
