- Navigations-Chrome (CategoryNavigation), Suche- und Newsroom-Seitendaten 300s gecacht; gemeinsamer WebPageCache-Helper (eine TTL/Key-Konvention). - Cache-Poisoning über ?page= geschlossen (Key an Paginator ausgerichtet, kein withQueryString im Cache); Newsrooms cachet nur den ungefilterten Fall und die ersten Seiten (kein unbegrenzter Key-Raum). - Detailseiten schreiben Hits/View-Events deferred nach der Response (M12); OpenAI-/Markt-/USt-ID-Aufrufe mit Connect-Timeouts und begrenzten Retries; Admin-Memo-Caches konsistent invalidiert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
120 lines
3.6 KiB
PHP
120 lines
3.6 KiB
PHP
<?php
|
||
|
||
namespace App\Services\PressRelease;
|
||
|
||
use App\Enums\ReviewCheckSource;
|
||
use App\Exceptions\ReviewLimitException;
|
||
use App\Models\PressRelease;
|
||
use App\Models\ReviewCheck;
|
||
use App\Models\User;
|
||
use App\Services\Billing\CreditPricingService;
|
||
use App\Services\Billing\CreditWalletService;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
/**
|
||
* Prüfzähler-Logik laut Decision-Update §4.2/§4.3.
|
||
*
|
||
* Reihenfolge je Prüfung:
|
||
* 1. Tageslimit (Burst-Schutz) – harte Bremse, nicht freikaufbar.
|
||
* 2. Monats-Freikontingent (tier-gestaffelt, aggregiert pro Account).
|
||
* 3. Overflow – ist das Freikontingent leer, zieht jede weitere Prüfung
|
||
* `credits.review.overflow_cost` Credits aus der Wallet.
|
||
*/
|
||
class ReviewCheckService
|
||
{
|
||
public function __construct(
|
||
private readonly CreditPricingService $pricing,
|
||
private readonly CreditWalletService $wallet,
|
||
) {}
|
||
|
||
/**
|
||
* Verbrauchte Prüfungen im laufenden Monat (Account-Aggregat).
|
||
*/
|
||
public function usedThisMonth(User $user): int
|
||
{
|
||
return $user->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
|
||
{
|
||
return DB::transaction(function () use ($user, $pressRelease): ReviewCheck {
|
||
$lockedUser = User::query()
|
||
->whereKey($user->getKey())
|
||
->lockForUpdate()
|
||
->firstOrFail();
|
||
|
||
if ($this->dailyLimitReached($lockedUser)) {
|
||
throw new ReviewLimitException($this->pricing->reviewDailyLimit());
|
||
}
|
||
|
||
$useFree = $this->freeRemaining($lockedUser) > 0;
|
||
$charged = 0;
|
||
|
||
if (! $useFree) {
|
||
$cost = $this->pricing->reviewOverflowCost();
|
||
$this->wallet->debit(
|
||
$lockedUser,
|
||
$cost,
|
||
'Prüfung (Overflow)',
|
||
$pressRelease,
|
||
);
|
||
$charged = $cost;
|
||
}
|
||
|
||
return $lockedUser->reviewChecks()->create([
|
||
'press_release_id' => $pressRelease?->id,
|
||
'source' => $useFree ? ReviewCheckSource::Free : ReviewCheckSource::Credit,
|
||
'charged_credits' => $charged,
|
||
]);
|
||
});
|
||
}
|
||
}
|