refactor(billing): Phase 2 — remove legacy ProcessWebhookJob + cascade test cleanup

Удалён рудимент pre-sharing эпохи:
- app/app/Jobs/ProcessWebhookJob.php (job целиком, 342 строки)
- app/tests/Feature/ProcessWebhookJobTest.php (тест целиком, 362 строки)

Каскадная чистка 4 тест-файлов:
- BalanceNotificationsTest: -128 строк (оставлены topup_success/invoice_paid)
- InAppNotificationTest: -168 строк (остался notifyInApp direct)
- NewLeadNotificationTest: целиком удалён (-199 строк)
- DealCreatePdLogTest: -36 строк webhook-кейса (остались API+Route)

Локальный smoke (7 тестов без --parallel): 7 passed / 20 assertions.

Phase 2 плана 2026-05-24-legacy-direct-webhook-removal.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-24 16:15:49 +03:00
parent c7f603aa75
commit ebca32a212
6 changed files with 3 additions and 1234 deletions
@@ -2,8 +2,6 @@
declare(strict_types=1);
use App\Jobs\ProcessWebhookJob;
use App\Mail\NewLeadNotification;
use App\Models\InAppNotification;
use App\Models\Tenant;
use App\Models\User;
@@ -14,12 +12,8 @@ use Illuminate\Support\Facades\Mail;
/**
* Тесты in-app канала уведомлений (schema v8.10 in_app_notifications).
*
* Канал inapp в матрице users.notification_preferences. INSERT row при
* триггере события (new_lead/...). UI читает unread-count и список
* последних 50 (этап 2b отдельный коммит).
*
* Schema-default: notification_preferences.new_lead.inapp=true в отличие
* от email, большинство user'ов получает in-app по умолчанию.
* Тесты через ProcessWebhookJob удалены job убран как legacy-рудимент.
* Оставлен прямой вызов NotificationService::notifyInApp.
*/
uses(DatabaseTransactions::class);
@@ -27,164 +21,6 @@ beforeEach(function () {
Mail::fake();
});
/**
* @return array<string, mixed>
*/
function inAppPayload(int $vid = 300, ?int $time = null): array
{
return [
'vid' => $vid,
'project' => 'B2_Caranga',
'tag' => 'Caranga',
'phone' => '79001234567',
'phones' => ['79001234567'],
'time' => $time ?? time(),
];
}
/**
* @param array<string, mixed> $newLeadPrefs
*/
function makeUserWithInAppPrefs(Tenant $tenant, string $email, array $newLeadPrefs): User
{
return User::factory()->create([
'tenant_id' => $tenant->id,
'email' => $email,
'notification_preferences' => [
'new_lead' => $newLeadPrefs,
'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],
],
]);
}
test('webhook: in_app_notification создаётся для user с inapp=true', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
$user = makeUserWithInAppPrefs($tenant, 'on@example.ru', ['inapp' => true, 'email' => false]);
(new ProcessWebhookJob($tenant->id, inAppPayload()))->handle();
expect(InAppNotification::query()->count())->toBe(1);
$notif = InAppNotification::query()->first();
expect($notif->user_id)->toBe($user->id);
expect($notif->tenant_id)->toBe($tenant->id);
expect($notif->event)->toBe('new_lead');
expect($notif->title)->toContain('Caranga');
expect($notif->body)->toBe('79001234567'); // phone (no contact_name)
expect($notif->read_at)->toBeNull();
expect($notif->payload['project_name'])->toBe('Caranga');
});
test('webhook: user с inapp=false НЕ получает in-app row', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
makeUserWithInAppPrefs($tenant, 'off@example.ru', ['inapp' => false, 'email' => false]);
(new ProcessWebhookJob($tenant->id, inAppPayload()))->handle();
expect(InAppNotification::query()->count())->toBe(0);
});
test('webhook: schema-default (inapp=true) ставит row', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
// Без override prefs — берётся schema DEFAULT (new_lead.inapp=true).
User::factory()->create([
'tenant_id' => $tenant->id,
'email' => 'default@example.ru',
]);
(new ProcessWebhookJob($tenant->id, inAppPayload()))->handle();
expect(InAppNotification::query()->count())->toBe(1);
});
test('webhook: 2 user\'а с inapp=true получают по 1 row, 1 user с inapp=false — нет', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
$a = makeUserWithInAppPrefs($tenant, 'a@example.ru', ['inapp' => true]);
$b = makeUserWithInAppPrefs($tenant, 'b@example.ru', ['inapp' => true]);
makeUserWithInAppPrefs($tenant, 'c@example.ru', ['inapp' => false]);
(new ProcessWebhookJob($tenant->id, inAppPayload()))->handle();
expect(InAppNotification::query()->count())->toBe(2);
expect(InAppNotification::query()->where('user_id', $a->id)->exists())->toBeTrue();
expect(InAppNotification::query()->where('user_id', $b->id)->exists())->toBeTrue();
});
test('webhook: inactive user НЕ получает in-app', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
User::factory()->inactive()->create([
'tenant_id' => $tenant->id,
'email' => 'inactive@example.ru',
'notification_preferences' => ['new_lead' => ['inapp' => true]],
]);
(new ProcessWebhookJob($tenant->id, inAppPayload()))->handle();
expect(InAppNotification::query()->count())->toBe(0);
});
test('webhook: user другого тенанта НЕ получает (RLS isolation)', function () {
$tenantA = Tenant::factory()->create(['balance_leads' => 10]);
$tenantB = Tenant::factory()->create(['balance_leads' => 10]);
$userA = makeUserWithInAppPrefs($tenantA, 'a@example.ru', ['inapp' => true]);
makeUserWithInAppPrefs($tenantB, 'b@example.ru', ['inapp' => true]);
(new ProcessWebhookJob($tenantA->id, inAppPayload()))->handle();
expect(InAppNotification::query()->count())->toBe(1);
expect(InAppNotification::query()->first()->user_id)->toBe($userA->id);
});
test('webhook: дубль (Биз-19) НЕ создаёт повторный in-app row', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
makeUserWithInAppPrefs($tenant, 'on@example.ru', ['inapp' => true]);
(new ProcessWebhookJob($tenant->id, inAppPayload(vid: 1)))->handle();
expect(InAppNotification::query()->count())->toBe(1);
// Второй webhook с тем же phone в окне 24ч → дубль, нет chargeNewLead → нет notify.
(new ProcessWebhookJob($tenant->id, inAppPayload(vid: 2)))->handle();
expect(InAppNotification::query()->count())->toBe(1);
});
test('webhook: повторный vid (UPDATE) НЕ создаёт повторный in-app row', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
makeUserWithInAppPrefs($tenant, 'on@example.ru', ['inapp' => true]);
(new ProcessWebhookJob($tenant->id, inAppPayload(vid: 100)))->handle();
expect(InAppNotification::query()->count())->toBe(1);
(new ProcessWebhookJob($tenant->id, inAppPayload(vid: 100)))->handle();
expect(InAppNotification::query()->count())->toBe(1);
});
test('webhook: оба канала (inapp+email=true) — 1 in-app row + 1 email', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
makeUserWithInAppPrefs($tenant, 'both@example.ru', ['inapp' => true, 'email' => true]);
(new ProcessWebhookJob($tenant->id, inAppPayload()))->handle();
expect(InAppNotification::query()->count())->toBe(1);
Mail::assertSent(NewLeadNotification::class, 1);
});
test('webhook: payload содержит deal_id для UI deep-link', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
makeUserWithInAppPrefs($tenant, 'on@example.ru', ['inapp' => true]);
(new ProcessWebhookJob($tenant->id, inAppPayload()))->handle();
$notif = InAppNotification::query()->first();
expect($notif->deal_id)->not->toBeNull();
expect($notif->payload)->toHaveKey('deal_id');
expect($notif->payload['deal_id'])->toBe($notif->deal_id);
});
test('NotificationService::notifyInApp: вызов напрямую создаёт row', function () {
$tenant = Tenant::factory()->create();
$user = User::factory()->create(['tenant_id' => $tenant->id]);