observability->diagnostics($collection->fresh()); } public function syncHooks(Collection $collection, array $attributes, ?User $actor = null): array { $workflowAttributes = array_intersect_key($attributes, array_flip([ 'placement_eligibility', ])); $experimentAttributes = array_intersect_key($attributes, array_flip([ 'experiment_key', 'experiment_treatment', 'placement_variant', 'ranking_mode_variant', 'collection_pool_version', 'test_label', ])); $partnerAttributes = array_intersect_key($attributes, array_flip([ 'partner_key', 'trust_tier', 'promotion_tier', 'sponsorship_state', 'ownership_domain', 'commercial_review_state', 'legal_review_state', ])); $updated = $collection->fresh(); if ($workflowAttributes !== []) { $updated = $this->workflow->update($updated->loadMissing('user'), $workflowAttributes, $actor); } if ($experimentAttributes !== []) { $updated = $this->experiments->sync($updated->loadMissing('user'), $experimentAttributes, $actor); } if ($partnerAttributes !== []) { $updated = $this->partnerPrograms->sync($updated->loadMissing('user'), $partnerAttributes, $actor); } $updated = $updated->fresh(); return [ 'collection' => $updated, 'diagnostics' => $this->observability->diagnostics($updated), ]; } public function mergeQueue(bool $ownerView = true, int $pendingLimit = 8, int $recentLimit = 8): array { return $this->merge->queueOverview($ownerView, $pendingLimit, $recentLimit); } public function canonicalizePair(Collection $source, Collection $target, ?User $actor = null): array { $updatedSource = $this->canonical->designate($source->loadMissing('user'), $target->loadMissing('user'), $actor); return [ 'source' => $updatedSource, 'target' => $target->fresh(), 'mergeQueue' => $this->mergeQueue(true), ]; } public function mergePair(Collection $source, Collection $target, ?User $actor = null): array { $result = $this->merge->mergeInto($source->loadMissing('user'), $target->loadMissing('user'), $actor); return [ 'source' => $result['source'], 'target' => $result['target'], 'attached_artwork_ids' => $result['attached_artwork_ids'], 'mergeQueue' => $this->mergeQueue(true), ]; } public function rejectPair(Collection $source, Collection $target, ?User $actor = null): array { $updatedSource = $this->merge->rejectCandidate($source->loadMissing('user'), $target->loadMissing('user'), $actor); return [ 'source' => $updatedSource, 'target' => $target->fresh(), 'mergeQueue' => $this->mergeQueue(true), ]; } public function assignments(): SupportCollection { return CollectionProgramAssignment::query() ->with(['collection.user:id,username,name', 'creator:id,username,name']) ->orderBy('program_key') ->orderByDesc('priority') ->orderBy('id') ->get(); } public function upsertAssignment(array $attributes, ?User $actor = null): CollectionProgramAssignment { $assignmentId = isset($attributes['id']) ? (int) $attributes['id'] : null; $payload = [ 'collection_id' => (int) $attributes['collection_id'], 'program_key' => (string) $attributes['program_key'], 'campaign_key' => $attributes['campaign_key'] ?? null, 'placement_scope' => $attributes['placement_scope'] ?? null, 'starts_at' => $attributes['starts_at'] ?? null, 'ends_at' => $attributes['ends_at'] ?? null, 'priority' => (int) ($attributes['priority'] ?? 0), 'notes' => $attributes['notes'] ?? null, 'created_by_user_id' => $actor?->id, ]; if ($assignmentId > 0) { $assignment = CollectionProgramAssignment::query()->findOrFail($assignmentId); $assignment->fill($payload)->save(); } else { $assignment = CollectionProgramAssignment::query()->create($payload); } $collection = Collection::query()->findOrFail((int) $payload['collection_id']); $collection->forceFill(['program_key' => $payload['program_key']])->save(); app(CollectionHistoryService::class)->record( $collection->fresh(), $actor, $assignmentId > 0 ? 'program_assignment_updated' : 'program_assignment_created', 'Collection program assignment updated.', null, $payload ); return $assignment->fresh(['collection.user', 'creator']); } public function previewProgram(string $programKey, int $limit = 12): SupportCollection { return Collection::query() ->public() ->where('program_key', $programKey) ->where('placement_eligibility', true) ->with(['user:id,username,name', 'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at']) ->orderByDesc('ranking_score') ->orderByDesc('health_score') ->limit(max(1, min($limit, 24))) ->get(); } public function refreshEligibility(?Collection $collection = null, ?User $actor = null): array { $items = $collection ? collect([$collection]) : Collection::query()->whereNotNull('program_key')->get(); $results = $items->map(function (Collection $item) use ($actor): array { $fresh = $this->health->refresh($item, $actor, 'programming-eligibility'); return [ 'collection_id' => (int) $fresh->id, 'placement_eligibility' => (bool) $fresh->placement_eligibility, 'health_state' => $fresh->health_state, 'readiness_state' => $fresh->readiness_state, ]; })->values(); return [ 'count' => $results->count(), 'items' => $results->all(), ]; } public function refreshRecommendations(?Collection $collection = null): array { $items = $collection ? collect([$collection]) : Collection::query()->where('placement_eligibility', true)->limit(100)->get(); $results = $items->map(function (Collection $item): array { $fresh = $this->ranking->refresh($item); return [ 'collection_id' => (int) $fresh->id, 'recommendation_tier' => $fresh->recommendation_tier, 'ranking_bucket' => $fresh->ranking_bucket, 'search_boost_tier' => $fresh->search_boost_tier, ]; })->values(); return [ 'count' => $results->count(), 'items' => $results->all(), ]; } public function duplicateScan(?Collection $collection = null): array { $items = $collection ? collect([$collection]) : Collection::query()->whereNull('canonical_collection_id')->limit(100)->get(); $results = $items->map(function (Collection $item): array { return [ 'collection_id' => (int) $item->id, 'candidates' => $this->merge->duplicateCandidates($item)->map(fn (Collection $candidate) => [ 'id' => (int) $candidate->id, 'title' => (string) $candidate->title, 'slug' => (string) $candidate->slug, ])->values()->all(), ]; })->filter(fn (array $row): bool => $row['candidates'] !== [])->values(); return [ 'count' => $results->count(), 'items' => $results->all(), ]; } }