Files

140 lines
6.3 KiB
PHP
Raw Permalink Normal View History

<?php
namespace MintyPHP\Tests\Module\Helpdesk\Service;
use MintyPHP\Module\Helpdesk\Service\SystemRecommendationEngine;
use PHPUnit\Framework\TestCase;
class SystemRecommendationEngineTest extends TestCase
{
public function testBuildRecommendationsAppliesRulesSortsAndLimits(): void
{
$engine = new SystemRecommendationEngine();
$config = [
'max_items' => 5,
'rules' => [
'escalation_overdue' => ['enabled' => true, 'priority' => 100, 'min_overdue_minutes' => 0],
'high_risk_aging' => ['enabled' => true, 'priority' => 90, 'codes' => ['MAX', 'HOCH'], 'min_age_hours' => 8],
'unassigned_open' => ['enabled' => true, 'priority' => 80, 'min_age_hours' => 2],
'stale_open' => ['enabled' => true, 'priority' => 70, 'min_age_hours' => 48],
'customer_backlog' => ['enabled' => true, 'priority' => 60, 'min_open_tickets' => 3],
],
];
$tickets = [
$this->openTicket('T001', 80, 'USR1'),
$this->openTicket('T002', 10, 'USR2'),
$this->openTicket('T003', 6, ''),
$this->openTicket('T004', 60, 'USR3'),
$this->openTicket('T005', 5, 'USR4'),
['No' => 'T999', 'Ticket_State' => 'Closed', 'Last_Activity_Date' => $this->isoHoursAgo(120)],
];
$escalationHealth = [
'ok' => true,
'entries' => [
['ticket_no' => 'T001', 'escalation_code' => 'MAX', 'overdue_seconds' => 72000],
['ticket_no' => 'T004', 'escalation_code' => 'NORMAL', 'overdue_seconds' => 600],
['ticket_no' => 'T001', 'escalation_code' => 'MAX', 'overdue_seconds' => 72000], // duplicate
],
'matches' => [
['ticket_no' => 'T001', 'escalation_code' => 'MAX', 'age_hours' => 80, 'is_overdue' => true],
['ticket_no' => 'T002', 'escalation_code' => 'HOCH', 'age_hours' => 10, 'is_overdue' => false],
['ticket_no' => 'T004', 'escalation_code' => 'NORMAL', 'age_hours' => 60, 'is_overdue' => true],
],
];
$result = $engine->buildRecommendations($tickets, $escalationHealth, $config);
$this->assertCount(5, $result['recommendations']);
$this->assertSame('escalation_overdue', $result['recommendations'][0]['rule_id']);
$this->assertSame('T001', $result['recommendations'][0]['ticket_no']);
$this->assertSame('escalation_overdue', $result['recommendations'][1]['rule_id']);
$this->assertSame('high_risk_aging', $result['recommendations'][2]['rule_id']);
$this->assertSame('helpdesk.recommendation.high_risk_aging', $result['recommendations'][2]['message_key']);
$this->assertSame(
['escalation_overdue', 'high_risk_aging', 'unassigned_open', 'stale_open', 'customer_backlog'],
$result['meta']['applied_rules']
);
$this->assertTrue($result['meta']['escalation_data_available']);
}
public function testBuildRecommendationsSkipsEscalationRulesWhenUnavailable(): void
{
$engine = new SystemRecommendationEngine();
$config = [
'max_items' => 10,
'rules' => [
'escalation_overdue' => ['enabled' => true, 'priority' => 100, 'min_overdue_minutes' => 0],
'high_risk_aging' => ['enabled' => true, 'priority' => 90, 'codes' => ['MAX', 'HOCH'], 'min_age_hours' => 8],
'unassigned_open' => ['enabled' => true, 'priority' => 80, 'min_age_hours' => 2],
'stale_open' => ['enabled' => true, 'priority' => 70, 'min_age_hours' => 48],
'customer_backlog' => ['enabled' => true, 'priority' => 60, 'min_open_tickets' => 1],
],
];
$tickets = [
$this->openTicket('T010', 72, ''),
];
$result = $engine->buildRecommendations($tickets, null, $config);
$ruleIds = array_map(static fn (array $item): string => (string) $item['rule_id'], $result['recommendations']);
$this->assertFalse($result['meta']['escalation_data_available']);
$this->assertNotContains('escalation_overdue', $ruleIds);
$this->assertNotContains('high_risk_aging', $ruleIds);
$this->assertContains('unassigned_open', $ruleIds);
$this->assertContains('stale_open', $ruleIds);
$this->assertContains('customer_backlog', $ruleIds);
}
public function testBuildRecommendationsCreatesBacklogRecommendationWithOldestTicket(): void
{
$engine = new SystemRecommendationEngine();
$config = [
'max_items' => 5,
'rules' => [
'escalation_overdue' => ['enabled' => false, 'priority' => 100, 'min_overdue_minutes' => 0],
'high_risk_aging' => ['enabled' => false, 'priority' => 90, 'codes' => ['MAX', 'HOCH'], 'min_age_hours' => 8],
'unassigned_open' => ['enabled' => false, 'priority' => 80, 'min_age_hours' => 2],
'stale_open' => ['enabled' => false, 'priority' => 70, 'min_age_hours' => 48],
'customer_backlog' => ['enabled' => true, 'priority' => 60, 'min_open_tickets' => 3],
],
];
$tickets = [
$this->openTicket('T100', 12, 'A'),
$this->openTicket('T101', 44, 'B'),
$this->openTicket('T102', 9, 'C'),
];
$result = $engine->buildRecommendations($tickets, null, $config);
$this->assertCount(1, $result['recommendations']);
$this->assertSame('customer_backlog', $result['recommendations'][0]['rule_id']);
$this->assertSame('T101', $result['recommendations'][0]['ticket_no']);
$this->assertSame(3, $result['recommendations'][0]['message_params']['open_tickets']);
}
/**
* @return array<string, mixed>
*/
private function openTicket(string $ticketNo, int $ageHours, string $supportUserName): array
{
return [
'No' => $ticketNo,
'Ticket_State' => 'Open',
'Support_User_Name' => $supportUserName,
'Last_Activity_Date' => $this->isoHoursAgo($ageHours),
];
}
private function isoHoursAgo(int $hours): string
{
$dt = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
$dt = $dt->modify('-' . max(0, $hours) . ' hours');
return $dt->format('Y-m-d\\TH:i:00\\Z');
}
}