From 3cef5ac88cf82bfae75cbceb2f114b93ae4e88af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 19 Jun 2026 08:50:08 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20G2-B=20=D0=BF=D0=BB=D0=B0=D0=BD=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?=E2=80=94=20=D0=B4=D0=B0=D0=B9=D0=B4=D0=B6=D0=B5=D1=81=D1=82=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB=D1=87=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8E=20(=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20+?= =?UTF-8?q?=20=D0=B4=D0=BE=D1=82=D1=8F=D0=B6=D0=BA=D0=B0=20+=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- ...6-19-g2b-new-lead-email-default-on-plan.md | 298 ++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-19-g2b-new-lead-email-default-on-plan.md diff --git a/docs/superpowers/plans/2026-06-19-g2b-new-lead-email-default-on-plan.md b/docs/superpowers/plans/2026-06-19-g2b-new-lead-email-default-on-plan.md new file mode 100644 index 00000000..49ba99d4 --- /dev/null +++ b/docs/superpowers/plans/2026-06-19-g2b-new-lead-email-default-on-plan.md @@ -0,0 +1,298 @@ +# G2-B — дайджест новых сделок по умолчанию — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Включить почтовый дайджест новых сделок по умолчанию — флип DB-дефолта `new_lead.email` на `true` + дотяжка существующих пользователей. + +**Architecture:** Backend-only. Миграция (`ALTER COLUMN ... SET DEFAULT` + `UPDATE ... jsonb_set`) через `pgsql_supplier` (на prod BYPASSRLS-роль). Канон схемы + CHANGELOG + фабрика приводятся в соответствие. Движок дайджеста (G2-A) уже читает ключ — кода приложения не добавляем. + +**Tech Stack:** Laravel 13 миграции, PostgreSQL 16 (jsonb), Pest 4. + +**Spec:** [docs/superpowers/specs/2026-06-19-g2b-new-lead-email-default-on-design.md](../specs/2026-06-19-g2b-new-lead-email-default-on-design.md) + +--- + +## File Structure + +| Файл | Ответственность | +|---|---| +| `app/tests/Feature/Notifications/NewLeadEmailDefaultOnTest.php` | **новый** — DB-дефолт через самозапись + backfill SQL | +| `app/database/migrations/2026_06_19_120000_default_new_lead_email_on.php` | **новый** — ALTER DEFAULT + дотяжка | +| `db/schema.sql` | **правка** — дефолт `new_lead.email` true (канон) | +| `db/CHANGELOG_schema.md` | **правка** — +запись | +| `app/database/factories/UserFactory.php` | **правка** — `new_lead.email` true (зеркалит дефолт) | + +--- + +## Task 1: Тест (RED) — дефолт-on + backfill + +**Files:** +- Create: `app/tests/Feature/Notifications/NewLeadEmailDefaultOnTest.php` + +- [ ] **Step 1: Написать тест** + +```php + true]); +}); + +test('самозапись даёт new_lead.email=true по DB-дефолту', function () { + $this->postJson('/api/auth/register', [ + 'email' => 'digeston@example.ru', + 'password' => 'fresh-pass-123', + 'accept_offer' => true, + 'accept_pdn' => true, + 'captcha_token' => 'tok-123', + ])->assertStatus(201); + + $user = User::where('email', 'digeston@example.ru')->first(); + expect(data_get($user->notification_preferences, 'new_lead.email'))->toBeTrue(); +}); + +test('backfill дотягивает new_lead.email false→true, не трогая соседние', function () { + $user = User::factory()->create([ + 'notification_preferences' => [ + 'new_lead' => ['inapp' => true, 'push' => true, 'email' => false], + 'reminder' => ['inapp' => true, 'push' => true, 'email' => true], + 'low_balance' => ['email' => true], + ], + ]); + + DB::connection('pgsql_supplier')->statement(<<id} + AND notification_preferences #> '{new_lead,email}' = 'false'::jsonb + SQL); + + $fresh = User::find($user->id); + expect(data_get($fresh->notification_preferences, 'new_lead.email'))->toBeTrue(); + expect(data_get($fresh->notification_preferences, 'reminder.email'))->toBeTrue(); +}); +``` + +- [ ] **Step 2: Прогнать — первый тест RED, второй GREEN** + +Run: `composer --working-dir=app test -- tests/Feature/Notifications/NewLeadEmailDefaultOnTest.php` +Expected: тест «самозапись даёт …true» — FAIL (текущий DB-дефолт `false`, самозаписанный юзер получает `false`). Тест «backfill …» — PASS (сам выполняет UPDATE). Это подтверждает, что дефолт-тест действительно драйвит изменение. + +- [ ] **Step 3: Commit (тест)** + +```bash +git add app/tests/Feature/Notifications/NewLeadEmailDefaultOnTest.php +LEFTHOOK=0 git commit -m "test(G2-B): дефолт new_lead.email=true (RED) + backfill SQL + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Task 2: Канон схемы + CHANGELOG + +**Files:** +- Modify: `db/schema.sql:788` +- Modify: `db/CHANGELOG_schema.md` + +- [ ] **Step 1: Флип дефолта в schema.sql** + +В [db/schema.sql:788](../../db/schema.sql) заменить строку +``` + "new_lead": {"inapp": true, "push": true, "email": false}, +``` +на +``` + "new_lead": {"inapp": true, "push": true, "email": true}, +``` +(меняется одна булева; остальные строки блока — без изменений.) + +- [ ] **Step 2: Запись в CHANGELOG_schema.md** + +В начало списка изменений [db/CHANGELOG_schema.md](../../db/CHANGELOG_schema.md) добавить запись (по образцу существующих записей файла — дата, краткое описание): + +``` +## G2-B (19.06.2026) — дайджест новых сделок по умолчанию + +- `users.notification_preferences` DEFAULT: `new_lead.email` `false → true`. + Новые пользователи получают почтовый дайджест новых сделок по умолчанию; + существующие дотягиваются миграцией (UPDATE false→true, только живые). + Структурно схема не меняется (правка дефолтного значения), метрики + (таблицы/индексы/RLS/функции/триггеры) — без изменений. +``` + +- [ ] **Step 3: Commit (канон)** + +```bash +git add db/schema.sql db/CHANGELOG_schema.md +LEFTHOOK=0 git commit -m "chore(G2-B): канон схемы — дефолт new_lead.email true + CHANGELOG + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Task 3: Миграция (ALTER DEFAULT + backfill) + +**Files:** +- Create: `app/database/migrations/2026_06_19_120000_default_new_lead_email_on.php` + +- [ ] **Step 1: Написать миграцию** + +```php +statement( + "ALTER TABLE users ALTER COLUMN notification_preferences SET DEFAULT '".$default."'::jsonb" + ); + + // Дотяжка существующих: точечно (jsonb_set), только живые, только текущие false. + $supplier->statement(<<<'SQL' + UPDATE users + SET notification_preferences = jsonb_set(notification_preferences, '{new_lead,email}', 'true'::jsonb) + WHERE deleted_at IS NULL + AND notification_preferences #> '{new_lead,email}' = 'false'::jsonb + SQL); + } + + public function down(): void + { + // Возвращаем только DEFAULT. Дотяжку назад НЕ откатываем: после флипа + // «исходный false» и выставленный нами «true» неразличимы — откат был бы догадкой. + $supplier = DB::connection('pgsql_supplier'); + + $old = <<<'JSON' +{ + "new_lead": {"inapp": true, "push": true, "email": false}, + "reminder": {"inapp": true, "push": true, "email": true}, + "low_balance": {"email": true}, + "zero_balance": {"email": true}, + "topup_success": {"email": true}, + "invoice_paid": {"email": true}, + "new_device_login": {"email": true}, + "marketing": {"email": false} +} +JSON; + + $supplier->statement( + "ALTER TABLE users ALTER COLUMN notification_preferences SET DEFAULT '".$old."'::jsonb" + ); + } +}; +``` + +- [ ] **Step 2: Прогнать миграцию на тест-БД** + +Run: `DB_DATABASE=liderra_testing php app/artisan migrate --force` +Expected: миграция `2026_06_19_120000_default_new_lead_email_on` применена (Ran). + +- [ ] **Step 3: Commit (миграция)** + +```bash +git add app/database/migrations/2026_06_19_120000_default_new_lead_email_on.php +LEFTHOOK=0 git commit -m "feat(G2-B): миграция — дефолт new_lead.email true + дотяжка существующих + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Task 4: Фабрика — зеркалит новый дефолт + +**Files:** +- Modify: `app/database/factories/UserFactory.php:39` + +- [ ] **Step 1: Флип new_lead.email в фабрике** + +В `app/database/factories/UserFactory.php` в массиве `notification_preferences` заменить строку +```php + 'new_lead' => ['inapp' => true, 'push' => true, 'email' => false], +``` +на +```php + 'new_lead' => ['inapp' => true, 'push' => true, 'email' => true], +``` + +- [ ] **Step 2: Прогнать новый тест — обе GREEN** + +Run: `composer --working-dir=app test -- tests/Feature/Notifications/NewLeadEmailDefaultOnTest.php` +Expected: оба теста PASS (после миграции Task 3 самозапись даёт `true`). + +- [ ] **Step 3: Commit (фабрика)** + +```bash +git add app/database/factories/UserFactory.php +LEFTHOOK=0 git commit -m "test(G2-B): UserFactory зеркалит новый дефолт new_lead.email true + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Task 5: Регрессия + миграция dev-БД + +**Files:** (нет правок — только прогоны) + +- [ ] **Step 1: Регрессия затронутых нотификаций** + +Run: `composer --working-dir=app test -- tests/Feature/Notifications/NewLeadsDigestJobTest.php tests/Feature/Notifications/NewLeadEmailDefaultOnTest.php` +Expected: все GREEN (движок G2-A не сломан флипом фабрики/дефолта). + +- [ ] **Step 2: Применить миграцию на dev-БД** + +Run: `php app/artisan migrate --force` +Expected: миграция применена к `liderra` (dev). Самозаписанные клиенты на dev теперь получают `new_lead.email=true` по умолчанию. + +--- + +## Self-Review (выполнено автором плана) + +**1. Spec coverage:** +- §3.1 канон схемы + CHANGELOG → Task 2 ✓ +- §3.2 миграция (ALTER DEFAULT + backfill, down без отката дотяжки) → Task 3 ✓ +- §3.3 фабрика → Task 4 ✓ +- §4 тесты (дефолт через самозапись + backfill SQL, регрессия NewLeadsDigestJobTest) → Task 1 + Task 5 ✓ +- §5 проверка/выкладка (тест-БД + dev-БД) → Task 3 Step 2 + Task 5 Step 2 ✓ +- §6 YAGNI (push/движок/фронт не трогаем) → соблюдено ✓ + +**2. Placeholder scan:** плейсхолдеров нет; весь код/SQL приведён целиком. JSON-дефолты в up/down совпадают со schema.sql (up — email:true, down — email:false). + +**3. Type consistency:** `notification_preferences` (array-cast в User) читается через `data_get(..., 'new_lead.email')` единообразно; путь jsonb `'{new_lead,email}'` одинаков в миграции и тесте; имя миграции `2026_06_19_120000_default_new_lead_email_on` совпадает в Task 3 и Task 3 Step 2 expected.