*/ private const CONTRACT_KEYS = [ 'deviceAndAppManagementAssignmentFilter' => 'assignmentFilter', 'notificationMessageTemplate' => 'notificationMessageTemplate', 'roleScopeTag' => 'roleScopeTag', ]; public function __construct( private readonly GraphContractRegistry $contracts, ) {} public function resolve(TenantConfigurationResourceType $resourceType, bool $allowBetaCapture = false): CoverageSourceContractDecision { $canonicalType = (string) $resourceType->canonical_type; $sourceClass = $resourceType->source_class instanceof SourceClass ? $resourceType->source_class : SourceClass::tryFrom((string) $resourceType->source_class); $supportState = $resourceType->support_state instanceof SupportState ? $resourceType->support_state : SupportState::tryFrom((string) $resourceType->support_state); if (in_array($supportState, [SupportState::Unsupported, SupportState::OutOfScope], true)) { return $this->blocked($canonicalType, CaptureOutcome::BlockedUnsupported, 'resource_type_unsupported'); } if ($sourceClass === SourceClass::GraphBetaExperimental && ! $allowBetaCapture) { return $this->blocked($canonicalType, CaptureOutcome::BlockedBeta, 'beta_capture_disabled'); } $contractKey = self::CONTRACT_KEYS[$canonicalType] ?? null; if (! is_string($contractKey) || $contractKey === '') { return $this->blocked($canonicalType, CaptureOutcome::BlockedMissingContract, 'missing_source_contract_mapping'); } $contract = $this->contracts->get($contractKey); $resource = is_string($contract['resource'] ?? null) ? trim((string) $contract['resource']) : ''; if ($contract === [] || $resource === '') { return $this->blocked($canonicalType, CaptureOutcome::BlockedMissingContract, 'missing_graph_contract_resource'); } $metadata = [ 'source_contract_key' => $contractKey, 'source_endpoint' => '/'.ltrim($resource, '/'), 'source_class' => $sourceClass?->value, 'support_state' => $supportState?->value, ]; return new CoverageSourceContractDecision( canonicalType: $canonicalType, outcome: CaptureOutcome::Captured, contractKey: $contractKey, sourceEndpoint: '/'.ltrim($resource, '/'), sourceVersion: $this->sourceVersion($contract), sourceSchemaHash: $this->sourceSchemaHash($contract), reasonCode: null, contract: $contract, sourceMetadata: array_filter($metadata, static fn (mixed $value): bool => $value !== null && $value !== ''), ); } private function blocked(string $canonicalType, CaptureOutcome $outcome, string $reasonCode): CoverageSourceContractDecision { return new CoverageSourceContractDecision( canonicalType: $canonicalType, outcome: $outcome, reasonCode: $reasonCode, sourceMetadata: ['reason_code' => $reasonCode], ); } /** * @param array $contract */ private function sourceVersion(array $contract): string { $version = $contract['version'] ?? $contract['graph_version'] ?? null; return is_string($version) && trim($version) !== '' ? trim($version) : 'v1.0'; } /** * @param array $contract */ private function sourceSchemaHash(array $contract): ?string { if ($contract === []) { return null; } $schema = [ 'resource' => $contract['resource'] ?? null, 'allowed_select' => $contract['allowed_select'] ?? [], 'type_family' => $contract['type_family'] ?? null, ]; return hash('sha256', $this->canonicalJson($schema)); } /** * @param array $payload */ private function canonicalJson(array $payload): string { ksort($payload); return json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); } }