## Summary - implement Spec 177 inventory coverage truth across resolver, badges, KPIs, coverage page, and operation run detail surfaces - add repo-native spec artifacts for the feature under `specs/177-inventory-coverage-truth` - add unit, feature, and browser coverage for truth derivation, continuity, and inventory item filter/pagination smoke paths ## Testing - `vendor/bin/sail bin pint --dirty --format agent` - focused Spec 177 browser smoke file passed with 2 tests / 57 assertions - extended inventory-focused test pack passed with 52 tests / 434 assertions Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #208
171 lines
7.6 KiB
PHP
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,
|
|
));
|
|
}
|
|
}
|