TenantAtlas/apps/platform/app/Services/TenantConfiguration/CoverageSourceContractResolver.php
Ahmed Darrazi 736e61c73e
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m37s
feat: add generic content-backed coverage capture
2026-06-25 21:55:27 +02:00

131 lines
4.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\TenantConfiguration;
use App\Models\TenantConfigurationResourceType;
use App\Services\Graph\GraphContractRegistry;
use App\Support\TenantConfiguration\CaptureOutcome;
use App\Support\TenantConfiguration\SourceClass;
use App\Support\TenantConfiguration\SupportState;
final class CoverageSourceContractResolver
{
/**
* Explicit source contract mappings only. Missing entries remain blocked.
*
* @var array<string, string>
*/
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<string, mixed> $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<string, mixed> $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<string, mixed> $payload
*/
private function canonicalJson(array $payload): string
{
ksort($payload);
return json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
}
}