feat(helpdesk): add customer engagement analytics page [skip ci]
New "Customer analytics" page under Monitoring: surfaces who is actively in contact vs. who has gone quiet, so the team can reach out before a customer slips away. - KPI tiles: total / active / no-contact / never-in-contact - "Customers without recent contact" — longest silence first - "Top customers by communication" — most ticket activity in the period - Configurable no-contact threshold (helpdesk settings, default 60 days) CustomerEngagementService aggregates the global ticket snapshot per customer (same single-pull model as RiskRadarService — no N+1 to BC). Last contact is the most recent ticket activity; silence beyond the threshold flags the customer. Revenue ranking deferred (BC OData exposes no per-customer ledger). All within modules/helpdesk/. Tests + PHPStan green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,53 @@ class HelpdeskSettingsGatewayTest extends TestCase
|
||||
$this->assertNull($gateway->getBasicPassword());
|
||||
}
|
||||
|
||||
public function testGetInactivityThresholdDaysDefaultsWhenUnset(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->method('getValue')->willReturn(null);
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$this->assertSame(
|
||||
HelpdeskSettingsGateway::DEFAULT_INACTIVITY_THRESHOLD_DAYS,
|
||||
$gateway->getInactivityThresholdDays()
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetInactivityThresholdDaysClampsToRange(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->method('getValue')->willReturn('999');
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
// Clamped to the max (365).
|
||||
$this->assertSame(365, $gateway->getInactivityThresholdDays());
|
||||
}
|
||||
|
||||
public function testGetInactivityThresholdDaysFallsBackOnNonNumeric(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->method('getValue')->willReturn('not-a-number');
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
$this->assertSame(
|
||||
HelpdeskSettingsGateway::DEFAULT_INACTIVITY_THRESHOLD_DAYS,
|
||||
$gateway->getInactivityThresholdDays()
|
||||
);
|
||||
}
|
||||
|
||||
public function testSetInactivityThresholdDaysClampsAndPersists(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
$settings->expects($this->once())
|
||||
->method('set')
|
||||
->with(HelpdeskSettingsGateway::KEY_INACTIVITY_THRESHOLD_DAYS, '7', 'helpdesk.inactivity_threshold_days')
|
||||
->willReturn(true);
|
||||
|
||||
$gateway = $this->createGateway($settings);
|
||||
// 1 is below the minimum (7) => clamped up.
|
||||
$this->assertTrue($gateway->setInactivityThresholdDays(1));
|
||||
}
|
||||
|
||||
public function testSetBasicPasswordEncryptsAndPersists(): void
|
||||
{
|
||||
$settings = $this->createMock(SettingRepositoryInterface::class);
|
||||
|
||||
Reference in New Issue
Block a user