TenantAtlas/apps/platform/app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php
Ahmed Darrazi b5671cbf47
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 3m49s
chore: commit all local changes (automated by Copilot)
2026-05-02 21:00:28 +02:00

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));
}
}