TenantAtlas/apps/platform/app/Support/PortfolioTriage/TenantTriageReviewStateResolver.php
2026-04-10 23:34:02 +02:00

193 lines
7.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\PortfolioTriage;
use App\Models\TenantTriageReview;
use App\Support\BackupHealth\TenantBackupHealthAssessment;
final readonly class TenantTriageReviewStateResolver
{
public function __construct(
private TenantTriageReviewFingerprint $fingerprints,
) {}
/**
* @param list<int> $tenantIds
* @param array<int, TenantBackupHealthAssessment> $backupHealthByTenant
* @param array<int, array<string, mixed>> $recoveryEvidenceByTenant
* @return array{
* rows: array<int, array{
* backup_health: array<string, mixed>,
* recovery_evidence: array<string, mixed>
* }>,
* summaries: array<string, array{
* concern_family: string,
* affected_total: int,
* reviewed_count: int,
* follow_up_needed_count: int,
* changed_since_review_count: int,
* not_reviewed_count: int
* }>
* }
*/
public function resolveMany(
int $workspaceId,
array $tenantIds,
array $backupHealthByTenant = [],
array $recoveryEvidenceByTenant = [],
): array {
$tenantIds = array_values(array_unique(array_map(static fn (int|string $tenantId): int => (int) $tenantId, $tenantIds)));
$emptySummary = [
PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH => $this->emptySummary(PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH),
PortfolioArrivalContextToken::FAMILY_RECOVERY_EVIDENCE => $this->emptySummary(PortfolioArrivalContextToken::FAMILY_RECOVERY_EVIDENCE),
];
if ($workspaceId <= 0 || $tenantIds === []) {
return [
'rows' => [],
'summaries' => $emptySummary,
];
}
$activeReviews = TenantTriageReview::query()
->with('reviewer:id,name,email')
->forWorkspace($workspaceId)
->whereIn('tenant_id', $tenantIds)
->active()
->orderByDesc('reviewed_at')
->orderByDesc('id')
->get()
->groupBy([
'tenant_id',
'concern_family',
]);
$rows = [];
$summaries = $emptySummary;
foreach ($tenantIds as $tenantId) {
$rows[$tenantId] = [
PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH => $this->resolveFamily(
tenantId: $tenantId,
concernFamily: PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH,
currentConcern: $this->fingerprints->forBackupHealth($backupHealthByTenant[$tenantId] ?? null),
activeReview: $activeReviews[$tenantId][PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH][0] ?? null,
),
PortfolioArrivalContextToken::FAMILY_RECOVERY_EVIDENCE => $this->resolveFamily(
tenantId: $tenantId,
concernFamily: PortfolioArrivalContextToken::FAMILY_RECOVERY_EVIDENCE,
currentConcern: $this->fingerprints->forRecoveryEvidence($recoveryEvidenceByTenant[$tenantId] ?? null),
activeReview: $activeReviews[$tenantId][PortfolioArrivalContextToken::FAMILY_RECOVERY_EVIDENCE][0] ?? null,
),
];
foreach ($rows[$tenantId] as $family => $row) {
if (($row['current_concern_present'] ?? false) !== true) {
continue;
}
$summaries[$family]['affected_total']++;
match ($row['derived_state']) {
TenantTriageReview::STATE_REVIEWED => $summaries[$family]['reviewed_count']++,
TenantTriageReview::STATE_FOLLOW_UP_NEEDED => $summaries[$family]['follow_up_needed_count']++,
TenantTriageReview::DERIVED_STATE_CHANGED_SINCE_REVIEW => $summaries[$family]['changed_since_review_count']++,
default => $summaries[$family]['not_reviewed_count']++,
};
}
}
return [
'rows' => $rows,
'summaries' => $summaries,
];
}
/**
* @param array{
* concern_family: string,
* concern_state: string,
* fingerprint: string,
* snapshot: array<string, mixed>
* }|null $currentConcern
* @return array<string, mixed>
*/
private function resolveFamily(
int $tenantId,
string $concernFamily,
?array $currentConcern,
?TenantTriageReview $activeReview,
): array {
$reviewerName = null;
if ($activeReview?->reviewer !== null) {
$reviewerName = filled($activeReview->reviewer->name)
? (string) $activeReview->reviewer->name
: (filled($activeReview->reviewer->email) ? (string) $activeReview->reviewer->email : null);
}
if ($currentConcern === null) {
return [
'tenant_id' => $tenantId,
'concern_family' => $concernFamily,
'current_concern_present' => false,
'current_state' => null,
'current_fingerprint' => null,
'review_fingerprint' => $activeReview?->review_fingerprint,
'derived_state' => null,
'reviewed_at' => $activeReview?->reviewed_at,
'reviewed_by_user_id' => $activeReview?->reviewed_by_user_id,
'reviewed_by_user_name' => $reviewerName,
'current_snapshot' => null,
'review_snapshot' => is_array($activeReview?->review_snapshot) ? $activeReview->review_snapshot : null,
];
}
$derivedState = match (true) {
! $activeReview instanceof TenantTriageReview => TenantTriageReview::DERIVED_STATE_NOT_REVIEWED,
$activeReview->review_fingerprint !== $currentConcern['fingerprint'] => TenantTriageReview::DERIVED_STATE_CHANGED_SINCE_REVIEW,
default => (string) $activeReview->current_state,
};
return [
'tenant_id' => $tenantId,
'concern_family' => $concernFamily,
'current_concern_present' => true,
'current_state' => $currentConcern['concern_state'],
'current_fingerprint' => $currentConcern['fingerprint'],
'review_fingerprint' => $activeReview?->review_fingerprint,
'derived_state' => $derivedState,
'reviewed_at' => $activeReview?->reviewed_at,
'reviewed_by_user_id' => $activeReview?->reviewed_by_user_id,
'reviewed_by_user_name' => $reviewerName,
'current_snapshot' => $currentConcern['snapshot'],
'review_snapshot' => is_array($activeReview?->review_snapshot) ? $activeReview->review_snapshot : $currentConcern['snapshot'],
];
}
/**
* @return array{
* concern_family: string,
* affected_total: int,
* reviewed_count: int,
* follow_up_needed_count: int,
* changed_since_review_count: int,
* not_reviewed_count: int
* }
*/
private function emptySummary(string $concernFamily): array
{
return [
'concern_family' => $concernFamily,
'affected_total' => 0,
'reviewed_count' => 0,
'follow_up_needed_count' => 0,
'changed_since_review_count' => 0,
'not_reviewed_count' => 0,
];
}
}