Some checks failed
Main Confidence / confidence (push) Failing after 1m45s
## 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
277 lines
9.7 KiB
PHP
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();
|
|
});
|