133 lines
3.7 KiB
PHP
133 lines
3.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Jobs\IndexArtworkJob;
|
|
use App\Models\Artwork;
|
|
use App\Models\ArtworkAward;
|
|
use App\Models\ArtworkAwardStat;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class ArtworkAwardService
|
|
{
|
|
/**
|
|
* Award an artwork with the given medal.
|
|
* Throws ValidationException if the user already awarded this artwork.
|
|
*/
|
|
public function award(Artwork $artwork, User $user, string $medal): ArtworkAward
|
|
{
|
|
$this->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.
|
|
*/
|
|
public function removeAward(Artwork $artwork, User $user): void
|
|
{
|
|
ArtworkAward::where('artwork_id', $artwork->id)
|
|
->where('user_id', $user->id)
|
|
->delete();
|
|
|
|
$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.',
|
|
]);
|
|
}
|
|
}
|
|
}
|