create(['is_active' => true]); UserPaymentOption::factory()->create([ 'user_id' => $customer->id, 'status' => UserPaymentOptionStatus::Active->value, 'current_period_start' => now()->subDay(), 'current_period_end' => now()->addMonth(), ]); $this->actingAs($customer); Livewire::test('customer.tokens') ->assertSee('API-Tokens') ->set('tokenName', 'Website Integration') ->set('selectedAbilities', ['press-releases:read', 'companies:read']) ->set('securityPassword', 'password') ->call('createToken') ->assertHasNoErrors() ->assertSee('Token wurde erstellt'); $token = $customer->tokens()->firstOrFail(); expect($token->name)->toBe('Website Integration'); expect($token->abilities)->toBe(['press-releases:read', 'companies:read']); Livewire::test('customer.tokens') ->call('revokeToken', $token->id) ->assertSee('Token wurde widerrufen.'); expect($customer->tokens()->count())->toBe(0); }); test('the tokens page links the api docs page and the named spec download', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $this->actingAs($customer); // Doku-Button öffnet die API-Seite; der Spec-Download ist ein eigener, // klar benannter Button (kein Sofort-Download hinter „Dokumentation“). Livewire::test('customer.tokens') ->assertSee('API-Dokumentation') ->assertSee(route('api', ['edition' => 'de'])) ->assertSee('OpenAPI-Spezifikation (YAML)') ->assertSee(route('docs.api.v1')); }); test('customer cannot create api token without active payment access', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $this->actingAs($customer); Livewire::test('customer.tokens') ->set('tokenName', 'Website Integration') ->set('selectedAbilities', ['press-releases:read']) ->call('createToken') ->assertSee('API-Tokens werden erst freigeschaltet'); expect($customer->tokens()->count())->toBe(0); }); test('api tokens cannot be created or revoked during impersonation', function () { /** @var TestCase $this */ $admin = User::factory()->create(); $customer = User::factory()->create(['is_active' => true]); UserPaymentOption::factory()->create([ 'user_id' => $customer->id, 'status' => UserPaymentOptionStatus::Active->value, 'current_period_start' => now()->subDay(), 'current_period_end' => now()->addMonth(), ]); $token = $customer->createToken('Existing token', ['press-releases:read'])->accessToken; $this->actingAs($customer) ->withSession([UserImpersonation::SessionKey => $admin->id]); Livewire::test('customer.tokens') ->assertSee('Während der Benutzer-Impersonation können keine API-Tokens erstellt oder widerrufen werden.') ->set('tokenName', 'Impersonated token') ->set('selectedAbilities', ['press-releases:read']) ->call('createToken') ->assertForbidden(); Livewire::test('customer.tokens') ->call('revokeToken', $token->id) ->assertForbidden(); expect($customer->tokens()->pluck('id')->all())->toBe([$token->id]); }); test('customer sees only own legacy invoices', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $otherUser = User::factory()->create(['is_active' => true]); LegacyInvoice::query()->create([ 'legacy_portal' => 'presseecho', 'legacy_id' => 1001, 'user_id' => $customer->id, 'legacy_user_id' => 501, 'number' => 'RE-CUSTOMER-001', 'amount_cents' => 9900, 'total_cents' => 9900, 'status' => 'paid', 'invoice_date' => now()->subMonth(), 'paid_at' => now()->subMonth()->addDays(2), 'imported_at' => now(), ]); LegacyInvoice::query()->create([ 'legacy_portal' => 'businessportal24', 'legacy_id' => 1002, 'user_id' => $otherUser->id, 'legacy_user_id' => 502, 'number' => 'RE-OTHER-001', 'amount_cents' => 14900, 'total_cents' => 14900, 'status' => 'paid', 'invoice_date' => now()->subMonth(), 'paid_at' => now()->subMonth()->addDays(2), 'imported_at' => now(), ]); // Nie bezahlte Alt-Rechnung (hold): erledigt, erscheint dem Kunden nicht. LegacyInvoice::query()->create([ 'legacy_portal' => 'presseecho', 'legacy_id' => 1003, 'user_id' => $customer->id, 'legacy_user_id' => 501, 'number' => 'RE-CUSTOMER-HOLD', 'amount_cents' => 9900, 'total_cents' => 9900, 'status' => 'hold', 'invoice_date' => now()->subYears(2), 'imported_at' => now(), ]); $this->actingAs($customer); Livewire::test('customer.invoices') ->assertSee('RE-CUSTOMER-001') ->assertDontSee('RE-OTHER-001') ->assertDontSee('RE-CUSTOMER-HOLD') ->assertSee('99,00 €'); }); test('the legacy invoice archive is hidden for customers without archived invoices', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); // Archiv eines anderen Users darf die Anzeige nicht auslösen. LegacyInvoice::query()->create([ 'legacy_portal' => 'presseecho', 'legacy_id' => 1003, 'user_id' => User::factory()->create(['is_active' => true])->id, 'legacy_user_id' => 503, 'number' => 'RE-FREMD-001', 'amount_cents' => 9900, 'total_cents' => 9900, 'status' => 'paid', 'invoice_date' => now()->subMonth(), 'paid_at' => now()->subMonth(), 'imported_at' => now(), ]); $this->actingAs($customer); Livewire::test('customer.invoices') ->assertSee('Aktuelle Rechnungen') ->assertDontSee('Rechnungsarchiv (Altsystem)') ->assertDontSee('Filter & Suche'); }); test('current STR and MAN invoices are listed with a stripe link where available', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $strInvoice = Invoice::factory()->create([ 'user_id' => $customer->id, 'user_payment_id' => null, 'number' => 'STR-00042', 'status' => InvoiceStatus::Paid->value, 'amount_cents' => 2900, 'tax_cents' => 551, 'total_cents' => 3451, 'stripe_invoice_id' => 'in_test_42', ]); Invoice::factory()->create([ 'user_id' => $customer->id, 'user_payment_id' => null, 'number' => 'MAN-00007', 'status' => InvoiceStatus::Paid->value, 'stripe_invoice_id' => null, ]); Invoice::factory()->create([ 'user_payment_id' => null, 'number' => 'STR-99999', 'stripe_invoice_id' => 'in_test_other', ]); $this->actingAs($customer); Livewire::test('customer.invoices') ->assertSee('Aktuelle Rechnungen') ->assertSee('STR-00042') ->assertSee('MAN-00007') ->assertDontSee('STR-99999') ->assertSee('PDF öffnen') ->assertSee(route('me.invoices.stripe-pdf', $strInvoice)) ->assertSee('Beleg bei Stripe') ->assertSee(route('me.invoices.stripe', $strInvoice)); }); test('the stripe invoice redirect rejects foreign invoices and ones without stripe id', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); $customer = User::factory()->create(['is_active' => true]); $customer->assignRole('customer'); $intruder = User::factory()->create(['is_active' => true]); $intruder->assignRole('customer'); $strInvoice = Invoice::factory()->create([ 'user_id' => $customer->id, 'user_payment_id' => null, 'stripe_invoice_id' => 'in_test_43', ]); $manInvoice = Invoice::factory()->create([ 'user_id' => $customer->id, 'user_payment_id' => null, 'stripe_invoice_id' => null, ]); $this->actingAs($intruder) ->get(route('me.invoices.stripe', $strInvoice)) ->assertNotFound(); $this->actingAs($customer) ->get(route('me.invoices.stripe', $manInvoice)) ->assertNotFound(); }); test('customer can open generated legacy invoice pdf inline from archive data', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); $customer = User::factory()->create(['is_active' => true]); $customer->assignRole('customer'); $invoice = LegacyInvoice::query()->create([ 'legacy_portal' => 'presseecho', 'legacy_id' => 1003, 'user_id' => $customer->id, 'legacy_user_id' => 503, 'number' => 'RE-CUSTOMER-002', 'amount_cents' => 9900, 'total_cents' => 9900, 'status' => 'paid', 'invoice_date' => now()->subMonth(), 'due_date' => now()->subMonth()->addDays(14), 'paid_at' => now()->subMonth()->addDays(2), 'payment_method' => 'SPK_Berlin', 'raw_snapshot' => [ 'number' => 'RE-CUSTOMER-002', 'service_period_begin_date' => now()->subYear()->toDateString(), 'service_period_end_date' => now()->toDateString(), ], 'pdf_payload' => [ 'billing_address' => [ 'name' => 'Customer GmbH', 'address' => 'Kundenstrasse 1', 'postal_code' => '10115', 'city' => 'Berlin', 'country_name' => 'Deutschland', ], ], 'imported_at' => now(), ]); $this->actingAs($customer); Livewire::test('customer.invoices') ->assertSee('Rechnungsarchiv (Altsystem)') ->assertSee('Rechnungsadresse im Profil pflegen') ->assertSee('Öffnen'); $this->get(route('me.invoices.pdf', $invoice)) ->assertSuccessful() ->assertHeader('content-type', 'application/pdf') ->assertHeader('content-disposition', 'inline; filename="Presseecho-RNr-RE-CUSTOMER-002.pdf"'); expect($invoice->refresh()->pdf_generated_at)->not->toBeNull(); }); test('creating an api token requires a fresh identity proof (Sicherheitsaudit S-06)', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); UserPaymentOption::factory()->create([ 'user_id' => $customer->id, 'status' => UserPaymentOptionStatus::Active->value, 'current_period_start' => now()->subDay(), 'current_period_end' => now()->addMonth(), ]); $this->actingAs($customer); Livewire::test('customer.tokens') ->set('tokenName', 'Ohne Nachweis') ->set('selectedAbilities', ['press-releases:read']) ->call('createToken') ->assertHasErrors(['securityPassword']); Livewire::test('customer.tokens') ->set('tokenName', 'Falsches Passwort') ->set('selectedAbilities', ['press-releases:read']) ->set('securityPassword', 'falsch') ->call('createToken') ->assertHasErrors(['securityPassword']); expect($customer->tokens()->count())->toBe(0); });