create(array_merge([ 'is_public' => true, 'is_approved' => true, 'published_at' => now()->subDay(), ], $attributes)); } test('featured hero ordering uses recent medal score as tie-break inside the same priority', function () { $owner = User::factory()->create(); $artworkA = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'A']); $artworkB = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'B']); foreach ([$artworkA, $artworkB] as $artwork) { DB::table('artwork_features')->insert([ 'artwork_id' => $artwork->id, 'featured_at' => now()->subHour(), 'expires_at' => null, 'priority' => 100, 'label' => null, 'note' => null, 'is_active' => true, 'created_by' => null, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null, ]); } DB::table('artwork_medal_stats')->insert([ 'artwork_id' => $artworkA->id, 'gold_count' => 0, 'silver_count' => 1, 'bronze_count' => 0, 'score_total' => 3, 'score_7d' => 3, 'score_30d' => 3, 'last_medaled_at' => now()->subDay(), 'created_at' => now(), 'updated_at' => now(), ]); DB::table('artwork_medal_stats')->insert([ 'artwork_id' => $artworkB->id, 'gold_count' => 1, 'silver_count' => 0, 'bronze_count' => 0, 'score_total' => 5, 'score_7d' => 5, 'score_30d' => 5, 'last_medaled_at' => now()->subHour(), 'created_at' => now(), 'updated_at' => now(), ]); $featured = app(ArtworkService::class)->getFeaturedArtworks(null, 1); expect($featured->items()[0]->id)->toBe($artworkB->id); }); test('featured query excludes inactive and expired feature rows', function () { $owner = User::factory()->create(); $activeArtwork = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Active']); $inactiveArtwork = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Inactive']); $expiredArtwork = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Expired']); DB::table('artwork_features')->insert([ [ 'artwork_id' => $activeArtwork->id, 'featured_at' => now()->subHour(), 'expires_at' => null, 'priority' => 50, 'label' => null, 'note' => null, 'is_active' => true, 'created_by' => null, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null, ], [ 'artwork_id' => $inactiveArtwork->id, 'featured_at' => now(), 'expires_at' => null, 'priority' => 999, 'label' => null, 'note' => null, 'is_active' => false, 'created_by' => null, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null, ], [ 'artwork_id' => $expiredArtwork->id, 'featured_at' => now(), 'expires_at' => now()->subMinute(), 'priority' => 999, 'label' => null, 'note' => null, 'is_active' => true, 'created_by' => null, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null, ], ]); $featuredIds = app(ArtworkService::class) ->getFeaturedArtworks(null, 10) ->getCollection() ->pluck('id') ->all(); expect($featuredIds)->toContain($activeArtwork->id) ->and($featuredIds)->not->toContain($inactiveArtwork->id) ->and($featuredIds)->not->toContain($expiredArtwork->id); }); test('featured hero sorts by priority before featured_at', function () { $owner = User::factory()->create(); $higherPriority = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Higher Priority']); $newerFeaturedAt = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Newer Featured']); DB::table('artwork_features')->insert([ [ 'artwork_id' => $higherPriority->id, 'featured_at' => now()->subDays(2), 'expires_at' => null, 'priority' => 200, 'label' => null, 'note' => null, 'is_active' => true, 'created_by' => null, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null, ], [ 'artwork_id' => $newerFeaturedAt->id, 'featured_at' => now(), 'expires_at' => null, 'priority' => 100, 'label' => null, 'note' => null, 'is_active' => true, 'created_by' => null, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null, ], ]); $featured = app(ArtworkService::class)->getFeaturedArtworks(null, 1); expect($featured->items()[0]->id)->toBe($higherPriority->id); }); test('force hero overrides normal homepage eligibility filters without leaking into the public featured listing', function () { $owner = User::factory()->create(); $forcedHero = makeFeaturedArtwork([ 'user_id' => $owner->id, 'title' => 'Forced Hero', 'has_missing_thumbnails' => true, ]); $naturalWinner = makeFeaturedArtwork([ 'user_id' => $owner->id, 'title' => 'Natural Winner', ]); DB::table('artwork_features')->insert([ [ 'artwork_id' => $forcedHero->id, 'featured_at' => now()->subDays(2), 'expires_at' => null, 'priority' => 50, 'label' => null, 'note' => null, 'is_active' => true, 'force_hero' => true, 'created_by' => null, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null, ], [ 'artwork_id' => $naturalWinner->id, 'featured_at' => now()->subHour(), 'expires_at' => null, 'priority' => 500, 'label' => null, 'note' => null, 'is_active' => true, 'force_hero' => false, 'created_by' => null, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null, ], ]); $winner = app(ArtworkService::class)->getFeaturedArtworkWinner(); $featuredIds = app(ArtworkService::class) ->getFeaturedArtworks(null, 10) ->getCollection() ->pluck('id') ->all(); expect($winner?->id)->toBe($forcedHero->id) ->and($featuredIds)->toContain($naturalWinner->id) ->and($featuredIds)->not->toContain($forcedHero->id); }); test('homepage hero payload uses the forced hero artwork when one is set', function () { $owner = User::factory()->create(); $forcedHero = makeFeaturedArtwork([ 'user_id' => $owner->id, 'title' => 'Forced Homepage Hero', 'has_missing_thumbnails' => true, ]); $naturalWinner = makeFeaturedArtwork([ 'user_id' => $owner->id, 'title' => 'Natural Homepage Winner', ]); DB::table('artwork_features')->insert([ [ 'artwork_id' => $forcedHero->id, 'featured_at' => now()->subDays(2), 'expires_at' => null, 'priority' => 10, 'label' => null, 'note' => null, 'is_active' => true, 'force_hero' => true, 'created_by' => null, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null, ], [ 'artwork_id' => $naturalWinner->id, 'featured_at' => now()->subHour(), 'expires_at' => null, 'priority' => 500, 'label' => null, 'note' => null, 'is_active' => true, 'force_hero' => false, 'created_by' => null, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null, ], ]); $hero = app(HomepageService::class)->getHeroArtwork(); expect($hero)->not->toBeNull() ->and($hero['id'])->toBe($forcedHero->id) ->and($hero['title'])->toBe('Forced Homepage Hero'); }); test('community favorites returns artworks ordered by recent medal score', function () { $owner = User::factory()->create(); $leader = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Leader']); $runnerUp = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Runner Up', 'published_at' => now()->subDays(2)]); DB::table('artwork_medal_stats')->insert([ [ 'artwork_id' => $runnerUp->id, 'gold_count' => 0, 'silver_count' => 2, 'bronze_count' => 0, 'score_total' => 6, 'score_7d' => 6, 'score_30d' => 6, 'last_medaled_at' => now()->subHour(), 'created_at' => now(), 'updated_at' => now(), ], [ 'artwork_id' => $leader->id, 'gold_count' => 2, 'silver_count' => 0, 'bronze_count' => 0, 'score_total' => 10, 'score_7d' => 10, 'score_30d' => 10, 'last_medaled_at' => now(), 'created_at' => now(), 'updated_at' => now(), ], ]); $results = app(HomepageService::class)->getCommunityFavorites(8); expect($results)->toHaveCount(2) ->and($results[0]['id'])->toBe($leader->id) ->and($results[1]['id'])->toBe($runnerUp->id); }); test('hall of fame returns artworks ordered by all time medal score', function () { $owner = User::factory()->create(); $leader = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Legend']); $runnerUp = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Veteran']); DB::table('artwork_medal_stats')->insert([ [ 'artwork_id' => $runnerUp->id, 'gold_count' => 1, 'silver_count' => 1, 'bronze_count' => 1, 'score_total' => 9, 'score_7d' => 0, 'score_30d' => 0, 'last_medaled_at' => now(), 'created_at' => now(), 'updated_at' => now(), ], [ 'artwork_id' => $leader->id, 'gold_count' => 3, 'silver_count' => 0, 'bronze_count' => 0, 'score_total' => 15, 'score_7d' => 0, 'score_30d' => 0, 'last_medaled_at' => now()->subDay(), 'created_at' => now(), 'updated_at' => now(), ], ]); $results = app(HomepageService::class)->getHallOfFame(8); expect($results)->toHaveCount(2) ->and($results[0]['id'])->toBe($leader->id) ->and($results[1]['id'])->toBe($runnerUp->id); });