TenantAtlas/apps/platform/app/Support/ReviewPacks/ReportDisclosurePolicy.php
ahmido b7907bd69d feat: add report profile and disclosure policy to rendered review reports (#428)
Implementing report profiles and disclosure policy as per spec 357.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #428
2026-06-06 09:41:19 +00:00

180 lines
7.6 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\ReviewPacks;
final class ReportDisclosurePolicy
{
public const string PROOF_VERIFIED = 'verified';
public const string PROOF_ASSUMED = 'assumed';
public const string PROOF_MISSING = 'missing';
public const string PROOF_UNKNOWN = 'unknown';
public const string PROOF_NOT_APPLICABLE = 'not_applicable';
/**
* @param array<string, mixed> $profile
* @param array<string, mixed> $readiness
* @param array<string, mixed> $metadata
* @return array{
* mandatory_disclosures:list<array{key:string,label:string,summary:string,proof_state:string}>,
* warnings:list<array{key:string,label:string,summary:string}>,
* blocking_reasons:list<array{key:string,label:string,summary:string}>,
* proof_states:array{audience_boundary:string,evidence_basis:string,protected_values:string,non_certification:string},
* show_section_appendix:bool,
* show_technical_details:bool
* }
*/
public static function evaluate(array $profile, array $readiness, array $metadata = []): array
{
$isCustomerFacing = (bool) ($profile['is_customer_facing'] ?? false);
$containsPii = (bool) ($readiness['contains_pii'] ?? false);
$protectedValuesHidden = (bool) ($readiness['protected_values_hidden'] ?? false);
$disclosurePresent = (bool) ($readiness['disclosure_present'] ?? false);
$displayedDisclosure = self::plainText(
$metadata['non_certification_disclosure'] ?? null,
__('localization.review.non_certification_disclosure_text'),
);
$proofStates = [
'audience_boundary' => self::PROOF_VERIFIED,
'evidence_basis' => self::evidenceBasisProofState((string) ($readiness['evidence_completeness_state'] ?? '')),
'protected_values' => self::protectedValuesProofState(
isCustomerFacing: $isCustomerFacing,
containsPii: $containsPii,
protectedValuesHidden: $protectedValuesHidden,
),
'non_certification' => $disclosurePresent
? self::PROOF_ASSUMED
: self::PROOF_MISSING,
];
$blockingReasons = [];
if ($isCustomerFacing && $containsPii) {
$blockingReasons[] = [
'key' => 'customer_profile_internal_only',
'label' => __('localization.review.report_disclosure_customer_profile_internal_only'),
'summary' => __('localization.review.report_disclosure_customer_profile_internal_only_summary'),
];
}
$warnings = [];
if ((bool) ($profile['is_fallback'] ?? false)) {
$warnings[] = [
'key' => 'profile_fallback',
'label' => __('localization.review.report_profile_fallback_notice'),
'summary' => __('localization.review.report_profile_fallback_summary'),
];
}
if ($isCustomerFacing && (string) ($readiness['customer_safe_state'] ?? '') !== ReviewPackOutputReadiness::STATE_CUSTOMER_SAFE_READY) {
$warnings[] = [
'key' => 'customer_profile_requires_review',
'label' => __('localization.review.report_external_sharing_warning'),
'summary' => __('localization.review.report_disclosure_customer_profile_requires_review'),
];
}
if ($proofStates['non_certification'] === self::PROOF_MISSING) {
$warnings[] = [
'key' => 'non_certification_missing',
'label' => __('localization.review.non_certification_disclosure'),
'summary' => __('localization.review.report_disclosure_non_certification_missing'),
];
}
$showDetailedContent = ! ($isCustomerFacing && $containsPii);
return [
'mandatory_disclosures' => [
[
'key' => 'audience_boundary',
'label' => __('localization.review.report_disclosure_audience_boundary'),
'summary' => __('localization.review.report_disclosure_audience_boundary_summary', [
'audience' => (string) ($profile['audience_label'] ?? __('localization.review.unavailable')),
]),
'proof_state' => $proofStates['audience_boundary'],
],
[
'key' => 'evidence_basis',
'label' => __('localization.review.report_disclosure_evidence_basis'),
'summary' => match ($proofStates['evidence_basis']) {
self::PROOF_VERIFIED => __('localization.review.report_disclosure_evidence_verified'),
self::PROOF_MISSING => __('localization.review.report_disclosure_evidence_missing'),
default => __('localization.review.report_disclosure_evidence_unknown'),
},
'proof_state' => $proofStates['evidence_basis'],
],
[
'key' => 'protected_values',
'label' => __('localization.review.report_disclosure_protected_values'),
'summary' => match ($proofStates['protected_values']) {
self::PROOF_ASSUMED => __('localization.review.report_disclosure_protected_values_assumed'),
self::PROOF_NOT_APPLICABLE => __('localization.review.report_disclosure_protected_values_not_applicable'),
self::PROOF_MISSING => __('localization.review.report_disclosure_protected_values_missing'),
default => __('localization.review.report_disclosure_protected_values_unknown'),
},
'proof_state' => $proofStates['protected_values'],
],
[
'key' => 'non_certification',
'label' => __('localization.review.non_certification_disclosure'),
'summary' => $displayedDisclosure,
'proof_state' => $proofStates['non_certification'],
],
],
'warnings' => $warnings,
'blocking_reasons' => $blockingReasons,
'proof_states' => $proofStates,
'show_section_appendix' => (bool) ($profile['show_section_appendix'] ?? false) && $showDetailedContent,
'show_technical_details' => (bool) ($profile['show_technical_details'] ?? false) && $showDetailedContent,
];
}
private static function evidenceBasisProofState(string $evidenceCompletenessState): string
{
return match ($evidenceCompletenessState) {
'complete' => self::PROOF_VERIFIED,
'missing', 'partial', 'stale' => self::PROOF_MISSING,
default => self::PROOF_UNKNOWN,
};
}
private static function protectedValuesProofState(
bool $isCustomerFacing,
bool $containsPii,
bool $protectedValuesHidden,
): string {
if (! $isCustomerFacing) {
return self::PROOF_NOT_APPLICABLE;
}
if ($containsPii || ! $protectedValuesHidden) {
return self::PROOF_MISSING;
}
return self::PROOF_ASSUMED;
}
private static function plainText(mixed $value, string $fallback): string
{
if (! is_scalar($value) && $value !== null) {
return $fallback;
}
$text = preg_replace('/\s+/', ' ', trim((string) $value));
if (! is_string($text) || $text === '') {
return $fallback;
}
return str_starts_with($text, 'localization.') ? $fallback : $text;
}
}