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>
149 lines
5.1 KiB
PHP
149 lines
5.1 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Module\Helpdesk\Service;
|
|
|
|
use MintyPHP\Http\SessionStoreInterface;
|
|
|
|
final class DebitorCacheControl
|
|
{
|
|
public const TTL_STANDARD_SECONDS = 300;
|
|
public const TTL_ESCALATION_DEFINITIONS_SECONDS = 1800;
|
|
|
|
private const REFRESH_TRUE_VALUES = ['1', 'true', 'yes'];
|
|
|
|
private function __construct()
|
|
{
|
|
}
|
|
|
|
/**
|
|
* @param mixed $value
|
|
*/
|
|
public static function parseRefreshFlag(mixed $value): bool
|
|
{
|
|
if (is_bool($value)) {
|
|
return $value;
|
|
}
|
|
|
|
$normalized = strtolower(trim((string) ($value ?? '')));
|
|
if ($normalized === '') {
|
|
return false;
|
|
}
|
|
|
|
return in_array($normalized, self::REFRESH_TRUE_VALUES, true);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $session
|
|
*/
|
|
public static function resolveTenantScope(array $session): string
|
|
{
|
|
$tenantId = (int) (($session['current_tenant']['id'] ?? 0));
|
|
|
|
return $tenantId > 0 ? (string) $tenantId : 'global';
|
|
}
|
|
|
|
public static function ticketsKey(string $tenantScope, string $customerNo): string
|
|
{
|
|
return 'module.helpdesk.debitor.v2.tickets.' . $tenantScope . '.' . trim($customerNo);
|
|
}
|
|
|
|
public static function contactsKey(string $tenantScope, string $customerNo): string
|
|
{
|
|
return 'module.helpdesk.debitor.v2.contacts.' . $tenantScope . '.' . trim($customerNo);
|
|
}
|
|
|
|
public static function communicationKey(string $tenantScope, string $customerNo): string
|
|
{
|
|
return 'module.helpdesk.debitor.v2.communication.' . $tenantScope . '.' . trim($customerNo);
|
|
}
|
|
|
|
public static function escalationKey(string $tenantScope, string $customerNo): string
|
|
{
|
|
return 'module.helpdesk.debitor.v2.escalation.' . $tenantScope . '.' . trim($customerNo);
|
|
}
|
|
|
|
public static function contractsKey(string $tenantScope, string $customerNo): string
|
|
{
|
|
return 'module.helpdesk.debitor.v2.contracts.' . $tenantScope . '.' . trim($customerNo);
|
|
}
|
|
|
|
public static function contractLinesKey(string $tenantScope, string $customerNo): string
|
|
{
|
|
return 'module.helpdesk.debitor.v2.contract-lines.' . $tenantScope . '.' . trim($customerNo);
|
|
}
|
|
|
|
public static function controllingTicketsKey(string $tenantScope, string $customerNo): string
|
|
{
|
|
return 'module.helpdesk.debitor.v2.controlling-tickets.' . $tenantScope . '.' . trim($customerNo);
|
|
}
|
|
|
|
public static function meetingsKey(string $tenantScope, string $customerNo): string
|
|
{
|
|
return 'module.helpdesk.debitor.v2.meetings.' . $tenantScope . '.' . trim($customerNo);
|
|
}
|
|
|
|
public static function escalationDefinitionsKey(string $tenantScope): string
|
|
{
|
|
return 'module.helpdesk.debitor.v1.escalation-definitions.' . $tenantScope;
|
|
}
|
|
|
|
public static function teamWorkloadKey(string $tenantScope): string
|
|
{
|
|
return 'module.helpdesk.team.v1.workload.' . $tenantScope;
|
|
}
|
|
|
|
public static function riskRadarKey(string $tenantScope, int $periodDays): string
|
|
{
|
|
return 'module.helpdesk.risk-radar.v1.' . $tenantScope . '.' . $periodDays;
|
|
}
|
|
|
|
public static function engagementKey(string $tenantScope, int $periodDays): string
|
|
{
|
|
return 'module.helpdesk.engagement.v1.' . $tenantScope . '.' . $periodDays;
|
|
}
|
|
|
|
/** @api Used by BcODataGateway::fetchUpdateTickets() */
|
|
public static function updateTicketsKey(string $tenantScope): string
|
|
{
|
|
return 'module.helpdesk.update-tickets.v1.' . $tenantScope;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
public static function allDebitorCacheKeys(string $tenantScope, string $customerNo): array
|
|
{
|
|
return [
|
|
self::ticketsKey($tenantScope, $customerNo),
|
|
self::contactsKey($tenantScope, $customerNo),
|
|
self::communicationKey($tenantScope, $customerNo),
|
|
self::escalationKey($tenantScope, $customerNo),
|
|
self::contractsKey($tenantScope, $customerNo),
|
|
self::contractLinesKey($tenantScope, $customerNo),
|
|
self::controllingTicketsKey($tenantScope, $customerNo),
|
|
self::meetingsKey($tenantScope, $customerNo),
|
|
// Legacy keys kept for hard refresh compatibility
|
|
'module.helpdesk.tickets_cache.' . $tenantScope . '.' . trim($customerNo),
|
|
'module.helpdesk.contacts_cache.v1.' . $tenantScope . '.' . trim($customerNo),
|
|
'module.helpdesk.debitor_comm_cache.v3.' . $tenantScope . '.' . trim($customerNo),
|
|
'module.helpdesk.escalation_cache.v1.' . $tenantScope . '.' . trim($customerNo),
|
|
'module.helpdesk.contracts_cache.v2.' . $tenantScope . '.' . trim($customerNo),
|
|
];
|
|
}
|
|
|
|
public static function invalidateDebitorCaches(
|
|
SessionStoreInterface $sessionStore,
|
|
string $tenantScope,
|
|
string $customerNo,
|
|
bool $includeTenantShared = true
|
|
): void {
|
|
foreach (self::allDebitorCacheKeys($tenantScope, $customerNo) as $key) {
|
|
$sessionStore->remove($key);
|
|
}
|
|
|
|
if ($includeTenantShared) {
|
|
$sessionStore->remove(self::escalationDefinitionsKey($tenantScope));
|
|
}
|
|
}
|
|
}
|