value => $this->evaluate( TenantOperabilityContext::forTenant( tenant: $tenant, lane: TenantInteractionLane::StandardActiveOperating, ), TenantOperabilityQuestion::SelectorEligibility, ), TenantOperabilityQuestion::AdministrativeDiscoverability->value => $this->evaluate( TenantOperabilityContext::forTenant( tenant: $tenant, lane: TenantInteractionLane::AdministrativeManagement, ), TenantOperabilityQuestion::AdministrativeDiscoverability, ), TenantOperabilityQuestion::ArchiveEligibility->value => $this->evaluate( TenantOperabilityContext::forTenant( tenant: $tenant, lane: TenantInteractionLane::AdministrativeManagement, ), TenantOperabilityQuestion::ArchiveEligibility, ), TenantOperabilityQuestion::RestoreEligibility->value => $this->evaluate( TenantOperabilityContext::forTenant( tenant: $tenant, lane: TenantInteractionLane::AdministrativeManagement, ), TenantOperabilityQuestion::RestoreEligibility, ), TenantOperabilityQuestion::ResumeOnboardingEligibility->value => $this->evaluate( TenantOperabilityContext::forTenant( tenant: $tenant, lane: TenantInteractionLane::AdministrativeManagement, ), TenantOperabilityQuestion::ResumeOnboardingEligibility, ), TenantOperabilityQuestion::CanonicalLinkedRecordViewability->value => $this->evaluate( TenantOperabilityContext::forTenant( tenant: $tenant, lane: TenantInteractionLane::CanonicalWorkspaceRecord, ), TenantOperabilityQuestion::CanonicalLinkedRecordViewability, ), ]); } public function evaluate(TenantOperabilityContext $context, TenantOperabilityQuestion $question): TenantOperabilityOutcome { $lifecycle = TenantLifecycle::fromTenant($context->tenant); if ($context->workspaceId !== null && (int) $context->tenant->workspace_id !== $context->workspaceId) { return TenantOperabilityOutcome::deny( question: $question, lifecycle: $lifecycle, lane: $context->lane, reasonCode: $question === TenantOperabilityQuestion::RememberedContextValidity ? TenantOperabilityReasonCode::RememberedContextStale : TenantOperabilityReasonCode::WorkspaceMismatch, discoverable: false, ); } if ($context->actor instanceof User && ! $this->capabilityResolver->isMember($context->actor, $context->tenant)) { return TenantOperabilityOutcome::deny( question: $question, lifecycle: $lifecycle, lane: $context->lane, reasonCode: TenantOperabilityReasonCode::TenantNotEntitled, discoverable: false, metadata: $this->metadata($context), ); } if ($context->requiredCapability !== null && $context->actor instanceof User && ! $this->capabilityResolver->can($context->actor, $context->tenant, $context->requiredCapability)) { return TenantOperabilityOutcome::deny( question: $question, lifecycle: $lifecycle, lane: $context->lane, reasonCode: TenantOperabilityReasonCode::MissingCapability, discoverable: $question === TenantOperabilityQuestion::AdministrativeDiscoverability || $question === TenantOperabilityQuestion::TenantBoundViewability, requiredCapability: $context->requiredCapability, metadata: $this->metadata($context), ); } return match ($question) { TenantOperabilityQuestion::SelectorEligibility => $this->selectorEligibilityOutcome($context, $lifecycle), TenantOperabilityQuestion::RememberedContextValidity => $this->rememberedContextOutcome($context, $lifecycle), TenantOperabilityQuestion::TenantBoundViewability => $this->tenantBoundViewabilityOutcome($context, $lifecycle), TenantOperabilityQuestion::CanonicalLinkedRecordViewability => $this->canonicalViewabilityOutcome($context, $lifecycle), TenantOperabilityQuestion::ArchiveEligibility => $this->archiveEligibilityOutcome($context, $lifecycle), TenantOperabilityQuestion::RestoreEligibility => $this->restoreEligibilityOutcome($context, $lifecycle), TenantOperabilityQuestion::ResumeOnboardingEligibility => $this->resumeOnboardingOutcome($context, $lifecycle), TenantOperabilityQuestion::OnboardingCompletionEligibility => $this->onboardingCompletionOutcome($context, $lifecycle), TenantOperabilityQuestion::VerificationReadinessEligibility => $this->verificationReadinessOutcome($context, $lifecycle), TenantOperabilityQuestion::AdministrativeDiscoverability => $this->administrativeDiscoverabilityOutcome($context, $lifecycle), }; } public function outcomeFor( Tenant $tenant, TenantOperabilityQuestion $question, ?User $actor = null, ?int $workspaceId = null, TenantInteractionLane $lane = TenantInteractionLane::AdministrativeManagement, ?TenantOnboardingSession $onboardingDraft = null, ?string $requiredCapability = null, ?Tenant $selectedTenant = null, ?string $linkedRecordType = null, ?int $linkedRecordId = null, ): TenantOperabilityOutcome { return $this->evaluate( TenantOperabilityContext::forTenant( tenant: $tenant, actor: $actor, workspaceId: $workspaceId, lane: $lane, onboardingDraft: $onboardingDraft, requiredCapability: $requiredCapability, selectedTenant: $selectedTenant, linkedRecordType: $linkedRecordType, linkedRecordId: $linkedRecordId, ), $question, ); } public function lifecycleFor(Tenant $tenant): TenantLifecycle { return $this->decisionFor($tenant)->lifecycle; } public function canSelectAsContext(Tenant $tenant): bool { return $this->outcomeFor( tenant: $tenant, question: TenantOperabilityQuestion::SelectorEligibility, lane: TenantInteractionLane::StandardActiveOperating, )->allowed; } public function canViewTenantSurface(Tenant $tenant): bool { return $this->outcomeFor( tenant: $tenant, question: TenantOperabilityQuestion::AdministrativeDiscoverability, lane: TenantInteractionLane::AdministrativeManagement, )->allowed; } public function canResumeOnboarding(Tenant $tenant): bool { return $this->outcomeFor( tenant: $tenant, question: TenantOperabilityQuestion::ResumeOnboardingEligibility, lane: TenantInteractionLane::AdministrativeManagement, )->allowed; } public function canArchive(Tenant $tenant): bool { return $this->outcomeFor( tenant: $tenant, question: TenantOperabilityQuestion::ArchiveEligibility, lane: TenantInteractionLane::AdministrativeManagement, )->allowed; } public function canRestore(Tenant $tenant): bool { return $this->outcomeFor( tenant: $tenant, question: TenantOperabilityQuestion::RestoreEligibility, lane: TenantInteractionLane::AdministrativeManagement, )->allowed; } public function primaryManagementActionKey(Tenant $tenant, bool $preferOnboarding = false): ?string { return $this->decisionFor($tenant)->primaryManagementActionKey($preferOnboarding); } public function canReferenceInWorkspaceMonitoring(Tenant $tenant): bool { return $this->outcomeFor( tenant: $tenant, question: TenantOperabilityQuestion::CanonicalLinkedRecordViewability, lane: TenantInteractionLane::CanonicalWorkspaceRecord, )->allowed; } /** * @param Collection $tenants * @return Collection */ public function filterSelectable(Collection $tenants): Collection { return $tenants ->filter(fn (mixed $tenant): bool => $tenant instanceof Tenant && $this->canSelectAsContext($tenant)) ->values(); } public function applySelectableScope(Builder $query, ?string $table = null): Builder { $prefix = $table !== null && $table !== '' ? "{$table}." : ''; return $query ->whereNull("{$prefix}deleted_at") ->where("{$prefix}status", TenantLifecycle::Active->value); } public function applyAdministrativeDiscoverabilityScope(Builder $query, ?string $table = null): Builder { $prefix = $table !== null && $table !== '' ? "{$table}." : ''; return $query ->withTrashed() ->where(function (Builder $builder) use ($prefix): void { $builder->whereIn("{$prefix}status", TenantLifecycle::values()) ->orWhereNotNull("{$prefix}deleted_at"); }); } private function selectorEligibilityOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome { if ($context->lane !== TenantInteractionLane::StandardActiveOperating) { return $this->wrongLaneOutcome($context, TenantOperabilityQuestion::SelectorEligibility, $lifecycle); } if (! $lifecycle->isSelectableInStandardLane()) { return TenantOperabilityOutcome::deny( question: TenantOperabilityQuestion::SelectorEligibility, lifecycle: $lifecycle, lane: $context->lane, reasonCode: TenantOperabilityReasonCode::SelectorIneligibleLifecycle, discoverable: false, metadata: $this->metadata($context), ); } return TenantOperabilityOutcome::allow( question: TenantOperabilityQuestion::SelectorEligibility, lifecycle: $lifecycle, lane: $context->lane, discoverable: true, metadata: $this->metadata($context), ); } private function rememberedContextOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome { if ($context->lane !== TenantInteractionLane::StandardActiveOperating) { return TenantOperabilityOutcome::deny( question: TenantOperabilityQuestion::RememberedContextValidity, lifecycle: $lifecycle, lane: $context->lane, reasonCode: TenantOperabilityReasonCode::RememberedContextStale, discoverable: false, metadata: $this->metadata($context), ); } if (! $lifecycle->isSelectableInStandardLane()) { return TenantOperabilityOutcome::deny( question: TenantOperabilityQuestion::RememberedContextValidity, lifecycle: $lifecycle, lane: $context->lane, reasonCode: TenantOperabilityReasonCode::RememberedContextStale, discoverable: false, metadata: $this->metadata($context), ); } return TenantOperabilityOutcome::allow( question: TenantOperabilityQuestion::RememberedContextValidity, lifecycle: $lifecycle, lane: $context->lane, discoverable: true, metadata: $this->metadata($context), ); } private function tenantBoundViewabilityOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome { if ($context->lane !== TenantInteractionLane::AdministrativeManagement) { return $this->wrongLaneOutcome($context, TenantOperabilityQuestion::TenantBoundViewability, $lifecycle); } return TenantOperabilityOutcome::allow( question: TenantOperabilityQuestion::TenantBoundViewability, lifecycle: $lifecycle, lane: $context->lane, discoverable: true, metadata: $this->metadata($context), ); } private function canonicalViewabilityOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome { if ($context->lane !== TenantInteractionLane::CanonicalWorkspaceRecord) { return $this->wrongLaneOutcome($context, TenantOperabilityQuestion::CanonicalLinkedRecordViewability, $lifecycle); } return TenantOperabilityOutcome::allow( question: TenantOperabilityQuestion::CanonicalLinkedRecordViewability, lifecycle: $lifecycle, lane: $context->lane, discoverable: true, metadata: $this->metadata($context), ); } private function archiveEligibilityOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome { if ($context->lane !== TenantInteractionLane::AdministrativeManagement) { return $this->wrongLaneOutcome($context, TenantOperabilityQuestion::ArchiveEligibility, $lifecycle); } if (! $lifecycle->canArchive()) { return TenantOperabilityOutcome::deny( question: TenantOperabilityQuestion::ArchiveEligibility, lifecycle: $lifecycle, lane: $context->lane, reasonCode: $lifecycle === TenantLifecycle::Archived ? TenantOperabilityReasonCode::TenantAlreadyArchived : TenantOperabilityReasonCode::SelectorIneligibleLifecycle, discoverable: true, requiredCapability: $context->requiredCapability, metadata: $this->metadata($context), ); } return TenantOperabilityOutcome::allow( question: TenantOperabilityQuestion::ArchiveEligibility, lifecycle: $lifecycle, lane: $context->lane, discoverable: true, requiredCapability: $context->requiredCapability, metadata: $this->metadata($context), ); } private function restoreEligibilityOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome { if ($context->lane !== TenantInteractionLane::AdministrativeManagement) { return $this->wrongLaneOutcome($context, TenantOperabilityQuestion::RestoreEligibility, $lifecycle); } if (! $lifecycle->canRestore()) { return TenantOperabilityOutcome::deny( question: TenantOperabilityQuestion::RestoreEligibility, lifecycle: $lifecycle, lane: $context->lane, reasonCode: TenantOperabilityReasonCode::TenantNotArchived, discoverable: true, requiredCapability: $context->requiredCapability, metadata: $this->metadata($context), ); } return TenantOperabilityOutcome::allow( question: TenantOperabilityQuestion::RestoreEligibility, lifecycle: $lifecycle, lane: $context->lane, discoverable: true, requiredCapability: $context->requiredCapability, metadata: $this->metadata($context), ); } private function resumeOnboardingOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome { if (! in_array($context->lane, [TenantInteractionLane::AdministrativeManagement, TenantInteractionLane::OnboardingWorkflow], true)) { return $this->wrongLaneOutcome($context, TenantOperabilityQuestion::ResumeOnboardingEligibility, $lifecycle); } if (! $lifecycle->canResumeOnboarding()) { return TenantOperabilityOutcome::deny( question: TenantOperabilityQuestion::ResumeOnboardingEligibility, lifecycle: $lifecycle, lane: $context->lane, reasonCode: TenantOperabilityReasonCode::SelectorIneligibleLifecycle, discoverable: $context->lane === TenantInteractionLane::AdministrativeManagement, requiredCapability: $context->requiredCapability, metadata: $this->metadata($context), ); } if ($context->onboardingDraft instanceof TenantOnboardingSession && ! $context->onboardingDraft->isWorkflowResumable()) { return TenantOperabilityOutcome::deny( question: TenantOperabilityQuestion::ResumeOnboardingEligibility, lifecycle: $lifecycle, lane: $context->lane, reasonCode: TenantOperabilityReasonCode::OnboardingNotResumable, discoverable: $context->lane === TenantInteractionLane::AdministrativeManagement, requiredCapability: $context->requiredCapability, metadata: $this->metadata($context), ); } return TenantOperabilityOutcome::allow( question: TenantOperabilityQuestion::ResumeOnboardingEligibility, lifecycle: $lifecycle, lane: $context->lane, discoverable: true, requiredCapability: $context->requiredCapability, metadata: $this->metadata($context), ); } private function onboardingCompletionOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome { if ($context->lane !== TenantInteractionLane::OnboardingWorkflow) { return $this->wrongLaneOutcome($context, TenantOperabilityQuestion::OnboardingCompletionEligibility, $lifecycle); } if (! in_array($lifecycle, [TenantLifecycle::Draft, TenantLifecycle::Onboarding], true)) { return TenantOperabilityOutcome::deny( question: TenantOperabilityQuestion::OnboardingCompletionEligibility, lifecycle: $lifecycle, lane: $context->lane, reasonCode: TenantOperabilityReasonCode::SelectorIneligibleLifecycle, discoverable: false, requiredCapability: $context->requiredCapability, metadata: $this->metadata($context), ); } return TenantOperabilityOutcome::allow( question: TenantOperabilityQuestion::OnboardingCompletionEligibility, lifecycle: $lifecycle, lane: $context->lane, discoverable: true, requiredCapability: $context->requiredCapability, metadata: $this->metadata($context), ); } private function verificationReadinessOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome { if (! $lifecycle->supportsQuestion(TenantOperabilityQuestion::VerificationReadinessEligibility, $context->lane)) { return TenantOperabilityOutcome::deny( question: TenantOperabilityQuestion::VerificationReadinessEligibility, lifecycle: $lifecycle, lane: $context->lane, reasonCode: $context->lane === TenantInteractionLane::CanonicalWorkspaceRecord ? TenantOperabilityReasonCode::CanonicalViewFollowupOnly : TenantOperabilityReasonCode::WrongLane, discoverable: $context->lane === TenantInteractionLane::AdministrativeManagement, requiredCapability: $context->requiredCapability, metadata: $this->metadata($context), ); } return TenantOperabilityOutcome::allow( question: TenantOperabilityQuestion::VerificationReadinessEligibility, lifecycle: $lifecycle, lane: $context->lane, discoverable: true, requiredCapability: $context->requiredCapability, metadata: $this->metadata($context), ); } private function administrativeDiscoverabilityOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome { return TenantOperabilityOutcome::allow( question: TenantOperabilityQuestion::AdministrativeDiscoverability, lifecycle: $lifecycle, lane: $context->lane, discoverable: $lifecycle->isAdministrativelyDiscoverable(), metadata: $this->metadata($context), ); } private function wrongLaneOutcome( TenantOperabilityContext $context, TenantOperabilityQuestion $question, TenantLifecycle $lifecycle, ): TenantOperabilityOutcome { return TenantOperabilityOutcome::deny( question: $question, lifecycle: $lifecycle, lane: $context->lane, reasonCode: TenantOperabilityReasonCode::WrongLane, discoverable: false, requiredCapability: $context->requiredCapability, metadata: $this->metadata($context), ); } /** * @return array */ private function metadata(TenantOperabilityContext $context): array { return array_filter([ 'selected_tenant_id' => $context->selectedTenant?->getKey(), 'linked_record_type' => $context->linkedRecordType, 'linked_record_id' => $context->linkedRecordId, 'page_category' => $context->pageCategory?->value, 'onboarding_draft_id' => $context->onboardingDraft?->getKey(), ], static fn (mixed $value): bool => $value !== null && $value !== ''); } }