Calculation errors for refunded amounts
This commit is contained in:
@@ -388,6 +388,14 @@ function saveParticipant() {
|
||||
<small v-else-if="props.participant.refund.status === 'accepted'">
|
||||
bestätigt am {{ props.participant.refund.acceptedAt }}
|
||||
</small>
|
||||
|
||||
<!-- Was beim Verband geblieben ist und warum. -->
|
||||
<small v-if="props.participant.refund.hasRetention" class="retention-note">
|
||||
<br />Einbehalten: {{ props.participant.refund.retainedAmount }} –
|
||||
{{ props.participant.refund.retentionReasonLabel }}<template
|
||||
v-if="props.participant.refund.retentionReasonNote"
|
||||
> ({{ props.participant.refund.retentionReasonNote }})</template>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -541,4 +549,8 @@ textarea {
|
||||
select {
|
||||
width: 262px;
|
||||
}
|
||||
|
||||
.retention-note {
|
||||
color: #8a6d00;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -51,15 +51,82 @@ const openRefundDialogSwitch = ref(false);
|
||||
// Der Erstattungsdialog. `captureMode` steuert den Weg: 'participant' schickt dem Teili einen Link, über
|
||||
// den er seine Bankverbindung selbst einträgt; 'management' heißt, sie liegt der Aktionsleitung bereits
|
||||
// vor -- dann wird die Erstattung sofort eingereicht.
|
||||
const refundForm = reactive({amount: '', reason: '', reasonNote: '', captureMode: 'participant', accountOwner: '', accountIban: ''});
|
||||
const refundForm = reactive({
|
||||
amount: '', reason: '', reasonNote: '',
|
||||
captureMode: 'participant', accountOwner: '', accountIban: '',
|
||||
retentionReason: '', retentionReasonNote: '',
|
||||
});
|
||||
const refundErrors = reactive({amount: '', reason: '', reasonNote: '', accountOwner: '', accountIban: ''});
|
||||
const refundReasons = ref([]);
|
||||
const retentionReasons = ref([]);
|
||||
const refundSaving = ref(false);
|
||||
|
||||
const selectedRefundReason = computed(
|
||||
() => refundReasons.value.find(r => r.value === refundForm.reason) ?? null
|
||||
);
|
||||
|
||||
const selectedRetentionReason = computed(
|
||||
() => retentionReasons.value.find(r => r.value === refundForm.retentionReason) ?? null
|
||||
);
|
||||
|
||||
/** Was nach der Erstattung beim Verband bleibt -- die Grundlage für den Einbehaltungsblock. */
|
||||
const retainedAmount = computed(() => {
|
||||
const paid = Number(showParticipant.value?.amountPaidValue ?? 0);
|
||||
const refunded = Number((refundForm.amount ?? '').replace(',', '.'));
|
||||
|
||||
if (!Number.isFinite(refunded)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const remaining = Math.round((paid - refunded) * 100) / 100;
|
||||
|
||||
return remaining > 0.005 ? remaining : 0;
|
||||
});
|
||||
|
||||
const hasRetention = computed(() => retainedAmount.value > 0);
|
||||
|
||||
const retainedAmountReadable = computed(
|
||||
() => retainedAmount.value.toFixed(2).replace('.', ',') + ' Euro'
|
||||
);
|
||||
|
||||
/**
|
||||
* Ob abgesendet werden kann. Der Knopf erscheint erst dann -- was noch fehlt, soll die Aktionsleitung
|
||||
* sehen, bevor sie klickt, statt danach eine Fehlermeldung zu lesen.
|
||||
*/
|
||||
const refundFormComplete = computed(() => {
|
||||
const amount = Number((refundForm.amount ?? '').replace(',', '.'));
|
||||
const paid = Number(showParticipant.value?.amountPaidValue ?? 0);
|
||||
|
||||
if (!refundForm.amount || !(amount > 0) || amount > paid + 0.005) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!refundForm.reason) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedRefundReason.value?.requiresNote && !refundForm.reasonNote.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bleibt etwas beim Verband, muss begründet sein, warum.
|
||||
if (hasRetention.value) {
|
||||
if (!refundForm.retentionReason) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedRetentionReason.value?.requiresNote && !refundForm.retentionReasonNote.trim()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (refundForm.captureMode === 'management') {
|
||||
return refundForm.accountOwner.trim() !== '' && refundForm.accountIban.trim() !== '';
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
defineEmits(['showParticipantDetails', 'markCocExisting', 'paymentComplete'])
|
||||
|
||||
function openParticipantDetails(input) {
|
||||
@@ -323,6 +390,8 @@ async function openRefundDialog(participant) {
|
||||
refundForm.captureMode = 'participant';
|
||||
refundForm.accountOwner = '';
|
||||
refundForm.accountIban = '';
|
||||
refundForm.retentionReason = '';
|
||||
refundForm.retentionReasonNote = '';
|
||||
|
||||
Object.keys(refundErrors).forEach(key => refundErrors[key] = '');
|
||||
|
||||
@@ -331,6 +400,11 @@ async function openRefundDialog(participant) {
|
||||
refundReasons.value = reasons ?? [];
|
||||
}
|
||||
|
||||
if (retentionReasons.value.length === 0) {
|
||||
const reasons = await request('/api/v1/core/retrieve-retention-reasons', {method: 'GET'});
|
||||
retentionReasons.value = reasons ?? [];
|
||||
}
|
||||
|
||||
openRefundDialogSwitch.value = true;
|
||||
}
|
||||
|
||||
@@ -384,6 +458,9 @@ async function execRefund() {
|
||||
// Leer beim Weg über den Teili -- dann verschickt der Server nur den Link.
|
||||
accountOwner: refundForm.captureMode === 'management' ? refundForm.accountOwner : '',
|
||||
accountIban: refundForm.captureMode === 'management' ? refundForm.accountIban : '',
|
||||
// Leer bei voller Erstattung -- dann gibt es nichts zu begründen.
|
||||
retentionReason: hasRetention.value ? refundForm.retentionReason : '',
|
||||
retentionReasonNote: hasRetention.value ? refundForm.retentionReasonNote : '',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -479,6 +556,15 @@ function mailToGroup(groupKey) {
|
||||
<td class="pl-amount" :id="'participant-' + participant.identifier +'-payment'" :class="participant.amount_left_value != 0 && !participant.unregistered ? 'not-paid' : ''">
|
||||
Gezahlt: <label :id="'participant-' + participant.identifier + '-paid'">{{ participant?.amountPaid.readable }}</label> /<br />
|
||||
Gesamt: <label :id="'participant-' + participant.identifier + '-expected'">{{ participant?.amountExpected.readable }}</label>
|
||||
|
||||
<!-- Warum ein Teil des Beitrags beim Verband geblieben ist. -->
|
||||
<span v-if="participant.refund?.hasRetention" class="retention-note">
|
||||
Einbehalten: {{ participant.refund.retainedAmount }}<br />
|
||||
{{ participant.refund.retentionReasonLabel }}<template
|
||||
v-if="participant.refund.retentionReasonNote"
|
||||
> – {{ participant.refund.retentionReasonNote }}</template>
|
||||
</span>
|
||||
|
||||
<br /><br />
|
||||
<span v-if="participant.amount_left_value != 0 && !participant.unregistered" :id="'participant-' + participant.identifier + '-actions'">
|
||||
<span class="link" style="font-size:10pt;" @click="paymentComplete(participant)">Zahlung buchen</span>
|
||||
@@ -655,6 +741,36 @@ function mailToGroup(groupKey) {
|
||||
<ErrorText :message="refundErrors.reasonNote" />
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Bleibt ein Teil beim Verband, muss begründet sein, warum -- ein einbehaltener Betrag ohne
|
||||
Grund ist in der Buchhaltung nicht haltbar. Bei voller Erstattung gibt es nichts zu zeigen.
|
||||
-->
|
||||
<template v-if="hasRetention">
|
||||
<p class="refund-hint">
|
||||
<strong>{{ retainedAmountReadable }}</strong> verbleiben beim Verband.
|
||||
</p>
|
||||
|
||||
<div class="refund-field">
|
||||
<label for="refund_retention_reason">Grund der Einbehaltung</label>
|
||||
<select id="refund_retention_reason" v-model="refundForm.retentionReason" class="form-input">
|
||||
<option value="">Bitte auswählen …</option>
|
||||
<option v-for="reason in retentionReasons" :key="reason.value" :value="reason.value">
|
||||
{{ reason.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedRetentionReason?.requiresNote" class="refund-field">
|
||||
<label for="refund_retention_note">Erläuterung zur Einbehaltung</label>
|
||||
<textarea
|
||||
id="refund_retention_note"
|
||||
v-model="refundForm.retentionReasonNote"
|
||||
class="form-input"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!--
|
||||
Liegt die Bankverbindung schon vor, entfällt der Umweg über den Teili: die Erstattung wird
|
||||
sofort eingereicht. Er bekommt den Beleg trotzdem.
|
||||
@@ -694,7 +810,13 @@ function mailToGroup(groupKey) {
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<button class="button" :disabled="refundSaving" @click="execRefund()">
|
||||
<!-- Erscheint erst, wenn alles ausgefüllt ist; während des Speicherns gesperrt statt weg. -->
|
||||
<button
|
||||
v-if="refundFormComplete"
|
||||
class="button"
|
||||
:disabled="refundSaving"
|
||||
@click="execRefund()"
|
||||
>
|
||||
<template v-if="refundSaving">Wird gespeichert…</template>
|
||||
<template v-else-if="refundForm.captureMode === 'management'">Erstattung einreichen</template>
|
||||
<template v-else>Erstattung freigeben</template>
|
||||
@@ -749,6 +871,14 @@ function mailToGroup(groupKey) {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.retention-note {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 10pt;
|
||||
color: #ca5a0a;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.refund-hint {
|
||||
margin-bottom: 14px;
|
||||
padding: 8px 10px;
|
||||
|
||||
@@ -68,6 +68,17 @@ const props = defineProps({
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!--
|
||||
Beiträge, die trotz Abmeldung beim Verband geblieben sind. Eigene Zeile, weil die
|
||||
Zeilen darüber nur aktive Anmeldungen führen.
|
||||
-->
|
||||
<tr v-if="props.event.retainedFromUnregistered.value > 0">
|
||||
<th style="padding-bottom: 20px" colspan="2">Einbehalten von Abmeldungen</th>
|
||||
<td style="padding-bottom: 20px" colspan="2">
|
||||
{{ props.event.retainedFromUnregistered.readable }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th colspan="2" style="border-width: 1px; border-top-style: solid">Gesamt</th>
|
||||
<td style="font-weight: bold; border-width: 1px; border-top-style: solid">
|
||||
|
||||
@@ -120,8 +120,8 @@ class AcceptRefundCommand
|
||||
$invoice = $this->createInvoice($refund, $costUnit, $document);
|
||||
|
||||
// Erst jetzt, nicht früher: Beleg und Anmerkung der Abrechnung weisen den gezahlten Beitrag
|
||||
// aus und läsen sonst bereits die 0.
|
||||
$this->clearAmountPaid($refund);
|
||||
// aus und läsen sonst bereits den verrechneten Stand.
|
||||
$this->settleAmountPaid($refund);
|
||||
|
||||
$refund->invoice_id = $invoice->id;
|
||||
$refund->save();
|
||||
@@ -255,16 +255,23 @@ class AcceptRefundCommand
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den gezahlten Beitrag des Teilis auf 0.
|
||||
* Zieht den erstatteten Betrag vom gezahlten Beitrag ab.
|
||||
*
|
||||
* Mit der eingereichten Abrechnung ist der Beitrag nicht mehr beim Verband, sondern auf dem Weg
|
||||
* zurück -- die Zahlungsübersichten der Aktionsleitung sollen ihn nicht länger als offen führen. Der
|
||||
* ursprüngliche Betrag steht zur Kontrolle in der Anmerkung der Abrechnung und auf dem Beleg.
|
||||
* Danach führt `amount_paid` genau das, was beim Verband geblieben ist -- bei voller Erstattung also
|
||||
* 0, bei einer Teilerstattung den einbehaltenen Rest. Auf diesem Feld baut die Einnahmenrechnung der
|
||||
* Veranstaltung auf; es muss deshalb den tatsächlichen Bestand abbilden und nicht die Zahlung von
|
||||
* einst. Der ursprüngliche Betrag steht zur Kontrolle in der Anmerkung der Abrechnung und auf dem
|
||||
* Beleg.
|
||||
*/
|
||||
private function clearAmountPaid(ParticipantRefund $refund): void
|
||||
private function settleAmountPaid(ParticipantRefund $refund): void
|
||||
{
|
||||
$participant = $refund->participant;
|
||||
$participant->amount_paid = new Amount(0.0, 'Euro');
|
||||
|
||||
$paid = $participant->amount_paid?->getAmount() ?? 0.0;
|
||||
$refunded = $refund->amount?->getAmount() ?? 0.0;
|
||||
|
||||
// `max` gegen Rundungsreste: Ein negativer gezahlter Betrag wäre in jeder Auswertung Unsinn.
|
||||
$participant->amount_paid = new Amount(max(0.0, round($paid - $refunded, 2)), 'Euro');
|
||||
$participant->save();
|
||||
}
|
||||
|
||||
|
||||
+30
@@ -104,6 +104,26 @@ 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 Hinweis, warum ein Teil des Beitrags beim Verband bleibt -- leer bei voller Erstattung.
|
||||
*
|
||||
* Der Beleg wandert in die Buchhaltung und ins Archiv; dort muss die Differenz zwischen gezahltem
|
||||
* und erstattetem Betrag ohne Rückfrage erklärt sein.
|
||||
*/
|
||||
private function retentionNote(): string
|
||||
{
|
||||
if (!$this->refund->hasRetention()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$text = trim($this->refund->retentionReasonText());
|
||||
$label = $this->refund->retentionReasonLabel();
|
||||
|
||||
return $text !== '' && $text !== $label
|
||||
? sprintf('%s (%s)', $label, $text)
|
||||
: $label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Vermerk, wenn die Aktionsleitung die Angaben aufgenommen hat.
|
||||
*
|
||||
@@ -199,6 +219,9 @@ class CreateRefundDocumentCommand
|
||||
'account_owner' => (string) $refund->account_owner,
|
||||
'account_iban' => $this->formatIban((string) $refund->account_iban),
|
||||
|
||||
'retained_amount' => $this->money($refund->retained_amount?->getAmount() ?? 0.0),
|
||||
'retention_note' => $this->retentionNote(),
|
||||
|
||||
'declaration_text' => $this->declarationText(),
|
||||
'capture_note' => $this->captureNote(),
|
||||
|
||||
@@ -242,6 +265,13 @@ class CreateRefundDocumentCommand
|
||||
$rows[] = ['Begründung', e($reasonText)];
|
||||
}
|
||||
|
||||
// Nur bei einer Teilerstattung: Ohne diese Zeile bliebe die Differenz zwischen gezahltem und
|
||||
// erstattetem Betrag im Beleg unerklärt.
|
||||
if ($refund->hasRetention()) {
|
||||
$rows[] = ['Einbehalten', $this->money($refund->retained_amount?->getAmount() ?? 0.0)];
|
||||
$rows[] = ['Grund der Einbehaltung', e($this->retentionNote())];
|
||||
}
|
||||
|
||||
$rows[] = ['Kontoinhaber*in', e((string) $refund->account_owner)];
|
||||
$rows[] = ['IBAN', e($this->formatIban((string) $refund->account_iban))];
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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\Enumerations\RetentionReason;
|
||||
use App\Mail\ParticipantRefundMails\RefundReleasedMail;
|
||||
use App\Models\EventParticipant;
|
||||
use App\Models\ParticipantRefund;
|
||||
@@ -57,6 +58,11 @@ class ReleaseRefundCommand
|
||||
'amount' => $this->request->amount,
|
||||
'reason' => $this->request->reason,
|
||||
'reason_note' => $this->reasonNote(),
|
||||
// Was beim Verband bleibt, wird hier festgeschrieben: Nach dem Einreichen führt
|
||||
// `amount_paid` bereits diesen Rest, eine spätere Differenz wäre falsch.
|
||||
'retained_amount' => $this->request->retainedAmount(),
|
||||
'retention_reason' => $this->retentionReason(),
|
||||
'retention_reason_note' => $this->retentionReasonNote(),
|
||||
'released_by' => auth()->id(),
|
||||
'released_at' => now(),
|
||||
]);
|
||||
@@ -141,7 +147,32 @@ class ReleaseRefundCommand
|
||||
return 'Für diesen Grund ist eine Erläuterung erforderlich.';
|
||||
}
|
||||
|
||||
return $this->rejectBankDetails();
|
||||
return $this->rejectRetention() ?? $this->rejectBankDetails();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüfungen zum einbehaltenen Teil.
|
||||
*
|
||||
* Sicherheitsnetz hinter der Oberfläche: Dort erscheint der Absende-Knopf erst, wenn ein Grund
|
||||
* gewählt ist. Über einen direkten Aufruf ginge das sonst vorbei, und ein einbehaltener Betrag ohne
|
||||
* Begründung ist in der Buchhaltung nicht haltbar.
|
||||
*/
|
||||
private function rejectRetention(): ?string
|
||||
{
|
||||
if (!$this->request->hasRetention()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$reason = RetentionReason::find($this->request->retentionReason);
|
||||
if ($reason === null) {
|
||||
return 'Bitte gib an, warum ein Teil des Beitrags einbehalten wird.';
|
||||
}
|
||||
|
||||
if ($reason->requires_note && trim((string) $this->request->retentionReasonNote) === '') {
|
||||
return 'Für diesen Einbehaltungsgrund ist eine Erläuterung erforderlich.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,6 +215,33 @@ class ReleaseRefundCommand
|
||||
return trim((string) $this->request->reasonNote);
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Einbehaltungsgrund -- nur, wenn tatsächlich etwas beim Verband bleibt.
|
||||
*
|
||||
* Bei voller Erstattung wird ein mitgeschickter Grund verworfen: In der Oberfläche ist das Feld dann
|
||||
* gar nicht sichtbar, und ein Wert ohne Bezug hätte in der Datenbank nichts zu suchen.
|
||||
*/
|
||||
private function retentionReason(): ?string
|
||||
{
|
||||
return $this->request->hasRetention() ? $this->request->retentionReason : null;
|
||||
}
|
||||
|
||||
/** Der Freitext dazu -- wie beim Erstattungsgrund nur bei Gründen, die ihn verlangen. */
|
||||
private function retentionReasonNote(): ?string
|
||||
{
|
||||
if (!$this->request->hasRetention()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$reason = RetentionReason::find($this->request->retentionReason);
|
||||
|
||||
if ($reason === null || !$reason->requires_note) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trim((string) $this->request->retentionReasonNote);
|
||||
}
|
||||
|
||||
/**
|
||||
* Teili und Kontaktperson bekommen je eine eigene Mail -- dasselbe Muster wie bei der Abmeldung
|
||||
* (siehe SetParticipationStateCommand).
|
||||
|
||||
@@ -20,6 +20,14 @@ class ReleaseRefundRequest
|
||||
*/
|
||||
public readonly ?string $accountOwner = null,
|
||||
public readonly ?string $accountIban = null,
|
||||
/**
|
||||
* Warum ein Teil des Beitrags beim Verband bleibt.
|
||||
*
|
||||
* Pflicht, sobald weniger erstattet wird als gezahlt wurde: Ein einbehaltener Betrag ohne Grund
|
||||
* ist in der Buchhaltung nicht haltbar.
|
||||
*/
|
||||
public readonly ?string $retentionReason = null,
|
||||
public readonly ?string $retentionReasonNote = null,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -28,4 +36,23 @@ class ReleaseRefundRequest
|
||||
{
|
||||
return filled($this->accountOwner) && filled($this->accountIban);
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Betrag, der beim Verband bleibt.
|
||||
*
|
||||
* Die halbe Cent-Toleranz fängt die Rundung des gespeicherten Floats ab -- ohne sie entstünden
|
||||
* Restbeträge von Bruchteilen eines Cents, die eine Begründung verlangen würden.
|
||||
*/
|
||||
public function retainedAmount(): float
|
||||
{
|
||||
$paid = $this->participant->amount_paid?->getAmount() ?? 0.0;
|
||||
$remaining = round($paid - $this->amount->getAmount(), 2);
|
||||
|
||||
return $remaining > 0.005 ? $remaining : 0.0;
|
||||
}
|
||||
|
||||
public function hasRetention(): bool
|
||||
{
|
||||
return $this->retainedAmount() > 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ class ReleaseRefundController extends CommonController
|
||||
// Leer, wenn der Teili die Bankverbindung selbst eintragen soll.
|
||||
accountOwner: Text::nullIfBlank($request->input('accountOwner')),
|
||||
accountIban: Text::nullIfBlank($request->input('accountIban')),
|
||||
// Leer, wenn der volle Beitrag erstattet wird -- dann gibt es nichts zu begründen.
|
||||
retentionReason: Text::nullIfBlank($request->input('retentionReason')),
|
||||
retentionReasonNote: Text::nullIfBlank($request->input('retentionReasonNote')),
|
||||
);
|
||||
|
||||
$response = new ReleaseRefundCommand($refundRequest)->execute();
|
||||
|
||||
@@ -61,6 +61,8 @@ final class ParticipantRefundTokens
|
||||
'paid_amount' => ['description' => 'Bereits gezahlter Teilnahmebeitrag', 'sample' => '300,00 €'],
|
||||
'invoice_number' => ['description' => 'Nummer der Teilnahmerechnung', 'sample' => 'WM-V-20260701-0005'],
|
||||
'refund_amount' => ['description' => 'Erstattungsbetrag', 'sample' => '220,00 €'],
|
||||
'retained_amount' => ['description' => 'Betrag, der beim Verband bleibt — 0,00 € bei voller Erstattung', 'sample' => '80,00 €'],
|
||||
'retention_note' => ['description' => 'Grund der Einbehaltung — leer bei voller Erstattung', 'sample' => 'Stornogebühr laut Ausschreibung'],
|
||||
'refund_reason' => ['description' => 'Bezeichnung des Grundes', 'sample' => 'Krankheitsbedingte Absage'],
|
||||
'refund_reason_text' => ['description' => 'Erläuterung des Grundes (bei „Sonstiger Grund" der Freitext)', 'sample' => 'Die Teilnahme konnte krankheitsbedingt nicht angetreten werden.'],
|
||||
'account_owner' => ['description' => 'Kontoinhaber*in', 'sample' => 'Mika Muster'],
|
||||
@@ -111,6 +113,8 @@ final class ParticipantRefundTokens
|
||||
. '<tr><td class="detail-key">Erstattungsbetrag</td><td class="detail-val">220,00 €</td></tr>'
|
||||
. '<tr><td class="detail-key">Grund</td><td class="detail-val">Krankheitsbedingte Absage</td></tr>'
|
||||
. '<tr><td class="detail-key">Begründung</td><td class="detail-val">Die Teilnahme konnte krankheitsbedingt nicht angetreten werden.</td></tr>'
|
||||
. '<tr><td class="detail-key">Einbehalten</td><td class="detail-val">80,00 €</td></tr>'
|
||||
. '<tr><td class="detail-key">Grund der Einbehaltung</td><td class="detail-val">Stornogebühr laut Ausschreibung</td></tr>'
|
||||
. '<tr><td class="detail-key">Kontoinhaber*in</td><td class="detail-val">Mika Muster</td></tr>'
|
||||
. '<tr><td class="detail-key">IBAN</td><td class="detail-val">DE02 1203 0000 0000 2020 51</td></tr>'
|
||||
. '</table>';
|
||||
|
||||
Reference in New Issue
Block a user