create(['delivered_in_month' => 100]); $tenantB = Tenant::factory()->create(['delivered_in_month' => 5]); $tenantC = Tenant::factory()->create(['delivered_in_month' => 0]); Project::factory()->create(['tenant_id' => $tenantA->id, 'delivered_in_month' => 50]); Project::factory()->create(['tenant_id' => $tenantA->id, 'delivered_in_month' => 50]); Project::factory()->create(['tenant_id' => $tenantB->id, 'delivered_in_month' => 5]); Artisan::call('projects:reset-monthly'); expect($tenantA->fresh()->delivered_in_month)->toBe(0); expect($tenantB->fresh()->delivered_in_month)->toBe(0); expect($tenantC->fresh()->delivered_in_month)->toBe(0); expect(Project::sum('delivered_in_month'))->toBe(0); }); it('is idempotent — second run reports 0 affected', function () { $tenant = Tenant::factory()->create(['delivered_in_month' => 10]); Project::factory()->create(['tenant_id' => $tenant->id, 'delivered_in_month' => 10]); // Первый прогон сбрасывает оба счётчика. Artisan::call('projects:reset-monthly'); // Второй прогон должен сообщить о 0 затронутых строках (WHERE delivered_in_month <> 0). // info() пишет ОДНУ строку «Monthly reset: 0 tenants, 0 projects.» — // expectsOutputToContain в Laravel 13 line-by-line, поэтому проверяем // обе подстроки одним substring'ом. $this->artisan('projects:reset-monthly') ->expectsOutputToContain('0 tenants, 0 projects') ->assertExitCode(0); }); it('Schedule entry registered for monthly on 1st 00:00 Europe/Moscow', function () { /** @var Schedule $schedule */ $schedule = app(Schedule::class); $found = collect($schedule->events())->first( fn ($event) => str_contains($event->command ?? '', 'projects:reset-monthly') ); expect($found)->not->toBeNull(); expect($found->expression)->toBe('0 0 1 * *'); expect($found->timezone)->toBe('Europe/Moscow'); }); it('uses pgsql_supplier BYPASSRLS connection (touches all tenants without SET LOCAL)', function () { Tenant::factory()->count(3)->create(['delivered_in_month' => 7]); // Без SET LOCAL app.current_tenant_id reset должен затронуть всех 3 tenant'ов. // Это тест на использование pgsql_supplier (BYPASSRLS), не default pgsql. Artisan::call('projects:reset-monthly'); expect(Tenant::where('delivered_in_month', '>', 0)->count())->toBe(0); });