where('artwork_id', $artwork->id) ->where('user_id', $user->id) ->first(); return $existing ? $this->changeMedal($artwork, $user, $medal) : $this->award($artwork, $user, $medal); } public function award(Artwork $artwork, User $user, string $medal): ArtworkMedal { $this->validateMedal($medal); $exists = ArtworkMedal::query() ->where('artwork_id', $artwork->id) ->where('user_id', $user->id) ->exists(); if ($exists) { throw ValidationException::withMessages([ 'medal' => 'You have already awarded this artwork. Use change to update.', ]); } return ArtworkMedal::query()->create([ 'artwork_id' => $artwork->id, 'user_id' => $user->id, 'medal_type' => $medal, 'weight' => ArtworkAward::weightFor($medal), ]); } public function changeMedal(Artwork $artwork, User $user, string $medal): ArtworkMedal { $this->validateMedal($medal); $award = ArtworkMedal::query() ->where('artwork_id', $artwork->id) ->where('user_id', $user->id) ->first(); if (! $award) { throw ValidationException::withMessages([ 'medal' => 'No existing medal found for this artwork.', ]); } $award->update([ 'medal_type' => $medal, 'weight' => ArtworkAward::weightFor($medal), ]); return $award->fresh(); } public function removeMedal(Artwork $artwork, User $user): void { $award = ArtworkMedal::query() ->where('artwork_id', $artwork->id) ->where('user_id', $user->id) ->first(); if ($award) { $award->delete(); } } public function recalculateStats(int $artworkId): ArtworkMedalStat { $rows = ArtworkMedal::query() ->where('artwork_id', $artworkId) ->get(['medal_type', 'weight', 'updated_at']); $cutoff7d = now()->subDays(7); $cutoff30d = now()->subDays(30); $goldCount = 0; $silverCount = 0; $bronzeCount = 0; $scoreTotal = 0; $score7d = 0; $score30d = 0; $lastMedaledAt = null; foreach ($rows as $row) { $medal = (string) $row->medal; $weight = (int) ($row->weight ?? ArtworkAward::weightFor($medal)); $updatedAt = $row->updated_at instanceof Carbon ? $row->updated_at : Carbon::parse($row->updated_at); if ($medal === 'gold') { $goldCount++; } elseif ($medal === 'silver') { $silverCount++; } elseif ($medal === 'bronze') { $bronzeCount++; } $scoreTotal += $weight; if ($updatedAt->greaterThanOrEqualTo($cutoff7d)) { $score7d += $weight; } if ($updatedAt->greaterThanOrEqualTo($cutoff30d)) { $score30d += $weight; } if ($lastMedaledAt === null || $updatedAt->greaterThan($lastMedaledAt)) { $lastMedaledAt = $updatedAt; } } $stat = ArtworkAwardStat::query()->updateOrCreate( ['artwork_id' => $artworkId], [ 'gold_count' => $goldCount, 'silver_count' => $silverCount, 'bronze_count' => $bronzeCount, 'score_total' => $scoreTotal, 'score_7d' => $score7d, 'score_30d' => $score30d, 'last_medaled_at' => $lastMedaledAt, ] ); return ArtworkMedalStat::query()->findOrFail($stat->artwork_id); } public function refreshArtworkMedalState(int $artworkId): ArtworkMedalStat { $stat = $this->recalculateStats($artworkId); $this->syncArtworkToSearch($artworkId); $this->homepage->clearFeaturedAndMedalCaches(); return $stat; } public function syncArtworkToSearch(int $artworkId): void { IndexArtworkJob::dispatch($artworkId); } private function validateMedal(string $medal): void { if (! in_array($medal, ArtworkAward::MEDALS, true)) { throw ValidationException::withMessages([ 'medal' => 'Invalid medal. Must be gold, silver, or bronze.', ]); } } }