$no, 'Customer_No' => $customerNo, 'Cust_Name' => $customerName, 'Escalation_Code' => $escalationCode, 'Created_On' => gmdate('Y-m-d\TH:i:s\Z', $createdTs), 'Last_Activity_Date' => gmdate('Y-m-d\TH:i:s\Z', $activityTs), 'Process_Stage' => $closed ? 40 : 10, 'Ticket_State' => $closed ? 'Geschlossen' : 'Offen', 'Support_User_Name' => 'NKS', 'Category_1_Code' => 'FEHLER', 'Process_Code' => 'NORMAL', ]; } public function testEmptyTicketsReturnsEmptyResult(): void { $result = RiskRadarService::buildRiskRadar([], [], 90); $this->assertSame(0, $result['summary']['total']); $this->assertSame([], $result['customers']); } public function testSkipsTicketsWithoutCustomerNo(): void { $tickets = [ self::ticket('T1', '', 'No Customer', 10), self::ticket('T2', '10610', 'Valid Customer', 10), ]; $result = RiskRadarService::buildRiskRadar($tickets, [], 90); $this->assertSame(1, $result['summary']['total']); $this->assertSame('10610', $result['customers'][0]['customer_no']); } public function testAggregatesPerCustomerNo(): void { $tickets = [ self::ticket('T1', '10610', 'Kunde A', 10), self::ticket('T2', '10610', 'Kunde A', 20), self::ticket('T3', '10620', 'Kunde B', 5), ]; $result = RiskRadarService::buildRiskRadar($tickets, [], 90); $this->assertSame(2, $result['summary']['total']); $customerNos = array_column($result['customers'], 'customer_no'); $this->assertContains('10610', $customerNos); $this->assertContains('10620', $customerNos); $kundeA = $result['customers'][array_search('10610', $customerNos)]; $this->assertSame(2, $kundeA['kpis']['open']); } public function testHighRiskLevel(): void { // Many open + critical tickets → high score $tickets = []; for ($i = 0; $i < 10; $i++) { $tickets[] = self::ticket('T' . $i, '10610', 'Stressed', 100); // all >48h → critical } $result = RiskRadarService::buildRiskRadar($tickets, [], 90); $this->assertSame('high', $result['customers'][0]['risk_level']); $this->assertGreaterThanOrEqual(70, $result['customers'][0]['risk_score']); } public function testLowRiskLevel(): void { // One recently closed ticket, one just opened $tickets = [ self::ticket('T1', '10610', 'Calm', 2), self::ticket('T2', '10610', 'Calm', 10, true, '', 2), ]; $result = RiskRadarService::buildRiskRadar($tickets, [], 90); $this->assertSame('low', $result['customers'][0]['risk_level']); $this->assertLessThan(40, $result['customers'][0]['risk_score']); } public function testSlaOverdueDetection(): void { $escalationDefs = [ ['Code' => 'MAX', 'Target_Time' => 'PT2H', 'No_of_Escalation_Level' => 1], ]; // Ticket created 10h ago with MAX escalation, target 2h → overdue $tickets = [ self::ticket('T1', '10610', 'SLA Breach', 10, false, 'MAX'), ]; $result = RiskRadarService::buildRiskRadar($tickets, $escalationDefs, 90); $this->assertSame(1, $result['customers'][0]['kpis']['sla_overdue']); } public function testSlaNotOverdueWhenWithinTarget(): void { $escalationDefs = [ ['Code' => 'NORMAL', 'Target_Time' => 'P30D', 'No_of_Escalation_Level' => 1], ]; $tickets = [ self::ticket('T1', '10610', 'Within SLA', 10, false, 'NORMAL'), ]; $result = RiskRadarService::buildRiskRadar($tickets, $escalationDefs, 90); $this->assertSame(0, $result['customers'][0]['kpis']['sla_overdue']); } public function testResolutionMedian(): void { $tickets = [ self::ticket('T1', '10610', 'Fast', 100, true, '', 10), self::ticket('T2', '10610', 'Fast', 100, true, '', 20), self::ticket('T3', '10610', 'Fast', 100, true, '', 30), self::ticket('T4', '10610', 'Fast', 5), // one open ]; $result = RiskRadarService::buildRiskRadar($tickets, [], 90); $this->assertSame(20.0, $result['customers'][0]['kpis']['resolution_median_hours']); } public function testResolutionNullRedistributesWeight(): void { // Only open tickets, no closed → resolution is null $tickets = [ self::ticket('T1', '10610', 'Open Only', 10), ]; $result = RiskRadarService::buildRiskRadar($tickets, [], 90); $customer = $result['customers'][0]; $resolutionDim = null; foreach ($customer['dimensions'] as $d) { if ($d['id'] === 'resolution') { $resolutionDim = $d; } } $this->assertNotNull($resolutionDim); $this->assertNull($resolutionDim['dimension_score']); $this->assertSame(0.0, $resolutionDim['weighted_points']); } public function testTruncatedFlagPassthrough(): void { $tickets = [self::ticket('T1', '10610', 'Any', 5)]; $result = RiskRadarService::buildRiskRadar($tickets, [], 90, true); $this->assertTrue($result['meta']['truncated']); $this->assertSame('medium', $result['meta']['confidence']); } public function testSortsByScoreDescending(): void { $tickets = [ self::ticket('T1', '10610', 'Low Risk', 2), self::ticket('T2', '10620', 'High Risk', 100), self::ticket('T3', '10620', 'High Risk', 100), self::ticket('T4', '10620', 'High Risk', 100), ]; $result = RiskRadarService::buildRiskRadar($tickets, [], 90); $this->assertSame('10620', $result['customers'][0]['customer_no']); $this->assertSame('10610', $result['customers'][1]['customer_no']); } public function testTopDriversSortedByWeightedPoints(): void { $tickets = []; for ($i = 0; $i < 8; $i++) { $tickets[] = self::ticket('T' . $i, '10610', 'Heavy', 200); } $result = RiskRadarService::buildRiskRadar($tickets, [], 90); $drivers = $result['customers'][0]['drivers']; $this->assertCount(3, $drivers); $this->assertGreaterThanOrEqual($drivers[1]['weighted_points'], $drivers[0]['weighted_points']); } public function testExcludesInactiveCustomers(): void { // Ticket created and closed outside the period window $tickets = [ self::ticket('T1', '10610', 'Old', 3000, true, '', 10), ]; $result = RiskRadarService::buildRiskRadar($tickets, [], 30); // Customer had no activity in the 30-day window and no open tickets $this->assertSame(0, $result['summary']['total']); } }