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 1/6] 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()); + } } -- 2.54.0 From b9795f08f0a81830d96de0e95831f6327fbad911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnrher?= Date: Sun, 6 Sep 2026 20:11:50 +0200 Subject: [PATCH 2/6] =?UTF-8?q?Besseres=20Handling=20R=C3=BCckerstattungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Event/Views/Partials/ParticipantData.vue | 4 +- .../Event/Views/Partials/ParticipantsList.vue | 16 +- .../AcceptRefund/AcceptRefundCommand.php | 54 +++- .../AcceptRefund/AcceptRefundRequest.php | 22 +- .../CreateRefundDocumentCommand.php | 72 ++++- .../CreateRefundDocumentRequest.php | 8 + .../ReleaseRefund/ReleaseRefundCommand.php | 31 +- .../ReleaseRefund/ReleaseRefundRequest.php | 13 + .../Controllers/AcceptRefundController.php | 2 + .../Controllers/RefundDocumentController.php | 6 +- .../Controllers/RefundPageController.php | 2 + .../Controllers/ReleaseRefundController.php | 3 + .../ParticipantRefundTokens.php | 3 +- .../ParticipantRefund/Views/RefundPage.vue | 274 +++++++++++++++--- .../RefundAcceptedMail.php | 7 +- app/Models/ParticipantRefund.php | 12 + app/Resources/ParticipantRefundResource.php | 3 + ..._add_refund_account_and_donation_texts.php | 50 ++++ .../participant-refund-template.sql | 10 +- .../emails/events/refund_accepted.blade.php | 57 +++- .../emails/events/refund_released.blade.php | 19 +- tests/Feature/ParticipantRefundTest.php | 127 ++++++++ tests/Feature/RefundDirectCaptureTest.php | 88 ++++++ tests/Feature/RefundDocumentTest.php | 64 +++- tests/Feature/RefundInvoiceTest.php | 83 +++++- tests/Feature/RefundRetentionTest.php | 1 + 26 files changed, 927 insertions(+), 104 deletions(-) create mode 100644 database/migrations/2026_09_10_140010_add_refund_account_and_donation_texts.php diff --git a/app/Domains/Event/Views/Partials/ParticipantData.vue b/app/Domains/Event/Views/Partials/ParticipantData.vue index 98eed73..eb5b6b6 100644 --- a/app/Domains/Event/Views/Partials/ParticipantData.vue +++ b/app/Domains/Event/Views/Partials/ParticipantData.vue @@ -386,7 +386,9 @@ function saveParticipant() { Bankverbindung des Teilis - bestätigt am {{ props.participant.refund.acceptedAt }} + bestätigt am {{ props.participant.refund.acceptedAt }} diff --git a/app/Domains/Event/Views/Partials/ParticipantsList.vue b/app/Domains/Event/Views/Partials/ParticipantsList.vue index 5783a92..6bf9f17 100644 --- a/app/Domains/Event/Views/Partials/ParticipantsList.vue +++ b/app/Domains/Event/Views/Partials/ParticipantsList.vue @@ -458,6 +458,8 @@ async function execRefund() { // Leer beim Weg über den Teili -- dann verschickt der Server nur den Link. accountOwner: refundForm.captureMode === 'management' ? refundForm.accountOwner : '', accountIban: refundForm.captureMode === 'management' ? refundForm.accountIban : '', + // Spende: kein Konto, trotzdem sofort eingereicht. + donation: refundForm.captureMode === 'donation', // Leer bei voller Erstattung -- dann gibt es nichts zu begründen. retentionReason: hasRetention.value ? refundForm.retentionReason : '', retentionReasonNote: hasRetention.value ? refundForm.retentionReasonNote : '', @@ -772,8 +774,8 @@ function mailToGroup(groupKey) {
+
+

+ Der Betrag wird nicht ausgezahlt, sondern als Spende gebucht. Die Erstattung wird sofort + eingereicht; der Teili erhält den Beleg per E-Mail. +

+