Files
portal/app/tests/Feature/Notifications/NewLeadNotificationTest.php
T
Дмитрий a4601fe84b phase2(notifications-stage1): NotificationService + new_lead email (P0 этап 1)
Старт closing «Notification delivery» из карты P0. Этап 1/6 плана:
NotificationService + Mailable + интеграция в ProcessWebhookJob::chargeNewLead.

- App\Services\NotificationService — диспетчер 8 событий × 3 каналов
  (inapp/push/email) согласно schema.sql:699 users.notification_preferences.
  Этап 1 реализует только email-канал для new_lead.
- App\Mail\NewLeadNotification + emails/new_lead.blade.php — HTML-письмо
  в Forest-палитре с таблицей phone/contact_name/received_at/deal_id.
- ProcessWebhookJob::chargeNewLead — после ActivityLog вызывает
  notifyNewLead. Throwable от Mail::send проглатывается + Log::warning
  (отказ канала не должен валить транзакцию).
- Pest 11/11 в tests/Feature/Notifications/NewLeadNotificationTest.php:
  email=true получает / email=false не получает / schema-default не шлёт /
  inactive не получает / soft-deleted не получает / другой тенант не
  получает / Биз-19 дубль не дублирует / повторный vid не дублирует /
  balance=0 не шлёт / subject содержит project_name.
- IDE-helper регенерирован (4 модели получили @mixin docblocks).
- PHPStan baseline регенерирован (138 ignore.unmatched схлопнулись).

