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,121 @@
<?php
namespace App\Domains\ParticipantRefund\Actions\AcceptRefund;
use App\Domains\ParticipantRefund\Actions\CreateRefundDocument\CreateRefundDocumentCommand;
use App\Domains\ParticipantRefund\Actions\CreateRefundDocument\CreateRefundDocumentRequest;
use App\Mail\ParticipantRefundMails\RefundAcceptedMail;
use App\Models\ParticipantRefund;
use App\Support\Iban;
use Illuminate\Support\Facades\Mail;
/**
* Der Teili bestätigt die Erstattung und hinterlegt seine Bankverbindung.
*
* Läuft ohne Login -- der Token aus der Mail ist die Autorisierung, dasselbe Modell wie bei
* /print-girocode/{identifier}. Betrag und Grund stehen fest und werden hier nicht angefasst: sie kommen
* aus der Freigabe der Aktionsleitung.
*
* Danach ist der Vorgang festgeschrieben; der Beleg geht mit der Bestätigungsmail raus.
*/
class AcceptRefundCommand
{
/**
* Wortlaut für jeden Fall, in dem der Link nicht (mehr) zu einem offenen Vorgang führt.
*
* Bewusst ein und derselbe Text für „Token unbekannt" und „abgebrochen": eine abgebrochene Freigabe
* soll sich verhalten, als hätte es sie nie gegeben.
*/
public const string NO_OPEN_REFUND = 'Zu deiner Anmeldung liegt keine freigegebene Rückerstattung vor. '
. 'Bitte wende dich an die Aktionsleitung.';
public function __construct(private readonly AcceptRefundRequest $request)
{
}
public function execute(): AcceptRefundResponse
{
$response = new AcceptRefundResponse();
$refund = $this->request->refund;
if ($refund === null || !$refund->isPending()) {
$response->message = $refund?->isAccepted() === true
? 'Deine Angaben liegen uns bereits vor.'
: self::NO_OPEN_REFUND;
return $response;
}
$owner = trim($this->request->accountOwner);
$iban = Iban::normalize($this->request->accountIban);
// Serverseitig und nicht nur im Formular: die Erklärung ist der einzige Grund, warum der Beleg
// als Eigenbeleg etwas wert ist. Ließe sie sich mit einem direkten Aufruf übergehen, stünde auf
// dem PDF eine Zusicherung, die niemand abgegeben hat.
if (!$this->request->declarationAccepted) {
$response->errorTypes['declaration'] = 'Bitte bestätige die Erklärung, damit wir erstatten können.';
}
if ($owner === '') {
$response->errorTypes['accountOwner'] = 'Bitte gib an, wem das Konto gehört.';
}
if ($iban === '') {
$response->errorTypes['accountIban'] = 'Bitte gib die IBAN des Kontos ein.';
} elseif (!Iban::isValid($iban)) {
$response->errorTypes['accountIban'] = 'Diese IBAN stimmt nicht. Bitte prüfe deine Eingabe.';
}
if ($response->errorTypes !== []) {
$response->message = 'Bitte prüfe deine Angaben.';
return $response;
}
$refund->account_owner = $owner;
$refund->account_iban = $iban;
$refund->status = ParticipantRefund::STATUS_ACCEPTED;
$refund->accepted_at = now();
$refund->save();
$this->notify($refund);
$response->success = true;
$response->message = 'Vielen Dank. Deine Angaben liegen uns vor.';
return $response;
}
/**
* Bestätigung mit dem Beleg im Anhang, an Teili und Kontaktperson.
*
* Scheitert die Belegerzeugung, geht die Mail trotzdem raus -- der Vorgang ist gespeichert, und die
* Aktionsleitung kann den Beleg jederzeit erneut abrufen. Ein Fehler hier darf nicht dazu führen,
* dass der Teili gar nichts hört.
*/
private function notify(ParticipantRefund $refund): void
{
$document = new CreateRefundDocumentCommand(new CreateRefundDocumentRequest($refund))->execute();
$pdf = $document->success ? $document->pdfContent : null;
$filename = $document->success ? $document->filename : null;
$participant = $refund->participant;
Mail::to($participant->email_1)->send(new RefundAcceptedMail(
participant: $participant,
refund: $refund,
pdfContent: $pdf,
pdfFilename: $filename,
));
if ($participant->email_2 !== null) {
Mail::to($participant->email_2)->send(new RefundAcceptedMail(
participant: $participant,
refund: $refund,
pdfContent: $pdf,
pdfFilename: $filename,
));
}
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Domains\ParticipantRefund\Actions\AcceptRefund;
use App\Models\ParticipantRefund;
class AcceptRefundRequest
{
public function __construct(
public readonly ?ParticipantRefund $refund,
public readonly string $accountOwner,
public readonly string $accountIban,
/** Ob der Teili die Erklärung auf der Seite angekreuzt hat. Ohne sie taugt der Beleg nichts. */
public readonly bool $declarationAccepted = false,
) {
}
}
@@ -0,0 +1,17 @@
<?php
namespace App\Domains\ParticipantRefund\Actions\AcceptRefund;
class AcceptRefundResponse
{
public bool $success = false;
public ?string $message = null;
/**
* Feldbezogene Fehler für das Formular. Schlüssel sind die Feldnamen des Frontends.
*
* @var array<string, string>
*/
public array $errorTypes = [];
}