274 lines
10 KiB
PHP
274 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Tenants;
|
|
|
|
use App\Models\Tenant;
|
|
use App\Models\TenantOnboardingSession;
|
|
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 Illuminate\Support\Facades\Gate;
|
|
|
|
class TenantActionPolicySurface
|
|
{
|
|
public function __construct(
|
|
private readonly TenantOperabilityService $tenantOperabilityService,
|
|
private readonly OnboardingLifecycleService $onboardingLifecycleService,
|
|
) {}
|
|
|
|
public function buildContext(Tenant $tenant, TenantActionSurface $surface, ?User $user = null): TenantActionContext
|
|
{
|
|
$user ??= auth()->user();
|
|
$draft = $user instanceof User ? $this->relatedOnboardingDraft($tenant, $user) : null;
|
|
$lifecycle = $this->tenantOperabilityService->lifecycleFor($tenant);
|
|
|
|
return new TenantActionContext(
|
|
tenant: $tenant,
|
|
lifecycle: $lifecycle,
|
|
surface: $surface,
|
|
relatedOnboardingDraft: $draft,
|
|
relatedOnboardingIsResumable: $draft instanceof TenantOnboardingSession
|
|
&& $this->onboardingLifecycleService->canResumeDraft($draft),
|
|
hasRelatedOnboardingDraft: $draft instanceof TenantOnboardingSession,
|
|
isArchived: $tenant->trashed() || $this->tenantOperabilityService->canRestore($tenant),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return list<TenantActionDescriptor>
|
|
*/
|
|
public function catalogForTenant(Tenant $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(Tenant $tenant): ?TenantActionDescriptor
|
|
{
|
|
return $this->lifecycleActionForContext($this->buildContext($tenant, TenantActionSurface::ContextMenu));
|
|
}
|
|
|
|
public function relatedOnboardingActionForTenant(Tenant $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 tenant',
|
|
icon: 'heroicon-m-plus',
|
|
group: 'primary',
|
|
),
|
|
};
|
|
}
|
|
|
|
public function relatedOnboardingDraft(Tenant $tenant, ?User $user = null): ?TenantOnboardingSession
|
|
{
|
|
$user ??= auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
return null;
|
|
}
|
|
|
|
return TenantOnboardingSession::query()
|
|
->where('workspace_id', (int) $tenant->workspace_id)
|
|
->where('tenant_id', (int) $tenant->getKey())
|
|
->orderByDesc('updated_at')
|
|
->get()
|
|
->first(fn (TenantOnboardingSession $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: 'primary',
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if ($this->tenantOperabilityService->canRestore($context->tenant)) {
|
|
return new TenantActionDescriptor(
|
|
key: 'restore',
|
|
family: TenantActionFamily::LifecycleManagement,
|
|
label: 'Restore',
|
|
icon: 'heroicon-o-arrow-uturn-left',
|
|
destructive: true,
|
|
requiresConfirmation: true,
|
|
auditActionId: AuditActionId::TenantRestored,
|
|
successNotificationTitle: 'Tenant restored',
|
|
successNotificationBody: 'The tenant is available again in normal tenant management flows and can be selected as active context.',
|
|
modalHeading: 'Restore tenant',
|
|
modalDescription: 'Restore this archived tenant so it can be selected again in normal tenant management flows.',
|
|
group: $group,
|
|
);
|
|
}
|
|
|
|
if ($this->tenantOperabilityService->canArchive($context->tenant)) {
|
|
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: 'Tenant archived',
|
|
successNotificationBody: 'The tenant remains available for inspection and audit history, but it is no longer selectable as active context.',
|
|
modalHeading: 'Archive tenant',
|
|
modalDescription: 'Archive this tenant 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 TenantOnboardingSession) {
|
|
return null;
|
|
}
|
|
|
|
if ($context->relatedOnboardingIsResumable && $context->lifecycle->canResumeOnboarding()) {
|
|
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,
|
|
);
|
|
}
|
|
}
|