247 lines
9.1 KiB
PHP
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(),
|
|
];
|
|
}
|
|
} |