refactor(helpdesk): remove resolution time dimension from risk score
Resolution time measures the past, not current risk. The remaining 4 dimensions (open pressure 35%, trend 30%, SLA overdue 25%, inactivity 10%) are all current and actionable. Removes resolution aggregation, median calculation, and weight redistribution logic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user