(float) ($collection->quality_score ?? 0), 'health_score' => (float) ($collection->health_score ?? 0), 'freshness_score' => (float) ($collection->freshness_score ?? 0), 'engagement_score' => (float) ($collection->engagement_score ?? 0), 'editorial_readiness_score' => (float) ($collection->editorial_readiness_score ?? 0), ]; $score = ($signals['quality_score'] * 0.3) + ($signals['health_score'] * 0.3) + ($signals['freshness_score'] * 0.15) + ($signals['engagement_score'] * 0.2) + ($signals['editorial_readiness_score'] * 0.05); if ($collection->is_featured) { $score += 5.0; } if ($context === 'campaign' && filled($collection->campaign_key)) { $score += 7.5; } if ($context === 'evergreen' && $signals['freshness_score'] < 35 && $signals['quality_score'] >= 70) { $score += 6.0; } $score = round(max(0.0, min(150.0, $score)), 2); return [ 'score' => $score, 'context' => $context, 'signals' => $signals, 'bucket' => $this->rankingBucket($score), 'recommendation_tier' => $this->recommendationTier($score), 'search_boost_tier' => $this->searchBoostTier($collection, $score), 'rationale' => [ sprintf('Quality %.1f', $signals['quality_score']), sprintf('Health %.1f', $signals['health_score']), sprintf('Freshness %.1f', $signals['freshness_score']), sprintf('Engagement %.1f', $signals['engagement_score']), ], ]; } public function refresh(Collection $collection, string $context = 'default'): Collection { $explanation = $this->explain($collection->fresh(), $context); $snapshotDate = now()->toDateString(); $collection->forceFill([ 'ranking_bucket' => $explanation['bucket'], 'recommendation_tier' => $explanation['recommendation_tier'], 'search_boost_tier' => $explanation['search_boost_tier'], 'last_recommendation_refresh_at' => now(), ])->save(); $snapshot = CollectionRecommendationSnapshot::query() ->where('collection_id', $collection->id) ->where('context_key', $context) ->whereDate('snapshot_date', $snapshotDate) ->first(); if ($snapshot) { $snapshot->forceFill([ 'recommendation_score' => $explanation['score'], 'rationale_json' => $explanation, ])->save(); } else { CollectionRecommendationSnapshot::query()->create([ 'collection_id' => $collection->id, 'context_key' => $context, 'recommendation_score' => $explanation['score'], 'rationale_json' => $explanation, 'snapshot_date' => $snapshotDate, ]); } return $collection->fresh(); } private function rankingBucket(float $score): string { return match (true) { $score >= 110 => 'elite', $score >= 85 => 'strong', $score >= 60 => 'steady', $score >= 35 => 'emerging', default => 'cold', }; } private function recommendationTier(float $score): string { return match (true) { $score >= 105 => 'premium', $score >= 80 => 'primary', $score >= 55 => 'secondary', default => 'fallback', }; } private function searchBoostTier(Collection $collection, float $score): string { if ($collection->type === Collection::TYPE_EDITORIAL && $score >= 80) { return 'editorial'; } if ($collection->placement_eligibility && $score >= 70) { return 'high'; } if ($score >= 45) { return 'standard'; } return 'low'; } }