diff --git a/modules/helpdesk/lib/Module/Helpdesk/Service/RiskRadarService.php b/modules/helpdesk/lib/Module/Helpdesk/Service/RiskRadarService.php index 3fda438..319d4ac 100644 --- a/modules/helpdesk/lib/Module/Helpdesk/Service/RiskRadarService.php +++ b/modules/helpdesk/lib/Module/Helpdesk/Service/RiskRadarService.php @@ -9,10 +9,9 @@ namespace MintyPHP\Module\Helpdesk\Service; final class RiskRadarService { // --- Score weights (must sum to 100) --- - private const W_OPEN_PRESSURE = 30; - private const W_TREND = 25; - private const W_SLA_OVERDUE = 20; - private const W_RESOLUTION = 15; + private const W_OPEN_PRESSURE = 35; + private const W_TREND = 30; + private const W_SLA_OVERDUE = 25; private const W_INACTIVITY = 10; // --- Level thresholds --- @@ -63,7 +62,6 @@ final class RiskRadarService 'created_prev' => 0, 'closed_prev' => 0, 'sla_overdue_count' => 0, - 'resolution_hours' => [], 'oldest_open_age_hours' => 0, 'open_tickets' => [], ]; @@ -79,9 +77,6 @@ final class RiskRadarService if ($isClosed) { if ($activityTs !== null && $activityTs >= $periodStart) { $customers[$customerNo]['closed_current']++; - if ($createdTs !== null && $activityTs > $createdTs) { - $customers[$customerNo]['resolution_hours'][] = intdiv($activityTs - $createdTs, 3600); - } } if ($activityTs !== null && $activityTs >= $prevPeriodStart && $activityTs < $periodStart) { $customers[$customerNo]['closed_prev']++; @@ -176,7 +171,6 @@ final class RiskRadarService }); $netFlow = $c['created_current'] - $c['closed_current']; - $resolutionMedian = self::median($c['resolution_hours']); $scoredCustomers[] = [ 'customer_no' => $c['customer_no'], @@ -188,7 +182,6 @@ final class RiskRadarService 'critical' => $c['critical'], 'sla_overdue' => $c['sla_overdue_count'], 'net_flow' => $netFlow, - 'resolution_median_hours' => $resolutionMedian, 'oldest_open_hours' => $c['oldest_open_age_hours'], ], 'drivers' => $topDrivers, @@ -241,7 +234,6 @@ final class RiskRadarService $critical = (int) $c['critical']; $netFlow = (int) $c['created_current'] - (int) $c['closed_current']; $slaOverdue = (int) $c['sla_overdue_count']; - $resolutionMedian = self::median($c['resolution_hours']); $oldestAge = (int) $c['oldest_open_age_hours']; // 1. Open Pressure: min(100, open*8 + critical*18) @@ -263,18 +255,7 @@ final class RiskRadarService default => 100, }; - // 4. Resolution (median hours): null→neutral, <=24:0, 25-48:35, 49-96:70, >96:100 - $resolutionScore = null; - if ($resolutionMedian !== null) { - $resolutionScore = match (true) { - $resolutionMedian <= 24 => 0, - $resolutionMedian <= 48 => 35, - $resolutionMedian <= 96 => 70, - default => 100, - }; - } - - // 5. Inactivity (oldest open): <=24:0, 25-72:35, 73-168:70, >168:100 + // 4. Inactivity (oldest open): <=24:0, 25-72:35, 73-168:70, >168:100 $inactivityScore = match (true) { $oldestAge <= 24 => 0, $oldestAge <= 72 => 35, @@ -286,27 +267,11 @@ final class RiskRadarService ['id' => 'open_pressure', 'label' => 'Open pressure', 'raw_value' => $open, 'dimension_score' => $openScore, 'weight' => self::W_OPEN_PRESSURE], ['id' => 'trend', 'label' => 'Trend', 'raw_value' => $netFlow, 'dimension_score' => $trendScore, 'weight' => self::W_TREND], ['id' => 'sla_overdue', 'label' => 'SLA overdue', 'raw_value' => $slaOverdue, 'dimension_score' => $slaScore, 'weight' => self::W_SLA_OVERDUE], - ['id' => 'resolution', 'label' => 'Resolution time', 'raw_value' => $resolutionMedian, 'dimension_score' => $resolutionScore, 'weight' => self::W_RESOLUTION], ['id' => 'inactivity', 'label' => 'Inactivity', 'raw_value' => $oldestAge, 'dimension_score' => $inactivityScore, 'weight' => self::W_INACTIVITY], ]; - // Compute weighted points; redistribute resolution weight when null - $effectiveWeightSum = 0; - foreach ($dimensions as $d) { - if ($d['dimension_score'] !== null) { - $effectiveWeightSum += $d['weight']; - } - } - foreach ($dimensions as $i => $d) { - if ($d['dimension_score'] === null) { - $dimensions[$i]['weighted_points'] = 0.0; - } else { - $adjustedWeight = $effectiveWeightSum > 0 - ? ($d['weight'] / $effectiveWeightSum) * 100 - : (float) $d['weight']; - $dimensions[$i]['weighted_points'] = round(($d['dimension_score'] / 100) * $adjustedWeight, 1); - } + $dimensions[$i]['weighted_points'] = round(($d['dimension_score'] / 100) * $d['weight'], 1); } return $dimensions; diff --git a/modules/helpdesk/tests/Module/Helpdesk/Service/RiskRadarServiceTest.php b/modules/helpdesk/tests/Module/Helpdesk/Service/RiskRadarServiceTest.php index 451dba6..daaac5c 100644 --- a/modules/helpdesk/tests/Module/Helpdesk/Service/RiskRadarServiceTest.php +++ b/modules/helpdesk/tests/Module/Helpdesk/Service/RiskRadarServiceTest.php @@ -134,40 +134,16 @@ class RiskRadarServiceTest extends TestCase $this->assertSame(0, $result['customers'][0]['kpis']['sla_overdue']); } - public function testResolutionMedian(): void + public function testFourDimensionsInOutput(): 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 + self::ticket('T1', '10610', 'Any', 10), ]; $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']); + $dimIds = array_column($result['customers'][0]['dimensions'], 'id'); + $this->assertSame(['open_pressure', 'trend', 'sla_overdue', 'inactivity'], $dimIds); } public function testTruncatedFlagPassthrough(): void