Some checks failed
Main Confidence / confidence (push) Failing after 52s
Add Customer Health decision card to tenant & workspace detail pages (spec 245). What I changed: - Render a decision-first Customer Health card on tenant and workspace detail pages. - Reuse `WorkspaceHealthSummaryQuery` and preserve `window` query param. - Update attention widget link text to "Review health details" and include `?window=`. - Add/adjust tests to cover new behavior and explainability. - Run Pint formatting. Compare URL: https://git.cloudarix.de/ahmido/TenantAtlas/compare/dev...245-customer-health-score Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #283
186 lines
6.4 KiB
PHP
186 lines
6.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\System\Pages\Dashboard;
|
|
use App\Filament\System\Widgets\CustomerHealthKpis;
|
|
use App\Models\Finding;
|
|
use App\Models\OperationRun;
|
|
use App\Models\PlatformUser;
|
|
use App\Models\ProductUsageEvent;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\ReviewPack;
|
|
use App\Models\Tenant;
|
|
use App\Models\Workspace;
|
|
use App\Support\Auth\PlatformCapabilities;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\ProductTelemetry\ProductUsageEventCatalog;
|
|
use App\Support\SystemConsole\SystemConsoleWindow;
|
|
use Carbon\CarbonImmutable;
|
|
use Filament\Facades\Filament;
|
|
use Filament\Widgets\StatsOverviewWidget\Stat;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function (): void {
|
|
Filament::setCurrentPanel('system');
|
|
Filament::bootCurrentPanel();
|
|
|
|
CarbonImmutable::setTestNow(CarbonImmutable::parse('2026-04-27 12:00:00'));
|
|
});
|
|
|
|
afterEach(function (): void {
|
|
CarbonImmutable::setTestNow();
|
|
});
|
|
|
|
it('renders aggregate healthy, warning, critical, and unknown counts with a visible time-basis cue', function (): void {
|
|
actingAsCustomerHealthSystemUser();
|
|
|
|
$baselineStats = customerHealthStats(Livewire::withQueryParams([
|
|
'window' => SystemConsoleWindow::LastDay,
|
|
])->test(CustomerHealthKpis::class));
|
|
|
|
seedCustomerHealthWorkspace('Healthy Workspace');
|
|
seedCustomerHealthWorkspace('Warning Workspace', recentUsage: false, historicalUsage: true);
|
|
seedCustomerHealthWorkspace('Critical Workspace', failedRun: true);
|
|
seedCustomerHealthWorkspace('Unknown Workspace', recentRun: false, readyReviewPack: false);
|
|
|
|
$stats = customerHealthStats(Livewire::withQueryParams([
|
|
'window' => SystemConsoleWindow::LastDay,
|
|
])->test(CustomerHealthKpis::class));
|
|
|
|
expect((int) $stats['Healthy']['value'] - (int) $baselineStats['Healthy']['value'])->toBe(1)
|
|
->and($stats['Healthy']['description'])->toBe('Operational stability, review-pack readiness, and engagement freshness honor Last 24 hours.')
|
|
->and((int) $stats['Warning']['value'] - (int) $baselineStats['Warning']['value'])->toBe(1)
|
|
->and($stats['Warning']['description'])->toBe('Onboarding readiness, provider health, and governance pressure stay point-in-time.')
|
|
->and((int) $stats['Critical']['value'] - (int) $baselineStats['Critical']['value'])->toBe(1)
|
|
->and((int) $stats['Unknown']['value'] - (int) $baselineStats['Unknown']['value'])->toBe(1)
|
|
->and($stats['Unknown']['description'])->toBe('Missing or stale inputs stay explicit instead of silently reading healthy.');
|
|
});
|
|
|
|
it('registers the customer health widget on the system dashboard for authorized users', function (): void {
|
|
$user = actingAsCustomerHealthSystemUser();
|
|
|
|
$response = $this->actingAs($user, 'platform')->get(Dashboard::getUrl(panel: 'system'));
|
|
|
|
$response->assertSuccessful()
|
|
->assertSee('Customer health')
|
|
->assertSeeLivewire(CustomerHealthKpis::class);
|
|
});
|
|
|
|
/**
|
|
* @return array<string, array{value:string,description:string|null}>
|
|
*/
|
|
function customerHealthStats($component): array
|
|
{
|
|
$method = new ReflectionMethod(CustomerHealthKpis::class, 'getStats');
|
|
$method->setAccessible(true);
|
|
|
|
return collect($method->invoke($component->instance()))
|
|
->mapWithKeys(fn (Stat $stat): array => [
|
|
(string) $stat->getLabel() => [
|
|
'value' => (string) $stat->getValue(),
|
|
'description' => $stat->getDescription(),
|
|
],
|
|
])
|
|
->all();
|
|
}
|
|
|
|
function actingAsCustomerHealthSystemUser(): PlatformUser
|
|
{
|
|
$user = PlatformUser::factory()->create([
|
|
'capabilities' => [
|
|
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
|
|
PlatformCapabilities::CONSOLE_VIEW,
|
|
],
|
|
'is_active' => true,
|
|
]);
|
|
|
|
test()->actingAs($user, 'platform');
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* @return array{workspace: Workspace, tenant: Tenant}
|
|
*/
|
|
function seedCustomerHealthWorkspace(
|
|
string $workspaceName,
|
|
bool $readyReviewPack = true,
|
|
bool $recentUsage = true,
|
|
bool $historicalUsage = false,
|
|
bool $recentRun = true,
|
|
bool $failedRun = false,
|
|
): array {
|
|
$workspace = Workspace::factory()->create(['name' => $workspaceName]);
|
|
$tenant = Tenant::factory()->for($workspace)->create([
|
|
'name' => $workspaceName.' Tenant',
|
|
'status' => Tenant::STATUS_ACTIVE,
|
|
]);
|
|
|
|
ProviderConnection::factory()
|
|
->for($tenant)
|
|
->verifiedHealthy()
|
|
->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'is_default' => true,
|
|
]);
|
|
|
|
if ($readyReviewPack) {
|
|
ReviewPack::factory()
|
|
->for($tenant)
|
|
->ready()
|
|
->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'created_at' => now()->subHour(),
|
|
'generated_at' => now()->subHour(),
|
|
'expires_at' => now()->addDays(30),
|
|
]);
|
|
}
|
|
|
|
if ($recentUsage) {
|
|
ProductUsageEvent::factory()
|
|
->for($tenant)
|
|
->forEvent(ProductUsageEventCatalog::ONBOARDING_CHECKPOINT_COMPLETED)
|
|
->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'occurred_at' => now()->subMinutes(20),
|
|
]);
|
|
} elseif ($historicalUsage) {
|
|
ProductUsageEvent::factory()
|
|
->for($tenant)
|
|
->forEvent(ProductUsageEventCatalog::ONBOARDING_CHECKPOINT_COMPLETED)
|
|
->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'occurred_at' => now()->subDays(3),
|
|
]);
|
|
}
|
|
|
|
if ($recentRun) {
|
|
OperationRun::factory()
|
|
->forTenant($tenant)
|
|
->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => $failedRun ? OperationRunOutcome::Failed->value : OperationRunOutcome::Succeeded->value,
|
|
'created_at' => now()->subMinutes(10),
|
|
'started_at' => now()->subMinutes(15),
|
|
'completed_at' => now()->subMinutes(5),
|
|
]);
|
|
}
|
|
|
|
Finding::factory()
|
|
->for($tenant)
|
|
->closed()
|
|
->create([
|
|
'severity' => Finding::SEVERITY_LOW,
|
|
]);
|
|
|
|
return [
|
|
'workspace' => $workspace,
|
|
'tenant' => $tenant,
|
|
];
|
|
} |