From e730d6db63cda05a53a205220e3e06ad7026bb39 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20G=C3=BCnrher?=
Date: Sun, 6 Sep 2026 16:48:33 +0200
Subject: [PATCH] Korrektur Buchungstexte bei Auslagenerstattungen
---
.../CostUnit/Controllers/ExportController.php | 2 +-
app/Models/Invoice.php | 20 ++++++
app/Resources/InvoiceResource.php | 2 +-
tests/Feature/RefundInvoiceTest.php | 61 +++++++++++++++++++
4 files changed, 83 insertions(+), 2 deletions(-)
diff --git a/app/Domains/CostUnit/Controllers/ExportController.php b/app/Domains/CostUnit/Controllers/ExportController.php
index 5d14f4c..d258088 100644
--- a/app/Domains/CostUnit/Controllers/ExportController.php
+++ b/app/Domains/CostUnit/Controllers/ExportController.php
@@ -93,7 +93,7 @@ class ExportController extends CommonController {
'amount' => $invoice->amount,
'recipient_name' => $invoice->contact_bank_owner,
'recipient_iban' => $invoice->contact_bank_iban,
- 'payment_purpose' => $invoice->payment_purpose ?? 'Auslagenerstattung Rechnungsnummer ' . $invoice->invoice_number,
+ 'payment_purpose' => $invoice->paymentPurposeText(),
]);
}
}
diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php
index 2639720..ddfa818 100644
--- a/app/Models/Invoice.php
+++ b/app/Models/Invoice.php
@@ -71,6 +71,26 @@ class Invoice extends InstancedModel
'denied_reason',
];
+ /**
+ * Der Verwendungszweck für Überweisung, Buchungstext und Anzeige.
+ *
+ * Der Freitext gewinnt, wenn einer erfasst wurde. Sonst benennt der Text den Vorgang: eine
+ * Beitragserstattung ist keine Auslage des Teilis, sondern die Rücknahme seiner Zahlung -- auf dem
+ * Kontoauszug muss der Unterschied erkennbar sein. Genannt wird die Abrechnungsnummer, und zwar als
+ * Belegnummer: unter "Rechnungsnummer" gibt es sie nirgends.
+ */
+ public function paymentPurposeText() : string {
+ if ($this->payment_purpose !== null) {
+ return $this->payment_purpose;
+ }
+
+ $subject = $this->type === InvoiceType::INVOICE_TYPE_PARTICIPATION_REFUND
+ ? 'Beitragserstattung'
+ : 'Auslagenerstattung';
+
+ return $subject . ' Belegnummer ' . $this->invoice_number;
+ }
+
public function costUnit() : BelongsTo{
return $this->belongsTo(CostUnit::class);
}
diff --git a/app/Resources/InvoiceResource.php b/app/Resources/InvoiceResource.php
index 14b46a1..59810b7 100644
--- a/app/Resources/InvoiceResource.php
+++ b/app/Resources/InvoiceResource.php
@@ -39,7 +39,7 @@ class InvoiceResource {
$returnData['id'] = $this->invoice->id;
$returnData['donation'] = $this->invoice->donation;
$returnData['externalPayment'] = null !== $this->invoice->payment_purpose;
- $returnData['paymentPurpose'] = $this->invoice->payment_purpose ?? 'Auslagenerstattung Rechnungsnummer ' . $returnData['invoiceNumber'];
+ $returnData['paymentPurpose'] = $this->invoice->paymentPurposeText();
$returnData['accountOwner'] = $this->invoice->contact_bank_owner ?? '--';
$returnData['accountIban'] = $this->invoice->contact_bank_iban ?? '--';
$returnData['status'] = $this->invoice->status;
diff --git a/tests/Feature/RefundInvoiceTest.php b/tests/Feature/RefundInvoiceTest.php
index 936297b..ceb7f21 100644
--- a/tests/Feature/RefundInvoiceTest.php
+++ b/tests/Feature/RefundInvoiceTest.php
@@ -70,6 +70,16 @@ class RefundInvoiceTest extends TestCase
DB::table('participation_fee_types')->insert(['slug' => 'fixed', 'name' => 'Fix']);
DB::table('cost_unit_types')->insert(['slug' => CostUnitType::COST_UNIT_TYPE_EVENT, 'name' => 'Veranstaltung']);
DB::table('invoice_status')->insert(['slug' => InvoiceStatus::INVOICE_STATUS_NEW]);
+
+ // Ein gewöhnlicher Aufwandstyp zum Vergleich; die Beitragserstattung bringt die Migration mit.
+ DB::table('invoice_types')->insert([
+ 'slug' => InvoiceType::INVOICE_TYPE_TRAVELLING,
+ 'name' => 'Fahrtkosten',
+ 'sort_order' => 1,
+ 'selectable' => true,
+ 'counts_as_expense' => true,
+ ]);
+
PaymentMethod::create(['slug' => PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION]);
EfzStatus::create(['slug' => EfzStatus::EFZ_STATUS_NOT_REQUIRED, 'name' => 'Nicht erforderlich']);
@@ -433,4 +443,55 @@ class RefundInvoiceTest extends TestCase
$this->assertStringContainsString('300,00 Euro', Invoice::first()->comment);
$this->assertEqualsWithDelta(80.0, $refund->participant->fresh()->amount_paid->getAmount(), 0.001);
}
+
+ /*
+ |--------------------------------------------------------------------------
+ | Der Verwendungszweck der Überweisung
+ |--------------------------------------------------------------------------
+ */
+
+ public function test_the_payment_purpose_of_a_refund_names_the_refund(): void
+ {
+ $this->runRefund();
+
+ $invoice = Invoice::first();
+
+ // Auf dem Kontoauszug des Teilis muss der Vorgang stehen, den es gab: Er hatte keine Auslage,
+ // er bekommt seinen Beitrag zurück.
+ $this->assertSame(
+ 'Beitragserstattung Belegnummer ' . $invoice->invoice_number,
+ $invoice->paymentPurposeText()
+ );
+ }
+
+ public function test_an_ordinary_invoice_keeps_the_expense_wording(): void
+ {
+ $invoice = Invoice::create([
+ 'tenant' => $this->tenant->slug,
+ 'cost_unit_id' => $this->makeCostUnit()->id,
+ 'invoice_number' => '2026-0042',
+ 'status' => InvoiceStatus::INVOICE_STATUS_NEW,
+ 'type' => InvoiceType::INVOICE_TYPE_TRAVELLING,
+ 'contact_name' => 'Mika Muster',
+ 'amount' => 42.0,
+ ]);
+
+ $this->assertSame('Auslagenerstattung Belegnummer 2026-0042', $invoice->paymentPurposeText());
+ }
+
+ public function test_a_free_text_purpose_wins(): void
+ {
+ $invoice = Invoice::create([
+ 'tenant' => $this->tenant->slug,
+ 'cost_unit_id' => $this->makeCostUnit()->id,
+ 'invoice_number' => '2026-0043',
+ 'status' => InvoiceStatus::INVOICE_STATUS_NEW,
+ 'type' => InvoiceType::INVOICE_TYPE_TRAVELLING,
+ 'contact_name' => 'Mika Muster',
+ 'amount' => 42.0,
+ 'payment_purpose' => 'Sommerlager',
+ ]);
+
+ $this->assertSame('Sommerlager', $invoice->paymentPurposeText());
+ }
}