mivita/tests/Feature/AboOneTimeViewTest.php
Kevin 65b867f8ee Abo Einmalprodukte: Phasen 5-7, konfigurierbare Sperrfristen, Lokalisierung & Dashboard
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>
2026-06-23 13:38:11 +00:00

311 lines
12 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\Models\UserAboOneTimeItem;
use App\Services\AboOrderCart;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(Tests\TestCase::class, RefreshDatabase::class);
describe('Abo Einmal-Produkte Views', function () {
beforeEach(fn () => makeShopEnv());
it('rendert die Einmal-Produktliste mit Position und offener Zwischensumme', function () {
$abo = makeMeAbo();
$product = makeProduct(['2'], 119, 19);
UserAboOneTimeItem::create([
'user_abo_id' => $abo->id,
'product_id' => $product->id,
'qty' => 2,
'price' => 119.0,
'price_net' => 100.0,
'tax_rate' => 19.0,
'tax' => 19.0,
'status' => 0,
]);
$summary = ['one_time' => ['gross' => 238.0, 'net' => 200.0, 'tax' => 38.0]];
$html = view('admin.abo._order_onetime_show', [
'user_abo' => $abo->fresh(),
'summary' => $summary,
])->render();
expect($html)->toContain($product->getLang('name'))
->and($html)->toContain(__('abo.onetime_pending_subtotal'))
->and($html)->toContain(__('abo.onetime_status_pending'))
->and($html)->toContain('238,00');
});
it('zeigt bestätigte und offene Artikel mit getrennten Zwischensummen', function () {
$abo = makeMeAbo();
$confirmed = makeProduct(['2'], 119, 19);
$pending = makeProduct(['2'], 50, 19);
UserAboOneTimeItem::create([
'user_abo_id' => $abo->id,
'product_id' => $confirmed->id,
'qty' => 1,
'confirmed_qty' => 1,
'confirmed_at' => now(),
'price' => 119.0,
'tax_rate' => 19.0,
'status' => 1,
]);
UserAboOneTimeItem::create([
'user_abo_id' => $abo->id,
'product_id' => $pending->id,
'qty' => 1,
'price' => 50.0,
'tax_rate' => 19.0,
'status' => 0,
]);
$html = view('admin.abo._order_onetime_show', [
'user_abo' => $abo->fresh(),
'summary' => ['one_time' => ['gross' => 119.0], 'total_with_shipping' => 219.0],
])->render();
expect($html)->toContain(__('abo.onetime_confirmed_subtotal'))
->and($html)->toContain(__('abo.onetime_pending_subtotal'))
->and($html)->toContain(__('abo.onetime_new_total'))
->and($html)->toContain(__('abo.onetime_status_confirmed'))
->and($html)->toContain(__('abo.onetime_status_pending'))
->and($html)->toContain(__('abo.onetime_pending_hint'))
->and($html)->toContain('50,00')
->and($html)->toContain('169,00');
});
it('zeigt bei unbestätigten Einmal-Artikeln die Bestätigungsbuttons', function () {
$abo = makeMeAbo();
$product = makeProduct(['2'], 119, 19);
UserAboOneTimeItem::create([
'user_abo_id' => $abo->id,
'product_id' => $product->id,
'qty' => 1,
'price' => 119.0,
'price_net' => 100.0,
'tax_rate' => 19.0,
'tax' => 19.0,
'status' => 0,
]);
$html = view('admin.abo._order_onetime_show', [
'user_abo' => $abo->fresh(),
'summary' => ['one_time' => ['gross' => 119.0], 'total_with_shipping' => 219.0],
])->render();
expect($html)->toContain(__('abo.onetime_pending_subtotal'))
->and($html)->toContain(__('abo.onetime_legal_notice', [
'nextBillingDate' => $abo->next_date,
'agb' => '<a href="'.url('/agb').'" target="_blank">'.__('abo.confirm_terms_link').'</a>',
'withdrawal' => '<a href="'.asset('download/mivita_widerruf_formular.pdf').'" target="_blank">'.__('abo.confirm_withdrawal_link').'</a>',
]))
->and($html)->toContain(__('abo.onetime_discard_changes'))
->and($html)->toContain(__('abo.onetime_confirm_paid'));
});
it('zeigt bei bestätigten Einmal-Artikeln den Erfolgshinweis', function () {
$abo = makeMeAbo();
$product = makeProduct(['2'], 119, 19);
UserAboOneTimeItem::create([
'user_abo_id' => $abo->id,
'product_id' => $product->id,
'qty' => 1,
'confirmed_qty' => 1,
'confirmed_at' => now(),
'price' => 119.0,
'price_net' => 100.0,
'tax_rate' => 19.0,
'tax' => 19.0,
'status' => 1,
]);
$html = view('admin.abo._order_onetime_show', [
'user_abo' => $abo->fresh(),
'summary' => ['one_time' => ['gross' => 119.0], 'total_with_shipping' => 219.0],
])->render();
expect($html)->toContain(__('abo.onetime_confirm_success'))
->and($html)->toContain('data-confirmation-state="confirmed"')
->and($html)->not->toContain(__('abo.onetime_confirm_paid'));
});
it('zeigt nach einer Änderung an bestätigten Einmal-Artikeln wieder die Bestätigungsbuttons', function () {
$abo = makeMeAbo();
$product = makeProduct(['2'], 119, 19);
UserAboOneTimeItem::create([
'user_abo_id' => $abo->id,
'product_id' => $product->id,
'qty' => 3,
'confirmed_qty' => 1,
'confirmed_at' => now(),
'price' => 119.0,
'price_net' => 100.0,
'tax_rate' => 19.0,
'tax' => 19.0,
'status' => 0,
]);
$html = view('admin.abo._order_onetime_show', [
'user_abo' => $abo->fresh(),
'summary' => ['one_time' => ['gross' => 357.0], 'total_with_shipping' => 457.0],
])->render();
expect($html)->toContain('data-confirmation-state="changed"')
->and($html)->toContain(__('abo.onetime_discard_changes'))
->and($html)->toContain(__('abo.onetime_confirm_paid'))
->and($html)->not->toContain(__('abo.onetime_confirm_success'));
});
it('zeigt den Leer-Hinweis, wenn keine Einmal-Produkte vorhanden sind', function () {
$abo = makeMeAbo();
$html = view('admin.abo._order_onetime_show', [
'user_abo' => $abo,
'summary' => ['one_time' => ['gross' => 0]],
])->render();
expect($html)->toContain(__('abo.onetime_empty'));
});
it('rendert die kombinierte Gesamtsumme mit getrennten Zwischensummen', function () {
$yard = Yard::instance(AboOrderCart::INSTANCE);
$yard->destroy();
$aboRow = $yard->add(1, 'Abo-Produkt', 1, 119.0, false, false, ['weight' => 100]);
Yard::setTax($aboRow->rowId, 19.0);
$oneTimeRow = $yard->add(2, 'Einmal-Produkt', 1, 59.5, false, false, ['weight' => 100, 'abo_addon' => true]);
Yard::setTax($oneTimeRow->rowId, 19.0);
$summary = AboOrderCart::getSplitSummary();
$html = view('admin.abo._order_combined_summary', ['summary' => $summary])->render();
expect($html)->toContain(__('abo.combined_summary_hl'))
->and($html)->toContain(__('abo.onetime_subtotal'))
->and($html)->toContain(__('abo.abo_subtotal'))
->and($html)->toContain(__('order.total_sum'));
$yard->destroy();
});
it('rendert die Abo-Liste im Split-Modus mit Abo-Zwischensumme und Endsumme', function () {
$abo = makeMeAbo();
$yard = Yard::instance(AboOrderCart::INSTANCE);
$yard->destroy();
$aboRow = $yard->add(1, 'Abo-Produkt', 1, 119.0, false, false, ['weight' => 100]);
Yard::setTax($aboRow->rowId, 19.0);
$summary = AboOrderCart::getSplitSummary();
$html = view('admin.abo._order_abo_show', [
'user_abo' => $abo,
'split_mode' => true,
'summary' => $summary,
'only_show_products' => false,
'add_only_mode' => false,
])->render();
expect($html)->toContain(__('abo.abo_subtotal'))
->and($html)->toContain(__('abo.abo_next_delivery_total'))
->and($html)->not->toContain(__('order.shipping_costs'))
->and($html)->not->toContain(__('order.total_sum'));
$yard->destroy();
});
it('rendert den Bestätigungsdialog für kostenpflichtige Abo-Erhöhungen', function () {
$abo = makeMeAbo();
$abo->amount = 1760;
$abo->save();
$html = view('admin.abo._confirm_add_modal', [
'user_abo' => $abo,
])->render();
expect($html)->toContain(__('abo.confirm_increase_title'))
->and($html)->toContain(__('abo.confirm_increase_info'))
->and($html)->toContain(__('abo.confirm_new_total'))
->and($html)->toContain(__('abo.confirm_legal_notice', [
'nextBillingDate' => '<span id="confirm-add-next-billing-date"></span>',
'agb' => '<a href="'.url('/agb').'" target="_blank">'.__('abo.confirm_terms_link').'</a>',
'withdrawal' => '<a href="'.asset('download/mivita_widerruf_formular.pdf').'" target="_blank">'.__('abo.confirm_withdrawal_link').'</a>',
]))
->and($html)->toContain(__('abo.confirm_add_ok'));
});
it('rendert nach einer Mengenänderung die aktuelle (persistierte) Menge – kein Stale-Cache', function () {
$abo = makeMeAbo();
$product = makeProduct(['12'], 50, 0);
$item = \App\Models\UserAboItem::create([
'user_abo_id' => $abo->id, 'product_id' => $product->id, 'comp' => 0, 'qty' => 1, 'status' => 1,
]);
// Relation cachen (wie initYard/makeOrderYard im Controller)
$abo->load('user_abo_items');
expect($abo->user_abo_items->first()->qty)->toBe(1);
// Menge über separate Instanz erhöhen (wie der Controller in updateCart/addProduct)
\App\Models\UserAboItem::find($item->id)->update(['qty' => 3]);
// Fix: Relation vor dem Render neu laden → gerenderte Menge = gespeicherter Stand
$abo->load('user_abo_items');
$html = view('admin.abo._order_abo_show', [
'user_abo' => $abo,
'split_mode' => true,
'summary' => ['abo' => ['gross' => 150], 'total_with_shipping' => 150],
])->render();
expect($html)->toContain('value="3"');
});
it('blendet Minus/Entfernen im Add-only-Modus aus (Mindestlaufzeit nicht erfüllt)', function () {
$abo = makeMeAbo();
$product = makeProduct(['12'], 50, 0);
\App\Models\UserAboItem::create([
'user_abo_id' => $abo->id, 'product_id' => $product->id, 'comp' => 0, 'qty' => 2, 'status' => 1,
]);
$abo->load('user_abo_items');
$summary = ['abo' => ['gross' => 100], 'total_with_shipping' => 100];
$addOnly = view('admin.abo._order_abo_show', [
'user_abo' => $abo, 'split_mode' => true, 'summary' => $summary, 'add_only_mode' => true,
])->render();
expect($addOnly)->not->toContain('remove-from-basket')
->and($addOnly)->not->toContain('auto-delete-product')
->and($addOnly)->toContain('add-from-basket') // Hinzufügen bleibt möglich
->and($addOnly)->toContain('readonly');
$free = view('admin.abo._order_abo_show', [
'user_abo' => $abo, 'split_mode' => true, 'summary' => $summary, 'add_only_mode' => false,
])->render();
expect($free)->toContain('remove-from-basket')
->and($free)->toContain('auto-delete-product');
});
it('friert alle Produkt-Steuerungen ein, wenn der Change-Lock greift', function () {
$abo = makeMeAbo();
$product = makeProduct(['12'], 50, 0);
\App\Models\UserAboItem::create([
'user_abo_id' => $abo->id, 'product_id' => $product->id, 'comp' => 0, 'qty' => 2, 'status' => 1,
]);
$abo->load('user_abo_items');
$html = view('admin.abo._order_abo_show', [
'user_abo' => $abo, 'split_mode' => true,
'summary' => ['abo' => ['gross' => 100], 'total_with_shipping' => 100],
'change_locked' => true,
])->render();
expect($html)->not->toContain('add-from-basket')
->and($html)->not->toContain('remove-from-basket')
->and($html)->not->toContain('auto-delete-product')
->and($html)->toContain('readonly');
});
});