nextVersionNumber($policy); $version = PolicyVersion::create([ 'tenant_id' => $policy->tenant_id, 'policy_id' => $policy->id, 'version_number' => $versionNumber, 'policy_type' => $policy->policy_type, 'platform' => $policy->platform, 'created_by' => $createdBy, 'captured_at' => CarbonImmutable::now(), 'snapshot' => $payload, 'metadata' => $metadata, 'assignments' => $assignments, 'scope_tags' => $scopeTags, 'assignments_hash' => $assignments ? hash('sha256', json_encode($assignments)) : null, 'scope_tags_hash' => $scopeTags ? hash('sha256', json_encode($scopeTags)) : null, ]); $this->auditLogger->log( tenant: $policy->tenant, action: 'policy.versioned', context: [ 'metadata' => [ 'policy_id' => $policy->id, 'version_number' => $versionNumber, ], ], actorEmail: $createdBy, resourceType: 'policy', resourceId: (string) $policy->id ); return $version; } public function captureFromGraph( Tenant $tenant, Policy $policy, ?string $createdBy = null, array $metadata = [], bool $includeAssignments = true, bool $includeScopeTags = true, ): PolicyVersion { $snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy); if (isset($snapshot['failure'])) { $reason = $snapshot['failure']['reason'] ?? 'Unable to fetch policy snapshot'; throw new \RuntimeException($reason); } $payload = $snapshot['payload']; $assignments = null; $scopeTags = null; $assignmentMetadata = []; if ($includeAssignments) { try { $rawAssignments = $this->assignmentFetcher->fetch($tenant->id, $policy->external_id); if (! empty($rawAssignments)) { $assignments = $rawAssignments; // Resolve groups $groupIds = collect($rawAssignments) ->pluck('target.groupId') ->filter() ->unique() ->values() ->toArray(); $resolvedGroups = $this->groupResolver->resolve($tenant->id, $groupIds); $assignmentMetadata['has_orphaned_assignments'] = ! empty($resolvedGroups['orphaned']); $assignmentMetadata['assignments_count'] = count($rawAssignments); } } catch (\Throwable $e) { $assignmentMetadata['assignments_fetch_failed'] = true; $assignmentMetadata['assignments_fetch_error'] = $e->getMessage(); } } if ($includeScopeTags) { $scopeTags = [ 'ids' => $payload['roleScopeTagIds'] ?? ['0'], 'names' => ['Default'], // Could be fetched from Graph if needed ]; } $metadata = array_merge( ['source' => 'version_capture'], $metadata, $assignmentMetadata ); return $this->captureVersion( policy: $policy, payload: $payload, createdBy: $createdBy, metadata: $metadata, assignments: $assignments, scopeTags: $scopeTags, ); } private function nextVersionNumber(Policy $policy): int { $current = PolicyVersion::query() ->where('policy_id', $policy->id) ->max('version_number'); return (int) ($current ?? 0) + 1; } }