'integer', 'comments_count' => 'integer', 'saves_count' => 'integer', 'collaborators_count' => 'integer', 'is_featured' => 'boolean', 'profile_order' => 'integer', 'views_count' => 'integer', 'likes_count' => 'integer', 'followers_count' => 'integer', 'shares_count' => 'integer', 'allow_submissions' => 'boolean', 'allow_comments' => 'boolean', 'allow_saves' => 'boolean', 'analytics_enabled' => 'boolean', 'commercial_eligibility' => 'boolean', 'smart_rules_json' => 'array', 'layout_modules_json' => 'array', 'health_flags_json' => 'array', 'quality_score' => 'decimal:2', 'ranking_score' => 'decimal:2', 'metadata_completeness_score' => 'decimal:2', 'editorial_readiness_score' => 'decimal:2', 'freshness_score' => 'decimal:2', 'engagement_score' => 'decimal:2', 'health_score' => 'decimal:2', 'placement_eligibility' => 'boolean', 'series_order' => 'integer', 'history_count' => 'integer', 'last_activity_at' => 'datetime', 'featured_at' => 'datetime', 'last_health_check_at' => 'datetime', 'last_recommendation_refresh_at' => 'datetime', 'published_at' => 'datetime', 'unpublished_at' => 'datetime', 'archived_at' => 'datetime', 'expired_at' => 'datetime', ]; protected ?bool $cachedVisibilityWindow = null; public function user(): BelongsTo { return $this->belongsTo(User::class); } public function managedBy(): BelongsTo { return $this->belongsTo(User::class, 'managed_by_user_id'); } public function editorialOwnerUser(): BelongsTo { return $this->belongsTo(User::class, 'editorial_owner_user_id'); } public function coverArtwork(): BelongsTo { return $this->belongsTo(Artwork::class, 'cover_artwork_id'); } public function likes(): HasMany { return $this->hasMany(CollectionLike::class); } public function saves(): HasMany { return $this->hasMany(CollectionSave::class); } public function follows(): HasMany { return $this->hasMany(CollectionFollow::class); } public function members(): HasMany { return $this->hasMany(CollectionMember::class); } public function submissions(): HasMany { return $this->hasMany(CollectionSubmission::class); } public function comments(): HasMany { return $this->hasMany(CollectionComment::class); } public function historyEntries(): HasMany { return $this->hasMany(CollectionHistory::class); } public function canonicalCollection(): BelongsTo { return $this->belongsTo(self::class, 'canonical_collection_id'); } public function dailyStats(): HasMany { return $this->hasMany(CollectionDailyStat::class); } public function programAssignments(): HasMany { return $this->hasMany(CollectionProgramAssignment::class); } public function qualitySnapshots(): HasMany { return $this->hasMany(CollectionQualitySnapshot::class); } public function recommendationSnapshots(): HasMany { return $this->hasMany(CollectionRecommendationSnapshot::class); } public function mergeActionsAsSource(): HasMany { return $this->hasMany(CollectionMergeAction::class, 'source_collection_id'); } public function mergeActionsAsTarget(): HasMany { return $this->hasMany(CollectionMergeAction::class, 'target_collection_id'); } public function entityLinks(): HasMany { return $this->hasMany(CollectionEntityLink::class); } public function savedNotes(): HasMany { return $this->hasMany(CollectionSavedNote::class); } public function placements(): HasMany { return $this->hasMany(CollectionSurfacePlacement::class); } public function artworks(): BelongsToMany { return $this->belongsToMany(Artwork::class, 'collection_artwork', 'collection_id', 'artwork_id') ->withPivot(['order_num']) ->withTimestamps() ->orderByPivot('order_num'); } public function publicArtworks(): BelongsToMany { return $this->artworks() ->whereNull('artworks.deleted_at') ->where('artworks.is_public', true) ->where('artworks.is_approved', true) ->whereNotNull('artworks.published_at') ->where('artworks.published_at', '<=', now()); } public function manualRelatedCollections(): BelongsToMany { return $this->belongsToMany(self::class, 'collection_related_links', 'collection_id', 'related_collection_id') ->withPivot(['sort_order', 'created_by_user_id']) ->withTimestamps() ->orderBy('collection_related_links.sort_order') ->orderBy('collection_related_links.id'); } public function scopePublic(Builder $query): Builder { return $query ->where('visibility', self::VISIBILITY_PUBLIC) ->where('moderation_status', self::MODERATION_ACTIVE) ->whereNotIn('lifecycle_state', [ self::LIFECYCLE_DRAFT, self::LIFECYCLE_EXPIRED, self::LIFECYCLE_HIDDEN, self::LIFECYCLE_RESTRICTED, self::LIFECYCLE_UNDER_REVIEW, ]) ->where(function (Builder $builder): void { $builder->whereNull('published_at') ->orWhere('published_at', '<=', now()); }) ->where(function (Builder $builder): void { $builder->whereNull('unpublished_at') ->orWhere('unpublished_at', '>', now()); }) ->where(function (Builder $builder): void { $builder->whereNull('expired_at') ->orWhere('expired_at', '>', now()); }); } public function scopePublicEligible(Builder $query): Builder { return $query ->public() ->where('placement_eligibility', true) ->whereIn('lifecycle_state', [ self::LIFECYCLE_PUBLISHED, self::LIFECYCLE_FEATURED, ]); } public function scopeFeaturedPublic(Builder $query): Builder { return $query ->publicEligible() ->where('is_featured', true); } public function scopeVisibleOnProfile(Builder $query): Builder { return $query->public(); } public function scopeOwnedBy(Builder $query, int $userId): Builder { return $query->where('user_id', $userId); } public function isOwnedBy(User|int|null $user): bool { $userId = $user instanceof User ? $user->id : $user; return $userId !== null && (int) $userId === (int) $this->user_id; } public function isPubliclyAccessible(): bool { if ($this->moderation_status !== self::MODERATION_ACTIVE) { return false; } if ($this->published_at && $this->published_at->isFuture()) { return false; } if ($this->unpublished_at && $this->unpublished_at->lte(now())) { return false; } if ($this->expired_at && $this->expired_at->lte(now()) && $this->lifecycle_state === self::LIFECYCLE_EXPIRED) { return false; } if (! in_array($this->lifecycle_state, [ self::LIFECYCLE_SCHEDULED, self::LIFECYCLE_PUBLISHED, self::LIFECYCLE_FEATURED, self::LIFECYCLE_ARCHIVED, ], true)) { return false; } return in_array($this->visibility, [self::VISIBILITY_PUBLIC, self::VISIBILITY_UNLISTED], true); } public function isPubliclyEngageable(): bool { return $this->visibility === self::VISIBILITY_PUBLIC; } public function displayOwnerName(): string { if ($this->type === self::TYPE_EDITORIAL && $this->editorial_owner_mode === self::EDITORIAL_OWNER_SYSTEM) { return (string) ($this->editorial_owner_label ?: config('collections.editorial.system_owner_label', 'Skinbase Editorial')); } $owner = $this->relationLoaded('user') ? $this->user : $this->user()->first(); return (string) ($owner?->name ?: $owner?->username ?: 'Skinbase Curator'); } public function displayOwnerUsername(): ?string { if ($this->type === self::TYPE_EDITORIAL && $this->editorial_owner_mode === self::EDITORIAL_OWNER_SYSTEM) { return null; } $owner = $this->relationLoaded('user') ? $this->user : $this->user()->first(); return $owner?->username; } public function hasSystemEditorialOwner(): bool { return $this->type === self::TYPE_EDITORIAL && $this->editorial_owner_mode === self::EDITORIAL_OWNER_SYSTEM; } public function isCollaborative(): bool { return $this->type !== self::TYPE_PERSONAL || $this->collaboration_mode !== self::COLLABORATION_CLOSED; } public function activeMemberRoleFor(User|int|null $user): ?string { $userId = $user instanceof User ? $user->id : $user; if ($userId === null) { return null; } if ($this->isOwnedBy($userId)) { return self::MEMBER_ROLE_OWNER; } $members = $this->relationLoaded('members') ? $this->members : $this->members()->where('status', self::MEMBER_STATUS_ACTIVE)->get(); return $members ->first(fn (CollectionMember $member) => (int) $member->user_id === (int) $userId && $member->status === self::MEMBER_STATUS_ACTIVE) ?->role; } public function hasActiveMember(User|int|null $user): bool { return $this->activeMemberRoleFor($user) !== null; } public function canBeManagedBy(User $user): bool { return in_array($this->activeMemberRoleFor($user), [ self::MEMBER_ROLE_OWNER, self::MEMBER_ROLE_EDITOR, ], true); } public function canManageArtworks(User $user): bool { return $this->canBeManagedBy($user); } public function canManageMembers(User $user): bool { return $this->isCollaborative() && $this->canBeManagedBy($user); } public function canReceiveSubmissionsFrom(?User $user): bool { if (! $user || ! $this->allow_submissions || ! $this->isPubliclyAccessible()) { return false; } if ($this->type === self::TYPE_EDITORIAL) { return false; } if ($this->collaboration_mode === self::COLLABORATION_CLOSED) { return false; } return ! $this->hasActiveMember($user); } public function canReceiveCommentsFrom(?User $user): bool { return $user !== null && $this->allow_comments && $this->canBeViewedBy($user); } public function canBeSavedBy(?User $user): bool { return $user !== null && $this->allow_saves && $this->visibility === self::VISIBILITY_PUBLIC && ! $this->isOwnedBy($user); } public function isFeatureablePublicly(): bool { return $this->visibility === self::VISIBILITY_PUBLIC && $this->moderation_status === self::MODERATION_ACTIVE && (bool) $this->placement_eligibility && in_array($this->lifecycle_state, [self::LIFECYCLE_PUBLISHED, self::LIFECYCLE_FEATURED], true); } public function isSmart(): bool { return $this->mode === self::MODE_SMART; } public function canBeViewedBy(?User $viewer): bool { if ($viewer && ($this->isOwnedBy($viewer) || $this->hasActiveMember($viewer))) { return true; } return $this->isPubliclyAccessible(); } public function resolvedCoverArtwork(bool $publicOnly = false): ?Artwork { $cover = $this->relationLoaded('coverArtwork') ? $this->coverArtwork : $this->coverArtwork()->first(); if ($cover && (! $publicOnly || $this->artworkIsPubliclyVisible($cover))) { return $cover; } $relation = $publicOnly ? 'publicArtworks' : 'artworks'; $artworks = $this->relationLoaded($relation) ? $this->getRelation($relation) : $this->{$relation}()->limit(1)->get(); return $artworks->first(); } public function syncArtworksCount(): void { $this->forceFill([ 'artworks_count' => $this->isSmart() ? (int) $this->artworks_count : $this->artworks()->count(), ])->save(); } public function inSeries(): bool { return filled($this->series_key); } public function hasCanonicalTarget(): bool { return $this->canonical_collection_id !== null; } public function isPlacementEligible(): bool { return (bool) $this->placement_eligibility; } public function supportsAnalytics(): bool { return (bool) $this->analytics_enabled; } public function usesPremiumPresentation(): bool { return $this->presentation_style !== self::PRESENTATION_STANDARD || $this->emphasis_mode !== self::EMPHASIS_BALANCED || filled($this->theme_token); } private function artworkIsPubliclyVisible(Artwork $artwork): bool { return ! $artwork->trashed() && (bool) $artwork->is_public && (bool) $artwork->is_approved && $artwork->published_at !== null && $artwork->published_at->lte(now()); } }