*/ private const array TERMINAL_STATUSES = [ FindingException::STATUS_REJECTED, FindingException::STATUS_REVOKED, FindingException::STATUS_SUPERSEDED, ]; /** * @param array $visibleTenants * @return array{ * rows: list>, * 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> $openRows */ $openRows = $rows ->where('register_state', 'open') ->sortBy([ ['due_at', 'asc'], ['exception_id', 'asc'], ]) ->values(); /** @var Collection> $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|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)); } }