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

116 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\Collection;
class EditorialAutomationService
{
public function __construct(
private readonly CollectionAiCurationService $curation,
private readonly CollectionRecommendationService $recommendations,
private readonly CollectionQualityService $quality,
private readonly CollectionCampaignService $campaigns,
) {
}
public function qualityReview(Collection $collection): array
{
$scores = $this->quality->scores($collection);
return [
'quality_score' => $scores['quality_score'],
'ranking_score' => $scores['ranking_score'],
'missing_metadata' => $this->missingMetadata($collection),
'attention_flags' => $this->attentionFlags($collection),
'campaign_summary' => $this->campaigns->campaignSummary($collection),
'suggested_surface_assignments' => $this->campaigns->suggestedSurfaceAssignments($collection),
'suggested_cover' => $this->curation->suggestCover($collection),
'suggested_summary' => $this->curation->suggestSummary($collection),
'suggested_related_collections' => $this->recommendations->relatedPublicCollections($collection, 4)->map(fn (Collection $item) => [
'id' => (int) $item->id,
'title' => $item->title,
'slug' => $item->slug,
])->values()->all(),
'source' => 'editorial-automation-v1',
];
}
private function missingMetadata(Collection $collection): array
{
$missing = [];
if (blank($collection->summary)) {
$missing[] = 'Add a sharper summary for social previews and homepage modules.';
}
if (! $collection->resolvedCoverArtwork(false)) {
$missing[] = 'Choose a cover artwork to strengthen click-through and placement eligibility.';
}
if ((int) $collection->artworks_count < 4) {
$missing[] = 'Expand the collection with a few more artworks to improve curation depth.';
}
if (blank($collection->campaign_key) && blank($collection->event_key) && blank($collection->season_key)) {
$missing[] = 'Set campaign or seasonal metadata if this collection should power event-aware discovery.';
}
if (blank($collection->brand_safe_status) && (bool) $collection->commercial_eligibility) {
$missing[] = 'Mark brand-safe status before using this collection for commercial or partner-facing placements.';
}
return $missing;
}
private function attentionFlags(Collection $collection): array
{
$flags = [];
if ($collection->last_activity_at?->lt(now()->subDays(90)) && $collection->isPubliclyAccessible()) {
$flags[] = [
'key' => 'stale_collection',
'severity' => 'medium',
'message' => 'This public collection has not been updated recently and may need editorial review.',
];
}
if ($this->hasDuplicateTitleWithinOwner($collection)) {
$flags[] = [
'key' => 'possible_duplicate',
'severity' => 'medium',
'message' => 'Another collection from the same owner has the same title, so this set may duplicate an existing curation theme.',
];
}
if ($collection->moderation_status !== Collection::MODERATION_ACTIVE) {
$flags[] = [
'key' => 'moderation_block',
'severity' => 'high',
'message' => 'Moderation status currently blocks this collection from editorial promotion.',
];
}
if ($collection->unpublished_at?->between(now(), now()->addDays(14))) {
$flags[] = [
'key' => 'campaign_expiring',
'severity' => 'low',
'message' => 'This collection is approaching the end of its scheduled campaign window.',
];
}
return $flags;
}
private function hasDuplicateTitleWithinOwner(Collection $collection): bool
{
return Collection::query()
->where('id', '!=', $collection->id)
->where('user_id', $collection->user_id)
->whereRaw('LOWER(title) = ?', [mb_strtolower(trim((string) $collection->title))])
->exists();
}
}