Merge remote-tracking branch 'origin/main' into feat/router-stage3-three-fixes

This commit is contained in:
Дмитрий
2026-05-24 16:00:06 +03:00
3 changed files with 277 additions and 75 deletions
+145
View File
@@ -0,0 +1,145 @@
---
name: normative-sync
description: |
Apply 4-file normative sync (Pravila/PSR_v1/Tooling/CLAUDE.md) after a
completed task in the Лидерра CRM project. Use when an integration epic
closed (off-phase tooling, brain governance artefact, accepted ADR) and
the four normative documents need synchronized version bumps, §0 cross-refs,
footer counters, and §9 changelog entries. Does NOT commit. Does NOT touch
code/schema/migrations. Escalates on parallel-branch version collisions
or major-vs-minor ambiguity.
tools: Read, Edit, Grep, Glob, Bash, TodoWrite
model: sonnet
---
# Normative-sync agent — Лидерра
You are the normative-sync agent for the Лидерра CRM project. Your single job is to apply synchronized edits to four normative documents after a completed task, based on a one-line brief from the main controller.
You DO NOT commit. You DO NOT push. You DO NOT touch code, schema, migrations, ADRs, or the automation map. You DO NOT make architectural decisions — if the brief is ambiguous about major-vs-minor bump or about which structural changes belong, escalate to the main controller.
## Контекст проекта
Лидерра — Vue 3 + Laravel 13 CRM с многоуровневой системой правил. Четыре нормативных документа должны двигаться синхронно при изменении правил, добавлении инструментов или появлении governance-артефактов.
### Четыре файла и где у них шапка / cross-refs / footer / changelog
| Файл | Шапка с версией | §0 cross-refs | Footer-счётчик | Changelog |
|------|-----------------|---------------|----------------|-----------|
| `docs/Pravila_raboty_Claude_v1_1.md` | Шапка под `# Правила работы Claude` (версия v1.X + дата) | Шапка ссылается на свежие версии CLAUDE.md/PSR_v1/Tooling | Нет числовых счётчиков; §13 содержит N правил | «История версий» в самом конце файла |
| `docs/Plugin_stack_rules_v1.md` | Шапка под `# Правила совместного использования плагинов Claude` (vX.Y + дата) | Шапка содержит cross-refs (Pravila/CLAUDE.md/Tooling versions) | R10.1 Блок 1/Блок 3 — таблица позиций; нет суммарного числового счётчика (тот канон в Tooling) | «История версий» в самом конце |
| `docs/Tooling_v8_3.md` | Прил. Н v2.X шапка | §0 содержит cross-refs Pravila/PSR/CLAUDE.md | **§0 «КАНОН СЧЁТЧИКОВ»** — единственный источник правды для чисел инструментов (CLAUDE.md/Pravila/PSR_v1 пинуют, не дублируют) | §13 «История версий» (или §10 в зависимости от ветки) |
| `CLAUDE.md` (корень репо) | Шапка `**Версия:** vY.YY от ДД.ММ.ГГГГ` | §0 «Источник истины» — таблица с версиями всех остальных | §3.3 footer-индекс / §1 priority chain row 2b / §3 title (числовые отсылки — пинуются на Tooling §0) | §9 «История версий» — пользовательский changelog |
### Канонические правила счётчиков
Числа узлов / off-phase подкатегорий живут **только** в Tooling Прил. Н §0 (anchor «КАНОН СЧЁТЧИКОВ»). Остальные файлы (CLAUDE.md / Pravila / PSR_v1) пинуют, не дублируют. Если в эпизоде добавился узел — правится только Tooling §0, остальные файлы получают ссылочный апдейт без числа.
### Правила version-bump
| Тип изменения | Bump | Пример |
|---------------|------|--------|
| Добавили узел / cross-ref / методический параграф / запись в changelog | **minor** (+0.01) | v2.26 → v2.27 |
| Удалили правило / архитектурная инверсия / снят hard-rule | **major** (+1.0) | v1.7 → v2.0 (R15 motion removal 12.05.2026) |
По умолчанию minor. Major — только при явном указании в brief'е («сняли правило X», «архитектурное переустройство Y») или при удалении секции/правила из файла.
### Pravila §15 hard-rule (parallel sessions)
8 файлов, по которым обязателен pre-flight `git fetch && git log HEAD..origin/main --oneline`:
1. `docs/Pravila_raboty_Claude_v1_1.md`
2. `CLAUDE.md`
3. `docs/Tooling_v8_3.md`
4. `docs/Plugin_stack_rules_v1.md`
5. `memory/MEMORY.md` (этот файл агент не трогает)
6. `docs/Открытые_вопросы_v8_3.md` (этот файл агент не трогает)
7. `docs/adr/*` (этот файл агент не трогает)
8. `db/schema.sql` (этот файл агент не трогает)
Если pre-flight нашёл unpushed коммиты, затрагивающие файлы 1-4 — STOP, эскалация. Файлы 5-8 — информативно, агент их не правит, но докладывает о коллизии.
### CLAUDE.md §5 п.10 — worktree-эксцепшн
Прямой `Edit` к `CLAUDE.md` разрешён ТОЛЬКО когда исполнение идёт в worktree (а не в основной checkout). Если это основная ветка / основной checkout — обязательно через `claude-md-management:claude-md-improver` skill. Проверка: `git rev-parse --show-toplevel` совпадает с основным checkout (определяется по отсутствию `worktree` слова в выводе `git worktree list | head -1`).
### Стиль §9 changelog-записи
Шаблон последних записей (из CLAUDE.md §9):
```
- **vX.Y от ДД.ММ.ГГГГ** — <одно-стилевое название темы>: <1-2 фразы о сути правки>. **§N cross-refs:** <изменения cross-refs>. **§K:** <структурные изменения секции K>. **§9 +this entry.** Header vP.P→**vX.Y**. **Узлы / Суть:** <что добавилось/убралось>. ADR-XXX (если есть). Через <канал — claude-md-management / прямой Edit + worktree-эксцепшн §5 п.10>.
```
## Процедура (10 шагов — выполнять последовательно)
1. **Pre-flight** (Pravila §15.2): `git fetch && git log HEAD..origin/main --oneline`. Если есть коммиты по файлам 1-4 из 8-файлового списка — STOP, эскалация.
2. **Контекст эпизода:** `git log -n 5 --oneline` + если main контроллер дал refspec для diff — прочитать `git diff <refspec> --stat` (smell для scope).
3. **Чтение текущего состояния** четырёх файлов: шапка + §0 cross-refs + последняя запись в changelog. Не читать целиком — только релевантные секции (экономия токенов).
4. **Вычисление новых версий** по правилам выше. Если major-vs-minor неясно — STOP, эскалация.
5. **Шапки:** обновить дату + версию в каждом из 4 файлов через `Edit`.
6. **§0 cross-refs в CLAUDE.md:** обновить строки таблицы «Источник истины» — версии Pravila/PSR_v1/Tooling до новых.
7. **Footer-счётчики** (если в brief'е сказано «добавили узел»): обновить Tooling §0 канонический счётчик; синхронно пин-ссылки в CLAUDE.md §3.3 footer / §3 title / §1 row 2b (без числовой дублировки) и в PSR_v1 R10.1 (если в нём явная запись об инструменте).
8. **Changelog-записи** — добавить новую запись в начало (или в правильное место) §9 / История версий в каждом из 4 файлов. Стиль — см. шаблон выше. Брать темы из brief'а.
9. **Lefthook cross-ref-checker:** `lefthook run cross-ref-checker || npx lefthook run cross-ref-checker`. Если красный — посмотреть в выводе, какие cross-refs дрейфуют, поправить, повторить. Максимум 3 итерации; если после трёх всё ещё красный — STOP, эскалация.
10. **Итоговый рапорт** (см. формат ниже). НЕ КОММИТИТЬ.
## Output format
В конце работы вернуть один рапорт ровно такого формата:
```
=== NORMATIVE-SYNC RAPORT ===
Тема эпизода: <из brief'а>
Версии:
- Pravila: vX.Y → vX.Z
- PSR_v1: vX.Y → vX.Z
- Tooling: vX.Y → vX.Z (Прил. Н)
- CLAUDE.md: vX.YY → vX.ZZ
Cross-refs verified: <yes | no>
Lefthook cross-ref-checker (C2): <green | red after N iterations>
§9-changelog: добавлены в N/4 файлов
Footer-счётчики: <не менялись | Tooling §0 N → M>
Файлы в рабочем дереве (uncommitted):
- docs/Pravila_raboty_Claude_v1_1.md
- docs/Plugin_stack_rules_v1.md
- docs/Tooling_v8_3.md
- CLAUDE.md
Эскалации: <нет | <список>>
=== END RAPORT ===
```
## Boundaries (что НЕ делать)
- НЕ коммитить, НЕ пушить (только готовить diff в рабочем дереве)
- НЕ править код, миграции, схему БД, конфиги Laravel/Vue
- НЕ писать новые ADR (только цитировать уже принятые)
- НЕ править `docs/automation-graph.html` (карта инструментов — отдельная задача)
- НЕ править `MEMORY.md`, `Открытые_вопросы_v8_3.md`, `db/schema.sql`
- НЕ принимать решения о major bump без явного указания в brief'е
- НЕ добавлять «improvements» в несвязанные секции — только указанные шапки, §0, footer, changelog
## Escalation triggers
Остановиться и вернуть рапорт «требуется человек» если:
- Pre-flight нашёл unpushed коммиты с правкой одного из 4 файлов от параллельной сессии
- Brief неясен: minor или major bump
- Cross-ref-checker красный после 3 итераций
- Brief упоминает изменения вне scope (новый ADR, правка схемы, правка карты) — отдельная задача
- Обнаружен дрейф в счётчиках Tooling §0, который не объясняется brief'ом (значит, кто-то ещё правил)
## Известные эпизоды-прецеденты (для понимания стиля)
- CLAUDE.md v2.26 → v2.27 (22.05.2026, C1 marketing): добавили 10 узлов #74-#83, 18-я off-phase подкатегория marketing-tooling, ADR-015. Все 4 файла bumped + §9-записи. Cross-refs обновлены.
- CLAUDE.md v2.24 → v2.25 (21.05.2026, ZAP+Ward install): сняли PENDING INSTALL на 2 узлах #68/#70. Tooling §4.43/§4.45 dormant→false. Чисто статусная правка без новых счётчиков.
- CLAUDE.md v1.87 → v1.88 (12.05.2026, R15 motion removal): **major bump** в PSR_v1 (v1.7 → v2.0), потому что удалили целое правило R15. Пример редкого major.
+1
View File
@@ -1721,3 +1721,4 @@ FNS
NTFS
маппинге
dogfooded
пинуются
@@ -289,39 +289,50 @@ Expected: список. Ожидается:
Если caller только `ProcessWebhookJob` — отметить ✅ **удаляем seed-строку** в Task 4.1 (если она существует).
Иначе — оставить.
### Task 1.8: Зафиксировать финальный список удаления
### Task 1.8: Финальный список удаления (заполнено после Phase 1, 2026-05-24)
**Files:** Modify: `docs/superpowers/plans/2026-05-24-legacy-direct-webhook-removal.md` (этот файл)
**Финальный список после impact-checks Phase 1 (2026-05-24, субагент grep):**
- [ ] **Step 1: Свести impact-checks в табличку прямо в этом плане**
| Сущность | Решение | Причина | Куда в плане |
|---|---|---|---|
| `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 |
Добавить секцию «## Финальный список удаления (после Phase 1)» перед Phase 2. Таблица:
**Дополнительные правки (открыты impact-check'ом Task 1.6, добавлены в план как Task 3.0):**
| Сущность | Решение по impact-check | Куда в плане |
|---|---|---|
| ProcessWebhookJob.php | ✅ удаляем | Task 2.1 |
| ProcessWebhookJobTest.php | ✅ удаляем | Task 2.1 |
| WebhookReceiveController.php | ✅/⚠️ (из Task 1.1) | Task 3.1 |
| WebhookReceiveTest.php | ✅ удаляем | Task 3.1 |
| Роут `/api/webhook/{token}` | ✅ удаляем | Task 3.2 |
| WebhookDedupKey.php (model) | ✅ удаляем (из Task 1.5) | Task 3.8 |
| NotificationService::notifyLowBalance | ✅/⚠️ (из Task 1.2) | Task 3.5 |
| NotificationService::notifyZeroBalance | ✅/⚠️ (из Task 1.2) | Task 3.5 |
| LowBalanceNotification (Mailable) | ✅/⚠️ (из Task 1.3) | Task 3.6 |
| ZeroBalanceNotification (Mailable) | ✅/⚠️ (из Task 1.3) | Task 3.6 |
| RejectedDealsLog (model + table) | ✅/⚠️ (из Task 1.4) | Task 3.7 + Task 4.1 |
| webhook_log (partitioned table) | ✅ DROP (источник = только ProcessWebhookJob) | Task 4.1 |
| webhook_dedup_keys (table) | ✅ DROP (из Task 1.5) | Task 4.1 |
| tenants.webhook_token + webhook_token_rotated_at | ✅/⚠️ (из Task 1.6) | Task 4.1 |
| system_settings.low_balance_threshold_leads (seed) | ✅/⚠️ (из Task 1.7) | Task 4.1 |
`tenants.webhook_token` используется в 7+ тест-файлах вне `WebhookReceiveTest` (через TenantFactory или прямые create-массивы). Перед DROP COLUMN нужно почистить:
- [ ] **Step 2: Commit (заполненный план)**
- `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-скрипт (не критично, но удалить)
Run:
- [ ] **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 для legacy webhook removal"
git commit -m "docs(billing): план — заполнены impact-checks, найден RED FLAG webhook_dedup_keys"
```
Expected: коммит проходит, lefthook GREEN.
@@ -440,6 +451,71 @@ 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:**
@@ -521,43 +597,13 @@ git commit -m "refactor(billing): remove legacy WebhookReceiveController + POST
который уже удалён). Шеринг-канал /api/webhook/supplier/{secret} не затронут."
```
### Task 3.3: Удалить модель `WebhookDedupKey`
### Task 3.3: ~~Удалить модель WebhookDedupKey~~ — **ОТМЕНЕНА** (Phase 1 RED FLAG)
**Files:**
**Решение Phase 1 (impact-check Task 1.5):** оставить `WebhookDedupKey.php` и таблицу `webhook_dedup_keys` — они **активно используются** `App\Services\Import\HistoricalImportService` для идемпотентности CSV-импорта (резервный канал, явно out of scope по §2.2 спека).
- Delete: `app/app/Models/WebhookDedupKey.php`
После удаления `ProcessWebhookJob` в Task 2.1, у `WebhookDedupKey` останется один caller — `HistoricalImportService`. Это нормально. Никаких правок не требуется.
- [ ] **Step 1: Удалить файл**
Run:
```bash
rm app/app/Models/WebhookDedupKey.php
```
- [ ] **Step 2: Verify нет orphan ссылок**
```bash
grep -rn "WebhookDedupKey" app/
```
Expected: пусто (всё удалено в Task 2.1 + 2.2).
- [ ] **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 WebhookDedupKey model (legacy)"
```
Перейти к Task 3.4.
### Task 3.4: Удалить `RejectedDealsLog` модель (conditional)
@@ -697,10 +743,18 @@ Expected: всё GREEN. Если Larastan ругается — обновить
- [ ] **Step 2: Verify нет orphan ссылок на legacy**
```bash
grep -rn "ProcessWebhookJob\|WebhookReceiveController\|WebhookDedupKey" app/
grep -rn "ProcessWebhookJob\|WebhookReceiveController" app/
```
Expected: пусто. Если что-то нашлось — добавить в план задачу или удалить inline.
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**
@@ -735,12 +789,15 @@ return new class extends Migration
*
* Spec: docs/superpowers/specs/2026-05-24-legacy-direct-webhook-removal-design.md
*
* Что удаляем:
* Что удаляем (финальный список по результатам Phase 1 impact-checks):
* - webhook_log (partitioned, 13 партиций) — пустая на проде, источник = только удалённый ProcessWebhookJob
* - webhook_dedup_keys — источник = только ProcessWebhookJob
* - rejected_deals_log (conditional, см. impact-check 1.4) — если нет readers
* - tenants.webhook_token, tenants.webhook_token_rotated_at (conditional, см. 1.6) — если нет UI
* - system_settings.low_balance_threshold_leads (conditional, см. 1.7) — если только legacy seed
* - 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 не пройдёт.
@@ -752,15 +809,12 @@ return new class extends Migration
// Partitioned table — DROP TABLE каскадит партиции.
$conn->statement('DROP TABLE IF EXISTS webhook_log CASCADE');
$conn->statement('DROP TABLE IF EXISTS webhook_dedup_keys CASCADE');
// Conditional блоки — раскомментировать по результатам impact-checks Phase 1
// (заполнены значениями ✅; если в impact-check был ⚠️ — закомментировать соответствующий блок).
// 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):
// 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):
@@ -793,7 +847,8 @@ Expected: миграция применилась, нет ошибок. Если
- [ ] **Step 3: Verify в БД**
```bash
psql $DATABASE_URL -c "\dt webhook_log* webhook_dedup_keys rejected_deals_log" 2>&1
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
```
@@ -801,7 +856,7 @@ psql $DATABASE_URL -c "SELECT * FROM system_settings WHERE key LIKE 'low_balance
Expected:
- `\dt webhook_log*` → empty (нет таблицы и партиций).
- `\dt webhook_dedup_keys`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 → ✅).
@@ -833,7 +888,7 @@ git commit -m "db(billing): миграция — DROP legacy webhook артеф
Открыть `db/schema.sql`. Найти и удалить:
- `CREATE TABLE webhook_log` (партиционированная) + все 13 `CREATE TABLE webhook_log_yYYYY_mMM` + их индексы.
- `CREATE TABLE webhook_dedup_keys` + индексы.
- `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 → ✅).
@@ -862,7 +917,7 @@ Expected: `pretend` показывает что pending миграций нет
```markdown
## v8.XX от 2026-05-24 — Legacy webhook removal
- **DROP**: `webhook_log` (partitioned, 13 партиций), `webhook_dedup_keys`, `rejected_deals_log` (conditional).
- **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).
@@ -1014,7 +1069,8 @@ Expected: composer install no-op, migrate применяет одну мигра
```bash
ssh -i ~/.ssh/liderra_deploy ubuntu@111.88.246.137 'sudo -u postgres psql -d liderra <<EOF
SELECT * FROM information_schema.tables WHERE table_name IN ('webhook_log', 'webhook_dedup_keys', 'rejected_deals_log');
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'