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,134 @@
<?php
namespace App\Domains\ParticipantRefund\Actions\ReleaseRefund;
use App\Enumerations\RefundReason;
use App\Mail\ParticipantRefundMails\RefundReleasedMail;
use App\Models\EventParticipant;
use App\Models\ParticipantRefund;
use App\Repositories\ParticipantRefundRepository;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
/**
* Gibt die Erstattung eines Teilnahmebeitrags frei.
*
* Der Vorgang entsteht hier nur als Absichtserklärung: Betrag und Grund stehen fest, die Bankverbindung
* fehlt noch. Der Teili ergänzt sie über den Link in der Mail. `amount_paid` bleibt unangetastet --
* gezahlt hat er bis zur Auszahlung weiterhin, was er gezahlt hat.
*/
class ReleaseRefundCommand
{
private EventParticipant $participant;
private ParticipantRefundRepository $refunds;
public function __construct(private readonly ReleaseRefundRequest $request)
{
$this->participant = $request->participant;
$this->refunds = new ParticipantRefundRepository();
}
public function execute(): ReleaseRefundResponse
{
$response = new ReleaseRefundResponse();
$rejection = $this->reject();
if ($rejection !== null) {
$response->message = $rejection;
return $response;
}
$refund = ParticipantRefund::create([
'tenant' => $this->participant->tenant,
'event_id' => $this->participant->event_id,
'event_participant_id' => $this->participant->id,
'token' => Str::random(32),
'status' => ParticipantRefund::STATUS_PENDING,
'amount' => $this->request->amount,
'reason' => $this->request->reason,
'reason_note' => $this->reasonNote(),
'released_by' => auth()->id(),
'released_at' => now(),
]);
$this->notify($refund);
$response->success = true;
$response->refund = $refund;
$response->message = 'Die Erstattung wurde freigegeben. Der Teili wurde per E-Mail informiert.';
return $response;
}
/**
* Alle Gründe, aus denen eine Freigabe nicht zulässig ist.
*
* @return string|null Meldung, oder null wenn nichts dagegen spricht.
*/
private function reject(): ?string
{
if ($this->participant->unregistered_at === null) {
return 'Eine Erstattung ist nur für abgemeldete Teilis möglich.';
}
if ($this->refunds->openFor($this->participant) !== null) {
return 'Für diese Anmeldung läuft bereits eine Erstattung.';
}
$amount = $this->request->amount->getAmount();
if ($amount <= 0) {
return 'Der Erstattungsbetrag muss größer als 0 sein.';
}
// Mehr zurückgeben als eingegangen ist wäre keine Erstattung mehr. Die halbe Cent-Toleranz
// fängt die Rundung des gespeicherten Floats ab.
$paid = $this->participant->amount_paid?->getAmount() ?? 0.0;
if ($amount > $paid + 0.005) {
return 'Der Erstattungsbetrag darf den gezahlten Beitrag nicht übersteigen.';
}
$reason = RefundReason::find($this->request->reason);
if ($reason === null) {
return 'Bitte wähle einen Erstattungsgrund aus.';
}
if ($reason->requires_note && trim((string) $this->request->reasonNote) === '') {
return 'Für diesen Grund ist eine Erläuterung erforderlich.';
}
return null;
}
/** Der Freitext gehört nur zu Gründen, die ihn verlangen -- sonst stünde er ungenutzt in der DB. */
private function reasonNote(): ?string
{
$reason = RefundReason::find($this->request->reason);
if ($reason === null || !$reason->requires_note) {
return null;
}
return trim((string) $this->request->reasonNote);
}
/**
* Teili und Kontaktperson bekommen je eine eigene Mail -- dasselbe Muster wie bei der Abmeldung
* (siehe SetParticipationStateCommand).
*/
private function notify(ParticipantRefund $refund): void
{
Mail::to($this->participant->email_1)->send(new RefundReleasedMail(
participant: $this->participant,
refund: $refund,
));
if ($this->participant->email_2 !== null) {
Mail::to($this->participant->email_2)->send(new RefundReleasedMail(
participant: $this->participant,
refund: $refund,
));
}
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Domains\ParticipantRefund\Actions\ReleaseRefund;
use App\Models\EventParticipant;
use App\ValueObjects\Amount;
class ReleaseRefundRequest
{
public function __construct(
public readonly EventParticipant $participant,
public readonly Amount $amount,
public readonly string $reason,
public readonly ?string $reasonNote = null,
) {
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Domains\ParticipantRefund\Actions\ReleaseRefund;
use App\Models\ParticipantRefund;
class ReleaseRefundResponse
{
public bool $success = false;
public ?ParticipantRefund $refund = null;
public ?string $message = null;
}