seed(RolesAndPermissionsSeeder::class); }); function manualPdfInvoice(array $overrides = []): Invoice { $user = User::factory()->create(); $agreement = UserPaymentOption::factory()->create([ 'user_id' => $user->id, 'payment_option_id' => PaymentOption::factory()->create(['interval' => 'yearly'])->id, 'status' => UserPaymentOptionStatus::Grandfathered->value, 'stripe_subscription_id' => null, 'current_period_start' => '2026-08-07', 'current_period_end' => '2027-08-07', 'legacy_conditions' => ['name' => 'Unbegrenzte PMs', 'legacy_portal' => 'businessportal24', 'interval' => 'yearly', 'net_cents' => 16723], ]); return Invoice::factory()->create([ 'user_id' => $user->id, 'user_payment_id' => null, 'user_payment_option_id' => $agreement->id, 'invoice_billing_address_id' => InvoiceBillingAddress::factory()->create([ 'company' => 'Muster GmbH', 'name' => 'Erika Muster', 'salutation_key' => 'mrs', 'address1' => 'Beispielweg 1', 'postal_code' => '33739', 'city' => 'Bielefeld', 'country_code' => 'DE', 'vat_id' => 'DE123456789', ])->id, 'number' => 'MAN-00042', 'status' => InvoiceStatus::Open->value, 'amount_cents' => 16723, 'tax_cents' => 3177, 'total_cents' => 19900, 'is_netto' => false, 'tax_note' => null, 'invoice_date' => '2026-09-16', 'due_date' => '2026-09-30', 'stripe_invoice_id' => null, 'replaces_legacy_invoice_number' => '42489-10', ...$overrides, ]); } /** * Textläufe aus dem PDF-Stream (nur zum Prüfen des Inhalts). */ function pdfTextRuns(string $pdf): string { preg_match_all('/\((.*?)\) Tj/', $pdf, $matches); return implode("\n", array_map(fn (string $run): string => mb_convert_encoding($run, 'UTF-8', 'Windows-1252'), $matches[1])); } test('the MAN invoice pdf carries issuer, recipient, service period, amounts, due date and bank details', function () { $invoice = manualPdfInvoice(); $pdf = app(ManualInvoicePdfRenderer::class)->render($invoice); $text = pdfTextRuns($pdf); expect($pdf)->toStartWith('%PDF-1.4'); expect($text) ->toContain('adametz.media') ->toContain('Muster GmbH') ->toContain('Frau Erika Muster') ->toContain('USt-ID: DE123456789') ->toContain('Unbegrenzte PMs auf Businessportal24') ->toContain('Leistungszeitraum: 07.08.2026 - 06.08.2027') ->toContain('Rechnungsnummer: MAN-00042') ->toContain('Ersetzt die Rechnung Nr. 42489-10') ->toContain('167,23') ->toContain('MwSt. 19%:') ->toContain('199,00') ->toContain('bis zum 30.09.2026') ->toContain('IBAN: DE96 4805 0161 0065 0356 02') ->toContain('Rechnungsnummer MAN-00042'); }); test('a tax exempt invoice omits the VAT row and prints the tax note', function () { $invoice = manualPdfInvoice([ 'tax_cents' => 0, 'total_cents' => 16723, 'is_netto' => true, 'tax_note' => 'Steuerschuldnerschaft des Leistungsempfängers (Reverse Charge, Art. 196 MwStSystRL).', ]); $text = pdfTextRuns(app(ManualInvoicePdfRenderer::class)->render($invoice)); expect($text)->not->toContain('MwSt.') ->toContain('Reverse Charge'); }); test('the customer downloads the own MAN pdf, not foreign or stripe invoices', function () { /** @var TestCase $this */ $invoice = manualPdfInvoice(); $invoice->user->assignRole('customer'); $stranger = User::factory()->create(['is_active' => true]); $stranger->assignRole('customer'); $this->actingAs($invoice->user) ->get(route('me.invoices.manual-pdf', $invoice)) ->assertOk() ->assertHeader('Content-Type', 'application/pdf'); $this->actingAs($stranger) ->get(route('me.invoices.manual-pdf', $invoice)) ->assertForbidden(); $stripeInvoice = manualPdfInvoice(['stripe_invoice_id' => 'in_test_123', 'number' => 'STR-00001']); $stripeInvoice->user->assignRole('customer'); $this->actingAs($stripeInvoice->user) ->get(route('me.invoices.manual-pdf', $stripeInvoice)) ->assertNotFound(); }); test('billing admins download any MAN pdf via the admin route', function () { /** @var TestCase $this */ $invoice = manualPdfInvoice(); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $this->actingAs($admin) ->get(route('admin.invoices.pdf', $invoice)) ->assertOk() ->assertHeader('Content-Type', 'application/pdf'); });