whereNotNull('hash') ->whereNotNull('thumb_ext') ->whereRaw('TRIM(hash) != ?',[ '' ]) ->whereRaw('TRIM(thumb_ext) != ?',[ '' ]); $this->applyLegacyUnsetFilter($query); if (! $includeExistingOpenFindings && Schema::hasTable('artwork_maturity_audit_findings')) { $query->whereDoesntHave('maturityAuditFinding', function (Builder $finding): void { $finding->where('status', ArtworkMaturityAuditFinding::STATUS_OPEN); }); } return $query; } public function openFindingsQuery(): Builder { return ArtworkMaturityAuditFinding::query() ->with(['artwork.user.profile', 'artwork.group', 'artwork.categories.contentType']) ->where('status', ArtworkMaturityAuditFinding::STATUS_OPEN) ->whereHas('artwork', function (Builder $query): void { $this->applyLegacyUnsetFilter($query); }); } public function openFindingsCount(): int { if (! Schema::hasTable('artwork_maturity_audit_findings')) { return 0; } return (int) $this->openFindingsQuery()->count(); } public function isArtworkEligible(Artwork $artwork): bool { return ! (bool) $artwork->is_mature && in_array((string) ($artwork->maturity_level ?? ArtworkMaturityService::LEVEL_SAFE), ['', ArtworkMaturityService::LEVEL_SAFE], true) && in_array((string) ($artwork->maturity_status ?? ArtworkMaturityService::STATUS_CLEAR), ['', ArtworkMaturityService::STATUS_CLEAR], true) && in_array((string) ($artwork->maturity_source ?? ArtworkMaturityService::SOURCE_LEGACY), ['', ArtworkMaturityService::SOURCE_LEGACY], true) && $artwork->maturity_declared_at === null && $artwork->maturity_reviewed_at === null; } /** * @param array $assessment */ public function shouldOpenFinding(array $assessment): bool { $status = Str::lower(trim((string) ($assessment['status'] ?? ArtworkMaturityService::AI_STATUS_FAILED))); if ($status !== ArtworkMaturityService::AI_STATUS_SUCCEEDED) { return false; } $actionHint = Str::lower(trim((string) ($assessment['action_hint'] ?? ''))); if (in_array($actionHint, [ArtworkMaturityService::AI_ACTION_REVIEW, ArtworkMaturityService::AI_ACTION_FLAG_HIGH], true)) { return true; } $label = Str::lower(trim((string) ($assessment['maturity_label'] ?? ''))); $confidence = is_numeric($assessment['confidence'] ?? null) ? (float) $assessment['confidence'] : 0.0; return $label === ArtworkMaturityService::LEVEL_MATURE && $confidence >= (float) config('maturity.ai.threshold', 0.68); } /** * @param array $assessment */ public function recordFinding(Artwork $artwork, array $assessment, string $thumbnailVariant): ArtworkMaturityAuditFinding { $finding = ArtworkMaturityAuditFinding::query()->updateOrCreate( ['artwork_id' => (int) $artwork->id], [ 'status' => ArtworkMaturityAuditFinding::STATUS_OPEN, 'thumbnail_variant' => $thumbnailVariant, 'ai_label' => $this->nullableLowerString($assessment['maturity_label'] ?? null), 'ai_confidence' => $this->nullableFloat($assessment['confidence'] ?? null), 'ai_score' => $this->nullableFloat($assessment['score'] ?? ($assessment['confidence'] ?? null)), 'ai_labels' => $this->normalizeLabels($assessment['labels'] ?? []), 'ai_model' => $this->nullableString($assessment['model'] ?? null), 'ai_threshold_used' => $this->nullableFloat($assessment['threshold_used'] ?? null), 'ai_analysis_time_ms' => is_numeric($assessment['analysis_time_ms'] ?? null) ? (int) $assessment['analysis_time_ms'] : null, 'ai_action_hint' => $this->nullableLowerString($assessment['action_hint'] ?? null), 'ai_status' => $this->nullableLowerString($assessment['status'] ?? ArtworkMaturityService::AI_STATUS_FAILED) ?? ArtworkMaturityService::AI_STATUS_FAILED, 'ai_advisory' => $this->nullableString($assessment['advisory'] ?? null), 'detected_at' => now(), 'last_scanned_at' => now(), 'resolution_action' => null, 'resolution_note' => null, 'resolved_by' => null, 'resolved_at' => null, ], ); return $finding->fresh(['artwork']); } public function markFindingCleared(Artwork $artwork, ?string $note = null): void { ArtworkMaturityAuditFinding::query() ->where('artwork_id', (int) $artwork->id) ->where('status', ArtworkMaturityAuditFinding::STATUS_OPEN) ->update([ 'status' => ArtworkMaturityAuditFinding::STATUS_CLEARED, 'resolution_action' => 'auto_cleared', 'resolution_note' => $note, 'resolved_at' => now(), 'last_scanned_at' => now(), ]); } public function resolveFindingForReview(Artwork $artwork, Authenticatable $moderator, string $action, ?string $note = null): void { $moderatorId = (int) $moderator->getAuthIdentifier(); ArtworkMaturityAuditFinding::query() ->where('artwork_id', (int) $artwork->id) ->where('status', ArtworkMaturityAuditFinding::STATUS_OPEN) ->update([ 'status' => ArtworkMaturityAuditFinding::STATUS_REVIEWED, 'resolution_action' => Str::lower(trim($action)), 'resolution_note' => $note, 'resolved_by' => $moderatorId, 'resolved_at' => now(), 'last_scanned_at' => now(), ]); } private function applyLegacyUnsetFilter(Builder $query): Builder { return $query ->where(function (Builder $builder): void { $builder->whereNull('maturity_declared_at') ->whereNull('maturity_reviewed_at') ->where(function (Builder $state): void { $state->whereNull('maturity_source') ->orWhere('maturity_source', ArtworkMaturityService::SOURCE_LEGACY); }) ->where(function (Builder $state): void { $state->whereNull('maturity_status') ->orWhere('maturity_status', ArtworkMaturityService::STATUS_CLEAR); }) ->where(function (Builder $state): void { $state->whereNull('maturity_level') ->orWhere('maturity_level', ArtworkMaturityService::LEVEL_SAFE); }) ->where(function (Builder $state): void { $state->whereNull('is_mature') ->orWhere('is_mature', false); }); }); } /** * @param mixed $value */ private function nullableFloat(mixed $value): ?float { return is_numeric($value) ? (float) $value : null; } /** * @param mixed $value */ private function nullableString(mixed $value): ?string { $resolved = trim((string) $value); return $resolved !== '' ? $resolved : null; } /** * @param mixed $value */ private function nullableLowerString(mixed $value): ?string { $resolved = $this->nullableString($value); return $resolved !== null ? Str::lower($resolved) : null; } /** * @param mixed $value * @return list|null */ private function normalizeLabels(mixed $value): ?array { if (! is_array($value)) { return null; } $labels = array_values(array_filter(array_map( static fn (mixed $label): string => Str::lower(trim((string) $label)), $value, ))); return $labels !== [] ? array_values(array_unique($labels)) : null; } }