validateMedal($medal); $existing = ArtworkAward::where('artwork_id', $artwork->id) ->where('user_id', $user->id) ->first(); if ($existing) { throw ValidationException::withMessages([ 'medal' => 'You have already awarded this artwork. Use change to update.', ]); } $award = ArtworkAward::create([ 'artwork_id' => $artwork->id, 'user_id' => $user->id, 'medal' => $medal, 'weight' => ArtworkAward::WEIGHTS[$medal], ]); $this->recalcStats($artwork->id); $this->syncToSearch($artwork); return $award; } /** * Change an existing award medal for a user/artwork pair. */ public function changeAward(Artwork $artwork, User $user, string $medal): ArtworkAward { $this->validateMedal($medal); $award = ArtworkAward::where('artwork_id', $artwork->id) ->where('user_id', $user->id) ->firstOrFail(); $award->update([ 'medal' => $medal, 'weight' => ArtworkAward::WEIGHTS[$medal], ]); $this->recalcStats($artwork->id); $this->syncToSearch($artwork); return $award->fresh(); } /** * Remove an award for a user/artwork pair. * Uses model-level delete so the ArtworkAwardObserver fires. */ public function removeAward(Artwork $artwork, User $user): void { $award = ArtworkAward::where('artwork_id', $artwork->id) ->where('user_id', $user->id) ->first(); if ($award) { $award->delete(); // fires ArtworkAwardObserver::deleted } else { // Nothing to remove, but still sync stats to be safe. $this->recalcStats($artwork->id); $this->syncToSearch($artwork); } } /** * Recalculate and persist stats for the given artwork. */ public function recalcStats(int $artworkId): ArtworkAwardStat { $counts = DB::table('artwork_awards') ->where('artwork_id', $artworkId) ->selectRaw(' SUM(medal = \'gold\') AS gold_count, SUM(medal = \'silver\') AS silver_count, SUM(medal = \'bronze\') AS bronze_count ') ->first(); $gold = (int) ($counts->gold_count ?? 0); $silver = (int) ($counts->silver_count ?? 0); $bronze = (int) ($counts->bronze_count ?? 0); $score = ($gold * 3) + ($silver * 2) + ($bronze * 1); $stat = ArtworkAwardStat::updateOrCreate( ['artwork_id' => $artworkId], [ 'gold_count' => $gold, 'silver_count' => $silver, 'bronze_count' => $bronze, 'score_total' => $score, 'updated_at' => now(), ] ); return $stat; } /** * Queue a non-blocking reindex for the artwork after award stats change. */ public function syncToSearch(Artwork $artwork): void { IndexArtworkJob::dispatch($artwork->id); } 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.', ]); } } }