Automated PR: commit all local changes and add feature 274-billing-subscription-truth. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #326
311 lines
12 KiB
PHP
311 lines
12 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\Models\WorkspaceSubscription;
|
|
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']);
|
|
});
|
|
|
|
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');
|
|
});
|