docs: G2-B план реализации — дайджест по умолчанию (миграция + дотяжка + тест)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SharesSupplierPdo;
|
||||
|
||||
// Самозапись и backfill пишут users через BYPASSRLS pgsql_supplier (нет tenant-GUC
|
||||
// на публичном роуте). SharesSupplierPdo шарит PDO под DatabaseTransactions.
|
||||
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config(['services.captcha.fake_passes' => 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(<<<SQL
|
||||
UPDATE users
|
||||
SET notification_preferences = jsonb_set(notification_preferences, '{new_lead,email}', 'true'::jsonb)
|
||||
WHERE id = {$user->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) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Миграция (ALTER DEFAULT + backfill)
|
||||
|
||||
**Files:**
|
||||
- Create: `app/database/migrations/2026_06_19_120000_default_new_lead_email_on.php`
|
||||
|
||||
- [ ] **Step 1: Написать миграцию**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// DDL/data через pgsql_supplier (как add_code_fields_to_email_verifications):
|
||||
// на dev = postgres superuser, на prod = crm_supplier_worker (BYPASSRLS) —
|
||||
// UPDATE users под RLS проходит без tenant-GUC.
|
||||
$supplier = DB::connection('pgsql_supplier');
|
||||
|
||||
$default = <<<'JSON'
|
||||
{
|
||||
"new_lead": {"inapp": true, "push": true, "email": true},
|
||||
"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 '".$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) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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) <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user