map(static fn ($id): int => (int) $id) ->filter(static fn (int $id): bool => $id > 0) ->unique() ->values(); if ($collectionIds->isEmpty()) { throw ValidationException::withMessages([ 'collection_ids' => 'Select at least one collection.', ]); } $collections = Collection::query() ->ownedBy((int) $user->id) ->whereIn('id', $collectionIds->all()) ->get() ->keyBy(fn (Collection $collection): int => (int) $collection->id); if ($collections->count() !== $collectionIds->count()) { throw ValidationException::withMessages([ 'collection_ids' => 'One or more selected collections are unavailable.', ]); } $items = []; $updatedCollections = $collectionIds->map(function (int $collectionId) use ($collections, $user, $payload, $action, &$items): Collection { $collection = $collections->get($collectionId); if (! $collection instanceof Collection) { throw ValidationException::withMessages([ 'collection_ids' => 'One or more selected collections are unavailable.', ]); } $updated = match ($action) { 'archive' => $this->archive($collection->loadMissing('user'), $user), 'assign_campaign' => $this->assignCampaign($collection->loadMissing('user'), $payload, $user), 'update_lifecycle' => $this->updateLifecycle($collection->loadMissing('user'), $payload, $user), 'request_ai_review' => $this->requestAiReview($collection->loadMissing('user'), $user, $items), 'mark_editorial_review' => $this->markEditorialReview($collection->loadMissing('user'), $user), default => throw ValidationException::withMessages([ 'action' => 'Unsupported bulk action.', ]), }; if ($action !== 'request_ai_review') { $items[] = [ 'collection_id' => (int) $updated->id, 'action' => $action, ]; } return $updated; }); return [ 'action' => $action, 'count' => $updatedCollections->count(), 'items' => $items, 'collections' => $updatedCollections, 'message' => $this->messageFor($action, $updatedCollections->count()), ]; } private function archive(Collection $collection, User $user): Collection { return $this->collections->updateCollection($collection, [ 'lifecycle_state' => Collection::LIFECYCLE_ARCHIVED, 'archived_at' => now(), ], $user); } private function assignCampaign(Collection $collection, array $payload, User $user): Collection { return $this->campaigns->updateCampaign($collection, [ 'campaign_key' => (string) $payload['campaign_key'], 'campaign_label' => $payload['campaign_label'] ?? null, ], $user); } private function updateLifecycle(Collection $collection, array $payload, User $user): Collection { $lifecycleState = (string) $payload['lifecycle_state']; $attributes = [ 'lifecycle_state' => $lifecycleState, ]; if ($lifecycleState === Collection::LIFECYCLE_ARCHIVED) { $attributes['archived_at'] = now(); } else { $attributes['archived_at'] = null; } if ($lifecycleState === Collection::LIFECYCLE_PUBLISHED) { $attributes['visibility'] = Collection::VISIBILITY_PUBLIC; $attributes['published_at'] = $collection->published_at ?? now(); $attributes['expired_at'] = null; } if ($lifecycleState === Collection::LIFECYCLE_DRAFT) { $attributes['visibility'] = Collection::VISIBILITY_PRIVATE; $attributes['expired_at'] = null; $attributes['unpublished_at'] = null; } return $this->collections->updateCollection($collection, $attributes, $user); } private function requestAiReview(Collection $collection, User $user, array &$items): Collection { $result = $this->backgroundJobs->dispatchQualityRefresh($collection, $user); $items[] = [ 'collection_id' => (int) $collection->id, 'job' => $result['job'] ?? 'quality_refresh', 'status' => $result['status'] ?? 'queued', ]; return $collection->fresh()->loadMissing('user'); } private function markEditorialReview(Collection $collection, User $user): Collection { if ($collection->workflow_state === Collection::WORKFLOW_IN_REVIEW) { return $collection->fresh()->loadMissing('user'); } return $this->workflow->update($collection, [ 'workflow_state' => Collection::WORKFLOW_IN_REVIEW, ], $user); } private function messageFor(string $action, int $count): string { return match ($action) { 'archive' => $count === 1 ? 'Collection archived.' : sprintf('%d collections archived.', $count), 'assign_campaign' => $count === 1 ? 'Campaign assigned.' : sprintf('Campaign assigned to %d collections.', $count), 'update_lifecycle' => $count === 1 ? 'Collection lifecycle updated.' : sprintf('Lifecycle updated for %d collections.', $count), 'request_ai_review' => $count === 1 ? 'AI review requested.' : sprintf('AI review requested for %d collections.', $count), 'mark_editorial_review' => $count === 1 ? 'Collection marked for editorial review.' : sprintf('%d collections marked for editorial review.', $count), default => 'Bulk action completed.', }; } }