tenant = Tenant::create([ 'slug' => 'wm', 'name' => 'Wilde Möhre', 'address_1' => 'Musterweg 1', 'email' => 't@example.com', 'email_finance' => 'finance@example.com', 'url' => parse_url(config('app.url'), PHP_URL_HOST), 'account_name' => 'Test e.V.', 'account_iban' => 'DE00', 'account_bic' => 'XY', 'city' => 'Stadt', 'postcode' => '00000', 'invoice_prefix' => 'WM', 'is_active_local_group' => true, 'has_active_instance' => true, ]); app()->instance('tenant', $this->tenant); DB::table('cost_unit_types')->insert(['slug' => CostUnitType::COST_UNIT_TYPE_EVENT, 'name' => 'Veranstaltung']); DB::table('invoice_status')->insert(['slug' => InvoiceStatus::INVOICE_STATUS_NEW]); // Die Beitragserstattung bringt die Migration mit; die beiden anderen Typen nicht. foreach ([ InvoiceType::INVOICE_TYPE_TRAVELLING => 'Fahrtkosten', InvoiceType::INVOICE_TYPE_OTHER => 'Sonstige Kosten', ] as $slug => $name) { DB::table('invoice_types')->insert([ 'slug' => $slug, 'name' => $name, 'sort_order' => 1, 'selectable' => true, 'counts_as_expense' => true, ]); } } private function makeCostUnit(): CostUnit { return CostUnit::create([ 'tenant' => $this->tenant->slug, 'name' => 'Sommerlager', 'type' => CostUnitType::COST_UNIT_TYPE_EVENT, 'distance_allowance' => 0.25, 'mail_on_new' => false, 'allow_new' => true, 'archived' => false, ]); } private function purposeOf(array $attributes): string { $this->sequence++; $invoice = Invoice::create(array_merge([ 'tenant' => $this->tenant->slug, 'cost_unit_id' => $this->makeCostUnit()->id, 'invoice_number' => sprintf('2026-%04d', $this->sequence), 'status' => InvoiceStatus::INVOICE_STATUS_NEW, 'contact_name' => 'Max Mustermann', 'amount' => 42.0, ], $attributes)); return new InvoiceResource($invoice)->toArray()['purpose']; } public function test_an_expense_shows_what_was_bought(): void { $this->assertSame('Bastelmaterial Sippenstunde', $this->purposeOf([ 'type' => InvoiceType::INVOICE_TYPE_OTHER, 'type_other' => 'Bastelmaterial Sippenstunde', ])); } public function test_travel_costs_show_the_reason_and_who_travelled(): void { // `type_other` bleibt bei Fahrtkosten leer -- die Strecke landet in `travel_direction`. $this->assertSame('Landeslager — Max Mustermann', $this->purposeOf([ 'type' => InvoiceType::INVOICE_TYPE_TRAVELLING, 'travel_direction' => 'Halle – Leipzig', 'travel_reason' => 'Landeslager', ])); } public function test_travel_costs_without_a_reason_still_name_the_person(): void { // Altbestand: der Reisegrund wurde erst später zur Pflicht. Kein führendes " — ". $this->assertSame('Max Mustermann', $this->purposeOf([ 'type' => InvoiceType::INVOICE_TYPE_TRAVELLING, 'travel_direction' => 'Halle – Leipzig', ])); } public function test_an_invoice_without_a_purpose_stays_empty(): void { // Beitragserstattungen entstehen ohne Freitext, ältere Belege haben keinen. Ein "--" würde in // der Liste nur Platz kosten. $this->assertSame('', $this->purposeOf([ 'type' => InvoiceType::INVOICE_TYPE_PARTICIPATION_REFUND, ])); } }