achievementCache = collect(); } // --------------------------------------------------------------- // Orchestration // --------------------------------------------------------------- /** * @var Collection $flights */ private Collection $flights; public function calculate(User $user): void { $this->user = $user; $this->achievementCache = Achievement::all()->keyBy('internal_name'); // Load once with every relationship any checker could need $this->flights = $user->flights()->with([ 'airline.alliance', 'aircraft', 'flightClass', 'departureAirport.region.continent', 'arrivalAirport.region.continent', ])->get(); foreach ($this->checkers as $checkerClass) { $checker = new $checkerClass($this); $checker->check($user); } } /** * @return Collection */ public function getFlights(): Collection { return $this->flights; } // --------------------------------------------------------------- // Award / revoke // --------------------------------------------------------------- /** * Upsert a user_achievement row and fire a notification if this * is a newly unlocked achievement. */ public function award(Achievement $achievement, User $user, ?int $progress = null): void { $existing = UserAchievement::where('user_id', $user->id) ->where('achievement_id', $achievement->id) ->first(); $alreadyUnlocked = $existing !== null; UserAchievement::updateOrCreate( ['user_id' => $user->id, 'achievement_id' => $achievement->id], ['progress' => $progress], ); if (! $alreadyUnlocked) { $this->createAchievementNotification($user, $achievement); } } /** * Update progress on a progressive achievement without marking it * as unlocked (i.e. progress exists but threshold not yet met). * Creates the row if it doesn't exist yet. */ public function updateProgress(Achievement $achievement, User $user, int $progress): void { UserAchievement::updateOrCreate( ['user_id' => $user->id, 'achievement_id' => $achievement->id], ['progress' => $progress], ); } /** * Remove a user_achievement row and its associated notification * if the achievement is no longer valid for this user. */ public function revoke(Achievement $achievement, User $user): void { UserAchievement::where('user_id', $user->id) ->where('achievement_id', $achievement->id) ->delete(); Notification::where('user_id', $user->id) ->where('achievement_id', $achievement->id) ->delete(); } // --------------------------------------------------------------- // Helpers for checkers // --------------------------------------------------------------- public function resolveAchievement(string $internalName): ?Achievement { $achievement = $this->achievementCache->get($internalName); return $achievement instanceof Achievement ? $achievement : null; } public function currentUser(): User { return $this->user; } // --------------------------------------------------------------- // Notifications // --------------------------------------------------------------- private function createAchievementNotification(User $user, Achievement $achievement): void { Notification::create([ 'user_id' => $user->id, 'title' => 'Achievement Unlocked!', 'body' => "You've earned the \"{$achievement->name}\" achievement — {$achievement->short_description}", 'is_achievement' => true, 'achievement_id' => $achievement->id, 'expires_at' => null, ]); } }