makeStoredSet('off', season: 'winter'); \Illuminate\Support\Facades\Mail::fake(); $this->artisan('tires:remind-seasonal')->assertSuccessful(); $this->assertEquals(0, ServiceReminderSent::where('type', 'tire_swap')->count()); Carbon::setTestNow(); } public function test_spring_window_notifies_winter_stored_sets(): void { Carbon::setTestNow(Carbon::create(2026, 3, 1)); $ctx = $this->makeStoredSet('spr', season: 'winter'); \Illuminate\Support\Facades\Mail::fake(); $this->artisan('tires:remind-seasonal')->assertSuccessful(); $sent = ServiceReminderSent::where('type', 'tire_swap') ->where('client_id', $ctx['client']->id)->first(); $this->assertNotNull($sent); Carbon::setTestNow(); } public function test_spring_window_ignores_summer_sets(): void { Carbon::setTestNow(Carbon::create(2026, 3, 1)); $this->makeStoredSet('sum', season: 'summer'); \Illuminate\Support\Facades\Mail::fake(); $this->artisan('tires:remind-seasonal')->assertSuccessful(); $this->assertEquals(0, ServiceReminderSent::where('type', 'tire_swap')->count()); Carbon::setTestNow(); } public function test_autumn_window_notifies_summer_stored_sets(): void { Carbon::setTestNow(Carbon::create(2026, 10, 1)); $ctx = $this->makeStoredSet('aut', season: 'summer'); \Illuminate\Support\Facades\Mail::fake(); $this->artisan('tires:remind-seasonal')->assertSuccessful(); $sent = ServiceReminderSent::where('type', 'tire_swap')->first(); $this->assertNotNull($sent); $this->assertEquals($ctx['client']->id, $sent->client_id); Carbon::setTestNow(); } public function test_cooldown_prevents_double_send(): void { Carbon::setTestNow(Carbon::create(2026, 3, 1)); $ctx = $this->makeStoredSet('cd', season: 'winter'); \Illuminate\Support\Facades\Mail::fake(); $this->artisan('tires:remind-seasonal')->assertSuccessful(); $this->assertEquals(1, ServiceReminderSent::where('type', 'tire_swap')->count()); // Second run same day → cooldown blocks it. $this->artisan('tires:remind-seasonal')->assertSuccessful(); $this->assertEquals(1, ServiceReminderSent::where('type', 'tire_swap')->count()); Carbon::setTestNow(); } public function test_force_runs_outside_window(): void { Carbon::setTestNow(Carbon::create(2026, 6, 2)); $ctx = $this->makeStoredSet('force', season: 'winter'); \Illuminate\Support\Facades\Mail::fake(); $this->artisan('tires:remind-seasonal', ['--force' => true])->assertSuccessful(); $this->assertEquals(1, ServiceReminderSent::where('type', 'tire_swap')->count()); Carbon::setTestNow(); } private function makeStoredSet(string $slug, string $season): array { $plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]); $company = Company::create([ 'plan_id' => $plan->id, 'slug' => $slug, 'name' => ucfirst($slug), 'status' => 'active', 'settings' => ['telegram' => ['bot_token' => 'FAKE:TOKEN']], ]); app(TenantManager::class)->setCurrent($company); $client = Client::create([ 'name' => 'TireClient', 'phone' => '+3736' . random_int(1000000, 9999999), 'email' => $slug . '@example.com', 'telegram_chat_id' => '9999' . random_int(1000, 9999), 'type' => 'individual', 'status' => 'active', ]); $vehicle = \App\Models\Tenant\Vehicle::create([ 'client_id' => $client->id, 'make' => 'BMW', 'model' => 'X5', 'plate' => 'TS-' . random_int(100, 999), ]); $set = TireSet::create([ 'client_id' => $client->id, 'vehicle_id' => $vehicle->id, 'season' => $season, 'brand' => 'Michelin', 'width' => 205, 'profile' => 55, 'diameter' => 16, ]); TireStorage::create([ 'tire_set_id' => $set->id, 'location' => 'A1-' . $slug, 'status' => 'stored', 'checked_in_at' => now()->subMonths(2), ]); // Telegram dispatch goes through HTTP; fake it so the test doesn't // hit the real Bot API. \Illuminate\Support\Facades\Http::fake([ 'api.telegram.org/*' => \Illuminate\Support\Facades\Http::response(['ok' => true]), ]); return compact('company', 'client', 'vehicle', 'set'); } }