Approven von Beitragserstattungen

This commit is contained in:
2026-09-03 23:08:41 +02:00
parent b01288b5e0
commit efee20c16b
15 changed files with 994 additions and 39 deletions
+89 -2
View File
@@ -10,10 +10,14 @@ use App\Domains\ParticipantRefund\Actions\CreateRefundDocument\CreateRefundDocum
use App\Domains\ParticipantRefund\Actions\ReleaseRefund\ReleaseRefundCommand;
use App\Domains\ParticipantRefund\Actions\ReleaseRefund\ReleaseRefundRequest;
use App\Enumerations\EfzStatus;
use App\Enumerations\CostUnitType;
use App\Enumerations\InvoiceStatus;
use App\Enumerations\InvoiceType;
use App\Enumerations\RefundReason;
use App\Enumerations\UserRole;
use App\Mail\ParticipantRefundMails\RefundAcceptedMail;
use App\Mail\ParticipantRefundMails\RefundReleasedMail;
use App\Models\CostUnit;
use App\Models\DocumentTemplate;
use App\Models\Event;
use App\Models\EventParticipant;
@@ -67,6 +71,10 @@ class ParticipantRefundTest extends TestCase
DB::table('participation_types')->insert(['slug' => 'participant', 'name' => 'Teilnehmer']);
DB::table('participation_fee_types')->insert(['slug' => 'fixed', 'name' => 'Fix']);
// Lookup-Tabellen mit Fremdschlüsselzwang -- ohne sie scheitert schon das Anlegen.
// `invoice_types.participation_refund` bringt die Migration bereits mit.
DB::table('cost_unit_types')->insert(['slug' => CostUnitType::COST_UNIT_TYPE_EVENT, 'name' => 'Veranstaltung']);
DB::table('invoice_status')->insert(['slug' => InvoiceStatus::INVOICE_STATUS_NEW]);
PaymentMethod::create(['slug' => PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION]);
PaymentMethod::create(['slug' => PaymentMethod::PAYMENT_NOT_DEFINED]);
EfzStatus::create(['slug' => EfzStatus::EFZ_STATUS_NOT_REQUIRED, 'name' => 'Nicht erforderlich']);
@@ -112,6 +120,20 @@ class ParticipantRefundTest extends TestCase
]);
}
/** Die Kostenstelle, auf die die Erstattung gebucht wird -- beim Anlegen einer Veranstaltung entsteht sie mit. */
private function makeCostUnit(array $attributes = []): CostUnit
{
return CostUnit::create(array_merge([
'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,
], $attributes));
}
private function makeEvent(array $attributes = []): Event
{
$fee = EventParticipationFee::create([
@@ -125,6 +147,7 @@ class ParticipantRefundTest extends TestCase
]);
return Event::create(array_merge([
'cost_unit_id' => $this->makeCostUnit()->id,
'tenant' => $this->tenant->slug,
'name' => 'Sommerlager',
'identifier' => 'evt-' . uniqid(),
@@ -532,18 +555,32 @@ class ParticipantRefundTest extends TestCase
|--------------------------------------------------------------------------
*/
public function test_amount_paid_is_untouched_through_the_whole_process(): void
public function test_amount_paid_survives_the_release_and_falls_with_the_confirmation(): void
{
$participant = $this->makeParticipant($this->makeEvent());
// Die Freigabe allein ändert nichts -- solange der Teili nicht bestätigt hat, ist der Beitrag
// unverändert beim Verband.
$refund = $this->release($participant)->refund;
$this->assertEqualsWithDelta(300.0, $participant->fresh()->amount_paid->getAmount(), 0.001);
$this->accept($refund);
$this->assertEqualsWithDelta(300.0, $participant->fresh()->amount_paid->getAmount(), 0.001);
$this->assertEqualsWithDelta(0.0, $participant->fresh()->amount_paid->getAmount(), 0.001);
// Der Sollbetrag bleibt: was der Teili hätte zahlen müssen, ändert die Erstattung nicht.
$this->assertEqualsWithDelta(300.0, $participant->fresh()->amount->getAmount(), 0.001);
}
public function test_a_cancelled_release_leaves_amount_paid_alone(): void
{
$participant = $this->makeParticipant($this->makeEvent());
$refund = $this->release($participant)->refund;
new CancelRefundCommand(new CancelRefundRequest($refund))->execute();
$this->assertEqualsWithDelta(300.0, $participant->fresh()->amount_paid->getAmount(), 0.001);
}
/*
|--------------------------------------------------------------------------
| Über HTTP: Routen, Zugriffsschutz und die öffentliche Seite
@@ -565,6 +602,22 @@ class ParticipantRefundTest extends TestCase
]);
}
/** Das Konto, an dem eine Anmeldung hängen kann -- entscheidet über den Login-Hinweis in der Mail. */
private function makeParticipantUser(): User
{
return User::create([
'username' => 'teili-' . uniqid() . '@example.com',
'email' => 'teili-' . uniqid() . '@example.com',
'firstname' => 'Mika',
'lastname' => 'Muster',
'password' => bcrypt('secret'),
'local_group' => $this->tenant->slug,
'user_role_main' => UserRole::USER_ROLE_USER,
'user_role_local_group' => UserRole::USER_ROLE_USER,
'active' => true,
]);
}
public function test_release_over_http_creates_the_refund(): void
{
$this->actingAs($this->makeAdmin());
@@ -753,6 +806,40 @@ class ParticipantRefundTest extends TestCase
$this->assertStringContainsString('Anhang', $html);
}
public function test_the_acceptance_mail_points_at_the_invoice(): void
{
$participant = $this->makeParticipant($this->makeEvent(), ['user_id' => $this->makeParticipantUser()->id]);
$refund = $this->release($participant)->refund;
$this->accept($refund);
$html = new RefundAcceptedMail($participant, $refund->fresh())->render();
// Die Mail ist nur noch informativ -- eingereicht ist die Erstattung bereits.
$this->assertStringContainsString('musst nichts weiter tun', $html);
$this->assertStringContainsString($refund->fresh()->invoice()->first()->invoice_number, $html);
$this->assertStringContainsString('/invoice/my-invoices/new', $html);
}
public function test_the_acceptance_mail_omits_the_login_hint_without_an_account(): void
{
// Ohne Nutzerkonto bliebe "Meine Abrechnungen" leer -- die Seite filtert über die Verknüpfung.
$participant = $this->makeParticipant($this->makeEvent(), ['user_id' => null]);
$refund = $this->release($participant)->refund;
$this->accept($refund);
$html = new RefundAcceptedMail($participant, $refund->fresh())->render();
$this->assertStringNotContainsString('/invoice/my-invoices/new', $html);
}
public function test_an_empty_second_address_gets_no_mail(): void
{
// Der Anmeldewizard legt das Feld bei Volljährigen als Leerstring an, nicht als NULL.
$this->release($this->makeParticipant($this->makeEvent(), ['email_2' => '']));
Mail::assertSent(RefundReleasedMail::class, 1);
}
public function test_the_acceptance_mail_renders_without_a_document(): void
{
$participant = $this->makeParticipant($this->makeEvent());