TenantAtlas/apps/platform/tests/Feature/Onboarding/ManagedTenantOnboardingEntitlementTest.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

277 lines
9.7 KiB
PHP

<?php
declare(strict_types=1);
use App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard;
use App\Models\AuditLog;
use App\Models\OperationRun;
use App\Models\PlatformUser;
use App\Models\ProviderConnection;
use App\Models\Tenant;
use App\Models\TenantOnboardingSession;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Entitlements\WorkspaceCommercialLifecycleResolver;
use App\Services\Entitlements\WorkspaceEntitlementResolver;
use App\Services\Settings\SettingsWriter;
use App\Support\Auth\PlatformCapabilities;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
/**
* @return array{workspace: Workspace, user: User, tenant: Tenant, draft: TenantOnboardingSession, component: \Livewire\Features\SupportTesting\Testable}
*/
function readyOnboardingEntitlementContext(
int $activeTenantCount = 0,
?int $limitOverride = null,
?string $overrideReason = null,
?string $commercialState = null,
): array
{
Queue::fake();
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
$tenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'status' => Tenant::STATUS_ONBOARDING,
]);
createUserWithTenant(
tenant: $tenant,
user: $user,
role: 'owner',
workspaceRole: 'owner',
ensureDefaultMicrosoftProviderConnection: false,
);
if ($activeTenantCount > 0) {
Tenant::factory()->count($activeTenantCount)->create([
'workspace_id' => (int) $workspace->getKey(),
'status' => Tenant::STATUS_ACTIVE,
]);
}
$connection = ProviderConnection::factory()->platform()->consentGranted()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => (string) $tenant->tenant_id,
'display_name' => 'Ready connection',
'is_default' => true,
'consent_status' => 'granted',
]);
$run = OperationRun::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'type' => 'provider.connection.check',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
'context' => [
'provider' => 'microsoft',
'module' => 'health_check',
'provider_connection_id' => (int) $connection->getKey(),
'target_scope' => [
'entra_tenant_id' => (string) $tenant->tenant_id,
],
],
]);
$draft = createOnboardingDraft([
'workspace' => $workspace,
'tenant' => $tenant,
'started_by' => $user,
'updated_by' => $user,
'current_step' => 'bootstrap',
'state' => [
'entra_tenant_id' => (string) $tenant->tenant_id,
'tenant_name' => (string) $tenant->name,
'provider_connection_id' => (int) $connection->getKey(),
'verification_operation_run_id' => (int) $run->getKey(),
],
]);
if ($limitOverride !== null) {
$writer = app(SettingsWriter::class);
$writer->updateWorkspaceSetting(
actor: $user,
workspace: $workspace,
domain: WorkspaceEntitlementResolver::SETTING_DOMAIN,
key: WorkspaceEntitlementResolver::SETTING_MANAGED_TENANT_LIMIT_OVERRIDE_VALUE,
value: $limitOverride,
);
if ($overrideReason !== null) {
$writer->updateWorkspaceSetting(
actor: $user,
workspace: $workspace,
domain: WorkspaceEntitlementResolver::SETTING_DOMAIN,
key: WorkspaceEntitlementResolver::SETTING_MANAGED_TENANT_LIMIT_OVERRIDE_REASON,
value: $overrideReason,
);
}
}
if ($commercialState !== null) {
app(SettingsWriter::class)->updateWorkspaceCommercialLifecycle(
actor: PlatformUser::factory()->create([
'capabilities' => [
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
PlatformCapabilities::DIRECTORY_VIEW,
PlatformCapabilities::COMMERCIAL_LIFECYCLE_MANAGE,
],
'is_active' => true,
]),
workspace: $workspace,
state: $commercialState,
reason: 'Onboarding entitlement test commercial state',
);
}
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$component = Livewire::actingAs($user)->test(ManagedTenantOnboardingWizard::class, [
'onboardingDraft' => (int) $draft->getKey(),
]);
return compact('workspace', 'user', 'tenant', 'draft', 'component');
}
it('allows onboarding activation when the workspace is within its managed tenant limit', function (): void {
$context = readyOnboardingEntitlementContext(activeTenantCount: 0);
$context['component']->call('completeOnboarding');
$context['tenant']->refresh();
expect($context['tenant']->status)->toBe(Tenant::STATUS_ACTIVE)
->and(AuditLog::query()
->where('workspace_id', (int) $context['workspace']->getKey())
->where('action', 'managed_tenant_onboarding.activation')
->exists())->toBeTrue();
});
it('blocks onboarding activation with a business-state reason when the workspace is at limit', function (): void {
$context = readyOnboardingEntitlementContext(
activeTenantCount: 1,
limitOverride: 1,
overrideReason: 'Customer currently allows one active tenant',
);
$decision = app(WorkspaceEntitlementResolver::class)->resolve(
$context['workspace'],
WorkspaceEntitlementResolver::KEY_MANAGED_TENANT_ACTIVATION_LIMIT,
);
expect($decision['is_blocked'])->toBeTrue();
$context['component']
->assertSee('Activation entitlement')
->assertSee('Blocked')
->call('completeOnboarding');
$context['tenant']->refresh();
expect($context['tenant']->status)->toBe(Tenant::STATUS_ONBOARDING)
->and(AuditLog::query()
->where('workspace_id', (int) $context['workspace']->getKey())
->where('action', 'managed_tenant_onboarding.activation')
->exists())->toBeFalse();
});
it('allows onboarding activation when a workspace override raises the limit above current usage', function (): void {
$context = readyOnboardingEntitlementContext(
activeTenantCount: 1,
limitOverride: 2,
overrideReason: 'Temporary support-approved exception',
);
$decision = app(WorkspaceEntitlementResolver::class)->resolve(
$context['workspace'],
WorkspaceEntitlementResolver::KEY_MANAGED_TENANT_ACTIVATION_LIMIT,
);
expect($decision)
->toMatchArray([
'source' => 'workspace_override',
'effective_value' => 2,
'current_usage' => 1,
'is_blocked' => false,
'rationale' => 'Temporary support-approved exception',
]);
$context['component']->call('completeOnboarding');
$context['tenant']->refresh();
expect($context['tenant']->status)->toBe(Tenant::STATUS_ACTIVE);
});
it('allows onboarding activation while a workspace is in trial', function (): void {
$context = readyOnboardingEntitlementContext(
activeTenantCount: 0,
commercialState: WorkspaceCommercialLifecycleResolver::STATE_TRIAL,
);
$context['component']
->assertSee('Activation entitlement')
->assertSee('Trial')
->call('completeOnboarding');
$context['tenant']->refresh();
expect($context['tenant']->status)->toBe(Tenant::STATUS_ACTIVE)
->and(AuditLog::query()
->where('workspace_id', (int) $context['workspace']->getKey())
->where('action', 'managed_tenant_onboarding.activation')
->exists())->toBeTrue();
});
it('blocks onboarding activation with a grace commercial-state reason before tenant mutation', function (): void {
$context = readyOnboardingEntitlementContext(
activeTenantCount: 0,
commercialState: WorkspaceCommercialLifecycleResolver::STATE_GRACE,
);
$context['component']
->assertSee('Activation entitlement')
->assertSee('Grace')
->assertSee('New managed-tenant activation is frozen while this workspace is in grace.')
->call('completeOnboarding');
$context['tenant']->refresh();
expect($context['tenant']->status)->toBe(Tenant::STATUS_ONBOARDING)
->and(AuditLog::query()
->where('workspace_id', (int) $context['workspace']->getKey())
->where('action', 'managed_tenant_onboarding.activation')
->exists())->toBeFalse();
});
it('blocks onboarding activation with a suspended read-only commercial-state reason before tenant mutation', function (): void {
$context = readyOnboardingEntitlementContext(
activeTenantCount: 0,
commercialState: WorkspaceCommercialLifecycleResolver::STATE_SUSPENDED_READ_ONLY,
);
$context['component']
->assertSee('Activation entitlement')
->assertSee('Suspended / read-only')
->assertSee('This workspace is suspended / read-only. New managed-tenant activation is blocked')
->call('completeOnboarding');
$context['tenant']->refresh();
expect($context['tenant']->status)->toBe(Tenant::STATUS_ONBOARDING)
->and(AuditLog::query()
->where('workspace_id', (int) $context['workspace']->getKey())
->where('action', 'managed_tenant_onboarding.activation')
->exists())->toBeFalse();
});