- 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>
121 lines
4 KiB
PHP
121 lines
4 KiB
PHP
<?php
|
||
|
||
use App\Enums\ReviewCheckSource;
|
||
use App\Exceptions\InsufficientCreditsException;
|
||
use App\Exceptions\ReviewLimitException;
|
||
use App\Models\User;
|
||
use App\Services\Billing\CreditWalletService;
|
||
use App\Services\PressRelease\ReviewCheckService;
|
||
|
||
beforeEach(function (): void {
|
||
$this->service = app(ReviewCheckService::class);
|
||
$this->wallet = app(CreditWalletService::class);
|
||
});
|
||
|
||
test('an Einzel user has four free checks per month', function () {
|
||
$user = User::factory()->create();
|
||
|
||
expect($this->service->freeRemaining($user))->toBe(4);
|
||
expect($this->service->nextCheckCosts($user))->toBeFalse();
|
||
});
|
||
|
||
test('free checks are consumed from the monthly quota first', function () {
|
||
$user = User::factory()->create();
|
||
|
||
$check = $this->service->recordCheck($user);
|
||
|
||
expect($check->source)->toBe(ReviewCheckSource::Free);
|
||
expect($check->charged_credits)->toBe(0);
|
||
expect($this->service->freeRemaining($user))->toBe(3);
|
||
expect($this->service->usedThisMonth($user))->toBe(1);
|
||
});
|
||
|
||
test('once the free quota is empty the overflow draws one credit per check', function () {
|
||
$user = User::factory()->create();
|
||
$this->wallet->credit($user, 5);
|
||
|
||
// Vier freie Prüfungen aufbrauchen.
|
||
for ($i = 0; $i < 4; $i++) {
|
||
$this->service->recordCheck($user);
|
||
}
|
||
|
||
expect($this->service->freeRemaining($user))->toBe(0);
|
||
expect($this->service->nextCheckCosts($user))->toBeTrue();
|
||
|
||
$overflow = $this->service->recordCheck($user);
|
||
|
||
expect($overflow->source)->toBe(ReviewCheckSource::Credit);
|
||
expect($overflow->charged_credits)->toBe(1);
|
||
expect($this->wallet->balance($user))->toBe(4);
|
||
});
|
||
|
||
test('stale user instances cannot consume the same final free review check', function () {
|
||
$user = User::factory()->create();
|
||
$this->wallet->credit($user, 5);
|
||
|
||
for ($i = 0; $i < 3; $i++) {
|
||
$this->service->recordCheck($user);
|
||
}
|
||
|
||
$firstRequestUser = $user->fresh();
|
||
$secondRequestUser = $user->fresh();
|
||
|
||
$lastFreeCheck = $this->service->recordCheck($firstRequestUser);
|
||
$overflowCheck = $this->service->recordCheck($secondRequestUser);
|
||
|
||
expect($lastFreeCheck->source)->toBe(ReviewCheckSource::Free)
|
||
->and($overflowCheck->source)->toBe(ReviewCheckSource::Credit)
|
||
->and($this->wallet->balance($user))->toBe(4);
|
||
});
|
||
|
||
test('overflow without enough credits throws and records nothing', function () {
|
||
$user = User::factory()->create();
|
||
for ($i = 0; $i < 4; $i++) {
|
||
$this->service->recordCheck($user);
|
||
}
|
||
|
||
expect(fn () => $this->service->recordCheck($user))
|
||
->toThrow(InsufficientCreditsException::class);
|
||
|
||
expect($this->service->usedThisMonth($user))->toBe(4); // kein Overflow-Eintrag
|
||
});
|
||
|
||
test('the daily limit is a hard brake that credits cannot bypass', function () {
|
||
$user = User::factory()->create();
|
||
$this->wallet->credit($user, 100);
|
||
|
||
// 10 Prüfungen heute (Tageslimit) – 4 frei, 6 per Credit.
|
||
for ($i = 0; $i < 10; $i++) {
|
||
$this->service->recordCheck($user);
|
||
}
|
||
|
||
expect($this->service->dailyLimitReached($user))->toBeTrue();
|
||
expect($this->service->canCheck($user))->toBeFalse();
|
||
expect(fn () => $this->service->recordCheck($user))
|
||
->toThrow(ReviewLimitException::class);
|
||
});
|
||
|
||
test('a higher tier check pool resets with the calendar month', function () {
|
||
$user = User::factory()->create();
|
||
|
||
// Letzten Monat verbrauchte Prüfungen zählen nicht ins aktuelle Kontingent.
|
||
ReviewCheckFactoryHelper($user, 3, now()->subMonth());
|
||
|
||
expect($this->service->usedThisMonth($user))->toBe(0);
|
||
expect($this->service->freeRemaining($user))->toBe(4);
|
||
});
|
||
|
||
/**
|
||
* Legt Prüfungen mit explizitem Datum an (umgeht den Service, der immer
|
||
* „jetzt" bucht).
|
||
*/
|
||
function ReviewCheckFactoryHelper(User $user, int $count, $at): void
|
||
{
|
||
for ($i = 0; $i < $count; $i++) {
|
||
$check = $user->reviewChecks()->create([
|
||
'source' => ReviewCheckSource::Free,
|
||
'charged_credits' => 0,
|
||
]);
|
||
$check->forceFill(['created_at' => $at, 'updated_at' => $at])->save();
|
||
}
|
||
}
|