$tenantId, 'deal_id' => 42, 'text' => 'Перезвонить', 'remind_at' => Carbon::now()->subMinute(), // due 'created_by' => $userId, 'is_sent' => false, ], $overrides)); } test('dispatch-due: due-reminder → email + inapp + is_sent=true', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create([ 'tenant_id' => $tenant->id, 'notification_preferences' => [ 'reminder' => ['inapp' => true, 'push' => true, 'email' => true], ], ]); $reminder = dueReminder($tenant->id, $user->id); $this->artisan('reminders:dispatch-due')->assertSuccessful(); Mail::assertSent(ReminderDueNotification::class, 1); Mail::assertSent(fn (ReminderDueNotification $m) => $m->reminder->id === $reminder->id && $m->hasTo($user->email)); expect(InAppNotification::query()->where('event', 'reminder')->count())->toBe(1); $reminder->refresh(); expect($reminder->is_sent)->toBeTrue(); expect($reminder->sent_at)->not->toBeNull(); }); test('dispatch-due: future-reminder skip', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); Reminder::create([ 'tenant_id' => $tenant->id, 'deal_id' => 42, 'text' => 'Future', 'remind_at' => Carbon::now()->addHour(), // future 'created_by' => $user->id, 'is_sent' => false, ]); $this->artisan('reminders:dispatch-due')->assertSuccessful(); Mail::assertNothingSent(); }); test('dispatch-due: completed reminder skip', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); dueReminder($tenant->id, $user->id, ['completed_at' => Carbon::now()->subMinute()]); $this->artisan('reminders:dispatch-due')->assertSuccessful(); Mail::assertNothingSent(); }); test('dispatch-due: уже sent skip (is_sent=true)', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); dueReminder($tenant->id, $user->id, ['is_sent' => true, 'sent_at' => Carbon::now()->subMinute()]); $this->artisan('reminders:dispatch-due')->assertSuccessful(); Mail::assertNothingSent(); }); test('dispatch-due: assignee получает (вместо created_by)', function () { $tenant = Tenant::factory()->create(); $creator = User::factory()->create(['tenant_id' => $tenant->id, 'email' => 'creator@example.ru']); $assignee = User::factory()->create([ 'tenant_id' => $tenant->id, 'email' => 'assignee@example.ru', 'notification_preferences' => [ 'reminder' => ['inapp' => false, 'push' => false, 'email' => true], ], ]); Reminder::create([ 'tenant_id' => $tenant->id, 'deal_id' => 42, 'text' => 'Перезвонить', 'remind_at' => Carbon::now()->subMinute(), 'created_by' => $creator->id, 'assignee_id' => $assignee->id, 'is_sent' => false, ]); $this->artisan('reminders:dispatch-due')->assertSuccessful(); Mail::assertSent(fn (ReminderDueNotification $m) => $m->hasTo('assignee@example.ru')); Mail::assertNotSent(fn (ReminderDueNotification $m) => $m->hasTo('creator@example.ru')); }); test('dispatch-due: deactivated user — НЕ получает', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->inactive()->create(['tenant_id' => $tenant->id]); $reminder = dueReminder($tenant->id, $user->id); $this->artisan('reminders:dispatch-due')->assertSuccessful(); Mail::assertNothingSent(); expect(InAppNotification::query()->count())->toBe(0); // Reminder всё равно помечается is_sent=true (чтобы не пытаться слать снова). $reminder->refresh(); expect($reminder->is_sent)->toBeTrue(); }); test('dispatch-due: prefs.reminder.email=false — только inapp', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create([ 'tenant_id' => $tenant->id, 'notification_preferences' => [ 'reminder' => ['inapp' => true, 'push' => false, 'email' => false], ], ]); dueReminder($tenant->id, $user->id); $this->artisan('reminders:dispatch-due')->assertSuccessful(); Mail::assertNothingSent(); expect(InAppNotification::query()->where('event', 'reminder')->count())->toBe(1); }); test('dispatch-due --dry-run: не шлёт + не помечает is_sent', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); $reminder = dueReminder($tenant->id, $user->id); $this->artisan('reminders:dispatch-due', ['--dry-run' => true])->assertSuccessful(); Mail::assertNothingSent(); $reminder->refresh(); expect($reminder->is_sent)->toBeFalse(); }); test('dispatch-due: 3 due-reminder → 3 sent', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create([ 'tenant_id' => $tenant->id, 'notification_preferences' => [ 'reminder' => ['inapp' => false, 'push' => false, 'email' => true], ], ]); dueReminder($tenant->id, $user->id, ['deal_id' => 1]); dueReminder($tenant->id, $user->id, ['deal_id' => 2]); dueReminder($tenant->id, $user->id, ['deal_id' => 3]); $this->artisan('reminders:dispatch-due')->assertSuccessful(); Mail::assertSent(ReminderDueNotification::class, 3); }); test('dispatch-due: --limit=1 ограничивает выдачу', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); dueReminder($tenant->id, $user->id, ['deal_id' => 1]); dueReminder($tenant->id, $user->id, ['deal_id' => 2]); $this->artisan('reminders:dispatch-due', ['--limit' => 1])->assertSuccessful(); Mail::assertSent(ReminderDueNotification::class, 1); }); test('dispatch-due: разные tenant\'ы изолируются (RLS)', function () { $tenantA = Tenant::factory()->create(); $userA = User::factory()->create([ 'tenant_id' => $tenantA->id, 'email' => 'a@example.ru', 'notification_preferences' => ['reminder' => ['inapp' => true, 'push' => false, 'email' => true]], ]); $tenantB = Tenant::factory()->create(); $userB = User::factory()->create([ 'tenant_id' => $tenantB->id, 'email' => 'b@example.ru', 'notification_preferences' => ['reminder' => ['inapp' => true, 'push' => false, 'email' => true]], ]); dueReminder($tenantA->id, $userA->id); dueReminder($tenantB->id, $userB->id); $this->artisan('reminders:dispatch-due')->assertSuccessful(); Mail::assertSent(ReminderDueNotification::class, 2); expect(InAppNotification::query()->where('tenant_id', $tenantA->id)->count())->toBe(1); expect(InAppNotification::query()->where('tenant_id', $tenantB->id)->count())->toBe(1); });