TenantAtlas/apps/platform/app/Support/ReviewPacks/ReviewPackOutputReadiness.php
ahmido 12ea7f9924 feat: review pack output contract and readiness semantics (spec 347/348) (#419)
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.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #419
2026-06-02 23:17:08 +00: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');
}
}