Files
SkinbaseNova/app/Services/ArtworkMedalService.php

176 lines
5.0 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\ArtworkMedal;
use App\Models\ArtworkMedalStat;
use App\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Validation\ValidationException;
final class ArtworkMedalService
{
public function __construct(private readonly HomepageService $homepage)
{
}
public function upsert(Artwork $artwork, User $user, string $medal): ArtworkMedal
{
$existing = ArtworkMedal::query()
->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.',
]);
}
}
}