Creating Participation refunds

This commit is contained in:
2026-09-03 21:23:26 +02:00
parent 9c4c28e566
commit 6a183d6498
55 changed files with 3985 additions and 42 deletions
@@ -0,0 +1,63 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* Gründe für die Erstattung eines Teilnahmebeitrags -- DB-gestützt wie die Befreiungsgründe der
* Umsatzsteuer, damit Label und der auf dem Beleg erscheinende Text zentral pflegbar sind.
*
* App-weit und ohne `tenant`-Spalte: die Gründe sind Stammdaten, mandantenspezifisch ist nur der
* einzelne Erstattungsvorgang.
*/
return new class extends Migration {
public function up(): void
{
Schema::create('refund_reasons', function (Blueprint $table) {
$table->string('slug')->primary();
$table->string('name');
$table->text('document_text')->nullable();
$table->boolean('requires_note')->default(false);
$table->integer('sort_order')->default(0);
$table->timestamps();
});
DB::table('refund_reasons')->insert([
[
'slug' => 'sickness',
'name' => 'Krankheitsbedingte Absage',
'document_text' => 'Die Teilnahme konnte krankheitsbedingt nicht angetreten werden.',
'requires_note' => false,
'sort_order' => 10,
'created_at' => now(),
'updated_at' => now(),
],
[
'slug' => 'event_cancelled',
'name' => 'Ausfall der Veranstaltung',
'document_text' => 'Die Veranstaltung wurde abgesagt und konnte nicht stattfinden.',
'requires_note' => false,
'sort_order' => 20,
'created_at' => now(),
'updated_at' => now(),
],
[
// Der Text entsteht erst aus dem Freitext der Aktionsleitung -- deshalb hier leer.
'slug' => 'other',
'name' => 'Sonstiger Grund',
'document_text' => null,
'requires_note' => true,
'sort_order' => 30,
'created_at' => now(),
'updated_at' => now(),
],
]);
}
public function down(): void
{
Schema::dropIfExists('refund_reasons');
}
};
@@ -0,0 +1,60 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Ein Erstattungsvorgang je Anmeldung.
*
* Der Vorgang läuft in zwei Schritten: die Aktionsleitung gibt Betrag und Grund frei (`pending`), der
* Teili ergänzt über den Token-Link seine Bankverbindung (`accepted`). Solange nicht bestätigt wurde,
* kann die Aktionsleitung abbrechen (`cancelled`) -- der Link verhält sich danach so, als hätte es nie
* eine Freigabe gegeben.
*
* `event_participants.amount_paid` bleibt in jedem Fall unangetastet: der Vorgang ist freigegeben, aber
* noch nicht ausgezahlt. Die Verrechnung kommt mit der Auszahlung.
*
* Bewusst KEIN Unique auf `event_participant_id`: abgebrochene Vorgänge bleiben als Historie stehen, und
* danach darf eine neue Freigabe entstehen. Dass es höchstens einen offenen Vorgang gibt, setzt
* ParticipantRefundRepository::openFor() durch.
*/
return new class extends Migration {
public function up(): void
{
Schema::create('participant_refunds', function (Blueprint $table) {
$table->id();
$table->string('tenant');
$table->foreignId('event_id')->constrained('events', 'id')->cascadeOnDelete()->cascadeOnUpdate();
$table->foreignId('event_participant_id')->constrained('event_participants', 'id')->cascadeOnDelete()->cascadeOnUpdate();
// Der öffentliche Link. Eigener Token statt des Teilnehmer-Identifiers, damit ein Abbruch den
// Link tötet, ohne andere per Identifier erreichbare Funktionen (GiroCode) mitzunehmen.
$table->string('token', 32)->unique();
$table->string('status');
$table->float('amount', 2)->default(0);
$table->string('reason')->nullable();
$table->text('reason_note')->nullable();
$table->string('account_owner')->nullable();
$table->string('account_iban')->nullable();
$table->foreignId('released_by')->nullable()->constrained('users', 'id')->nullOnDelete();
$table->dateTime('released_at')->nullable();
$table->dateTime('accepted_at')->nullable();
$table->dateTime('cancelled_at')->nullable();
$table->timestamps();
$table->foreign('tenant')->references('slug')->on('tenants')->restrictOnDelete()->cascadeOnUpdate();
$table->foreign('reason')->references('slug')->on('refund_reasons')->restrictOnDelete()->cascadeOnUpdate();
});
}
public function down(): void
{
Schema::dropIfExists('participant_refunds');
}
};
@@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
/**
* Der Erklärungssatz, den der Teili auf der Erstattungsseite bestätigt und der anschließend auf dem
* Eigenbeleg steht.
*
* Er liegt in `page_texts` und nicht in der Dokumentvorlage, damit Seite und Beleg denselben Wortlaut
* zeigen: was der Teili gelesen hat, muss wörtlich das sein, was das PDF ihm zuschreibt. Zwei getrennte
* Quellen würden früher oder später auseinanderlaufen.
*
* Als Migration und nicht als Seed-Skript, weil ohne diesen Text weder die Seite noch der Beleg
* vollständig sind -- anders als bei den Dokumentvorlagen, wo ein Release gepflegte Texte nicht
* überschreiben soll. `updateOrInsert` lässt eine bereits angepasste Fassung deshalb auch in Ruhe:
* geschrieben wird nur, was noch fehlt.
*/
return new class extends Migration {
private const string NAME = 'CONFIRMATION_PARTICIPANT_REFUND';
public function up(): void
{
$existing = DB::table('page_texts')->where('name', self::NAME)->first();
if ($existing !== null) {
return;
}
DB::table('page_texts')->insert([
'name' => self::NAME,
'content' => 'Ich versichere, dass ich den genannten Betrag beglichen habe und nicht '
. 'anderweitig zurückerstattet bekomme.',
'created_at' => now(),
'updated_at' => now(),
]);
}
public function down(): void
{
DB::table('page_texts')->where('name', self::NAME)->delete();
}
};