feat(helpdesk): add multi-tenant BC connection settings with risk radar refinements

Introduce per-tenant override for helpdesk BC connection config.
New table helpdesk_tenant_settings stores tenant-specific credentials
with encrypted secrets (AES-256-GCM). EffectiveHelpdeskSettingsService
resolves global vs tenant config per request. Settings UI extended with
tenant override tab, toggle, and dual editing mode.

All runtime consumers (OData, SOAP, OAuth) read through the new
resolver. Token cache flushed on any config change. Default behavior
unchanged — tenants use global config until override explicitly enabled.

Also includes risk radar UI refinements: Stripe-style card redesign
with accent borders and score pills, removal of redundant KPI tiles
and search, and filtering of zero-score entries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 10:29:59 +02:00
parent 97dbc7f57b
commit ea786f5341
27 changed files with 1150 additions and 109 deletions

View File

@@ -0,0 +1,199 @@
<?php
namespace MintyPHP\Module\Helpdesk\Service;
use MintyPHP\Http\SessionStoreInterface;
/**
* Resolves the effective helpdesk BC config for the current request.
*
* Resolution rule:
* - If the current tenant has an active override (override_enabled=1): tenant config.
* - Otherwise: global config from the settings table.
*
* All runtime consumers (OData, SOAP, OAuth) should read through this service
* instead of accessing HelpdeskSettingsGateway directly.
*/
class EffectiveHelpdeskSettingsService
{
public function __construct(
private readonly HelpdeskSettingsGateway $globalGateway,
private readonly HelpdeskTenantSettingsGateway $tenantGateway,
private readonly SessionStoreInterface $sessionStore
) {
}
/**
* Resolve the effective settings for the given tenant.
*/
public function resolveForTenant(int $tenantId): EffectiveHelpdeskSettings
{
if ($tenantId > 0 && $this->tenantGateway->isOverrideEnabled($tenantId)) {
return $this->buildFromTenant($tenantId);
}
return $this->buildFromGlobal();
}
/**
* Resolve for the current session tenant.
*/
public function resolveForCurrentTenant(): EffectiveHelpdeskSettings
{
$tenantId = $this->resolveCurrentTenantId();
return $this->resolveForTenant($tenantId);
}
// --- Proxy methods for gateway compatibility ---
// These allow consumers to swap HelpdeskSettingsGateway → EffectiveHelpdeskSettingsService
// with minimal code changes.
public function getAuthMode(): string
{
return $this->resolveForCurrentTenant()->authMode;
}
public function getODataBaseUrl(): string
{
return $this->resolveForCurrentTenant()->odataBaseUrl;
}
public function getCompanyName(): string
{
return $this->resolveForCurrentTenant()->companyName;
}
public function getBasicUser(): ?string
{
return $this->resolveForCurrentTenant()->basicUser;
}
public function getBasicPassword(): ?string
{
return $this->resolveForCurrentTenant()->basicPassword;
}
public function getOAuthTenantId(): ?string
{
return $this->resolveForCurrentTenant()->oauthTenantId;
}
public function getOAuthClientId(): ?string
{
return $this->resolveForCurrentTenant()->oauthClientId;
}
public function getOAuthClientSecret(): ?string
{
return $this->resolveForCurrentTenant()->oauthClientSecret;
}
public function getOAuthTokenEndpoint(): ?string
{
return $this->resolveForCurrentTenant()->oauthTokenEndpoint;
}
public function buildEntityUrl(string $entitySet): string
{
return $this->resolveForCurrentTenant()->buildEntityUrl($entitySet);
}
/**
* @return array<string>
*/
public function validateConfiguration(): array
{
return $this->resolveForCurrentTenant()->validateConfiguration();
}
public function isConfigured(): bool
{
return $this->resolveForCurrentTenant()->isConfigured();
}
/**
* @return array{
* config: array<string, mixed>,
* source: 'default'|'settings'|'fallback_invalid_json'
* }
*/
public function getSystemRecommendationsConfigEnvelope(): array
{
return $this->globalGateway->getSystemRecommendationsConfigEnvelope();
}
/**
* @return array<string, mixed>
*/
public function getSystemRecommendationsConfig(): array
{
return $this->globalGateway->getSystemRecommendationsConfig();
}
/**
* @return array{
* config: array<string, mixed>,
* source: 'default'|'settings'|'fallback_invalid_json'
* }
*/
public function getControllingRiskConfigEnvelope(): array
{
return $this->globalGateway->getControllingRiskConfigEnvelope();
}
/**
* @return array<string, mixed>
*/
public function getControllingRiskConfig(): array
{
return $this->globalGateway->getControllingRiskConfig();
}
private function buildFromGlobal(): EffectiveHelpdeskSettings
{
return new EffectiveHelpdeskSettings(
source: 'global',
authMode: $this->globalGateway->getAuthMode(),
odataBaseUrl: $this->globalGateway->getODataBaseUrl(),
companyName: $this->globalGateway->getCompanyName(),
basicUser: $this->globalGateway->getBasicUser(),
basicPassword: $this->globalGateway->getBasicPassword(),
oauthTenantId: $this->globalGateway->getOAuthTenantId(),
oauthClientId: $this->globalGateway->getOAuthClientId(),
oauthClientSecret: $this->globalGateway->getOAuthClientSecret(),
oauthTokenEndpoint: $this->globalGateway->getOAuthTokenEndpoint(),
systemRecommendationsConfigRaw: null,
controllingRiskConfigRaw: null,
);
}
private function buildFromTenant(int $tenantId): EffectiveHelpdeskSettings
{
return new EffectiveHelpdeskSettings(
source: 'tenant',
authMode: $this->tenantGateway->getAuthMode($tenantId),
odataBaseUrl: $this->tenantGateway->getODataBaseUrl($tenantId),
companyName: $this->tenantGateway->getCompanyName($tenantId),
basicUser: $this->tenantGateway->getBasicUser($tenantId),
basicPassword: $this->tenantGateway->getBasicPassword($tenantId),
oauthTenantId: $this->tenantGateway->getOAuthTenantId($tenantId),
oauthClientId: $this->tenantGateway->getOAuthClientId($tenantId),
oauthClientSecret: $this->tenantGateway->getOAuthClientSecret($tenantId),
oauthTokenEndpoint: $this->tenantGateway->getOAuthTokenEndpoint($tenantId),
systemRecommendationsConfigRaw: $this->tenantGateway->getSystemRecommendationsConfig($tenantId),
controllingRiskConfigRaw: $this->tenantGateway->getControllingRiskConfig($tenantId),
);
}
private function resolveCurrentTenantId(): int
{
$session = $this->sessionStore->all();
$tenant = $session['current_tenant'] ?? null;
if (!is_array($tenant)) {
return 0;
}
return (int) ($tenant['id'] ?? 0);
}
}