*/ 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 $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'); }); });