|null $recoveryEvidence */ public function markReviewed( Tenant $tenant, string $concernFamily, ?TenantBackupHealthAssessment $backupHealth = null, ?array $recoveryEvidence = null, ?User $actor = null, ): TenantTriageReview { return $this->store( tenant: $tenant, concernFamily: $concernFamily, manualState: TenantTriageReview::STATE_REVIEWED, backupHealth: $backupHealth, recoveryEvidence: $recoveryEvidence, actor: $actor, ); } /** * @param array|null $recoveryEvidence */ public function markFollowUpNeeded( Tenant $tenant, string $concernFamily, ?TenantBackupHealthAssessment $backupHealth = null, ?array $recoveryEvidence = null, ?User $actor = null, ): TenantTriageReview { return $this->store( tenant: $tenant, concernFamily: $concernFamily, manualState: TenantTriageReview::STATE_FOLLOW_UP_NEEDED, backupHealth: $backupHealth, recoveryEvidence: $recoveryEvidence, actor: $actor, ); } /** * @param array|null $recoveryEvidence */ private function store( Tenant $tenant, string $concernFamily, string $manualState, ?TenantBackupHealthAssessment $backupHealth, ?array $recoveryEvidence, ?User $actor, ): TenantTriageReview { if (! in_array($manualState, TenantTriageReview::MANUAL_STATES, true)) { throw new InvalidArgumentException('Unsupported triage review state.'); } if (! is_numeric($tenant->workspace_id) || (int) $tenant->workspace_id <= 0) { throw new InvalidArgumentException('Tenant must belong to a workspace.'); } $currentConcern = $this->fingerprints->forConcernFamily($concernFamily, $backupHealth, $recoveryEvidence); if ($currentConcern === null) { throw new InvalidArgumentException('No current triage concern is available for review.'); } $workspaceId = (int) $tenant->workspace_id; $now = now(); /** @var TenantTriageReview $review */ $review = DB::transaction(function () use ( $tenant, $workspaceId, $manualState, $currentConcern, $actor, $now, ): TenantTriageReview { TenantTriageReview::query() ->forWorkspace($workspaceId) ->forTenant((int) $tenant->getKey()) ->where('concern_family', $currentConcern['concern_family']) ->active() ->update([ 'resolved_at' => $now, 'updated_at' => $now, ]); return TenantTriageReview::query()->create([ 'workspace_id' => $workspaceId, 'tenant_id' => (int) $tenant->getKey(), 'concern_family' => $currentConcern['concern_family'], 'current_state' => $manualState, 'reviewed_at' => $now, 'reviewed_by_user_id' => $actor?->getKey(), 'review_fingerprint' => $currentConcern['fingerprint'], 'review_snapshot' => $currentConcern['snapshot'], 'last_seen_matching_at' => $now, 'resolved_at' => null, ]); }); $review->loadMissing('reviewer'); $this->auditLogger->log( workspace: $tenant->workspace, action: $manualState === TenantTriageReview::STATE_REVIEWED ? AuditActionId::TenantTriageReviewMarkedReviewed : AuditActionId::TenantTriageReviewMarkedFollowUpNeeded, context: [ 'metadata' => [ 'concern_family' => $currentConcern['concern_family'], 'concern_state' => $currentConcern['concern_state'], 'reason_code' => $currentConcern['snapshot']['reasonCode'] ?? null, 'review_state' => $manualState, ], ], actor: $actor, resourceType: 'tenant_triage_review', resourceId: (string) $review->getKey(), targetLabel: $tenant->name, tenant: $tenant, summary: $this->summaryFor($currentConcern['concern_family'], $manualState), ); return $review; } private function summaryFor(string $concernFamily, string $manualState): string { $family = match ($concernFamily) { 'backup_health' => 'Backup health', 'recovery_evidence' => 'Recovery evidence', default => 'Portfolio concern', }; $state = $manualState === TenantTriageReview::STATE_REVIEWED ? 'reviewed' : 'follow-up needed'; return sprintf('%s marked %s', $family, $state); } }