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']); }); it('creates subscription truth through the confirmed system action and renders subscription-backed detail', function (): void { $workspace = Workspace::factory()->create(['name' => 'Subscription Workspace']); $operator = PlatformUser::factory()->create([ 'name' => 'Platform Operator', 'capabilities' => [ PlatformCapabilities::ACCESS_SYSTEM_PANEL, PlatformCapabilities::DIRECTORY_VIEW, PlatformCapabilities::COMMERCIAL_LIFECYCLE_MANAGE, ], 'is_active' => true, ]); $trialEndsAt = now()->addDays(14)->startOfMinute(); Livewire::actingAs($operator, 'platform') ->test(ViewWorkspace::class, ['workspace' => $workspace]) ->assertActionVisible('update_subscription_truth') ->assertActionExists('update_subscription_truth', fn (Action $action): bool => $action->getLabel() === 'Update subscription truth' && $action->isConfirmationRequired()) ->callAction('update_subscription_truth', data: [ 'state' => 'trial', 'billing_reference' => 'sub_trial_001', 'trial_ends_at' => $trialEndsAt->toDateTimeString(), 'current_period_starts_at' => null, 'current_period_ends_at' => null, 'status_reason' => 'Trial access for onboarding.', ]) ->assertNotified('Subscription truth updated'); $subscription = WorkspaceSubscription::query() ->where('workspace_id', (int) $workspace->getKey()) ->first(); expect($subscription) ->not->toBeNull() ->and($subscription?->state)->toBe('trial') ->and($subscription?->billing_reference)->toBe('sub_trial_001') ->and($subscription?->status_reason)->toBe('Trial access for onboarding.'); $summary = app(WorkspaceCommercialLifecycleResolver::class)->summary($workspace->fresh()); expect($summary) ->toMatchArray([ 'source' => WorkspaceCommercialLifecycleResolver::SOURCE_WORKSPACE_SUBSCRIPTION, 'subscription_present' => true, 'subscription_state' => 'trial', 'subscription_state_label' => 'Trial', 'state' => WorkspaceCommercialLifecycleResolver::STATE_TRIAL, ]); $this->actingAs($operator, 'platform') ->get(ViewWorkspace::getUrl(panel: 'system', parameters: ['workspace' => $workspace])) ->assertSuccessful() ->assertSee('Workspace subscription') ->assertSee('subscription-backed') ->assertSee('Trial access for onboarding.') ->assertSee('sub_trial_001') ->assertSee('Trial ends'); }); it('requires a trial end date before changing subscription truth to trial', function (): void { $workspace = Workspace::factory()->create(['name' => 'Trial Validation Workspace']); $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('update_subscription_truth', data: [ 'state' => 'trial', 'billing_reference' => 'sub_trial_missing_date', 'trial_ends_at' => null, 'current_period_starts_at' => null, 'current_period_ends_at' => null, 'status_reason' => 'Trial access needs an explicit end date.', ]) ->assertHasActionErrors(['trial_ends_at']); expect(WorkspaceSubscription::query() ->where('workspace_id', (int) $workspace->getKey()) ->exists())->toBeFalse(); }); it('keeps manual lifecycle fallback controls only for fallback-backed workspaces', 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]) ->assertActionVisible('update_subscription_truth') ->assertActionVisible('change_commercial_state'); WorkspaceSubscription::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), 'state' => 'active', 'current_period_starts_at' => now()->subDay(), 'current_period_ends_at' => now()->addDays(29), 'status_reason' => 'Annual plan is current.', ]); Livewire::actingAs($operator, 'platform') ->test(ViewWorkspace::class, ['workspace' => $workspace->fresh()]) ->assertActionVisible('update_subscription_truth') ->assertActionHidden('change_commercial_state'); });