TenantAtlas/apps/platform/app/Support/ReviewPacks/ReviewPackOutputReadiness.php
Ahmed Darrazi 549a9a0004
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m0s
feat: review pack output contract and readiness semantics (spec 347)
Implemented the output contract and readiness semantics for review packs. Also added spec 348.
Includes changes to ChooseEnvironment, CustomerReviewWorkspace, GenerateReviewPackJob and related blade views.
Added comprehensive tests.
2026-06-03 01:14:29 +02:00

179 lines
6.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\ReviewPacks;
use App\Support\EnvironmentReviewCompletenessState;
final class ReviewPackOutputReadiness
{
public const string STATE_CUSTOMER_SAFE_READY = 'customer_safe_ready';
public const string STATE_PUBLISHED_WITH_LIMITATIONS = 'published_with_limitations';
public const string STATE_INTERNAL_REVIEW_PACKAGE_AVAILABLE = 'internal_review_package_available';
public const string STATE_EXPORT_NOT_READY = 'export_not_ready';
/**
* @param array<string, int> $sectionStateCounts
* @param array<string, int> $requiredSectionStateCounts
* @param list<mixed> $publishBlockers
* @return array{
* review_status: string,
* review_completeness_state: string,
* evidence_completeness_state: string,
* has_ready_export: bool,
* section_state_counts: array<string, int>,
* required_section_count: int,
* required_section_state_counts: array<string, int>,
* required_section_limited_count: int,
* contains_pii: bool,
* protected_values_hidden: bool,
* disclosure_present: bool,
* customer_safe_state: string,
* readiness_state: string,
* primary_reason: string,
* primary_action: string,
* limitations: list<array{code: string}>,
* section_summary: array{
* required_total: int,
* required_complete: int,
* required_limited: int,
* partial: int,
* missing: int,
* stale: int
* }
* }
*/
public static function derive(
string $reviewStatus,
string $reviewCompletenessState,
string $evidenceCompletenessState,
array $sectionStateCounts,
int $requiredSectionCount,
array $requiredSectionStateCounts,
array $publishBlockers,
bool $hasReadyExport,
bool $includePii,
bool $protectedValuesHidden = true,
bool $disclosurePresent = true,
): array {
$sectionStateCounts = self::normalizeCounts($sectionStateCounts);
$requiredSectionStateCounts = self::normalizeCounts($requiredSectionStateCounts);
$requiredLimitedCount = max(
0,
(int) ($requiredSectionStateCounts[EnvironmentReviewCompletenessState::Partial->value] ?? 0)
+ (int) ($requiredSectionStateCounts[EnvironmentReviewCompletenessState::Missing->value] ?? 0)
+ (int) ($requiredSectionStateCounts[EnvironmentReviewCompletenessState::Stale->value] ?? 0)
);
$limitations = [];
if (! $hasReadyExport) {
$limitations[] = ['code' => 'export_not_ready'];
}
if ($evidenceCompletenessState !== EnvironmentReviewCompletenessState::Complete->value) {
$limitations[] = ['code' => match ($evidenceCompletenessState) {
EnvironmentReviewCompletenessState::Missing->value => 'evidence_basis_missing',
EnvironmentReviewCompletenessState::Stale->value => 'evidence_basis_stale',
default => 'evidence_basis_incomplete',
}];
}
if ($requiredLimitedCount > 0) {
$limitations[] = ['code' => 'required_sections_incomplete'];
}
if ($publishBlockers !== []) {
$limitations[] = ['code' => 'publish_blockers_present'];
}
if ($includePii) {
$limitations[] = ['code' => 'contains_pii'];
}
if (! $disclosurePresent) {
$limitations[] = ['code' => 'disclosure_missing'];
}
$readinessState = match (true) {
! $hasReadyExport => self::STATE_EXPORT_NOT_READY,
self::hasMaterialLimitations($limitations) => self::STATE_PUBLISHED_WITH_LIMITATIONS,
$includePii => self::STATE_INTERNAL_REVIEW_PACKAGE_AVAILABLE,
default => self::STATE_CUSTOMER_SAFE_READY,
};
$customerSafeState = match ($readinessState) {
self::STATE_CUSTOMER_SAFE_READY => 'customer_safe_ready',
self::STATE_INTERNAL_REVIEW_PACKAGE_AVAILABLE => 'internal_only',
self::STATE_EXPORT_NOT_READY => 'not_ready',
default => 'requires_review',
};
$primaryReason = $limitations[0]['code'] ?? 'customer_safe_ready';
$primaryAction = match ($readinessState) {
self::STATE_CUSTOMER_SAFE_READY => 'download_customer_safe_review_pack',
self::STATE_INTERNAL_REVIEW_PACKAGE_AVAILABLE => 'review_package_contents',
self::STATE_EXPORT_NOT_READY => 'open_evidence_basis',
default => 'review_output_limitations',
};
$requiredComplete = max(0, min(
$requiredSectionCount,
(int) ($requiredSectionStateCounts[EnvironmentReviewCompletenessState::Complete->value] ?? 0)
));
return [
'review_status' => $reviewStatus,
'review_completeness_state' => $reviewCompletenessState,
'evidence_completeness_state' => $evidenceCompletenessState,
'has_ready_export' => $hasReadyExport,
'section_state_counts' => $sectionStateCounts,
'required_section_count' => $requiredSectionCount,
'required_section_state_counts' => $requiredSectionStateCounts,
'required_section_limited_count' => $requiredLimitedCount,
'contains_pii' => $includePii,
'protected_values_hidden' => $protectedValuesHidden,
'disclosure_present' => $disclosurePresent,
'customer_safe_state' => $customerSafeState,
'readiness_state' => $readinessState,
'primary_reason' => $primaryReason,
'primary_action' => $primaryAction,
'limitations' => $limitations,
'section_summary' => [
'required_total' => $requiredSectionCount,
'required_complete' => $requiredComplete,
'required_limited' => $requiredLimitedCount,
'partial' => (int) ($requiredSectionStateCounts[EnvironmentReviewCompletenessState::Partial->value] ?? 0),
'missing' => (int) ($requiredSectionStateCounts[EnvironmentReviewCompletenessState::Missing->value] ?? 0),
'stale' => (int) ($requiredSectionStateCounts[EnvironmentReviewCompletenessState::Stale->value] ?? 0),
],
];
}
/**
* @param array<string, int> $counts
* @return array<string, int>
*/
private static function normalizeCounts(array $counts): array
{
return collect($counts)
->mapWithKeys(static fn (mixed $count, string|int $key): array => [(string) $key => max(0, (int) $count)])
->all();
}
/**
* @param list<array{code: string}> $limitations
*/
private static function hasMaterialLimitations(array $limitations): bool
{
return collect($limitations)
->pluck('code')
->contains(static fn (string $code): bool => $code !== 'contains_pii');
}
}