176 lines
5.0 KiB
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.',
|
|
]);
|
|
}
|
|
}
|
|
} |