presseportale/tests/Feature/Billing/AdminPaymentsPageTest.php
Kevin Adametz 2cde7284a1 livewire: Volt entfernt — Komponenten als Livewire-4-Single-File-Components, Route::livewire(), Livewire::test(); livewire/volt deinstalliert
- 83 Komponenten: Livewire\Volt\Component → Livewire\Component
- 73 Routen (admin, auth, customer): Volt::route() → Route::livewire()
- 88 Testdateien: Volt::test/actingAs → Livewire::test/actingAs
- VoltServiceProvider gelöscht; composer remove livewire/volt
- config/livewire.php auf v4-Schlüssel (component_layout, component_placeholder,
  component_locations, make_command: SFC ohne ⚡). Der alte Schlüssel layout war
  wirkungslos; alle gerouteten Komponenten setzen ihr Layout selbst.
- CLAUDE.md, AGENTS.md, boost.json, .cursorrules, Skill volt-development
- Roadmap: erledigt; Larastan erfasst SFCs weiterhin nicht (Blade-Dateien)

Suite 2.325 grün, einer übersprungen; Larastan und Pint ohne Meldung;
route:cache und view:cache laufen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 13:59:36 +00:00

366 lines
14 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\InvoiceStatus;
use App\Enums\UserPaymentOptionStatus;
use App\Mail\GrandfatheringTerminated;
use App\Mail\ManualInvoiceIssued;
use App\Models\BillingAddress;
use App\Models\Invoice;
use App\Models\InvoiceBillingAddress;
use App\Models\PaymentOption;
use App\Models\Plan;
use App\Models\SinglePurchase;
use App\Models\User;
use App\Models\UserPaymentOption;
use Database\Seeders\RolesAndPermissionsSeeder;
use Illuminate\Support\Facades\Mail;
use Livewire\Livewire;
use Tests\TestCase;
beforeEach(function (): void {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
});
function paymentsPageAdmin(): User
{
$admin = User::factory()->create(['is_active' => true]);
$admin->assignRole('admin');
return $admin;
}
test('the payments page shows subscriptions with plan name and mrr', function () {
/** @var TestCase $this */
$customer = User::factory()->create(['name' => 'Abo Kunde']);
$plan = Plan::factory()->create([
'name' => 'Business',
'monthly_price_cents' => 4900,
'stripe_price_id_monthly' => 'price_test_m_biz',
]);
subscribeUserToPlan($customer, $plan);
$this->actingAs(paymentsPageAdmin());
Livewire::test('admin.payments.index')
->assertSee('Aktive Abos')
->assertSee('MRR (netto)')
->assertSee('49,00 €')
->assertSee('Abo Kunde')
->assertSee('Business')
->assertSee('monatlich');
});
test('a yearly subscription contributes one twelfth to the mrr', function () {
/** @var TestCase $this */
$customer = User::factory()->create();
$plan = Plan::factory()->create([
'monthly_price_cents' => 4900,
'yearly_price_cents' => 49000,
'stripe_price_id_monthly' => 'price_test_m_y',
'stripe_price_id_yearly' => 'price_test_y_y',
]);
subscribeUserToPlan($customer, $plan, 'yearly');
$this->actingAs(paymentsPageAdmin());
Livewire::test('admin.payments.index')
->assertSee('40,83 €')
->assertSee('jährlich');
});
test('single purchases appear with type and status', function () {
/** @var TestCase $this */
$customer = User::factory()->create(['name' => 'Einzel Käufer']);
SinglePurchase::factory()->paid()->create(['user_id' => $customer->id]);
$this->actingAs(paymentsPageAdmin());
Livewire::test('admin.payments.index')
->assertSee('Einzel Käufer')
->assertSee('Einzel-Pressemitteilung')
->assertSee('Bezahlt')
->assertSee('19,00 €');
});
test('local invoices appear with number and circle badge', function () {
/** @var TestCase $this */
$customer = User::factory()->create();
Invoice::factory()->create([
'user_id' => $customer->id,
'number' => 'STR-2026-000042',
'status' => InvoiceStatus::Paid->value,
'paid_at' => now(),
'stripe_invoice_id' => 'in_test_123',
]);
Invoice::factory()->create([
'user_id' => $customer->id,
'number' => 'MAN-2026-000007',
'status' => InvoiceStatus::Open->value,
'stripe_invoice_id' => null,
]);
$this->actingAs(paymentsPageAdmin());
// Default ist der Bearbeitungsstapel (offene MAN-Rechnungen) …
Livewire::test('admin.payments.index')
->assertSee('MAN-2026-000007')
->assertSee('Manuell (MAN)')
->assertDontSee('STR-2026-000042')
// … der Filter „Alle" zeigt auch den STR-Spiegel.
->set('invoiceFilter', 'all')
->assertSee('STR-2026-000042')
->assertSee('Stripe (STR)')
->assertSee('MAN-2026-000007');
});
test('paid invoices of the last 30 days are summed as revenue', function () {
/** @var TestCase $this */
Invoice::factory()->create([
'status' => InvoiceStatus::Paid->value,
'paid_at' => now()->subDays(5),
'total_cents' => 11900,
]);
// Außerhalb des 30-Tage-Fensters: erscheint in der Tabelle,
// zählt aber nicht in die Umsatz-KPI (sonst stünde dort 1.118,00 €).
Invoice::factory()->create([
'status' => InvoiceStatus::Paid->value,
'paid_at' => now()->subDays(60),
'total_cents' => 99900,
]);
$this->actingAs(paymentsPageAdmin());
Livewire::test('admin.payments.index')
->assertSee('Umsatz 30 Tage')
->assertSee('119,00 €')
->assertDontSee('1.118,00 €');
});
test('the search filters all panels by user name or email', function () {
/** @var TestCase $this */
$match = User::factory()->create(['name' => 'Maria Treffer']);
$other = User::factory()->create(['name' => 'Olaf Anders']);
SinglePurchase::factory()->paid()->create(['user_id' => $match->id]);
SinglePurchase::factory()->paid()->create(['user_id' => $other->id]);
$this->actingAs(paymentsPageAdmin());
Livewire::test('admin.payments.index')
->set('search', 'Maria')
->assertSee('Maria Treffer')
->assertDontSee('Olaf Anders');
});
test('the payments page is not accessible for customers', function () {
/** @var TestCase $this */
$customer = User::factory()->create(['is_active' => true]);
$customer->assignRole('customer');
$this->actingAs($customer)
->get(route('admin.payments.index'))
->assertForbidden();
});
/**
* Offene MAN-Rechnung samt Bestandsschutz-Posten eines Überweisungskunden.
*
* @return array{0: Invoice, 1: UserPaymentOption, 2: User}
*/
function openManualInvoice(array $overrides = []): array
{
$customer = User::factory()->create(['name' => 'Ulla Überweisung']);
$agreement = UserPaymentOption::factory()->create([
'user_id' => $customer->id,
'payment_option_id' => PaymentOption::factory()->create(['interval' => 'yearly'])->id,
'status' => UserPaymentOptionStatus::Grandfathered->value,
'stripe_subscription_id' => null,
'cancelled_at' => null,
'current_period_start' => today(),
'current_period_end' => today()->addYear(),
'legacy_conditions' => ['name' => 'Unbegrenzte PMs', 'legacy_portal' => 'businessportal24', 'interval' => 'yearly', 'net_cents' => 16723],
]);
$invoice = Invoice::factory()->create([
'user_id' => $customer->id,
'user_payment_id' => null,
'user_payment_option_id' => $agreement->id,
'invoice_billing_address_id' => InvoiceBillingAddress::factory(),
'number' => 'MAN-00021',
'status' => InvoiceStatus::Open->value,
'total_cents' => 19900,
'stripe_invoice_id' => null,
'sent_at' => null,
'due_date' => today()->addDays(14),
...$overrides,
]);
return [$invoice, $agreement, $customer];
}
test('an unsent MAN invoice offers send, paid and terminate; sending mails the customer and sets the due date', function () {
/** @var TestCase $this */
Mail::fake();
[$invoice, , $customer] = openManualInvoice();
$this->actingAs(paymentsPageAdmin());
Livewire::test('admin.payments.index')
->assertSee('Offene MAN-Rechnungen')
->assertSee('MAN-00021')
->assertSee('noch nicht gesendet')
->assertSee('Rechnung senden')
->assertSee('Bestandsschutz beenden')
->assertSee(route('admin.invoices.pdf', $invoice))
->call('sendInvoice', $invoice->id)
->assertSet('notificationType', 'success')
->assertSee('gesendet '.today()->format('d.m.Y'))
->assertSee('zahlbar bis '.today()->addDays(14)->format('d.m.Y'))
->assertSee('Erinnerung');
Mail::assertQueued(ManualInvoiceIssued::class, fn (ManualInvoiceIssued $mail): bool => $mail->hasTo($customer->email));
expect($invoice->fresh()->sent_at)->not->toBeNull();
});
test('a sent MAN invoice can be reminded and marked as paid with a payment date', function () {
/** @var TestCase $this */
Mail::fake();
[$invoice] = openManualInvoice(['sent_at' => now()->subDays(20), 'due_date' => today()->subDays(6)]);
$this->actingAs(paymentsPageAdmin());
Livewire::test('admin.payments.index')
->assertSee('Überfällig')
->call('remindInvoice', $invoice->id)
->assertSet('notificationType', 'success')
->assertSee('erinnert '.today()->format('d.m.Y').' (1×)')
->set('paidAtInput', today()->subDay()->toDateString())
->call('markInvoicePaid', $invoice->id)
->assertSet('notificationType', 'success')
// Bezahlt → der Bearbeitungsstapel ist leer …
->assertSee('Keine offenen MAN-Rechnungen')
->set('invoiceFilter', 'all')
->assertSee('MAN-00021')
->assertSee('bezahlt: '.today()->subDay()->format('d.m.Y'));
Mail::assertQueued(ManualInvoiceIssued::class, fn (ManualInvoiceIssued $mail): bool => $mail->isReminder);
expect($invoice->fresh()->status)->toBe(InvoiceStatus::Paid);
});
test('a future payment date is rejected by validation', function () {
/** @var TestCase $this */
[$invoice] = openManualInvoice(['sent_at' => now()]);
$this->actingAs(paymentsPageAdmin());
Livewire::test('admin.payments.index')
->set('paidAtInput', 'kein-datum')
->call('markInvoicePaid', $invoice->id)
->assertHasErrors(['paidAtInput']);
expect($invoice->fresh()->status)->toBe(InvoiceStatus::Open);
});
test('terminating from the page voids the invoice, cancels the bundle and honours the notify checkbox', function () {
/** @var TestCase $this */
Mail::fake();
[$invoice, $agreement, $customer] = openManualInvoice(['sent_at' => now()->subDays(20), 'due_date' => today()->subDays(6)]);
$this->actingAs(paymentsPageAdmin());
Livewire::test('admin.payments.index')
->assertSee('Ulla Überweisung')
->set('terminateNotify', false)
->set('terminateNote', 'Nicht erreichbar.')
->call('terminateInvoice', $invoice->id)
->assertSet('notificationType', 'success')
->assertSee('1 Bestandsschutz-Posten gekündigt')
->assertSee('Keine offenen MAN-Rechnungen')
->assertSee('Keine laufenden Bestandsschutz-Posten');
expect($invoice->fresh()->status)->toBe(InvoiceStatus::Void);
expect($invoice->fresh()->void_reason)->toContain('Nicht erreichbar.');
expect($agreement->fresh()->status)->toBe(UserPaymentOptionStatus::Cancelled);
Mail::assertNotQueued(GrandfatheringTerminated::class);
});
test('service refusals surface as an error notification instead of an exception', function () {
/** @var TestCase $this */
Mail::fake();
[$invoice] = openManualInvoice();
$this->actingAs(paymentsPageAdmin());
Livewire::test('admin.payments.index')
->call('remindInvoice', $invoice->id)
->assertSet('notificationType', 'error')
->assertSee('noch nicht versendet');
Mail::assertNothingQueued();
});
test('due agreements the daily run could not invoice are listed as blocked and can be invoiced once the address exists', function () {
/** @var TestCase $this */
$customer = User::factory()->create(['name' => 'Bianca Blockiert']);
$agreement = UserPaymentOption::factory()->create([
'user_id' => $customer->id,
'payment_option_id' => PaymentOption::factory()->create(['interval' => 'yearly', 'price_cents' => 4118])->id,
'status' => UserPaymentOptionStatus::Grandfathered->value,
'stripe_subscription_id' => null,
'cancelled_at' => null,
'current_period_start' => today()->subYear()->subMonth(),
'current_period_end' => today()->subMonth(),
'legacy_conditions' => ['name' => 'Kleine Mappe', 'legacy_portal' => 'presseecho', 'interval' => 'yearly', 'net_cents' => 4118],
]);
$this->actingAs(paymentsPageAdmin());
$page = Livewire::test('admin.payments.index')
->assertSee('Blockierte Vereinbarungen')
->assertSee('Bianca Blockiert')
->assertSee('Keine Rechnungsadresse')
->call('invoiceNow', $agreement->id)
->assertSet('notificationType', 'error');
BillingAddress::factory()->create(['user_id' => $customer->id, 'country_code' => 'DE']);
$page->call('invoiceNow', $agreement->id)
->assertSet('notificationType', 'success')
->assertDontSee('Blockierte Vereinbarungen')
->assertSee('MAN-00001')
->assertSee('noch nicht gesendet');
expect(Invoice::query()->manual()->open()->where('user_payment_option_id', $agreement->id)->exists())->toBeTrue();
});
test('a blocked agreement can be removed without invoicing', function () {
/** @var TestCase $this */
Mail::fake();
$customer = User::factory()->create(['name' => 'Rita Rückläufer']);
$agreement = UserPaymentOption::factory()->create([
'user_id' => $customer->id,
'payment_option_id' => PaymentOption::factory()->create(['interval' => 'yearly'])->id,
'status' => UserPaymentOptionStatus::Grandfathered->value,
'stripe_subscription_id' => null,
'cancelled_at' => null,
'current_period_start' => today()->subYear()->subMonth(),
'current_period_end' => today()->subMonth(),
'legacy_conditions' => ['name' => 'Unbegrenzte PMs', 'legacy_portal' => 'businessportal24', 'interval' => 'yearly', 'net_cents' => 16723],
]);
$this->actingAs(paymentsPageAdmin());
Livewire::test('admin.payments.index')
->assertSee('Blockierte Vereinbarungen')
->assertSee('Rita Rückläufer')
->assertSee('Entfernen')
->set('abandonNote', 'Nicht erreichbar.')
->call('abandonAgreement', $agreement->id)
->assertSet('notificationType', 'success')
->assertSee('keine Rechnung gestellt')
->assertDontSee('Blockierte Vereinbarungen')
->assertSee('Keine laufenden Bestandsschutz-Posten');
expect($agreement->fresh()->status)->toBe(UserPaymentOptionStatus::Cancelled);
expect(Invoice::count())->toBe(0);
Mail::assertNothingQueued();
});