TenantAtlas/apps/platform/app/Services/Tenants/TenantActionPolicySurface.php
ahmido 292d555eac refactor: consolidate internal tenant model naming (#355)
## Summary
- consolidate internal platform naming from `Tenant` to `Environment` / `ManagedEnvironment` across models, controllers, services, and Filament resources
- rename environment-scoped UI surfaces such as dashboards, chooser flows, navigation, and related widgets to match the updated environment-first domain language
- align middleware, onboarding/review lifecycle services, jobs, and route/context controllers with the new environment-scoped architecture

## Validation
- not rerun as part of this commit/push/PR request

## Notes
- branch is 1 commit ahead of `platform-dev`
- main commit: `refactor: consolidate internal tenant model naming`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #355
2026-05-14 11:13:28 +00:00

327 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Tenants;
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentOnboardingSession;
use App\Models\User;
use App\Services\Onboarding\OnboardingLifecycleService;
use App\Support\Audit\AuditActionId;
use App\Support\Tenants\TenantActionContext;
use App\Support\Tenants\TenantActionDescriptor;
use App\Support\Tenants\TenantActionFamily;
use App\Support\Tenants\TenantActionSurface;
use App\Support\Tenants\TenantInteractionLane;
use App\Support\Tenants\TenantOperabilityOutcome;
use App\Support\Tenants\TenantOperabilityQuestion;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Support\Facades\Gate;
class TenantActionPolicySurface
{
public function __construct(
private readonly TenantOperabilityService $tenantOperabilityService,
private readonly OnboardingLifecycleService $onboardingLifecycleService,
private readonly WorkspaceContext $workspaceContext,
) {}
public function buildContext(ManagedEnvironment $tenant, TenantActionSurface $surface, ?User $user = null): TenantActionContext
{
$user ??= auth()->user();
$draft = $user instanceof User ? $this->relatedOnboardingDraft($tenant, $user) : null;
$lifecycle = $this->tenantOperabilityService->lifecycleFor($tenant);
$lane = $surface->isOnboardingSurface()
? TenantInteractionLane::OnboardingWorkflow
: TenantInteractionLane::AdministrativeManagement;
$workspaceId = request() !== null
? $this->workspaceContext->currentWorkspaceId(request())
: null;
$resumeOutcome = $draft instanceof ManagedEnvironmentOnboardingSession
? $this->tenantOperabilityService->outcomeFor(
tenant: $tenant,
question: TenantOperabilityQuestion::ResumeOnboardingEligibility,
actor: $user instanceof User ? $user : null,
workspaceId: $workspaceId,
lane: $lane,
onboardingDraft: $draft,
)
: null;
return new TenantActionContext(
tenant: $tenant,
lifecycle: $lifecycle,
surface: $surface,
actor: $user instanceof User ? $user : null,
workspaceId: $workspaceId,
lane: $lane,
relatedOnboardingDraft: $draft,
relatedOnboardingIsResumable: $resumeOutcome?->allowed ?? false,
hasRelatedOnboardingDraft: $draft instanceof ManagedEnvironmentOnboardingSession,
isArchived: $lifecycle->canRestore(),
);
}
/**
* @return list<TenantActionDescriptor>
*/
public function catalogForTenant(ManagedEnvironment $tenant, TenantActionSurface $surface, ?User $user = null): array
{
$context = $this->buildContext($tenant, $surface, $user);
$actions = [
$this->viewAction(),
];
$primaryAction = $this->primaryActionForContext($context);
if ($primaryAction instanceof TenantActionDescriptor) {
$actions[] = $primaryAction;
}
$secondaryActions = [
$this->secondaryRelatedOnboardingActionForContext($context, $primaryAction),
$this->secondaryLifecycleActionForContext($context, $primaryAction),
];
foreach ($secondaryActions as $secondaryAction) {
if ($secondaryAction instanceof TenantActionDescriptor) {
$actions[] = $secondaryAction;
}
}
return array_values(array_filter($actions, static fn (mixed $action): bool => $action instanceof TenantActionDescriptor && $action->visible));
}
public function lifecycleActionForTenant(ManagedEnvironment $tenant): ?TenantActionDescriptor
{
return $this->lifecycleActionForContext($this->buildContext($tenant, TenantActionSurface::ContextMenu));
}
public function relatedOnboardingActionForTenant(ManagedEnvironment $tenant, TenantActionSurface $surface, ?User $user = null): ?TenantActionDescriptor
{
return $this->relatedOnboardingActionForContext($this->buildContext($tenant, $surface, $user));
}
public function onboardingEntryDescriptor(int $resumableDraftCount): TenantActionDescriptor
{
return match (true) {
$resumableDraftCount === 1 => new TenantActionDescriptor(
key: 'resume_onboarding',
family: TenantActionFamily::OnboardingWorkflow,
label: 'Resume onboarding',
icon: 'heroicon-m-arrow-path',
group: 'primary',
),
$resumableDraftCount > 1 => new TenantActionDescriptor(
key: 'choose_onboarding_draft',
family: TenantActionFamily::OnboardingWorkflow,
label: 'Choose onboarding draft',
icon: 'heroicon-m-queue-list',
group: 'primary',
),
default => new TenantActionDescriptor(
key: 'add_tenant',
family: TenantActionFamily::OnboardingWorkflow,
label: 'Add environment',
icon: 'heroicon-m-plus',
group: 'primary',
),
};
}
public function relatedOnboardingDraft(ManagedEnvironment $tenant, ?User $user = null): ?ManagedEnvironmentOnboardingSession
{
$user ??= auth()->user();
if (! $user instanceof User) {
return null;
}
return ManagedEnvironmentOnboardingSession::query()
->where('workspace_id', (int) $tenant->workspace_id)
->where('managed_environment_id', (int) $tenant->getKey())
->orderByDesc('updated_at')
->get()
->first(fn (ManagedEnvironmentOnboardingSession $draft): bool => Gate::forUser($user)->allows('view', $draft));
}
private function viewAction(): TenantActionDescriptor
{
return new TenantActionDescriptor(
key: 'view',
family: TenantActionFamily::Neutral,
label: 'View',
icon: 'heroicon-o-eye',
group: 'inspect',
);
}
private function primaryActionForContext(TenantActionContext $context): ?TenantActionDescriptor
{
if ($context->relatedOnboardingIsResumable && $context->lifecycle->canResumeOnboarding()) {
return $this->relatedOnboardingActionForContext($context, 'primary');
}
return $this->lifecycleActionForContext($context, 'primary');
}
private function secondaryLifecycleActionForContext(
TenantActionContext $context,
?TenantActionDescriptor $primaryAction,
): ?TenantActionDescriptor {
$action = $this->lifecycleActionForContext($context);
if (! $action instanceof TenantActionDescriptor) {
return null;
}
if ($primaryAction instanceof TenantActionDescriptor && $primaryAction->key === $action->key) {
return null;
}
return $action;
}
private function secondaryRelatedOnboardingActionForContext(
TenantActionContext $context,
?TenantActionDescriptor $primaryAction,
): ?TenantActionDescriptor {
$action = $this->relatedOnboardingActionForContext($context);
if (! $action instanceof TenantActionDescriptor) {
return null;
}
if ($primaryAction instanceof TenantActionDescriptor && $primaryAction->key === $action->key) {
return null;
}
return $action;
}
private function lifecycleActionForContext(
TenantActionContext $context,
string $group = 'overflow',
): ?TenantActionDescriptor {
if (! $context->isGenericTenantManagementSurface()) {
return null;
}
$restoreOutcome = $this->tenantOperabilityService->outcomeFor(
tenant: $context->tenant,
question: TenantOperabilityQuestion::RestoreEligibility,
actor: $context->actor,
workspaceId: $context->workspaceId,
lane: $context->lane,
);
if ($this->shouldExposeAction($restoreOutcome)) {
return new TenantActionDescriptor(
key: 'restore',
family: TenantActionFamily::LifecycleManagement,
label: 'Restore',
icon: 'heroicon-o-arrow-uturn-left',
destructive: true,
requiresConfirmation: true,
auditActionId: AuditActionId::TenantRestored,
successNotificationTitle: 'Environment restored',
successNotificationBody: 'The environment is available again in normal environment management flows and can be selected as active context.',
modalHeading: 'Restore environment',
modalDescription: 'Restore this archived environment so it can be selected again in normal environment management flows.',
group: $group,
);
}
$archiveOutcome = $this->tenantOperabilityService->outcomeFor(
tenant: $context->tenant,
question: TenantOperabilityQuestion::ArchiveEligibility,
actor: $context->actor,
workspaceId: $context->workspaceId,
lane: $context->lane,
);
if ($this->shouldExposeAction($archiveOutcome)) {
return new TenantActionDescriptor(
key: 'archive',
family: TenantActionFamily::LifecycleManagement,
label: 'Archive',
icon: 'heroicon-o-archive-box-x-mark',
destructive: true,
requiresConfirmation: true,
auditActionId: AuditActionId::TenantArchived,
successNotificationTitle: 'Environment archived',
successNotificationBody: 'The environment remains available for inspection and audit history, but it is no longer selectable as active context.',
modalHeading: 'Archive environment',
modalDescription: 'Archive this environment to retain it for inspection and audit history while removing it from active management flows.',
group: $group,
);
}
return null;
}
private function relatedOnboardingActionForContext(
TenantActionContext $context,
string $group = 'overflow',
): ?TenantActionDescriptor {
$draft = $context->relatedOnboardingDraft;
if (! $draft instanceof ManagedEnvironmentOnboardingSession) {
return null;
}
$resumeOutcome = $this->tenantOperabilityService->outcomeFor(
tenant: $context->tenant,
question: TenantOperabilityQuestion::ResumeOnboardingEligibility,
actor: $context->actor,
workspaceId: $context->workspaceId,
lane: $context->lane,
onboardingDraft: $draft,
);
if ($resumeOutcome->allowed) {
return new TenantActionDescriptor(
key: 'related_onboarding',
family: TenantActionFamily::OnboardingWorkflow,
label: 'Resume onboarding',
icon: 'heroicon-o-arrow-path',
group: $group,
);
}
if ($draft->isCancelled()) {
return new TenantActionDescriptor(
key: 'related_onboarding',
family: TenantActionFamily::OnboardingWorkflow,
label: 'View cancelled onboarding draft',
icon: 'heroicon-o-eye',
group: $group,
);
}
if ($draft->isCompleted()) {
return new TenantActionDescriptor(
key: 'related_onboarding',
family: TenantActionFamily::OnboardingWorkflow,
label: 'View completed onboarding',
icon: 'heroicon-o-eye',
group: $group,
);
}
return new TenantActionDescriptor(
key: 'related_onboarding',
family: TenantActionFamily::OnboardingWorkflow,
label: 'View related onboarding',
icon: 'heroicon-o-eye',
group: $group,
);
}
private function shouldExposeAction(TenantOperabilityOutcome $outcome): bool
{
return $outcome->allowed || $outcome->isDeniedForCapability();
}
}