loadMissing(['tenant', 'sections', 'currentExportReviewPack']); $tenant = $review->tenant; if (! $tenant instanceof Tenant) { throw new InvalidArgumentException('Review tenant could not be resolved.'); } $blockers = $this->readinessGate->blockersForReview($review); $beforeStatus = (string) $review->status; if ($blockers !== []) { throw new InvalidArgumentException(implode(' ', $blockers)); } $review->forceFill([ 'status' => TenantReviewStatus::Published->value, 'published_at' => now(), 'published_by_user_id' => (int) $user->getKey(), 'summary' => array_merge(is_array($review->summary) ? $review->summary : [], [ 'publish_blockers' => [], ]), ])->save(); $this->auditLogger->log( workspace: $tenant->workspace, action: AuditActionId::TenantReviewPublished, context: [ 'metadata' => [ 'review_id' => (int) $review->getKey(), 'before_status' => $beforeStatus, 'after_status' => TenantReviewStatus::Published->value, ], ], actor: $user, resourceType: 'tenant_review', resourceId: (string) $review->getKey(), targetLabel: sprintf('Tenant review #%d', (int) $review->getKey()), tenant: $tenant, ); return $review->refresh()->load(['tenant', 'sections', 'currentExportReviewPack']); } public function archive(TenantReview $review, User $user): TenantReview { $review->loadMissing('tenant'); $tenant = $review->tenant; if (! $tenant instanceof Tenant) { throw new InvalidArgumentException('Review tenant could not be resolved.'); } $beforeStatus = (string) $review->status; if ($review->statusEnum()->isTerminal()) { return $review; } $review->forceFill([ 'status' => TenantReviewStatus::Archived->value, 'archived_at' => now(), ])->save(); $this->auditLogger->log( workspace: $tenant->workspace, action: AuditActionId::TenantReviewArchived, context: [ 'metadata' => [ 'review_id' => (int) $review->getKey(), 'before_status' => $beforeStatus, 'after_status' => TenantReviewStatus::Archived->value, ], ], actor: $user, resourceType: 'tenant_review', resourceId: (string) $review->getKey(), targetLabel: sprintf('Tenant review #%d', (int) $review->getKey()), tenant: $tenant, ); return $review->refresh()->load(['tenant', 'sections', 'currentExportReviewPack']); } public function createNextReview(TenantReview $review, User $user, ?EvidenceSnapshot $snapshot = null): TenantReview { $review->loadMissing(['tenant', 'evidenceSnapshot']); $tenant = $review->tenant; if (! $tenant instanceof Tenant) { throw new InvalidArgumentException('Review tenant could not be resolved.'); } if (! $review->isPublished()) { throw new InvalidArgumentException('Only published reviews can start the next cycle.'); } $snapshot ??= $this->reviewService->resolveLatestSnapshot($tenant) ?? $review->evidenceSnapshot; if (! $snapshot instanceof EvidenceSnapshot) { throw new InvalidArgumentException('An eligible evidence snapshot is required to create the next review.'); } return DB::transaction(function () use ($review, $user, $snapshot, $tenant): TenantReview { $nextReview = $this->reviewService->create($tenant, $snapshot, $user); if ((int) $nextReview->getKey() !== (int) $review->getKey()) { $review->forceFill([ 'status' => TenantReviewStatus::Superseded->value, 'superseded_by_review_id' => (int) $nextReview->getKey(), ])->save(); } $this->auditLogger->log( workspace: $tenant->workspace, action: AuditActionId::TenantReviewSuccessorCreated, context: [ 'metadata' => [ 'review_id' => (int) $review->getKey(), 'next_review_id' => (int) $nextReview->getKey(), 'before_status' => TenantReviewStatus::Published->value, 'after_status' => TenantReviewStatus::Superseded->value, ], ], actor: $user, resourceType: 'tenant_review', resourceId: (string) $nextReview->getKey(), targetLabel: sprintf('Tenant review #%d', (int) $nextReview->getKey()), tenant: $tenant, ); return $nextReview->refresh()->load(['tenant', 'evidenceSnapshot', 'sections', 'operationRun', 'initiator', 'publisher']); }); } }