collections->updateCollection( $collection->loadMissing('user'), $this->normalizeAttributes($collection, $attributes), $actor, ); } public function campaignSummary(Collection $collection): array { return [ 'campaign_key' => $collection->campaign_key, 'campaign_label' => $collection->campaign_label, 'event_key' => $collection->event_key, 'event_label' => $collection->event_label, 'season_key' => $collection->season_key, 'spotlight_style' => $collection->spotlight_style, 'schedule' => [ 'published_at' => $collection->published_at?->toIso8601String(), 'unpublished_at' => $collection->unpublished_at?->toIso8601String(), 'expired_at' => $collection->expired_at?->toIso8601String(), 'is_scheduled' => $collection->published_at?->isFuture() ?? false, 'is_expiring_soon' => $collection->unpublished_at?->between(now(), now()->addDays(14)) ?? false, ], 'eligibility' => $this->eligibility($collection), 'surface_assignments' => $this->surfaceAssignments($collection), 'recommended_surfaces' => $this->suggestedSurfaceAssignments($collection), 'editorial_notes' => $collection->editorial_notes, 'staff_commercial_notes' => $collection->staff_commercial_notes, ]; } public function eligibility(Collection $collection): array { $reasons = []; if ($collection->visibility !== Collection::VISIBILITY_PUBLIC) { $reasons[] = 'Collection must be public before it can drive public campaign surfaces.'; } if ($collection->moderation_status !== Collection::MODERATION_ACTIVE) { $reasons[] = 'Only moderation-approved collections are eligible for campaign promotion.'; } if (in_array($collection->lifecycle_state, [ Collection::LIFECYCLE_DRAFT, Collection::LIFECYCLE_SCHEDULED, Collection::LIFECYCLE_EXPIRED, Collection::LIFECYCLE_HIDDEN, Collection::LIFECYCLE_RESTRICTED, Collection::LIFECYCLE_UNDER_REVIEW, ], true)) { $reasons[] = 'Collection lifecycle must be published, featured, or archived before campaign placement.'; } if ($collection->published_at?->isFuture()) { $reasons[] = 'Collection publish window has not opened yet.'; } if ($collection->unpublished_at?->lte(now())) { $reasons[] = 'Collection campaign window has already ended.'; } return [ 'is_campaign_ready' => count($reasons) === 0, 'is_publicly_featureable' => $collection->isFeatureablePublicly(), 'has_campaign_context' => filled($collection->campaign_key) || filled($collection->event_key) || filled($collection->season_key), 'reasons' => $reasons, ]; } public function surfaceAssignments(Collection $collection): array { return $collection->placements() ->orderBy('surface_key') ->orderByDesc('priority') ->orderBy('starts_at') ->get() ->map(function ($placement): array { return [ 'id' => (int) $placement->id, 'surface_key' => (string) $placement->surface_key, 'placement_type' => (string) $placement->placement_type, 'priority' => (int) $placement->priority, 'campaign_key' => $placement->campaign_key, 'starts_at' => $placement->starts_at?->toIso8601String(), 'ends_at' => $placement->ends_at?->toIso8601String(), 'is_active' => (bool) $placement->is_active, 'notes' => $placement->notes, ]; }) ->values() ->all(); } public function suggestedSurfaceAssignments(Collection $collection): array { $suggestions = []; if (filled($collection->campaign_key) || filled($collection->event_key) || filled($collection->season_key)) { $suggestions[] = $this->surfaceSuggestion('homepage.featured_collections', 'campaign', 'Campaign-aware collection suitable for the homepage featured rail.', 90); $suggestions[] = $this->surfaceSuggestion('discover.featured_collections', 'campaign', 'Campaign metadata makes this collection a strong discover spotlight candidate.', 85); } if ($collection->type === Collection::TYPE_EDITORIAL) { $suggestions[] = $this->surfaceSuggestion('homepage.editorial_collections', 'editorial', 'Editorial ownership makes this collection a homepage editorial fit.', 88); } if ($collection->type === Collection::TYPE_COMMUNITY) { $suggestions[] = $this->surfaceSuggestion('homepage.community_collections', 'community', 'Community curation makes this collection suitable for the community row.', 82); } if ((float) ($collection->ranking_score ?? 0) >= 60 || (bool) $collection->is_featured) { $suggestions[] = $this->surfaceSuggestion('homepage.trending_collections', 'algorithmic', 'Strong ranking signals make this collection a trending candidate.', 76); } return collect($suggestions) ->unique('surface_key') ->values() ->all(); } public function expiringCampaignsForOwner(User $user, int $days = 14, int $limit = 6): EloquentCollection { return Collection::query() ->with(['user:id,username,name', 'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at']) ->ownedBy((int) $user->id) ->whereNotNull('unpublished_at') ->whereBetween('unpublished_at', [now(), now()->addDays(max(1, $days))]) ->orderBy('unpublished_at') ->limit(max(1, $limit)) ->get(); } public function publicLanding(string $campaignKey, int $limit = 18): array { $normalizedKey = trim($campaignKey); $surfaceItems = $this->surfaces->resolveSurfaceItems(sprintf('campaign.%s.featured_collections', $normalizedKey), $limit); $collections = $surfaceItems->isNotEmpty() ? $surfaceItems : $this->discovery->publicCampaignCollections($normalizedKey, $limit); $editorialCollections = $this->discovery->publicCampaignCollectionsByType($normalizedKey, Collection::TYPE_EDITORIAL, 6); $communityCollections = $this->discovery->publicCampaignCollectionsByType($normalizedKey, Collection::TYPE_COMMUNITY, 6); $trendingCollections = $this->discovery->publicTrendingCampaignCollections($normalizedKey, 6); $recentCollections = $this->discovery->publicRecentCampaignCollections($normalizedKey, 6); $leadCollection = $collections->first(); $placementSurfaces = CollectionSurfacePlacement::query() ->where('campaign_key', $normalizedKey) ->where('is_active', true) ->where(function ($query): void { $query->whereNull('starts_at')->orWhere('starts_at', '<=', now()); }) ->where(function ($query): void { $query->whereNull('ends_at')->orWhere('ends_at', '>', now()); }) ->orderBy('surface_key') ->pluck('surface_key') ->unique() ->values() ->all(); return [ 'campaign' => [ 'key' => $normalizedKey, 'label' => $leadCollection?->campaign_label ?: Str::headline(str_replace(['_', '-'], ' ', $normalizedKey)), 'description' => $leadCollection?->banner_text ?: sprintf('Public collections grouped under the %s campaign, including editorial, community, and discovery-ready showcases.', Str::headline(str_replace(['_', '-'], ' ', $normalizedKey))), 'badge_label' => $leadCollection?->badge_label, 'event_key' => $leadCollection?->event_key, 'event_label' => $leadCollection?->event_label, 'season_key' => $leadCollection?->season_key, 'active_surface_keys' => $placementSurfaces, 'collections_count' => $collections->count(), ], 'collections' => $collections, 'editorial_collections' => $editorialCollections, 'community_collections' => $communityCollections, 'trending_collections' => $trendingCollections, 'recent_collections' => $recentCollections, ]; } public function batchEditorialPlan(array $collectionIds, array $attributes): array { $collections = Collection::query() ->with(['user:id,username,name']) ->whereIn('id', collect($collectionIds)->map(fn ($id) => (int) $id)->filter()->values()->all()) ->orderBy('title') ->get(); $campaignAttributes = $this->batchCampaignAttributes($attributes); $placementAttributes = $this->batchPlacementAttributes($attributes); $surfaceKey = $placementAttributes['surface_key'] ?? null; $items = $collections->map(function (Collection $collection) use ($campaignAttributes, $placementAttributes, $surfaceKey): array { $campaignPreview = $this->normalizeAttributes($collection, $campaignAttributes); $placementEligible = $surfaceKey ? $collection->isFeatureablePublicly() : null; $placementReasons = []; if ($surfaceKey && ! $collection->isFeatureablePublicly()) { $placementReasons[] = 'Collection is not publicly featureable for staff surface placement.'; } return [ 'collection' => [ 'id' => (int) $collection->id, 'title' => (string) $collection->title, 'slug' => (string) $collection->slug, 'visibility' => (string) $collection->visibility, 'lifecycle_state' => (string) $collection->lifecycle_state, 'moderation_status' => (string) $collection->moderation_status, 'owner' => $collection->user ? [ 'id' => (int) $collection->user->id, 'username' => $collection->user->username, 'name' => $collection->user->name, ] : null, ], 'campaign_updates' => $campaignPreview, 'placement' => $surfaceKey ? [ 'surface_key' => $surfaceKey, 'placement_type' => $placementAttributes['placement_type'] ?? 'campaign', 'priority' => (int) ($placementAttributes['priority'] ?? 0), 'starts_at' => $placementAttributes['starts_at'] ?? null, 'ends_at' => $placementAttributes['ends_at'] ?? null, 'is_active' => array_key_exists('is_active', $placementAttributes) ? (bool) $placementAttributes['is_active'] : true, 'campaign_key' => $placementAttributes['campaign_key'] ?? ($campaignPreview['campaign_key'] ?? $collection->campaign_key), 'notes' => $placementAttributes['notes'] ?? null, 'eligible' => $placementEligible, 'reasons' => $placementReasons, ] : null, 'existing_assignments' => $this->surfaceAssignments($collection), 'eligibility' => $this->eligibility($collection), ]; })->values(); return [ 'collections_count' => $collections->count(), 'campaign_updates_count' => $items->filter(fn (array $item): bool => count($item['campaign_updates']) > 0)->count(), 'placement_candidates_count' => $items->filter(fn (array $item): bool => is_array($item['placement']))->count(), 'placement_eligible_count' => $items->filter(fn (array $item): bool => ($item['placement']['eligible'] ?? false) === true)->count(), 'items' => $items->all(), ]; } public function applyBatchEditorialPlan(array $collectionIds, array $attributes, ?User $actor = null): array { $plan = $this->batchEditorialPlan($collectionIds, $attributes); $campaignAttributes = $this->batchCampaignAttributes($attributes); $placementAttributes = $this->batchPlacementAttributes($attributes); $results = []; foreach ($plan['items'] as $item) { $collection = Collection::query()->find((int) Arr::get($item, 'collection.id')); if (! $collection) { continue; } $updatedCollection = count($campaignAttributes) > 0 ? $this->updateCampaign($collection, $campaignAttributes, $actor) : $collection->fresh(); $placementResult = null; if (is_array($item['placement'])) { if (($item['placement']['eligible'] ?? false) === true) { $existingPlacement = CollectionSurfacePlacement::query() ->where('surface_key', $item['placement']['surface_key']) ->where('collection_id', $updatedCollection->id) ->first(); $placementPayload = array_merge($placementAttributes, [ 'id' => $existingPlacement?->id, 'surface_key' => $item['placement']['surface_key'], 'collection_id' => $updatedCollection->id, 'campaign_key' => $item['placement']['campaign_key'], 'created_by_user_id' => $existingPlacement?->created_by_user_id ?: $actor?->id, ]); $placement = $this->surfaces->upsertPlacement($placementPayload); $placementResult = [ 'status' => $existingPlacement ? 'updated' : 'created', 'placement_id' => (int) $placement->id, 'surface_key' => (string) $placement->surface_key, ]; } else { $placementResult = [ 'status' => 'skipped', 'reasons' => $item['placement']['reasons'] ?? [], ]; } } $results[] = [ 'collection_id' => (int) $updatedCollection->id, 'campaign_updated' => count($campaignAttributes) > 0, 'placement' => $placementResult, ]; } return [ 'plan' => $plan, 'results' => $results, ]; } private function normalizeAttributes(Collection $collection, array $attributes): array { if (array_key_exists('campaign_key', $attributes) && blank($attributes['campaign_key']) && ! array_key_exists('campaign_label', $attributes)) { $attributes['campaign_label'] = null; } if (array_key_exists('event_key', $attributes) && blank($attributes['event_key']) && ! array_key_exists('event_label', $attributes)) { $attributes['event_label'] = null; } if ( filled($attributes['campaign_key'] ?? $collection->campaign_key) && blank($attributes['campaign_label'] ?? $collection->campaign_label) && filled($attributes['event_label'] ?? $collection->event_label) ) { $attributes['campaign_label'] = $attributes['event_label'] ?? $collection->event_label; } return $attributes; } private function batchCampaignAttributes(array $attributes): array { return collect([ 'campaign_key', 'campaign_label', 'event_key', 'event_label', 'season_key', 'banner_text', 'badge_label', 'spotlight_style', 'editorial_notes', ])->reduce(function (array $carry, string $key) use ($attributes): array { if (array_key_exists($key, $attributes)) { $carry[$key] = $attributes[$key]; } return $carry; }, []); } private function batchPlacementAttributes(array $attributes): array { return collect([ 'surface_key', 'placement_type', 'priority', 'starts_at', 'ends_at', 'is_active', 'campaign_key', 'notes', ])->reduce(function (array $carry, string $key) use ($attributes): array { if (array_key_exists($key, $attributes)) { $carry[$key] = $attributes[$key]; } return $carry; }, []); } private function surfaceSuggestion(string $surfaceKey, string $placementType, string $reason, int $priority): array { return [ 'surface_key' => $surfaceKey, 'placement_type' => $placementType, 'reason' => $reason, 'priority' => $priority, ]; } }