Files
SkinbaseNova/app/Services/CollectionProgrammingService.php
2026-03-28 19:15:39 +01:00

247 lines
9.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\Collection;
use App\Models\CollectionProgramAssignment;
use App\Models\User;
use App\Services\CollectionCanonicalService;
use App\Services\CollectionExperimentService;
use App\Services\CollectionHealthService;
use App\Services\CollectionHistoryService;
use App\Services\CollectionObservabilityService;
use App\Services\CollectionPartnerProgramService;
use App\Services\CollectionWorkflowService;
use Illuminate\Support\Collection as SupportCollection;
class CollectionProgrammingService
{
public function __construct(
private readonly CollectionHealthService $health,
private readonly CollectionRankingService $ranking,
private readonly CollectionMergeService $merge,
private readonly CollectionCanonicalService $canonical,
private readonly CollectionWorkflowService $workflow,
private readonly CollectionExperimentService $experiments,
private readonly CollectionPartnerProgramService $partnerPrograms,
private readonly CollectionObservabilityService $observability,
) {
}
public function diagnostics(Collection $collection): array
{
return $this->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(),
];
}
}