TenantAtlas/app/Support/Inventory/TenantCoverageTruthResolver.php
2026-04-05 14:18:37 +02:00

171 lines
7.6 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Inventory;
use App\Models\InventoryItem;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Services\Inventory\CoverageCapabilitiesResolver;
use Illuminate\Support\Collection;
final class TenantCoverageTruthResolver
{
public function __construct(
private readonly CoverageCapabilitiesResolver $coverageCapabilities,
) {}
public function resolve(Tenant|int $tenant): TenantCoverageTruth
{
$tenantId = $tenant instanceof Tenant
? (int) $tenant->getKey()
: (int) $tenant;
$basisRun = OperationRun::latestCompletedCoverageBearingInventorySyncForTenant($tenantId);
$basisCoverage = $basisRun?->inventoryCoverage();
/** @var array<string, int> $countsByType */
$countsByType = InventoryItem::query()
->where('tenant_id', $tenantId)
->selectRaw('policy_type, COUNT(*) as aggregate')
->groupBy('policy_type')
->pluck('aggregate', 'policy_type')
->map(static fn (mixed $value): int => (int) $value)
->all();
$rows = $this->supportedTypes()
->map(function (array $meta) use ($basisCoverage, $countsByType): TenantCoverageTypeTruth {
$type = (string) $meta['type'];
$segment = (string) $meta['segment'];
$basisRow = $basisCoverage?->row($type);
$coverageState = is_string($basisRow['status'] ?? null)
? (string) $basisRow['status']
: TenantCoverageTypeTruth::StateUnknown;
$observedItemCount = (int) ($countsByType[$type] ?? 0);
$basisItemCount = is_int($basisRow['item_count'] ?? null)
? (int) $basisRow['item_count']
: null;
$basisErrorCode = is_string($basisRow['error_code'] ?? null)
? (string) $basisRow['error_code']
: null;
$followUpRequired = $coverageState !== TenantCoverageTypeTruth::StateSucceeded;
return new TenantCoverageTypeTruth(
key: sprintf('%s:%s', $segment, $type),
type: $type,
segment: $segment,
label: (string) ($meta['label'] ?? $type),
category: (string) ($meta['category'] ?? 'Other'),
platform: is_string($meta['platform'] ?? null) ? (string) $meta['platform'] : null,
coverageState: $coverageState,
followUpRequired: $followUpRequired,
followUpPriority: self::followUpPriorityForState($coverageState),
observedItemCount: $observedItemCount,
basisItemCount: $basisItemCount,
basisErrorCode: $basisErrorCode,
restoreMode: is_string($meta['restore'] ?? null) ? (string) $meta['restore'] : null,
riskLevel: is_string($meta['risk'] ?? null) ? (string) $meta['risk'] : null,
supportsDependencies: $segment === 'policy' && $this->coverageCapabilities->supportsDependencies($type),
followUpGuidance: self::followUpGuidanceForState($coverageState, $basisErrorCode),
isBasisPayloadBacked: $basisRow !== null,
);
})
->sort(function (TenantCoverageTypeTruth $left, TenantCoverageTypeTruth $right): int {
$priority = $left->followUpPriority <=> $right->followUpPriority;
if ($priority !== 0) {
return $priority;
}
$observed = $right->observedItemCount <=> $left->observedItemCount;
if ($observed !== 0) {
return $observed;
}
return strnatcasecmp($left->label, $right->label);
})
->values()
->all();
return new TenantCoverageTruth(
tenantId: $tenantId,
basisRun: $basisRun,
hasCurrentCoverageResult: $basisCoverage instanceof InventoryCoverage,
supportedTypeCount: count($rows),
succeededTypeCount: $this->countRowsByState($rows, TenantCoverageTypeTruth::StateSucceeded),
failedTypeCount: $this->countRowsByState($rows, TenantCoverageTypeTruth::StateFailed),
skippedTypeCount: $this->countRowsByState($rows, TenantCoverageTypeTruth::StateSkipped),
unknownTypeCount: $this->countRowsByState($rows, TenantCoverageTypeTruth::StateUnknown),
followUpTypeCount: count(array_filter(
$rows,
static fn (TenantCoverageTypeTruth $row): bool => $row->followUpRequired,
)),
observedItemTotal: array_sum($countsByType),
rows: $rows,
);
}
/**
* @return Collection<int, array{type: string, label: string, category: string, platform?: string|null, restore?: string|null, risk?: string|null, segment: 'policy'|'foundation'}>
*/
private function supportedTypes(): Collection
{
$supported = collect(InventoryPolicyTypeMeta::supported())
->filter(static fn (array $row): bool => is_string($row['type'] ?? null) && $row['type'] !== '')
->map(static fn (array $row): array => array_merge($row, ['segment' => 'policy']));
$foundations = collect(InventoryPolicyTypeMeta::foundations())
->filter(static fn (array $row): bool => is_string($row['type'] ?? null) && $row['type'] !== '')
->map(static fn (array $row): array => array_merge($row, ['segment' => 'foundation']));
return $supported
->merge($foundations)
->values();
}
public static function followUpPriorityForState(string $coverageState): int
{
return match ($coverageState) {
TenantCoverageTypeTruth::StateFailed => 0,
TenantCoverageTypeTruth::StateUnknown => 1,
TenantCoverageTypeTruth::StateSkipped => 2,
default => 3,
};
}
public static function followUpGuidanceForState(string $coverageState, ?string $basisErrorCode): string
{
return match (true) {
$coverageState === TenantCoverageTypeTruth::StateFailed && in_array($basisErrorCode, [
'graph_forbidden',
'provider_consent_missing',
'provider_permission_missing',
'provider_permission_denied',
], true) => 'Review provider consent or permissions, then rerun inventory sync.',
$coverageState === TenantCoverageTypeTruth::StateFailed && in_array($basisErrorCode, [
'graph_throttled',
'graph_transient',
'rate_limited',
'network_unreachable',
], true) => 'Retry inventory sync after the provider recovers.',
$coverageState === TenantCoverageTypeTruth::StateFailed => 'Review the latest inventory sync details before retrying.',
$coverageState === TenantCoverageTypeTruth::StateSkipped => 'Run inventory sync again with the required types selected.',
$coverageState === TenantCoverageTypeTruth::StateUnknown => 'No current basis result exists for this type. Run inventory sync to confirm coverage.',
default => 'No follow-up is currently required.',
};
}
/**
* @param list<TenantCoverageTypeTruth> $rows
*/
private function countRowsByState(array $rows, string $state): int
{
return count(array_filter(
$rows,
static fn (TenantCoverageTypeTruth $row): bool => $row->coverageState === $state,
));
}
}