TenantAtlas/apps/platform/app/Support/ReviewPacks/CustomerOutputGate.php
Ahmed Darrazi 59b45becc1
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m35s
feat: enforce Spec392 customer output gating
2026-06-20 22:52:43 +02:00

135 lines
5.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\ReviewPacks;
use App\Models\EnvironmentReview;
use App\Models\ManagedEnvironment;
use App\Models\ReviewPack;
use App\Models\User;
use App\Support\Auth\Capabilities;
use App\Support\ReviewPackStatus;
final class CustomerOutputGate
{
public const string INTERNAL_PREVIEW_QUERY_KEY = 'internal_preview';
public const string STATE_READY = 'ready';
public const string STATE_NEEDS_ATTENTION = 'needs_attention';
public const string STATE_BLOCKED = 'blocked';
public const string STATE_INTERNAL_ONLY = 'internal_only';
public const string STATE_NOT_AVAILABLE = 'not_available';
public const string STATE_EXPIRED = 'expired';
public const string STATE_UNKNOWN = 'unknown';
public function decisionForReviewPack(ReviewPack $reviewPack, ?User $actor = null): CustomerOutputGateDecision
{
$reviewPack->loadMissing([
'tenant',
'environmentReview.evidenceSnapshot',
'environmentReview.currentExportReviewPack',
'environmentReview.sections',
]);
$tenant = $reviewPack->tenant;
$review = $reviewPack->environmentReview;
$hasReadyArtifact = $this->hasReadyArtifact($reviewPack);
$canViewReviewPack = $tenant instanceof ManagedEnvironment
&& $actor instanceof User
&& $actor->can(Capabilities::REVIEW_PACK_VIEW, $tenant);
$canManageReviewPack = $tenant instanceof ManagedEnvironment
&& $actor instanceof User
&& $actor->can(Capabilities::REVIEW_PACK_MANAGE, $tenant);
if ($this->isExpired($reviewPack)) {
return new CustomerOutputGateDecision(
state: self::STATE_EXPIRED,
guidanceState: ReviewPackOutputResolutionGuidance::STATE_EXPORT_NOT_READY,
hasReadyArtifact: false,
canStreamCustomerOutput: false,
canStreamInternalPreview: false,
reason: __('localization.review.expired'),
);
}
if (! $hasReadyArtifact) {
return new CustomerOutputGateDecision(
state: self::STATE_NOT_AVAILABLE,
guidanceState: ReviewPackOutputResolutionGuidance::STATE_EXPORT_NOT_READY,
hasReadyArtifact: false,
canStreamCustomerOutput: false,
canStreamInternalPreview: false,
reason: __('localization.review.export_not_ready'),
);
}
if (! $review instanceof EnvironmentReview || ! $this->isCurrentReviewExport($reviewPack, $review)) {
return new CustomerOutputGateDecision(
state: self::STATE_UNKNOWN,
guidanceState: ReviewPackOutputResolutionGuidance::STATE_UNKNOWN,
hasReadyArtifact: true,
canStreamCustomerOutput: false,
canStreamInternalPreview: $canManageReviewPack,
reason: __('localization.review.requires_review'),
);
}
$readiness = ReviewPackOutputResolutionGuidance::readinessForReview($review);
$guidance = ReviewPackOutputResolutionGuidance::fromReadiness($readiness);
$guidanceState = (string) ($guidance['state'] ?? ReviewPackOutputResolutionGuidance::STATE_UNKNOWN);
$canStreamCustomerOutput = $guidanceState === ReviewPackOutputResolutionGuidance::STATE_CUSTOMER_SAFE_READY
&& $canViewReviewPack;
return new CustomerOutputGateDecision(
state: $this->stateForGuidance($guidanceState),
guidanceState: $guidanceState,
hasReadyArtifact: true,
canStreamCustomerOutput: $canStreamCustomerOutput,
canStreamInternalPreview: $canManageReviewPack,
reason: (string) ($guidance['primary_reason'] ?? __('localization.review.requires_review')),
readiness: $readiness,
guidance: $guidance,
);
}
private function hasReadyArtifact(ReviewPack $reviewPack): bool
{
if ($reviewPack->status !== ReviewPackStatus::Ready->value) {
return false;
}
return filled($reviewPack->file_path) && filled($reviewPack->file_disk);
}
private function isExpired(ReviewPack $reviewPack): bool
{
return $reviewPack->status === ReviewPackStatus::Expired->value
|| ($reviewPack->expires_at !== null && $reviewPack->expires_at->isPast());
}
private function isCurrentReviewExport(ReviewPack $reviewPack, EnvironmentReview $review): bool
{
return (int) $review->managed_environment_id === (int) $reviewPack->managed_environment_id
&& (int) ($review->current_export_review_pack_id ?? 0) === (int) $reviewPack->getKey();
}
private function stateForGuidance(string $guidanceState): string
{
return match ($guidanceState) {
ReviewPackOutputResolutionGuidance::STATE_CUSTOMER_SAFE_READY => self::STATE_READY,
ReviewPackOutputResolutionGuidance::STATE_PUBLICATION_BLOCKED => self::STATE_BLOCKED,
ReviewPackOutputResolutionGuidance::STATE_INTERNAL_ONLY => self::STATE_INTERNAL_ONLY,
ReviewPackOutputResolutionGuidance::STATE_EXPORT_NOT_READY => self::STATE_NOT_AVAILABLE,
ReviewPackOutputResolutionGuidance::STATE_PUBLISHED_WITH_LIMITATIONS => self::STATE_NEEDS_ATTENTION,
default => self::STATE_UNKNOWN,
};
}
}