148 lines
5.8 KiB
PHP
148 lines
5.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Jobs\RefreshCollectionHealthJob;
|
|
use App\Jobs\RefreshCollectionQualityJob;
|
|
use App\Jobs\RefreshCollectionRecommendationJob;
|
|
use App\Jobs\ScanCollectionDuplicateCandidatesJob;
|
|
use App\Models\Collection;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Collection as SupportCollection;
|
|
|
|
class CollectionBackgroundJobService
|
|
{
|
|
public function dispatchQualityRefresh(Collection $collection, ?User $actor = null): array
|
|
{
|
|
RefreshCollectionQualityJob::dispatch((int) $collection->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<int, Collection> */
|
|
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<int, Collection> */
|
|
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<int, Collection> */
|
|
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<int, Collection> $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,
|
|
];
|
|
}
|
|
} |