presseportale/tests/Feature/ReviewCheckServiceTest.php
Kevin Adametz 3e8844245d Pruefzaehler + Pruefkontingent (Decision-Update Phase-2 vorgezogen)
Eigener Pruef-Zaehler, getrennt von der Credit-Wallet (Paragraph 4.2/4.3):

- review_checks Ledger (eine Zeile je Pruefung, source free|credit,
  charged_credits), aggregiert pro Account/Monat statt pro PM
- ReviewCheckService: Tageslimit (harte Bremse, nicht freikaufbar) ->
  Monats-Freikontingent (tier-gestaffelt 4/12/30/60/120) -> Overflow
  zieht 1 Credit/Pruefung aus der Wallet
- ReviewLimitException fuer das erreichte Tageslimit

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:19:37 +00:00

102 lines
3.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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('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();
}
}