*/ 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 $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]); $service = app(NotificationService::class); $service->notifyInApp($user, 'reminder', 'Срок касания', 'Перезвонить клиенту через 30 мин', ['deal_id' => 42]); $notif = InAppNotification::query()->first(); expect($notif)->not->toBeNull(); expect($notif->event)->toBe('reminder'); expect($notif->title)->toBe('Срок касания'); expect($notif->body)->toBe('Перезвонить клиенту через 30 мин'); expect($notif->deal_id)->toBe(42); expect($notif->payload['deal_id'])->toBe(42); });