|array|null $types * @param array|null $policyIds */ public function __construct( public readonly int $tenantId, public readonly ?array $types = null, public readonly ?array $policyIds = null, ?OperationRun $operationRun = null ) { $this->operationRun = $operationRun; } public function middleware(): array { return [new TrackOperationRun]; } public function handle(PolicySyncService $service, OperationRunService $operationRunService): void { $graph = app(GraphClientInterface::class); if (! config('graph.enabled') || $graph instanceof NullGraphClient) { if ($this->operationRun) { $operationRunService->updateRun( $this->operationRun, status: OperationRunStatus::Completed->value, outcome: OperationRunOutcome::Failed->value, failures: [ [ 'code' => 'graph.disabled', 'message' => 'Microsoft Graph is not enabled. Set GRAPH_ENABLED=true (and/or GRAPH_TENANT_ID) in .env to use the real Graph client.', ], ], ); return; } throw new \RuntimeException('Microsoft Graph is not enabled (GRAPH_ENABLED/GRAPH_TENANT_ID missing).'); } $tenant = Tenant::findOrFail($this->tenantId); if ($this->policyIds !== null) { $ids = collect($this->policyIds) ->map(static fn ($id): int => (int) $id) ->unique() ->sort() ->values(); $syncedCount = 0; $skippedCount = 0; $failureSummary = []; foreach ($ids as $policyId) { $policy = Policy::query() ->whereKey($policyId) ->where('tenant_id', $tenant->getKey()) ->first(); if (! $policy) { $failureSummary[] = [ 'code' => 'policy.not_found', 'message' => "Policy {$policyId} not found", ]; continue; } if ($policy->ignored_at !== null) { $skippedCount++; continue; } try { $service->syncPolicy($tenant, $policy); $syncedCount++; } catch (\Throwable $e) { $failureSummary[] = [ 'code' => 'policy.sync_failed', 'message' => $e->getMessage(), ]; } } $failureCount = count($failureSummary); $outcome = match (true) { $failureCount === 0 => OperationRunOutcome::Succeeded->value, $syncedCount > 0 => OperationRunOutcome::PartiallySucceeded->value, default => OperationRunOutcome::Failed->value, }; if ($this->operationRun) { $operationRunService->updateRun( $this->operationRun, status: OperationRunStatus::Completed->value, outcome: $outcome, summaryCounts: [ 'total' => $ids->count(), 'processed' => $ids->count(), 'succeeded' => $syncedCount, 'failed' => $failureCount, 'skipped' => $skippedCount, ], failures: $failureSummary, ); } return; } $supported = config('tenantpilot.supported_policy_types', []); if ($this->types !== null) { $first = $this->types[0] ?? null; $typesLookLikeSupportedConfig = is_array($first) && array_key_exists('type', $first); if ($typesLookLikeSupportedConfig) { $supported = array_values(array_filter( $this->types, static fn ($type): bool => is_array($type) && isset($type['type']) && is_string($type['type']) && $type['type'] !== '' )); } else { $requestedTypes = array_values(array_unique(array_filter(array_map( static fn ($type): ?string => is_string($type) ? $type : (is_array($type) ? (string) ($type['type'] ?? '') : null), $this->types, ), static fn ($type): bool => is_string($type) && $type !== ''))); $supported = array_values(array_filter( $supported, static fn ($type): bool => is_array($type) && isset($type['type']) && is_string($type['type']) && in_array($type['type'], $requestedTypes, true) )); } } if ($supported === []) { if ($this->operationRun) { $operationRunService->updateRun( $this->operationRun, status: OperationRunStatus::Completed->value, outcome: OperationRunOutcome::Failed->value, failures: [ [ 'code' => $this->types === null ? 'tenantpilot.supported_policy_types.empty' : 'tenantpilot.supported_policy_types.no_match', 'message' => $this->types === null ? 'No supported policy types configured (tenantpilot.supported_policy_types is empty).' : 'No requested policy types matched the supported policy type configuration.', ], ], ); } return; } $result = $service->syncPoliciesWithReport($tenant, $supported); $syncedCount = count($result['synced'] ?? []); $failures = $result['failures'] ?? []; $failureCount = count($failures); $outcome = match (true) { $failureCount === 0 => OperationRunOutcome::Succeeded->value, $syncedCount > 0 => OperationRunOutcome::PartiallySucceeded->value, default => OperationRunOutcome::Failed->value, }; $failureSummary = []; foreach ($failures as $failure) { if (! is_array($failure)) { continue; } $policyType = (string) ($failure['policy_type'] ?? 'unknown'); $status = is_numeric($failure['status'] ?? null) ? (int) $failure['status'] : null; $errors = $failure['errors'] ?? null; $firstErrorMessage = null; if (is_array($errors) && isset($errors[0]) && is_array($errors[0])) { $firstErrorMessage = $errors[0]['message'] ?? null; } $message = $status !== null ? "{$policyType}: Graph returned {$status}" : "{$policyType}: Graph request failed"; if (is_string($firstErrorMessage) && $firstErrorMessage !== '') { $message .= ' - '.trim($firstErrorMessage); } $failureSummary[] = [ 'code' => $status !== null ? "GRAPH_HTTP_{$status}" : 'GRAPH_ERROR', 'message' => $message, ]; } if ($this->operationRun) { $total = $syncedCount + $failureCount; $operationRunService->updateRun( $this->operationRun, status: OperationRunStatus::Completed->value, outcome: $outcome, summaryCounts: [ 'total' => $total, 'processed' => $total, 'succeeded' => $syncedCount, 'failed' => $failureCount, ], failures: $failureSummary, ); } } }