TenantAtlas/app/Support/Inventory/TenantCoverageTruth.php
ahmido f52d52540c feat: implement inventory coverage truth (#208)
## 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
2026-04-05 12:35:20 +00:00

137 lines
4.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Inventory;
use App\Models\OperationRun;
use InvalidArgumentException;
final readonly class TenantCoverageTruth
{
/**
* @param list<TenantCoverageTypeTruth> $rows
*/
public function __construct(
public int $tenantId,
public ?OperationRun $basisRun,
public bool $hasCurrentCoverageResult,
public int $supportedTypeCount,
public int $succeededTypeCount,
public int $failedTypeCount,
public int $skippedTypeCount,
public int $unknownTypeCount,
public int $followUpTypeCount,
public int $observedItemTotal,
public array $rows,
) {
if ($this->tenantId <= 0) {
throw new InvalidArgumentException('Tenant coverage truth requires a positive tenant id.');
}
if ($this->supportedTypeCount < 0 || $this->observedItemTotal < 0) {
throw new InvalidArgumentException('Tenant coverage truth counts must be zero or greater.');
}
}
public function basisRunId(): ?int
{
return $this->basisRun instanceof OperationRun
? (int) $this->basisRun->getKey()
: null;
}
public function basisRunOutcome(): ?string
{
return $this->basisRun instanceof OperationRun
? (string) $this->basisRun->outcome
: null;
}
public function basisCompletedAtLabel(): ?string
{
if (! $this->basisRun instanceof OperationRun) {
return null;
}
$timestamp = $this->basisRun->completed_at ?? $this->basisRun->started_at ?? $this->basisRun->created_at;
return $timestamp?->diffForHumans(['short' => true]);
}
public function topPriorityFollowUpRow(): ?TenantCoverageTypeTruth
{
foreach ($this->rows as $row) {
if ($row->followUpRequired) {
return $row;
}
}
return null;
}
public function observedTypeCount(): int
{
return count(array_filter(
$this->rows,
static fn (TenantCoverageTypeTruth $row): bool => $row->observedItemCount > 0,
));
}
/**
* @return list<TenantCoverageTypeTruth>
*/
public function followUpRows(): array
{
return array_values(array_filter(
$this->rows,
static fn (TenantCoverageTypeTruth $row): bool => $row->followUpRequired,
));
}
/**
* @return array{
* tenantId: int,
* basisRun: array{id: int, outcome: string, completedAt: string|null}|null,
* hasCurrentCoverageResult: bool,
* summary: array{
* supportedTypes: int,
* succeededTypes: int,
* failedTypes: int,
* skippedTypes: int,
* unknownTypes: int,
* followUpTypes: int,
* observedItems: int
* },
* rows: list<array<string, mixed>>
* }
*/
public function toArray(): array
{
return [
'tenantId' => $this->tenantId,
'basisRun' => $this->basisRun instanceof OperationRun
? [
'id' => (int) $this->basisRun->getKey(),
'outcome' => (string) $this->basisRun->outcome,
'completedAt' => $this->basisRun->completed_at?->toIso8601String(),
]
: null,
'hasCurrentCoverageResult' => $this->hasCurrentCoverageResult,
'summary' => [
'supportedTypes' => $this->supportedTypeCount,
'succeededTypes' => $this->succeededTypeCount,
'failedTypes' => $this->failedTypeCount,
'skippedTypes' => $this->skippedTypeCount,
'unknownTypes' => $this->unknownTypeCount,
'followUpTypes' => $this->followUpTypeCount,
'observedItems' => $this->observedItemTotal,
],
'rows' => array_map(
static fn (TenantCoverageTypeTruth $row): array => $row->toArray(),
$this->rows,
),
];
}
}