create(['key' => 'low', 'label' => 'Niski', 'color' => '#75798c', 'sort_order' => 2]); $ticket = makeTicket(['created_at' => now()->subMinutes(90), 'last_customer_activity_at' => now()->subMinutes(90)]); $rule = AutomationRule::query()->create([ 'label' => 'Podnieś priorytet po 60 min', 'enabled' => true, 'condition_minutes' => 60, 'action_type' => 'change_priority', 'action_value' => 'low', ]); Artisan::call('automation:run-rules'); expect($ticket->fresh()->priority_key)->toBe('low'); expect($rule->fresh()->hasFiredFor($ticket->fresh()))->toBeTrue(); // TicketService::setPriority() writes its own "Priorytet zmieniony na: ..." // history line; RunAutomationRules adds a second, clearly-attributed one. expect($ticket->fresh()->histories()->count())->toBe(2); }); test('a rule does not fire again on the same ticket once already latched', function () { seedStatusesAndPriorities(); Priority::query()->create(['key' => 'low', 'label' => 'Niski', 'color' => '#75798c', 'sort_order' => 2]); $ticket = makeTicket(['created_at' => now()->subMinutes(90), 'last_customer_activity_at' => now()->subMinutes(90)]); AutomationRule::query()->create([ 'label' => 'Podnieś priorytet po 60 min', 'enabled' => true, 'condition_minutes' => 60, 'action_type' => 'change_priority', 'action_value' => 'low', ]); Artisan::call('automation:run-rules'); $ticket->refresh()->update(['priority_key' => 'high']); // simulate an operator manually reverting it Artisan::call('automation:run-rules'); expect($ticket->fresh()->priority_key)->toBe('high'); }); test('a fresh client reply resets the latch so the rule can fire again after new silence', function () { seedStatusesAndPriorities(); Priority::query()->create(['key' => 'low', 'label' => 'Niski', 'color' => '#75798c', 'sort_order' => 2]); $ticket = makeTicket(['created_at' => now()->subMinutes(90), 'last_customer_activity_at' => now()->subMinutes(90)]); $client = User::query()->create(['name' => 'Klient', 'email' => 'client-auto@example.com', 'roles' => ['client']]); AutomationRule::query()->create([ 'label' => 'Podnieś priorytet po 60 min', 'enabled' => true, 'condition_minutes' => 60, 'action_type' => 'change_priority', 'action_value' => 'low', ]); Artisan::call('automation:run-rules'); expect($ticket->fresh()->priority_key)->toBe('low'); app(TicketService::class)->clientReply($ticket->fresh(), $client, 'Nadal potrzebuję pomocy'); $ticket->refresh()->update(['last_customer_activity_at' => now()->subMinutes(90), 'priority_key' => 'high']); Artisan::call('automation:run-rules'); expect($ticket->fresh()->priority_key)->toBe('low'); }); test('a rule scoped to a priority ignores tickets with a different priority', function () { seedStatusesAndPriorities(); Priority::query()->create(['key' => 'low', 'label' => 'Niski', 'color' => '#75798c', 'sort_order' => 2]); $ticket = makeTicket(['priority_key' => 'low', 'created_at' => now()->subMinutes(90), 'last_customer_activity_at' => now()->subMinutes(90)]); AutomationRule::query()->create([ 'label' => 'Tylko dla wysokiego priorytetu', 'enabled' => true, 'condition_minutes' => 60, 'scope_priority_key' => 'high', 'action_type' => 'change_priority', 'action_value' => 'low', ]); Artisan::call('automation:run-rules'); expect($ticket->fresh()->priority_key)->toBe('low'); expect($ticket->fresh()->histories()->count())->toBe(0); }); test('a disabled rule never fires', function () { seedStatusesAndPriorities(); Priority::query()->create(['key' => 'low', 'label' => 'Niski', 'color' => '#75798c', 'sort_order' => 2]); $ticket = makeTicket(['created_at' => now()->subMinutes(90), 'last_customer_activity_at' => now()->subMinutes(90)]); AutomationRule::query()->create([ 'label' => 'Wyłączona reguła', 'enabled' => false, 'condition_minutes' => 60, 'action_type' => 'change_priority', 'action_value' => 'low', ]); Artisan::call('automation:run-rules'); expect($ticket->fresh()->priority_key)->toBe('high'); }); test('a closed ticket is never matched even if it has been silent long enough', function () { seedStatusesAndPriorities(); Priority::query()->create(['key' => 'low', 'label' => 'Niski', 'color' => '#75798c', 'sort_order' => 2]); $ticket = makeTicket(['status_key' => 'closed', 'created_at' => now()->subMinutes(90), 'last_customer_activity_at' => now()->subMinutes(90)]); AutomationRule::query()->create([ 'label' => 'Podnieś priorytet po 60 min', 'enabled' => true, 'condition_minutes' => 60, 'action_type' => 'change_priority', 'action_value' => 'low', ]); Artisan::call('automation:run-rules'); expect($ticket->fresh()->priority_key)->toBe('high'); }); test('two independent rules can both fire on the same ticket in one run', function () { seedStatusesAndPriorities(); Priority::query()->create(['key' => 'low', 'label' => 'Niski', 'color' => '#75798c', 'sort_order' => 2]); $team = Team::query()->create(['name' => 'Wsparcie L2']); $ticket = makeTicket(['created_at' => now()->subMinutes(90), 'last_customer_activity_at' => now()->subMinutes(90)]); AutomationRule::query()->create([ 'label' => 'Podnieś priorytet', 'enabled' => true, 'condition_minutes' => 60, 'action_type' => 'change_priority', 'action_value' => 'low', ]); AutomationRule::query()->create([ 'label' => 'Przekaż do L2', 'enabled' => true, 'condition_minutes' => 60, 'action_type' => 'change_team', 'action_value' => (string) $team->id, ]); Artisan::call('automation:run-rules'); $ticket->refresh(); expect($ticket->priority_key)->toBe('low'); expect($ticket->team_id)->toBe($team->id); expect($ticket->histories()->count())->toBe(4); }); test('reopening a closed ticket clears its automation logs so rules can fire again', function () { seedStatusesAndPriorities(); Priority::query()->create(['key' => 'low', 'label' => 'Niski', 'color' => '#75798c', 'sort_order' => 2]); $ticket = makeTicket(['created_at' => now()->subMinutes(90), 'last_customer_activity_at' => now()->subMinutes(90)]); $rule = AutomationRule::query()->create([ 'label' => 'Podnieś priorytet', 'enabled' => true, 'condition_minutes' => 60, 'action_type' => 'change_priority', 'action_value' => 'low', ]); Artisan::call('automation:run-rules'); expect($rule->logs()->count())->toBe(1); app(TicketService::class)->setStatus($ticket->fresh(), 'closed'); expect($rule->logs()->count())->toBe(0); });