reviewChecks()->thisMonth()->count(); } public function usedToday(User $user): int { return $user->reviewChecks()->today()->count(); } /** * Verbleibende Frei-Prüfungen diesen Monat. */ public function freeRemaining(User $user): int { $quota = $this->pricing->reviewFreeQuota($user->currentTier()); return max(0, $quota - $this->usedThisMonth($user)); } public function dailyLimitReached(User $user): bool { return $this->usedToday($user) >= $this->pricing->reviewDailyLimit(); } /** * Würde die nächste Prüfung Credits kosten (Freikontingent leer)? */ public function nextCheckCosts(User $user): bool { return $this->freeRemaining($user) === 0; } /** * Kann der User jetzt eine Prüfung auslösen? Tageslimit nicht erreicht und * entweder Freikontingent übrig oder genug Guthaben für den Overflow. */ public function canCheck(User $user): bool { if ($this->dailyLimitReached($user)) { return false; } if ($this->freeRemaining($user) > 0) { return true; } return $this->wallet->canAfford($user, $this->pricing->reviewOverflowCost()); } /** * Bucht eine Prüfung. Wirft ReviewLimitException am Tageslimit und * InsufficientCreditsException, wenn der Overflow nicht gedeckt ist. */ public function recordCheck(User $user, ?PressRelease $pressRelease = null): ReviewCheck { if ($this->dailyLimitReached($user)) { throw new ReviewLimitException($this->pricing->reviewDailyLimit()); } return DB::transaction(function () use ($user, $pressRelease): ReviewCheck { $useFree = $this->freeRemaining($user) > 0; $charged = 0; if (! $useFree) { $cost = $this->pricing->reviewOverflowCost(); $this->wallet->debit( $user, $cost, 'Prüfung (Overflow)', $pressRelease, ); $charged = $cost; } return $user->reviewChecks()->create([ 'press_release_id' => $pressRelease?->id, 'source' => $useFree ? ReviewCheckSource::Free : ReviewCheckSource::Credit, 'charged_credits' => $charged, ]); }); } }