Rechnung/Lieferschein: - PDF-Gruppierung Abo- vs. Einmal-Artikel (is_abo_addon), rueckwaertskompatibel - Vorschau-Command sales:preview-invoice (umgeht Test-System-Gate, ohne Rechnungsnummer) Reminder-Mail (vorbereitet, Cron inaktiv): - MailAboExecutionReminder + Template, abo:send-execution-reminder, Spalte onetime_reminder_sent_for - Berater -> Sales Center, Kunde -> Portal; Vorschau abo:preview-reminder - Datum-Fix: formatDateDB() statt formatDate() (ICU-Format) Sperrfristen konfigurierbar (Admin-Settings): - abo-lock-days-change (Liefertag + Abo-Produkte), abo-lock-days-pause-cancel - Meldungen mit :lock-Platzhalter; Pausieren/Kuendigen vom Change-Lock entkoppelt - Abo-Produkt-Aenderungen im Lock-Fenster server- + clientseitig eingefroren - Selbstbedienung: Add-only/Lock nach $view (kein Admin-Bypass), Stale-Cache-Fix beim Mengen-Update Dashboard: - Einmalprodukte-Card + Team-Abo-KPI-Card, News-Card responsiv (col-xl-6) Lokalisierung: - Sprachdateien en/es/fr vollstaendig mit de synchronisiert (inkl. marketingplan/portal) - LocalizationSyncTest sichert Key-Gleichheit ab Tests: AboOneTimeInvoiceGrouping, AboExecutionReminder, AboLockDaysSetting, AboOnetimeDashboardAndMail, PreviewInvoiceCommand, TeamAboOverview + erweiterte View-Tests Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
148 lines
4.9 KiB
PHP
148 lines
4.9 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\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(Tests\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 nicht sichtbar bei geschlossenem Fenster', function () {
|
|
$abo = new UserAbo;
|
|
$abo->next_date = now()->addDays(30); // außerhalb des Fensters
|
|
|
|
expect(AboHelper::isOneTimeFeatureVisible($abo))->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('UserAboOneTimeItem Model', function () {
|
|
it('definiert die Beziehung one_time_items auf UserAbo', function () {
|
|
$relation = (new UserAbo)->one_time_items();
|
|
|
|
expect($relation)->toBeInstanceOf(Illuminate\Database\Eloquent\Relations\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();
|
|
});
|
|
});
|