Refundings without contacting the participant
This commit is contained in:
@@ -67,7 +67,10 @@ class AcceptRefundCommand
|
||||
// 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) {
|
||||
//
|
||||
// Nimmt die Aktionsleitung die Angaben auf, kreuzt naturgemäß niemand etwas an. Nachvollziehbar
|
||||
// bleibt es trotzdem: `captured_by` hält fest, wer sie aufgenommen hat, und der Beleg weist es aus.
|
||||
if (!$this->request->declarationAccepted && $this->request->capturedBy === null) {
|
||||
$response->errorTypes['declaration'] = 'Bitte bestätige die Erklärung, damit wir erstatten können.';
|
||||
}
|
||||
|
||||
@@ -107,6 +110,7 @@ class AcceptRefundCommand
|
||||
$document = DB::transaction(function () use ($refund, $owner, $iban, $costUnit) {
|
||||
$refund->account_owner = $owner;
|
||||
$refund->account_iban = $iban;
|
||||
$refund->captured_by = $this->request->capturedBy;
|
||||
$refund->status = ParticipantRefund::STATUS_ACCEPTED;
|
||||
$refund->accepted_at = now();
|
||||
$refund->save();
|
||||
|
||||
@@ -12,6 +12,13 @@ class AcceptRefundRequest
|
||||
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,
|
||||
/**
|
||||
* Die Aktionsleitung, wenn sie die Bankverbindung aufgenommen hat, weil sie ihr vorlag.
|
||||
*
|
||||
* Dann kreuzt niemand die Erklärung an -- sie wird stellvertretend aufgenommen, und der Beleg
|
||||
* weist genau das aus.
|
||||
*/
|
||||
public readonly ?int $capturedBy = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -104,6 +104,28 @@ class CreateRefundDocumentCommand
|
||||
* Fallback stünde hier ein Fatal Error auf `null` -- so steht es heute im Deckblatt-Code der
|
||||
* Auslagenerstattung, und daran soll sich der Beleg kein Beispiel nehmen.
|
||||
*/
|
||||
/**
|
||||
* Der Vermerk, wenn die Aktionsleitung die Angaben aufgenommen hat.
|
||||
*
|
||||
* Er nennt Name und Datum, weil in diesem Fall niemand die Erklärung darüber angekreuzt hat: Wer den
|
||||
* Beleg prüft, soll erkennen, dass dort eine aufgenommene Angabe steht und keine Bestätigung des
|
||||
* Teilis selbst. Beim gewöhnlichen Weg bleibt der Platzhalter leer und der Block fällt weg.
|
||||
*/
|
||||
private function captureNote(): string
|
||||
{
|
||||
if (!$this->refund->wasCapturedByManagement()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$name = $this->refund->capturedBy()->first()?->getOfficialName();
|
||||
|
||||
return sprintf(
|
||||
'Angaben aufgenommen durch %s am %s.',
|
||||
trim((string) $name) !== '' ? $name : 'die Aktionsleitung',
|
||||
$this->refund->accepted_at?->format('d.m.Y') ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
private function declarationText(): string
|
||||
{
|
||||
$text = PageText::where('name', self::DECLARATION_TEXT)->first()?->content;
|
||||
@@ -178,6 +200,7 @@ class CreateRefundDocumentCommand
|
||||
'account_iban' => $this->formatIban((string) $refund->account_iban),
|
||||
|
||||
'declaration_text' => $this->declarationText(),
|
||||
'capture_note' => $this->captureNote(),
|
||||
|
||||
'details_table' => $this->renderDetails(),
|
||||
];
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
|
||||
namespace App\Domains\ParticipantRefund\Actions\ReleaseRefund;
|
||||
|
||||
use App\Domains\ParticipantRefund\Actions\AcceptRefund\AcceptRefundCommand;
|
||||
use App\Domains\ParticipantRefund\Actions\AcceptRefund\AcceptRefundRequest;
|
||||
use App\Enumerations\RefundReason;
|
||||
use App\Mail\ParticipantRefundMails\RefundReleasedMail;
|
||||
use App\Models\EventParticipant;
|
||||
use App\Models\ParticipantRefund;
|
||||
use App\Repositories\ParticipantRefundRepository;
|
||||
use App\Support\Iban;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Gibt die Erstattung eines Teilnahmebeitrags frei.
|
||||
@@ -40,28 +45,66 @@ class ReleaseRefundCommand
|
||||
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(),
|
||||
]);
|
||||
// Liegt die Bankverbindung schon vor, entsteht in einem Zug auch die Abrechnung. Scheitert die,
|
||||
// soll keine halbe Freigabe zurückbleiben -- deshalb beides in einer Transaktion.
|
||||
$refund = DB::transaction(function (): ParticipantRefund {
|
||||
$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);
|
||||
if ($this->request->hasBankDetails()) {
|
||||
$this->submitDirectly($refund);
|
||||
}
|
||||
|
||||
return $refund;
|
||||
});
|
||||
|
||||
if (!$this->request->hasBankDetails()) {
|
||||
$this->notify($refund);
|
||||
}
|
||||
|
||||
$response->success = true;
|
||||
$response->refund = $refund;
|
||||
$response->message = 'Die Erstattung wurde freigegeben. Der Teili wurde per E-Mail informiert.';
|
||||
$response->refund = $refund->fresh();
|
||||
$response->message = $this->request->hasBankDetails()
|
||||
? 'Die Erstattung wurde eingereicht. Der Teili hat den Beleg per E-Mail erhalten.'
|
||||
: 'Die Erstattung wurde freigegeben. Der Teili wurde per E-Mail informiert.';
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reicht die Erstattung sofort ein, ohne den Umweg über den Teili.
|
||||
*
|
||||
* Über denselben Command, den sonst der Bestätigungslink auslöst: Beleg, Abrechnung, das Nullstellen
|
||||
* des gezahlten Beitrags und die Mail mit dem Beleg laufen dadurch in beiden Wegen identisch ab.
|
||||
*/
|
||||
private function submitDirectly(ParticipantRefund $refund): void
|
||||
{
|
||||
$acceptResponse = new AcceptRefundCommand(new AcceptRefundRequest(
|
||||
refund: $refund,
|
||||
accountOwner: (string) $this->request->accountOwner,
|
||||
accountIban: (string) $this->request->accountIban,
|
||||
// Niemand kreuzt hier eine Erklärung an; wer die Angaben aufgenommen hat, hält `captured_by`
|
||||
// fest, und der Beleg weist es aus.
|
||||
capturedBy: auth()->id(),
|
||||
))->execute();
|
||||
|
||||
if (!$acceptResponse->success) {
|
||||
// Rollt die Freigabe zurück -- die Aktionsleitung soll den Fehler sehen und nicht einen
|
||||
// Vorgang vorfinden, der nirgends eingereicht ist.
|
||||
throw new RuntimeException($acceptResponse->message ?? 'Die Erstattung konnte nicht eingereicht werden.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Gründe, aus denen eine Freigabe nicht zulässig ist.
|
||||
*
|
||||
@@ -98,6 +141,34 @@ class ReleaseRefundCommand
|
||||
return 'Für diesen Grund ist eine Erläuterung erforderlich.';
|
||||
}
|
||||
|
||||
return $this->rejectBankDetails();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüfungen, die nur den Direktweg betreffen -- die Erstattung wird dabei sofort eingereicht, es gibt
|
||||
* also keine zweite Gelegenheit, Angaben zu berichtigen.
|
||||
*/
|
||||
private function rejectBankDetails(): ?string
|
||||
{
|
||||
// Halb ausgefüllt ist keine Absicht: entweder beides oder der Weg über den Teili.
|
||||
if (filled($this->request->accountOwner) !== filled($this->request->accountIban)) {
|
||||
return 'Für die sofortige Erstattung werden Kontoinhaber*in und IBAN benötigt.';
|
||||
}
|
||||
|
||||
if (!$this->request->hasBankDetails()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Iban::isValid((string) $this->request->accountIban)) {
|
||||
return 'Diese IBAN stimmt nicht. Bitte prüfe die Eingabe.';
|
||||
}
|
||||
|
||||
// Ohne Kostenstelle ließe sich die Abrechnung nicht anlegen. Hier abfangen und nicht erst in der
|
||||
// Transaktion, damit die Aktionsleitung eine verständliche Meldung sieht.
|
||||
if ($this->participant->event->cost_unit_id === null) {
|
||||
return 'Die Veranstaltung hat keine Kostenstelle -- die Erstattung kann nicht eingereicht werden.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,20 @@ class ReleaseRefundRequest
|
||||
public readonly Amount $amount,
|
||||
public readonly string $reason,
|
||||
public readonly ?string $reasonNote = null,
|
||||
/**
|
||||
* Die Bankverbindung, wenn sie der Aktionsleitung bereits vorliegt.
|
||||
*
|
||||
* Sind beide gesetzt, entfällt der Umweg über den Teili: die Erstattung wird sofort eingereicht.
|
||||
* Bleiben sie leer, läuft der übliche Weg über den Bestätigungslink.
|
||||
*/
|
||||
public readonly ?string $accountOwner = null,
|
||||
public readonly ?string $accountIban = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/** Ob die Erstattung ohne Zutun des Teilis eingereicht werden kann. */
|
||||
public function hasBankDetails(): bool
|
||||
{
|
||||
return filled($this->accountOwner) && filled($this->accountIban);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ class ReleaseRefundController extends CommonController
|
||||
amount: Amount::fromString((string) $request->input('amount'), 'Euro'),
|
||||
reason: (string) $request->input('reason'),
|
||||
reasonNote: Text::nullIfBlank($request->input('reasonNote')),
|
||||
// Leer, wenn der Teili die Bankverbindung selbst eintragen soll.
|
||||
accountOwner: Text::nullIfBlank($request->input('accountOwner')),
|
||||
accountIban: Text::nullIfBlank($request->input('accountIban')),
|
||||
);
|
||||
|
||||
$response = new ReleaseRefundCommand($refundRequest)->execute();
|
||||
|
||||
@@ -66,6 +66,7 @@ final class ParticipantRefundTokens
|
||||
'account_owner' => ['description' => 'Kontoinhaber*in', 'sample' => 'Mika Muster'],
|
||||
'account_iban' => ['description' => 'IBAN', 'sample' => 'DE02 1203 0000 0000 2020 51'],
|
||||
'declaration_text' => ['description' => 'Die Erklärung, die die teilnehmende Person bestätigt hat (gepflegt als Seitentext CONFIRMATION_PARTICIPANT_REFUND)', 'sample' => 'Ich versichere, dass ich den genannten Betrag beglichen habe und nicht anderweitig zurückerstattet bekomme.'],
|
||||
'capture_note' => ['description' => 'Vermerk, wenn die Aktionsleitung die Bankverbindung aufgenommen hat — sonst leer', 'sample' => 'Angaben aufgenommen durch Aktions Leitung am 18.06.2026.'],
|
||||
],
|
||||
],
|
||||
'body' => [
|
||||
|
||||
Reference in New Issue
Block a user