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
179 lines
6.9 KiB
PHP
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');
|
|
}
|
|
}
|