$context */ public function translate(string $reasonCode, string $surface = 'detail', array $context = []): ?ReasonResolutionEnvelope { $reasonCode = trim($reasonCode); if ($reasonCode === '') { return null; } $normalizedCode = ProviderReasonCodes::isKnown($reasonCode) ? $reasonCode : ProviderReasonCodes::UnknownError; $tenant = $context['tenant'] ?? null; $connection = $context['connection'] ?? null; if (! $tenant instanceof Tenant) { $nextSteps = $this->fallbackNextSteps($normalizedCode); } else { $nextSteps = $this->nextStepsFor($tenant, $normalizedCode, $connection instanceof ProviderConnection ? $connection : null); } return match ($normalizedCode) { ProviderReasonCodes::ProviderConnectionMissing => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Provider connection required', shortExplanation: 'This tenant does not have a usable provider connection for Microsoft operations.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::ProviderConnectionInvalid => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Provider connection needs review', shortExplanation: 'The selected provider connection is incomplete or no longer valid for this workflow.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::ProviderCredentialMissing => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Credentials missing', shortExplanation: 'The provider connection is missing the credentials required to authenticate.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::ProviderCredentialInvalid => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Credentials need review', shortExplanation: 'Stored provider credentials are no longer valid for the selected provider connection.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::ProviderConnectionTypeInvalid => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Connection type unsupported', shortExplanation: 'The selected provider connection type cannot be used for this workflow.', actionability: 'permanent_configuration', nextSteps: $nextSteps, ), ProviderReasonCodes::PlatformIdentityMissing => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Platform identity missing', shortExplanation: 'The platform provider connection is missing the app identity details required to continue.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::PlatformIdentityIncomplete => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Platform identity incomplete', shortExplanation: 'The platform provider connection needs more app identity details before it can continue.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::DedicatedCredentialMissing => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Dedicated credentials required', shortExplanation: 'This dedicated provider connection cannot continue until dedicated credentials are configured.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::DedicatedCredentialInvalid => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Dedicated credentials need review', shortExplanation: 'The dedicated credentials are no longer valid for this provider connection.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::ProviderConsentMissing => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Admin consent required', shortExplanation: 'The provider connection cannot continue until admin consent is granted.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::ProviderConsentFailed => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Admin consent check failed', shortExplanation: 'TenantPilot could not confirm admin consent for this provider connection.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::ProviderConsentRevoked => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Admin consent revoked', shortExplanation: 'Previously granted admin consent is no longer valid for this provider connection.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::ProviderConnectionReviewRequired => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Connection classification needs review', shortExplanation: 'TenantPilot needs you to confirm how this provider connection should be used.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::ProviderAuthFailed => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Provider authentication failed', shortExplanation: 'The provider connection could not authenticate with the stored credentials.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::ProviderPermissionMissing => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Permissions missing', shortExplanation: 'The provider app is missing required Microsoft Graph permissions.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::ProviderPermissionDenied => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Permission denied', shortExplanation: 'Microsoft Graph denied the requested permission for this provider connection.', actionability: 'permanent_configuration', nextSteps: $nextSteps, ), ProviderReasonCodes::ProviderPermissionRefreshFailed => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Permission refresh failed', shortExplanation: 'TenantPilot could not refresh the provider permission snapshot.', actionability: 'retryable_transient', nextSteps: $nextSteps, ), ProviderReasonCodes::IntuneRbacPermissionMissing => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Intune RBAC permission missing', shortExplanation: 'The provider app lacks the Intune RBAC permission needed for this workflow.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::TenantTargetMismatch => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Connection targets a different tenant', shortExplanation: 'The selected provider connection points to a different Microsoft tenant than the current scope.', actionability: 'permanent_configuration', nextSteps: $nextSteps, ), ProviderReasonCodes::NetworkUnreachable => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Microsoft Graph temporarily unreachable', shortExplanation: 'TenantPilot could not reach Microsoft Graph or the provider dependency.', actionability: 'retryable_transient', nextSteps: $nextSteps, ), ProviderReasonCodes::RateLimited => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Request rate limited', shortExplanation: 'Microsoft Graph asked TenantPilot to slow down before retrying.', actionability: 'retryable_transient', nextSteps: $nextSteps, ), ProviderReasonCodes::IntuneRbacNotConfigured => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Intune RBAC not configured', shortExplanation: 'Intune RBAC has not been configured for this tenant yet.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::IntuneRbacUnhealthy => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Intune RBAC health degraded', shortExplanation: 'The latest Intune RBAC health check found a blocking issue.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), ProviderReasonCodes::IntuneRbacStale => $this->envelope( reasonCode: $normalizedCode, operatorLabel: 'Intune RBAC check is stale', shortExplanation: 'The latest Intune RBAC health check is too old to trust for write operations.', actionability: 'prerequisite_missing', nextSteps: $nextSteps, ), default => $this->envelope( reasonCode: $normalizedCode, operatorLabel: str_starts_with($normalizedCode, 'ext.') ? 'Provider configuration needs review' : 'Provider check needs review', shortExplanation: 'TenantPilot recorded a provider error that does not yet have a domain-specific translation.', actionability: 'permanent_configuration', nextSteps: $nextSteps, ), }; } /** * @param array $nextSteps */ private function envelope( string $reasonCode, string $operatorLabel, string $shortExplanation, string $actionability, array $nextSteps, ): ReasonResolutionEnvelope { return new ReasonResolutionEnvelope( internalCode: $reasonCode, operatorLabel: $operatorLabel, shortExplanation: $shortExplanation, actionability: $actionability, nextSteps: $nextSteps, showNoActionNeeded: false, diagnosticCodeLabel: $reasonCode, ); } /** * @return array */ private function fallbackNextSteps(string $reasonCode): array { return match ($reasonCode) { ProviderReasonCodes::NetworkUnreachable, ProviderReasonCodes::RateLimited => [ NextStepOption::instruction('Retry after the provider dependency recovers.'), ], ProviderReasonCodes::UnknownError => [ NextStepOption::instruction('Review the provider connection and retry once the cause is understood.'), ], default => [ NextStepOption::instruction('Review the provider connection before retrying.'), ], }; } /** * @return array */ private function nextStepsFor( Tenant $tenant, string $reasonCode, ?ProviderConnection $connection = null, ): array { return match ($reasonCode) { ProviderReasonCodes::ProviderConnectionMissing, ProviderReasonCodes::ProviderConnectionInvalid, ProviderReasonCodes::TenantTargetMismatch, ProviderReasonCodes::PlatformIdentityMissing, ProviderReasonCodes::PlatformIdentityIncomplete, ProviderReasonCodes::ProviderConnectionReviewRequired => [ NextStepOption::link( label: $connection instanceof ProviderConnection ? 'Review migration classification' : 'Manage Provider Connections', destination: $connection instanceof ProviderConnection ? ProviderConnectionResource::getUrl('view', ['tenant' => $tenant->external_id, 'record' => (int) $connection->getKey()], panel: 'admin') : ProviderConnectionResource::getUrl('index', ['tenant' => $tenant->external_id], panel: 'admin'), ), NextStepOption::link( label: 'Review effective app details', destination: $connection instanceof ProviderConnection ? ProviderConnectionResource::getUrl('edit', ['tenant' => $tenant->external_id, 'record' => (int) $connection->getKey()], panel: 'admin') : ProviderConnectionResource::getUrl('index', ['tenant' => $tenant->external_id], panel: 'admin'), ), ], ProviderReasonCodes::DedicatedCredentialMissing, ProviderReasonCodes::DedicatedCredentialInvalid => [ NextStepOption::link( label: $connection instanceof ProviderConnection ? 'Manage dedicated connection' : 'Manage Provider Connections', destination: $connection instanceof ProviderConnection ? ProviderConnectionResource::getUrl('edit', ['tenant' => $tenant->external_id, 'record' => (int) $connection->getKey()], panel: 'admin') : ProviderConnectionResource::getUrl('index', ['tenant' => $tenant->external_id], panel: 'admin'), ), ], ProviderReasonCodes::ProviderCredentialMissing, ProviderReasonCodes::ProviderCredentialInvalid, ProviderReasonCodes::ProviderConsentFailed, ProviderReasonCodes::ProviderConsentRevoked, ProviderReasonCodes::ProviderAuthFailed, ProviderReasonCodes::ProviderConsentMissing => [ NextStepOption::link( label: 'Grant admin consent', destination: RequiredPermissionsLinks::adminConsentPrimaryUrl($tenant), ), NextStepOption::link( label: $connection instanceof ProviderConnection ? ($connection->connection_type === ProviderConnectionType::Dedicated ? 'Manage dedicated connection' : 'Review platform connection') : 'Manage Provider Connections', destination: $connection instanceof ProviderConnection ? ProviderConnectionResource::getUrl('edit', ['tenant' => $tenant->external_id, 'record' => (int) $connection->getKey()], panel: 'admin') : ProviderConnectionResource::getUrl('index', ['tenant' => $tenant->external_id], panel: 'admin'), ), ], ProviderReasonCodes::ProviderPermissionMissing, ProviderReasonCodes::ProviderPermissionDenied, ProviderReasonCodes::ProviderPermissionRefreshFailed, ProviderReasonCodes::IntuneRbacPermissionMissing => [ NextStepOption::link( label: 'Open Required Permissions', destination: RequiredPermissionsLinks::requiredPermissions($tenant), ), ], ProviderReasonCodes::IntuneRbacNotConfigured, ProviderReasonCodes::IntuneRbacUnhealthy, ProviderReasonCodes::IntuneRbacStale => [ NextStepOption::link( label: 'Review provider connections', destination: ProviderConnectionResource::getUrl('index', ['tenant' => $tenant->external_id], panel: 'admin'), ), NextStepOption::instruction('Refresh the tenant RBAC health check before retrying.', scope: 'tenant'), ], ProviderReasonCodes::NetworkUnreachable, ProviderReasonCodes::RateLimited => [ NextStepOption::instruction('Retry after the provider dependency recovers.', scope: 'tenant'), NextStepOption::link( label: 'Review provider connection', destination: $connection instanceof ProviderConnection ? ProviderConnectionResource::getUrl('edit', ['tenant' => $tenant->external_id, 'record' => (int) $connection->getKey()], panel: 'admin') : ProviderConnectionResource::getUrl('index', ['tenant' => $tenant->external_id], panel: 'admin'), ), ], default => [ NextStepOption::link( label: 'Manage Provider Connections', destination: ProviderConnectionResource::getUrl('index', ['tenant' => $tenant->external_id], panel: 'admin'), ), ], }; } }