156 lines
4.9 KiB
PHP
156 lines
4.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\GovernanceDecisions;
|
|
|
|
use App\Models\FindingException;
|
|
use App\Models\FindingExceptionDecision;
|
|
use App\Models\Tenant;
|
|
use App\Models\Workspace;
|
|
use Carbon\CarbonInterface;
|
|
use Illuminate\Support\Collection;
|
|
|
|
final readonly class GovernanceDecisionRegisterBuilder
|
|
{
|
|
private const int RECENTLY_CLOSED_DAYS = 30;
|
|
|
|
/**
|
|
* @var list<string>
|
|
*/
|
|
private const array TERMINAL_STATUSES = [
|
|
FindingException::STATUS_REJECTED,
|
|
FindingException::STATUS_REVOKED,
|
|
FindingException::STATUS_SUPERSEDED,
|
|
];
|
|
|
|
/**
|
|
* @param array<int, Tenant> $visibleTenants
|
|
* @return array{
|
|
* rows: list<array<string, mixed>>,
|
|
* counts: array{open: int, recently_closed: int},
|
|
* }
|
|
*/
|
|
public function build(Workspace $workspace, array $visibleTenants, string $registerState = 'open'): array
|
|
{
|
|
$visibleTenantIds = array_values(array_map(
|
|
static fn (Tenant $tenant): int => (int) $tenant->getKey(),
|
|
$visibleTenants,
|
|
));
|
|
|
|
if ($visibleTenantIds === []) {
|
|
return [
|
|
'rows' => [],
|
|
'counts' => [
|
|
'open' => 0,
|
|
'recently_closed' => 0,
|
|
],
|
|
];
|
|
}
|
|
|
|
$rows = FindingException::query()
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
->whereIn('tenant_id', $visibleTenantIds)
|
|
->with(['tenant:id,name', 'owner:id,name', 'currentDecision'])
|
|
->get()
|
|
->map(fn (FindingException $exception): ?array => $this->buildRow($exception))
|
|
->filter()
|
|
->values();
|
|
|
|
/** @var Collection<int, array<string, mixed>> $openRows */
|
|
$openRows = $rows
|
|
->where('register_state', 'open')
|
|
->sortBy([
|
|
['due_at', 'asc'],
|
|
['exception_id', 'asc'],
|
|
])
|
|
->values();
|
|
|
|
/** @var Collection<int, array<string, mixed>> $recentlyClosedRows */
|
|
$recentlyClosedRows = $rows
|
|
->where('register_state', 'recently_closed')
|
|
->sortByDesc('decision_at')
|
|
->values();
|
|
|
|
return [
|
|
'rows' => match ($registerState) {
|
|
'recently_closed' => $recentlyClosedRows->all(),
|
|
default => $openRows->all(),
|
|
},
|
|
'counts' => [
|
|
'open' => $openRows->count(),
|
|
'recently_closed' => $recentlyClosedRows->count(),
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
private function buildRow(FindingException $exception): ?array
|
|
{
|
|
$currentDecision = $exception->currentDecision;
|
|
|
|
if (! $currentDecision instanceof FindingExceptionDecision) {
|
|
return null;
|
|
}
|
|
|
|
$registerState = $this->resolveRegisterState($exception, $currentDecision);
|
|
|
|
if ($registerState === null) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'exception_id' => (int) $exception->getKey(),
|
|
'register_state' => $registerState,
|
|
'tenant_name' => $exception->tenant?->name,
|
|
'owner_name' => $exception->owner?->name,
|
|
'status' => (string) $exception->status,
|
|
'current_validity_state' => (string) $exception->current_validity_state,
|
|
'next_action_label' => $registerState === 'open'
|
|
? $this->resolveNextActionLabel($exception, $currentDecision)
|
|
: 'Decision closed',
|
|
'closure_reason' => $registerState === 'recently_closed'
|
|
? (string) $currentDecision->reason
|
|
: null,
|
|
'due_at' => $exception->review_due_at ?? $exception->expires_at,
|
|
'decision_at' => $currentDecision->decided_at,
|
|
];
|
|
}
|
|
|
|
private function resolveRegisterState(FindingException $exception, FindingExceptionDecision $currentDecision): ?string
|
|
{
|
|
$status = (string) $exception->status;
|
|
|
|
if (in_array($status, self::TERMINAL_STATUSES, true)) {
|
|
return $this->isRecentlyClosed($currentDecision->decided_at)
|
|
? 'recently_closed'
|
|
: null;
|
|
}
|
|
|
|
return 'open';
|
|
}
|
|
|
|
private function resolveNextActionLabel(FindingException $exception, FindingExceptionDecision $currentDecision): string
|
|
{
|
|
if ($exception->isPendingRenewal() || $currentDecision->decision_type === FindingExceptionDecision::TYPE_RENEWAL_REQUESTED) {
|
|
return 'Review renewal';
|
|
}
|
|
|
|
if ($exception->isPending()) {
|
|
return 'Review approval';
|
|
}
|
|
|
|
return 'Review follow-up';
|
|
}
|
|
|
|
private function isRecentlyClosed(?CarbonInterface $decidedAt): bool
|
|
{
|
|
if (! $decidedAt instanceof CarbonInterface) {
|
|
return false;
|
|
}
|
|
|
|
return $decidedAt->greaterThanOrEqualTo(now()->startOfDay()->subDays(self::RECENTLY_CLOSED_DAYS));
|
|
}
|
|
} |