*/ protected function casts(): array { return [ 'summary' => 'array', 'generated_at' => 'datetime', 'published_at' => 'datetime', 'archived_at' => 'datetime', ]; } /** * @return BelongsTo */ public function workspace(): BelongsTo { return $this->belongsTo(Workspace::class); } /** * @return BelongsTo */ public function tenant(): BelongsTo { return $this->belongsTo(Tenant::class); } /** * @return BelongsTo */ public function evidenceSnapshot(): BelongsTo { return $this->belongsTo(EvidenceSnapshot::class); } /** * @return BelongsTo */ public function operationRun(): BelongsTo { return $this->belongsTo(OperationRun::class); } /** * @return BelongsTo */ public function initiator(): BelongsTo { return $this->belongsTo(User::class, 'initiated_by_user_id'); } /** * @return BelongsTo */ public function publisher(): BelongsTo { return $this->belongsTo(User::class, 'published_by_user_id'); } /** * @return BelongsTo */ public function currentExportReviewPack(): BelongsTo { return $this->belongsTo(ReviewPack::class, 'current_export_review_pack_id'); } /** * @return BelongsTo */ public function supersededByReview(): BelongsTo { return $this->belongsTo(self::class, 'superseded_by_review_id'); } /** * @return HasMany */ public function supersededReviews(): HasMany { return $this->hasMany(self::class, 'superseded_by_review_id'); } /** * @return HasMany */ public function sections(): HasMany { return $this->hasMany(TenantReviewSection::class)->orderBy('sort_order')->orderBy('id'); } /** * @return HasMany */ public function reviewPacks(): HasMany { return $this->hasMany(ReviewPack::class)->latest('generated_at'); } /** * @param Builder $query * @return Builder */ public function scopeForTenant(Builder $query, int $tenantId): Builder { return $query->where('tenant_id', $tenantId); } /** * @param Builder $query * @return Builder */ public function scopeForWorkspace(Builder $query, int $workspaceId): Builder { return $query->where('workspace_id', $workspaceId); } /** * @param Builder $query * @return Builder */ public function scopePublished(Builder $query): Builder { return $query->where('status', TenantReviewStatus::Published->value); } /** * @param Builder $query * @return Builder */ public function scopeMutable(Builder $query): Builder { return $query->whereIn('status', [ TenantReviewStatus::Draft->value, TenantReviewStatus::Ready->value, TenantReviewStatus::Failed->value, ]); } public function statusEnum(): TenantReviewStatus { return TenantReviewStatus::from((string) $this->status); } public function completenessEnum(): TenantReviewCompletenessState { return TenantReviewCompletenessState::tryFrom((string) $this->completeness_state) ?? TenantReviewCompletenessState::Missing; } public function isPublished(): bool { return $this->statusEnum()->isPublished(); } public function isMutable(): bool { return $this->statusEnum()->isMutable(); } /** * @return list */ public function publishBlockers(): array { $summary = is_array($this->summary) ? $this->summary : []; $blockers = $summary['publish_blockers'] ?? []; return is_array($blockers) ? array_values(array_map('strval', $blockers)) : []; } /** * @return list> */ public function canonicalControlReferences(): array { $summary = is_array($this->summary) ? $this->summary : []; $references = $summary['canonical_controls'] ?? []; return is_array($references) ? array_values(array_filter($references, static fn (mixed $reference): bool => is_array($reference))) : []; } /** * @return array */ public function controlInterpretation(): array { $summary = is_array($this->summary) ? $this->summary : []; $interpretation = $summary['control_interpretation'] ?? []; return is_array($interpretation) ? $interpretation : []; } public function controlInterpretationVersion(): ?string { $version = $this->controlInterpretation()['version_key'] ?? null; return is_string($version) && trim($version) !== '' ? $version : null; } /** * @return list> */ public function controlInterpretationControls(): array { $controls = $this->controlInterpretation()['controls'] ?? []; return is_array($controls) ? array_values(array_filter($controls, static fn (mixed $control): bool => is_array($control))) : []; } /** * @return array */ public function controlInterpretationLimitationCounts(): array { $counts = $this->controlInterpretation()['limitation_counts'] ?? []; if (! is_array($counts)) { return []; } return collect($counts) ->mapWithKeys(static fn (mixed $count, string|int $key): array => [(string) $key => (int) $count]) ->all(); } public function controlInterpretationSection(): ?TenantReviewSection { if ($this->relationLoaded('sections')) { $section = $this->sections->firstWhere('section_key', ComplianceEvidenceMappingV1::SECTION_KEY); return $section instanceof TenantReviewSection ? $section : null; } return $this->sections() ->where('section_key', ComplianceEvidenceMappingV1::SECTION_KEY) ->first(); } }