TenantAtlas/apps/platform/tests/Feature/System/ViewWorkspaceEntitlementsTest.php
ahmido 7ee4909212
Some checks failed
Main Confidence / confidence (push) Failing after 1m45s
feat: commercial lifecycle overlay for workspace entitlements (#292)
## Summary
- add the bounded workspace commercial lifecycle overlay from spec 251 on top of the existing entitlement substrate
- expose audited commercial state inspection and mutation on the system workspace detail surface
- gate onboarding activation and review-pack start actions through the shared lifecycle decision while preserving suspended read-only access to existing review, evidence, and generated-pack history
- add focused Pest coverage plus the spec/plan/tasks/data-model/contract artifacts for the feature

## Validation
- targeted Pest unit and feature lanes for lifecycle resolution, system-plane mutation, onboarding gating, review-pack enforcement, download preservation, customer review workspace access, and evidence snapshot access
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- integrated browser smoke on the system workspace detail and the preserved read-only review/evidence/review-pack surfaces

## Notes
- branch: `251-commercial-entitlements-billing-state`
- base: `dev`
- commit: `606e9760`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #292
2026-04-28 13:39:33 +00:00

193 lines
7.4 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\System\Pages\Directory\ViewWorkspace;
use App\Models\AuditLog;
use App\Models\PlatformUser;
use App\Models\Tenant;
use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Models\WorkspaceSetting;
use App\Services\Entitlements\WorkspaceCommercialLifecycleResolver;
use App\Services\Settings\SettingsWriter;
use App\Support\Audit\AuditActionId;
use App\Support\Auth\PlatformCapabilities;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Livewire\Livewire;
beforeEach(function (): void {
Filament::setCurrentPanel('system');
Filament::bootCurrentPanel();
});
it('renders the read-only workspace entitlement summary on the system workspace detail page', function (): void {
$workspace = Workspace::factory()->create(['name' => 'Acme Workspace']);
$manager = User::factory()->create(['name' => 'Workspace Manager']);
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $manager->getKey(),
'role' => 'manager',
]);
Tenant::factory()->count(2)->create([
'workspace_id' => (int) $workspace->getKey(),
'status' => Tenant::STATUS_ACTIVE,
]);
$writer = app(SettingsWriter::class);
$writer->updateWorkspaceSetting(
actor: $manager,
workspace: $workspace,
domain: 'entitlements',
key: 'plan_profile',
value: 'starter',
);
$writer->updateWorkspaceSetting(
actor: $manager,
workspace: $workspace,
domain: 'entitlements',
key: 'managed_tenant_limit_override_value',
value: 2,
);
$writer->updateWorkspaceSetting(
actor: $manager,
workspace: $workspace,
domain: 'entitlements',
key: 'managed_tenant_limit_override_reason',
value: 'Pilot workspace',
);
$writer->updateWorkspaceSetting(
actor: $manager,
workspace: $workspace,
domain: 'entitlements',
key: 'review_pack_generation_override_value',
value: false,
);
$writer->updateWorkspaceSetting(
actor: $manager,
workspace: $workspace,
domain: 'entitlements',
key: 'review_pack_generation_override_reason',
value: 'Escalation only',
);
$platformUser = PlatformUser::factory()->create([
'capabilities' => [
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
PlatformCapabilities::DIRECTORY_VIEW,
],
'is_active' => true,
]);
$this->actingAs($platformUser, 'platform')
->get(ViewWorkspace::getUrl(panel: 'system', parameters: ['workspace' => $workspace]))
->assertSuccessful()
->assertSee('Workspace entitlements')
->assertSee('Starter')
->assertSee('Pilot workspace')
->assertSee('Escalation only')
->assertSee('workspace override')
->assertSee('Commercial lifecycle')
->assertSee('Active paid')
->assertSee('default active paid')
->assertDontSee('Save');
});
it('gates the commercial lifecycle mutation action behind a dedicated platform capability', function (): void {
$workspace = Workspace::factory()->create();
$viewer = PlatformUser::factory()->create([
'capabilities' => [
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
PlatformCapabilities::DIRECTORY_VIEW,
],
'is_active' => true,
]);
Livewire::actingAs($viewer, 'platform')
->test(ViewWorkspace::class, ['workspace' => $workspace])
->assertActionHidden('change_commercial_state');
});
it('changes commercial lifecycle state through the confirmed system action and records audit truth', function (): void {
$workspace = Workspace::factory()->create(['name' => 'Lifecycle Workspace']);
$operator = PlatformUser::factory()->create([
'name' => 'Platform Operator',
'capabilities' => [
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
PlatformCapabilities::DIRECTORY_VIEW,
PlatformCapabilities::COMMERCIAL_LIFECYCLE_MANAGE,
],
'is_active' => true,
]);
Livewire::actingAs($operator, 'platform')
->test(ViewWorkspace::class, ['workspace' => $workspace])
->assertActionVisible('change_commercial_state')
->assertActionExists('change_commercial_state', fn (Action $action): bool => $action->getLabel() === 'Change commercial state'
&& $action->isConfirmationRequired())
->callAction('change_commercial_state', data: [
'state' => WorkspaceCommercialLifecycleResolver::STATE_SUSPENDED_READ_ONLY,
'reason' => 'Commercial suspension approved by support',
])
->assertNotified('Commercial state updated');
expect(WorkspaceSetting::query()
->where('workspace_id', (int) $workspace->getKey())
->where('domain', WorkspaceCommercialLifecycleResolver::SETTING_DOMAIN)
->where('key', WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_STATE)
->value('value'))->toBe(WorkspaceCommercialLifecycleResolver::STATE_SUSPENDED_READ_ONLY)
->and(WorkspaceSetting::query()
->where('workspace_id', (int) $workspace->getKey())
->where('domain', WorkspaceCommercialLifecycleResolver::SETTING_DOMAIN)
->where('key', WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_REASON)
->value('value'))->toBe('Commercial suspension approved by support');
$audit = AuditLog::query()
->where('workspace_id', (int) $workspace->getKey())
->where('action', AuditActionId::WorkspaceSettingUpdated->value)
->where('resource_id', WorkspaceCommercialLifecycleResolver::SETTING_DOMAIN.'.'.WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_STATE)
->latest('id')
->first();
expect($audit)->not->toBeNull()
->and($audit?->actor_name)->toBe('Platform Operator')
->and($audit?->metadata['before_state'] ?? null)->toBeNull()
->and($audit?->metadata['after_state'] ?? null)->toBe(WorkspaceCommercialLifecycleResolver::STATE_SUSPENDED_READ_ONLY)
->and($audit?->metadata['after_reason'] ?? null)->toBe('Commercial suspension approved by support');
$summary = app(WorkspaceCommercialLifecycleResolver::class)->summary($workspace);
expect($summary)
->toMatchArray([
'state' => WorkspaceCommercialLifecycleResolver::STATE_SUSPENDED_READ_ONLY,
'source' => WorkspaceCommercialLifecycleResolver::SOURCE_WORKSPACE_SETTING,
'rationale' => 'Commercial suspension approved by support',
'last_changed_by' => 'Platform Operator',
]);
});
it('requires a rationale before changing commercial lifecycle state', function (): void {
$workspace = Workspace::factory()->create();
$operator = PlatformUser::factory()->create([
'capabilities' => [
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
PlatformCapabilities::DIRECTORY_VIEW,
PlatformCapabilities::COMMERCIAL_LIFECYCLE_MANAGE,
],
'is_active' => true,
]);
Livewire::actingAs($operator, 'platform')
->test(ViewWorkspace::class, ['workspace' => $workspace])
->callAction('change_commercial_state', data: [
'state' => WorkspaceCommercialLifecycleResolver::STATE_GRACE,
'reason' => '',
])
->assertHasActionErrors(['reason']);
});