mivita/tests/Feature/AboOneTimeWindowTest.php
Kevin 671022a28d Abo: Kreditkarte aktualisieren (CRM, Portal, Berater-Link) + Admin-Abo-Löschung
- Karten-Update ohne Abbuchung via PAYONE creditcardcheck/pseudocardpan
  (AboPaymentMethodService, CardUpdateResult/CreditCardCheckData-DTOs)
- Eingabe nur durch Abo-Inhaber: CRM- und Portal-Formular + zeitlich
  begrenzter Berater→Kunde-Link (AboCardUpdateLink + Mail)
- Änderungshistorie in user_abo_card_changes, Anzeige im Admin-Abo-Detail
- Admin: Abo löschen (destroy) inkl. Test
- Portal-Fix: myOrderCreate wirft 404 statt TypeError bei ungültiger ID
- VIP-Sichtbarkeitstests für Einmalartikel-Fenster ergänzt
- Enthält bereits die Freeze-Guards für den Stripe-Notbetrieb (Folge-Commit)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:56:19 +00:00

165 lines
5.4 KiB
PHP

<?php
use App\Models\Setting;
use App\Models\ShoppingOrderItem;
use App\Models\UserAbo;
use App\Models\UserAboOneTimeItem;
use App\Services\AboHelper;
use App\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
afterEach(function () {
Carbon::setTestNow();
});
describe('getOneTimeWindowDays', function () {
it('liefert den Standardwert, wenn kein Setting gesetzt ist', function () {
expect(AboHelper::getOneTimeWindowDays())->toBe(AboHelper::DEFAULT_ONETIME_WINDOW_DAYS);
});
it('liefert den konfigurierten Wert aus den Settings', function () {
Setting::setContentBySlug('abo-onetime-window-days', 6, 'int');
expect(AboHelper::getOneTimeWindowDays())->toBe(6);
});
it('fällt bei 0 oder ungültigem Wert auf den Standard zurück', function () {
Setting::setContentBySlug('abo-onetime-window-days', 0, 'int');
expect(AboHelper::getOneTimeWindowDays())->toBe(AboHelper::DEFAULT_ONETIME_WINDOW_DAYS);
});
});
describe('isOneTimeWindowOpen', function () {
beforeEach(function () {
Carbon::setTestNow(Carbon::parse('2026-06-05 10:00:00', 'Europe/Berlin'));
});
it('ist offen, wenn die Ausführung innerhalb des Fensters liegt', function () {
$abo = new UserAbo;
$abo->next_date = '2026-06-09'; // 4 Tage entfernt, Default-Fenster = 4
expect(AboHelper::isOneTimeWindowOpen($abo))->toBeTrue();
});
it('ist offen am Tag der Ausführung', function () {
$abo = new UserAbo;
$abo->next_date = '2026-06-05';
expect(AboHelper::isOneTimeWindowOpen($abo))->toBeTrue();
});
it('ist geschlossen, wenn die Ausführung weiter als das Fenster entfernt ist', function () {
$abo = new UserAbo;
$abo->next_date = '2026-06-10'; // 5 Tage entfernt, Fenster = 4
expect(AboHelper::isOneTimeWindowOpen($abo))->toBeFalse();
});
it('berücksichtigt ein konfiguriertes größeres Fenster', function () {
Setting::setContentBySlug('abo-onetime-window-days', 7, 'int');
$abo = new UserAbo;
$abo->next_date = '2026-06-10'; // 5 Tage entfernt, Fenster = 7
expect(AboHelper::isOneTimeWindowOpen($abo))->toBeTrue();
});
it('ist geschlossen, wenn das Ausführungsdatum in der Vergangenheit liegt', function () {
$abo = new UserAbo;
$abo->next_date = '2026-06-04';
expect(AboHelper::isOneTimeWindowOpen($abo))->toBeFalse();
});
it('ist geschlossen, wenn kein next_date gesetzt ist', function () {
$abo = new UserAbo;
$abo->next_date = null;
expect(AboHelper::isOneTimeWindowOpen($abo))->toBeFalse();
});
});
describe('isOneTimeFeatureVisible', function () {
it('ist sichtbar, wenn das Fenster offen ist (Sales Center, eingeloggter User)', function () {
$user = new User;
$user->admin = 0;
$this->actingAs($user, 'user');
$abo = new UserAbo;
$abo->next_date = now()->addDays(2); // innerhalb des 4-Tage-Fensters
expect(AboHelper::isOneTimeFeatureVisible($abo))->toBeTrue();
});
it('ist sichtbar ohne User auf dem user-Guard (Portal/Endkunde), solange das Fenster offen ist', function () {
$abo = new UserAbo;
$abo->next_date = now()->addDays(2);
expect(AboHelper::isOneTimeFeatureVisible($abo))->toBeTrue();
});
it('ist für normale User nicht sichtbar bei geschlossenem Fenster', function () {
$user = new User;
$user->admin = 0;
$this->actingAs($user, 'user');
$abo = new UserAbo;
$abo->next_date = now()->addDays(30); // außerhalb des Fensters
expect(AboHelper::isOneTimeFeatureVisible($abo))->toBeFalse();
});
it('ist für VIP (Admin) dauerhaft sichtbar, auch bei geschlossenem Fenster', function () {
$user = new User;
$user->admin = 1;
$this->actingAs($user, 'user');
$abo = new UserAbo;
$abo->next_date = now()->addDays(30); // außerhalb des Fensters
expect(AboHelper::isOneTimeFeatureVisible($abo))->toBeTrue();
});
});
describe('UserAboOneTimeItem Model', function () {
it('definiert die Beziehung one_time_items auf UserAbo', function () {
$relation = (new UserAbo)->one_time_items();
expect($relation)->toBeInstanceOf(HasMany::class)
->and($relation->getRelated())->toBeInstanceOf(UserAboOneTimeItem::class);
});
it('castet die Snapshot-Felder korrekt', function () {
$item = new UserAboOneTimeItem([
'qty' => '3',
'price' => '12.50',
'tax_rate' => '19.00',
]);
expect($item->qty)->toBe(3)
->and($item->price)->toBe(12.5)
->and($item->tax_rate)->toBe(19.0);
});
});
describe('ShoppingOrderItem is_abo_addon', function () {
it('grenzt Abo- und Einmal-Positionen über Scopes ab', function () {
$aboSql = ShoppingOrderItem::query()->aboItems()->toSql();
$addonSql = ShoppingOrderItem::query()->addonItems()->toSql();
expect($aboSql)->toContain('is_abo_addon')
->and($addonSql)->toContain('is_abo_addon');
});
it('castet is_abo_addon als bool', function () {
$item = new ShoppingOrderItem(['is_abo_addon' => 1]);
expect($item->is_abo_addon)->toBeTrue();
});
});