id, $actor?->id)->afterCommit(); return [ 'status' => 'queued', 'job' => 'quality_refresh', 'scope' => 'single', 'count' => 1, 'collection_ids' => [(int) $collection->id], 'message' => 'Quality refresh queued.', ]; } public function dispatchHealthRefresh(?Collection $collection = null, ?User $actor = null): array { $targets = $collection ? collect([$collection]) : $this->healthTargets(); $targets->each(fn (Collection $item) => RefreshCollectionHealthJob::dispatch((int) $item->id, $actor?->id, 'programming-eligibility')->afterCommit()); return $this->queuedPayload('health_refresh', $targets, 'Health and eligibility refresh queued.'); } public function dispatchRecommendationRefresh(?Collection $collection = null, ?User $actor = null, string $context = 'default'): array { $targets = $collection ? collect([$collection]) : $this->recommendationTargets(); $targets->each(fn (Collection $item) => RefreshCollectionRecommendationJob::dispatch((int) $item->id, $actor?->id, $context)->afterCommit()); return $this->queuedPayload('recommendation_refresh', $targets, 'Recommendation refresh queued.'); } public function dispatchDuplicateScan(?Collection $collection = null, ?User $actor = null): array { $targets = $collection ? collect([$collection]) : $this->duplicateTargets(); $targets->each(fn (Collection $item) => ScanCollectionDuplicateCandidatesJob::dispatch((int) $item->id, $actor?->id)->afterCommit()); return $this->queuedPayload('duplicate_scan', $targets, 'Duplicate scan queued.'); } public function dispatchScheduledMaintenance(bool $health = true, bool $recommendations = true, bool $duplicates = true): array { $summary = []; if ($health) { $summary['health'] = $this->dispatchHealthRefresh(); } if ($recommendations) { $summary['recommendations'] = $this->dispatchRecommendationRefresh(); } if ($duplicates) { $summary['duplicates'] = $this->dispatchDuplicateScan(); } return $summary; } /** @return SupportCollection */ private function healthTargets(): SupportCollection { $cutoff = now()->subHours(max(1, (int) config('collections.v5.queue.health_stale_after_hours', 24))); return Collection::query() ->where(function ($query) use ($cutoff): void { $query->whereNull('last_health_check_at') ->orWhere('last_health_check_at', '<=', $cutoff) ->orWhereColumn('updated_at', '>', 'last_health_check_at'); }) ->orderBy('last_health_check_at') ->orderByDesc('updated_at') ->limit(max(1, (int) config('collections.v5.queue.health_batch_size', 40))) ->get(['id']); } /** @return SupportCollection */ private function recommendationTargets(): SupportCollection { $cutoff = now()->subHours(max(1, (int) config('collections.v5.queue.recommendation_stale_after_hours', 12))); return Collection::query() ->where('placement_eligibility', true) ->where(function ($query) use ($cutoff): void { $query->whereNull('last_recommendation_refresh_at') ->orWhere('last_recommendation_refresh_at', '<=', $cutoff) ->orWhereColumn('updated_at', '>', 'last_recommendation_refresh_at'); }) ->orderBy('last_recommendation_refresh_at') ->orderByDesc('ranking_score') ->limit(max(1, (int) config('collections.v5.queue.recommendation_batch_size', 40))) ->get(['id']); } /** @return SupportCollection */ private function duplicateTargets(): SupportCollection { $cutoff = now()->subHours(max(1, (int) config('collections.v5.queue.duplicate_stale_after_hours', 24))); return Collection::query() ->whereNull('canonical_collection_id') ->where(function ($query) use ($cutoff): void { $query->where('updated_at', '>=', $cutoff) ->orWhereDoesntHave('mergeActionsAsSource', function ($mergeQuery) use ($cutoff): void { $mergeQuery->where('action_type', 'suggested') ->where('updated_at', '>=', $cutoff); }); }) ->orderByDesc('updated_at') ->limit(max(1, (int) config('collections.v5.queue.duplicate_batch_size', 30))) ->get(['id']); } /** * @param SupportCollection $targets */ private function queuedPayload(string $job, SupportCollection $targets, string $message): array { $ids = $targets->pluck('id')->map(static fn ($id): int => (int) $id)->values()->all(); return [ 'status' => 'queued', 'job' => $job, 'scope' => count($ids) === 1 ? 'single' : 'batch', 'count' => count($ids), 'collection_ids' => $ids, 'items' => [], 'message' => $ids === [] ? 'No collections needed this refresh.' : $message, ]; } }