Pest 280/280 за 31.27 сек (+11 от 269, 1029 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:03:43 +03:00

199 lines
8.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Jobs\ProcessWebhookJob;
use App\Mail\NewLeadNotification;
use App\Models\Deal;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Mail;
/**
* Тесты email-уведомления о новом лиде (ТЗ §18.5, событие new_lead).
*
* Проверяет интеграцию NotificationService → ProcessWebhookJob: после успешного
* chargeNewLead все активные user'ы тенанта с notification_preferences.new_lead.email=true
* получают NewLeadNotification. Mail::fake() перехватывает реальную отправку.
*
* Schema-default: notification_preferences.new_lead.email=false → по умолчанию
* никто не получает emails. Тесты явно ставят email=true для нужных user'ов.
*/
uses(DatabaseTransactions::class);
beforeEach(function () {
Mail::fake();
});
/**
* @return array<string, mixed>
*/
function newLeadPayload(int $vid = 200, ?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 makeUserWithPrefs(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: NewLeadNotification отправляется user\'ам с email=true', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
$userOn = makeUserWithPrefs($tenant, 'on@example.ru', ['inapp' => true, 'push' => true, 'email' => true]);
(new ProcessWebhookJob($tenant->id, newLeadPayload()))->handle();
Mail::assertSent(NewLeadNotification::class, 1);
Mail::assertSent(NewLeadNotification::class, function (NewLeadNotification $mail) use ($userOn): bool {
return $mail->manager->id === $userOn->id
&& $mail->hasTo('on@example.ru');
});
});
test('webhook: user с email=false НЕ получает', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
makeUserWithPrefs($tenant, 'off@example.ru', ['inapp' => true, 'push' => true, 'email' => false]);
(new ProcessWebhookJob($tenant->id, newLeadPayload()))->handle();
Mail::assertNothingSent();
});
test('webhook: schema-default не шлёт (new_lead.email=false по дефолту)', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
// Не передаём notification_preferences — берётся schema DEFAULT.
User::factory()->create([
'tenant_id' => $tenant->id,
'email' => 'default@example.ru',
]);
(new ProcessWebhookJob($tenant->id, newLeadPayload()))->handle();
Mail::assertNothingSent();
});
test('webhook: рассылается всем активным user\'ам с email=true', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
makeUserWithPrefs($tenant, 'a@example.ru', ['email' => true]);
makeUserWithPrefs($tenant, 'b@example.ru', ['email' => true]);
makeUserWithPrefs($tenant, 'c@example.ru', ['email' => false]);
(new ProcessWebhookJob($tenant->id, newLeadPayload()))->handle();
Mail::assertSent(NewLeadNotification::class, 2);
Mail::assertSent(fn (NewLeadNotification $mail) => $mail->hasTo('a@example.ru'));
Mail::assertSent(fn (NewLeadNotification $mail) => $mail->hasTo('b@example.ru'));
Mail::assertNotSent(fn (NewLeadNotification $mail) => $mail->hasTo('c@example.ru'));
});
test('webhook: inactive user с email=true НЕ получает (is_active=false)', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
User::factory()->inactive()->create([
'tenant_id' => $tenant->id,
'email' => 'inactive@example.ru',
'notification_preferences' => [
'new_lead' => ['email' => true],
],
]);
(new ProcessWebhookJob($tenant->id, newLeadPayload()))->handle();
Mail::assertNothingSent();
});
test('webhook: soft-deleted user НЕ получает', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
$user = makeUserWithPrefs($tenant, 'deleted@example.ru', ['email' => true]);
$user->delete();
(new ProcessWebhookJob($tenant->id, newLeadPayload()))->handle();
Mail::assertNothingSent();
});
test('webhook: user другого тенанта НЕ получает (изоляция)', function () {
$tenantA = Tenant::factory()->create(['balance_leads' => 10]);
$tenantB = Tenant::factory()->create(['balance_leads' => 10]);
makeUserWithPrefs($tenantA, 'a@example.ru', ['email' => true]);
makeUserWithPrefs($tenantB, 'b@example.ru', ['email' => true]);
(new ProcessWebhookJob($tenantA->id, newLeadPayload()))->handle();
Mail::assertSent(NewLeadNotification::class, 1);
Mail::assertSent(fn (NewLeadNotification $mail) => $mail->hasTo('a@example.ru'));
Mail::assertNotSent(fn (NewLeadNotification $mail) => $mail->hasTo('b@example.ru'));
});
test('webhook: дубль-сделка (Биз-19) НЕ шлёт повторное уведомление', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
makeUserWithPrefs($tenant, 'on@example.ru', ['email' => true]);
// Первая сделка — master.
(new ProcessWebhookJob($tenant->id, newLeadPayload(vid: 1)))->handle();
Mail::assertSent(NewLeadNotification::class, 1);
// Вторая сделка с тем же phone в окне 24 ч — дубль, баланс НЕ списывается,
// chargeNewLead НЕ вызывается, уведомление НЕ шлётся.
(new ProcessWebhookJob($tenant->id, newLeadPayload(vid: 2)))->handle();
Mail::assertSent(NewLeadNotification::class, 1); // всё ещё одно
});
test('webhook: повторный vid (idempotent UPDATE) НЕ шлёт повторное уведомление', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
makeUserWithPrefs($tenant, 'on@example.ru', ['email' => true]);
(new ProcessWebhookJob($tenant->id, newLeadPayload(vid: 100)))->handle();
Mail::assertSent(NewLeadNotification::class, 1);
// Повторный webhook с тем же vid — UPDATE, не INSERT. wasRecentlyCreated=false → return.
(new ProcessWebhookJob($tenant->id, newLeadPayload(vid: 100)))->handle();
Mail::assertSent(NewLeadNotification::class, 1);
});
test('webhook: balance=0 (RejectedDealsLog) НЕ шлёт уведомление', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 0]);
makeUserWithPrefs($tenant, 'on@example.ru', ['email' => true]);
(new ProcessWebhookJob($tenant->id, newLeadPayload()))->handle();
// chargeNewLead не вызывается при balance=0 — уведомление не отправляется.
Mail::assertNothingSent();
expect(Deal::query()->where('tenant_id', $tenant->id)->count())->toBe(0);
});
test('NewLeadNotification: subject содержит project_name', function () {
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
makeUserWithPrefs($tenant, 'on@example.ru', ['email' => true]);
(new ProcessWebhookJob($tenant->id, newLeadPayload()))->handle();
Mail::assertSent(NewLeadNotification::class, function (NewLeadNotification $mail): bool {
return str_contains($mail->envelope()->subject, 'Caranga');
});
});