2026-05-09 11:03:43 +03:00
|
|
|
|
<?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);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-09 12:51:32 +03:00
|
|
|
|
test('webhook: balance=0 (RejectedDealsLog) НЕ шлёт NewLeadNotification', function () {
|
2026-05-09 11:03:43 +03:00
|
|
|
|
$tenant = Tenant::factory()->create(['balance_leads' => 0]);
|
|
|
|
|
|
makeUserWithPrefs($tenant, 'on@example.ru', ['email' => true]);
|
|
|
|
|
|
|
|
|
|
|
|
(new ProcessWebhookJob($tenant->id, newLeadPayload()))->handle();
|
|
|
|
|
|
|
2026-05-09 12:51:32 +03:00
|
|
|
|
// chargeNewLead НЕ вызывается при balance=0 — NewLeadNotification не шлётся.
|
|
|
|
|
|
// (ZeroBalanceNotification ШЛЁТСЯ — это покрывается отдельным тестом.)
|
|
|
|
|
|
Mail::assertNotSent(NewLeadNotification::class);
|
2026-05-09 11:03:43 +03:00
|
|
|
|
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');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|