lifecycleActionForTenant($tenant)) ->toBeNull(); })->with([ 'draft' => [fn (): Tenant => Tenant::factory()->draft()->create()], 'onboarding' => [fn (): Tenant => Tenant::factory()->onboarding()->create()], ]); it('returns archive only for active tenants and restore only for archived tenants', function ( \Closure $tenantFactory, string $expectedKey, string $expectedLabel, ): void { $tenant = $tenantFactory(); $descriptor = app(TenantActionPolicySurface::class)->lifecycleActionForTenant($tenant); expect($descriptor) ->not->toBeNull() ->and($descriptor?->key)->toBe($expectedKey) ->and($descriptor?->label)->toBe($expectedLabel) ->and($descriptor?->requiresConfirmation)->toBeTrue(); })->with([ 'active' => [fn (): Tenant => Tenant::factory()->active()->create(), 'archive', 'Archive'], 'archived' => [fn (): Tenant => Tenant::factory()->archived()->create(), 'restore', 'Restore'], ]); it('returns resume onboarding as the primary action for draft and onboarding tenants with resumable drafts', function (\Closure $tenantFactory): void { $tenant = $tenantFactory(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner', ensureDefaultMicrosoftProviderConnection: false); createOnboardingDraft([ 'workspace' => $tenant->workspace, 'tenant' => $tenant, 'started_by' => $user, 'updated_by' => $user, 'state' => [ 'entra_tenant_id' => (string) $tenant->tenant_id, 'tenant_name' => (string) $tenant->name, ], ]); $catalog = tenantActionCatalog($tenant, TenantActionSurface::TenantIndexRow, $user); expect(tenantActionKeys($catalog)) ->toBe(['view', 'related_onboarding']) ->and($catalog[1]->label)->toBe('Resume onboarding'); })->with([ 'draft' => [fn (): Tenant => Tenant::factory()->draft()->create()], 'onboarding' => [fn (): Tenant => Tenant::factory()->onboarding()->create()], ]); it('keeps completed onboarding as a view-only overflow action for active tenants', function (): void { $tenant = Tenant::factory()->active()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner', ensureDefaultMicrosoftProviderConnection: false); createOnboardingDraft([ 'workspace' => $tenant->workspace, 'tenant' => $tenant, 'started_by' => $user, 'updated_by' => $user, 'status' => 'completed', 'state' => [ 'entra_tenant_id' => (string) $tenant->tenant_id, 'tenant_name' => (string) $tenant->name, ], ]); $catalog = tenantActionCatalog($tenant, TenantActionSurface::TenantIndexRow, $user); expect(tenantActionKeys($catalog)) ->toBe(['view', 'archive', 'related_onboarding']) ->and($catalog[2]->label)->toBe('View completed onboarding'); }); it('keeps tenant index catalogs within the two-primary-action overflow contract', function (): void { $tenant = Tenant::factory()->active()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner', ensureDefaultMicrosoftProviderConnection: false); createOnboardingDraft([ 'workspace' => $tenant->workspace, 'tenant' => $tenant, 'started_by' => $user, 'updated_by' => $user, 'status' => 'completed', 'state' => [ 'entra_tenant_id' => (string) $tenant->tenant_id, 'tenant_name' => (string) $tenant->name, ], ]); $catalog = tenantActionCatalog($tenant, TenantActionSurface::TenantIndexRow, $user); $primaryKeys = collect($catalog) ->filter(static fn ($action): bool => $action->group === 'primary') ->map(static fn ($action): string => $action->key) ->values() ->all(); $overflowKeys = collect($catalog) ->filter(static fn ($action): bool => $action->group === 'overflow') ->map(static fn ($action): string => $action->key) ->values() ->all(); expect($primaryKeys)->toBe(['view', 'archive']) ->and($overflowKeys)->toBe(['related_onboarding']) ->and(TenantResource::actionSurfaceDeclaration()->listRowPrimaryActionLimit())->toBe(2); }); it('invalidates cached tenant index catalogs when the related onboarding draft lifecycle changes', function (): void { $tenant = Tenant::factory()->active()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner', ensureDefaultMicrosoftProviderConnection: false); $draft = createOnboardingDraft([ 'workspace' => $tenant->workspace, 'tenant' => $tenant, 'started_by' => $user, 'updated_by' => $user, 'status' => 'in_progress', 'state' => [ 'entra_tenant_id' => (string) $tenant->tenant_id, 'tenant_name' => (string) $tenant->name, ], ]); $initialCatalog = tenantActionCatalog($tenant, TenantActionSurface::TenantIndexRow, $user); expect(tenantActionKeys($initialCatalog))->toBe(['view', 'archive', 'related_onboarding']) ->and($initialCatalog[2]->group)->toBe('overflow') ->and($initialCatalog[2]->label)->toBe('View related onboarding'); $draft->forceFill([ 'completed_at' => now()->addSecond(), 'lifecycle_state' => 'completed', 'updated_at' => now()->addSecond(), ])->save(); $tenant->refresh(); $updatedCatalog = tenantActionCatalog($tenant, TenantActionSurface::TenantIndexRow, $user); expect(tenantActionKeys($updatedCatalog))->toBe(['view', 'archive', 'related_onboarding']) ->and($updatedCatalog[2]->group)->toBe('overflow') ->and($updatedCatalog[2]->label)->toBe('View completed onboarding'); }); it('uses workflow-accurate onboarding entry labels', function (int $draftCount, string $expectedLabel): void { $descriptor = app(TenantActionPolicySurface::class)->onboardingEntryDescriptor($draftCount); expect($descriptor->label)->toBe($expectedLabel); })->with([ 'no drafts' => [0, 'Add tenant'], 'one draft' => [1, 'Resume onboarding'], 'multiple drafts' => [2, 'Choose onboarding draft'], ]);