*/ protected function casts(): array { return [ 'requested_at' => 'datetime', 'approved_at' => 'datetime', 'rejected_at' => 'datetime', 'revoked_at' => 'datetime', 'effective_from' => 'datetime', 'expires_at' => 'datetime', 'review_due_at' => 'datetime', 'evidence_summary' => 'array', ]; } /** * @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 finding(): BelongsTo { return $this->belongsTo(Finding::class); } /** * @return BelongsTo */ public function requester(): BelongsTo { return $this->belongsTo(User::class, 'requested_by_user_id'); } /** * @return BelongsTo */ public function owner(): BelongsTo { return $this->belongsTo(User::class, 'owner_user_id'); } /** * @return BelongsTo */ public function approver(): BelongsTo { return $this->belongsTo(User::class, 'approved_by_user_id'); } /** * @return BelongsTo */ public function currentDecision(): BelongsTo { return $this->belongsTo(FindingExceptionDecision::class, 'current_decision_id'); } /** * @return HasMany */ public function decisions(): HasMany { return $this->hasMany(FindingExceptionDecision::class) ->orderBy('decided_at') ->orderBy('id'); } /** * @return HasMany */ public function evidenceReferences(): HasMany { return $this->hasMany(FindingExceptionEvidenceReference::class) ->orderBy('id'); } /** * @param Builder $query * @return Builder */ public function scopeForFinding(Builder $query, Finding $finding): Builder { return $query->where('finding_id', (int) $finding->getKey()); } /** * @param Builder $query * @return Builder */ public function scopePending(Builder $query): Builder { return $query->where('status', self::STATUS_PENDING); } /** * @param Builder $query * @return Builder */ public function scopeCurrent(Builder $query): Builder { return $query->whereIn('status', [ self::STATUS_PENDING, self::STATUS_ACTIVE, self::STATUS_EXPIRING, ]); } public function isPending(): bool { return (string) $this->status === self::STATUS_PENDING; } public function isActiveLike(): bool { return in_array((string) $this->status, [ self::STATUS_ACTIVE, self::STATUS_EXPIRING, ], true); } public function hasPriorApproval(): bool { return $this->approved_at !== null && $this->effective_from !== null && is_numeric($this->approved_by_user_id); } public function hasValidGovernance(): bool { return in_array((string) $this->current_validity_state, [ self::VALIDITY_VALID, self::VALIDITY_EXPIRING, ], true); } public function currentDecisionType(): ?string { $decision = $this->relationLoaded('currentDecision') ? $this->currentDecision : $this->currentDecision()->first(); return $decision instanceof FindingExceptionDecision ? (string) $decision->decision_type : null; } public function isPendingRenewal(): bool { return $this->isPending() && $this->hasPriorApproval() && $this->currentDecisionType() === FindingExceptionDecision::TYPE_RENEWAL_REQUESTED; } public function requiresFreshDecisionForFinding(Finding $finding): bool { return ! $finding->isRiskAccepted() && ! $this->isPending() && $this->hasValidGovernance(); } public function canBeRenewed(): bool { return in_array((string) $this->status, [ self::STATUS_ACTIVE, self::STATUS_EXPIRING, self::STATUS_EXPIRED, ], true); } public function canBeRevoked(): bool { if ($this->isPendingRenewal()) { return true; } return in_array((string) $this->status, [ self::STATUS_ACTIVE, self::STATUS_EXPIRING, ], true); } }