Automatischer Zahlungsparser

This commit is contained in:
2026-09-10 09:50:14 +02:00
parent 025035190d
commit 6301c342e4
24 changed files with 1692 additions and 83 deletions
@@ -28,8 +28,35 @@ const props = defineProps({
reasonNote: String,
acceptedAt: String,
donation: Boolean,
/*
* Das Konto aus dem Zahlungseingang, falls bekannt: {owner, ibanMasked}.
*
* Ist es gesetzt, wird es nicht mehr abgefragt -- es steht bereits am Vorgang und der Server
* nimmt ohnehin nur dieses. Die IBAN kommt maskiert an: zum Wiedererkennen genug, zum
* Mitschreiben zu wenig.
*/
knownAccount: {type: Object, default: null},
/*
* Woher das Erstattungskonto kommt -- entschieden von der Zahlungsart, nicht hier:
* known -- liegt vor, wird nur bestätigt
* origin -- es gab ein Ursprungskonto, wir fragen danach
* none -- es gab nie eines (Barzahlung); dann ist die Herkunftsfrage sinnlos
*/
accountSource: {type: String, default: 'origin'},
/** Name des page_texts-Eintrags mit der zweiten Erklärung -- der Server wählt ihn. */
accountDeclarationText: {type: String, default: 'CONFIRMATION_PARTICIPANT_REFUND_ACCOUNT'},
})
const hasKnownAccount = computed(() => props.accountSource === 'known')
/**
* Ob nach dem Ursprungskonto gefragt wird.
*
* Nur wenn es eines gab: Wer bar gezahlt hat, könnte die Frage nicht sinnvoll beantworten -- und landete
* bei „Nein" in einer Sackgasse, die ihn auffordert, das Konto zu nennen, von dem überwiesen wurde.
*/
const asksForOrigin = computed(() => props.accountSource === 'origin')
// Der Zustand wandert in ein ref, damit die Seite nach dem Absenden umschalten kann, ohne neu zu laden.
const state = ref(props.state)
@@ -70,6 +97,32 @@ const accountComplete = computed(
const isDonation = computed(() => decision.value === 'donation')
/**
* Wann die Erklärungen des Auszahlungswegs erscheinen.
*
* Bekanntes Konto: sofort -- es steht ja schon da. Sonst erst, wenn die Kontoangaben vollständig sind;
* wo nach dem Ursprungskonto gefragt wird, zusätzlich erst nach einem „Ja".
*/
const showDeclarations = computed(() => {
if (decision.value !== 'payout') return false
if (hasKnownAccount.value) return true
if (!accountComplete.value) return false
return asksForOrigin.value ? sameAccount.value === true : true
})
/**
* Wann nach Kontoinhaber*in und IBAN gefragt wird.
*
* Überall dort, wo das Konto nicht schon feststeht -- beim Ursprungskonto-Weg allerdings erst, nachdem
* die Herkunftsfrage bejaht wurde: Sonst tippt jemand ein Konto ab, das wir anschließend ablehnen müssen.
*/
const showAccountFields = computed(() => {
if (decision.value !== 'payout' || hasKnownAccount.value) return false
return asksForOrigin.value ? sameAccount.value === true : true
})
/**
* Wechselt den Weg und nimmt dabei jedes Kreuz zurück.
*
@@ -95,8 +148,13 @@ function validate() {
errors.declaration = form.declarationAccepted ? '' : 'Bitte bestätige die Erklärung.'
if (!isDonation.value) {
errors.accountOwner = form.accountOwner.trim() ? '' : 'Bitte gib an, wem das Konto gehört.'
errors.accountIban = form.accountIban.trim() ? '' : 'Bitte gib die IBAN des Kontos ein.'
// Beim bekannten Konto gibt es nichts einzugeben und damit auch nichts zu prüfen -- nur die
// Bestätigung, dass es stimmt.
if (!hasKnownAccount.value) {
errors.accountOwner = form.accountOwner.trim() ? '' : 'Bitte gib an, wem das Konto gehört.'
errors.accountIban = form.accountIban.trim() ? '' : 'Bitte gib die IBAN des Kontos ein.'
}
errors.accountDeclaration = form.accountDeclarationAccepted
? ''
: 'Bitte bestätige, dass es das Konto ist, von dem der Beitrag gezahlt wurde.'
@@ -111,15 +169,23 @@ async function submit() {
saving.value = true
// Bei einer Spende geht keine Bankverbindung mit -- es gibt keine, und der Server erwartet auch keine.
// Beim bekannten Konto gehen ebenfalls keine Kontofelder mit: Der Server nimmt ausschließlich,
// was am Vorgang steht, alles Mitgeschickte würde er verwerfen.
const body = isDonation.value
? {donation: true, declarationAccepted: form.declarationAccepted}
: {
donation: false,
accountOwner: form.accountOwner,
accountIban: form.accountIban,
declarationAccepted: form.declarationAccepted,
accountDeclarationAccepted: form.accountDeclarationAccepted,
}
: hasKnownAccount.value
? {
donation: false,
declarationAccepted: form.declarationAccepted,
accountDeclarationAccepted: form.accountDeclarationAccepted,
}
: {
donation: false,
accountOwner: form.accountOwner,
accountIban: form.accountIban,
declarationAccepted: form.declarationAccepted,
accountDeclarationAccepted: form.accountDeclarationAccepted,
}
try {
const response = await request('/api/v1/participant-refund/' + props.token + '/accept', {
@@ -262,39 +328,84 @@ async function submit() {
sonst tippt jemand ein Konto ab, das wir anschließend ablehnen müssen.
-->
<template v-else-if="decision === 'payout'">
<h3>Soll der Betrag auf das Konto erstattet werden, von dem der Beitrag
gezahlt wurde?</h3>
<!--
Konto bekannt: Es steht schon am Vorgang und stammt aus dem
Zahlungseingang. Die Frage danach erübrigt sich, eingegeben wird nichts
mehr -- gezeigt wird es trotzdem, damit ein falsch zugeordneter
Zahlungseingang hier auffällt und nicht erst beim Rückläufer der Bank.
-->
<template v-if="hasKnownAccount">
<h3>Wir erstatten auf das Konto, von dem dein Beitrag kam</h3>
<div class="choices">
<button
type="button"
class="choice"
:class="{active: sameAccount === true}"
@click="sameAccount = true"
>Ja</button>
<button
type="button"
class="choice"
:class="{active: sameAccount === false}"
@click="sameAccount = false"
>Nein</button>
</div>
<div class="known-account">
<span class="known-account__owner">{{ knownAccount.owner }}</span>
<span class="known-account__iban">{{ knownAccount.ibanMasked }}</span>
</div>
<template v-if="sameAccount === false">
<p class="hint">
So können wir den Betrag leider nicht erstatten: Zurück geht er nur auf
das Konto, von dem der Teilnahmebeitrag gezahlt wurde. Wurde er von
einem anderen Konto überwiesen &ndash; etwa dem eines Elternteils
&ndash;, gib bitte dieses an. Hilft das nicht weiter, wende dich bitte
an die Aktionsleitung: {{ props.eventEmail }}
Aus Sicherheitsgründen zeigen wir die IBAN nur teilweise. Gehört das
Konto nicht zu deiner Zahlung, wende dich bitte an die Aktionsleitung:
{{ props.eventEmail }}
</p>
<button type="button" class="button" @click="reset()">
Zurück zur Auswahl
</button>
</template>
<template v-else-if="sameAccount === true">
<!--
Kein Ursprungskonto (bar gezahlt): Die Herkunftsfrage entfällt. Sonst
landete der Teili bei Nein" in einer Sackgasse, die ihn nach dem Konto
fragt, von dem überwiesen wurde -- es gab keines. Erstattet wird auf ein
Konto, das ihm gehört.
-->
<template v-else-if="!asksForOrigin">
<h3>Auf welches Konto sollen wir erstatten?</h3>
<p class="choice-hint">
Deinen Beitrag hast du nicht überwiesen, deshalb brauchen wir eine
Bankverbindung von dir. Bitte gib ein Konto an, das auf deinen Namen
läuft oder über das du verfügen darfst.
</p>
</template>
<template v-else>
<h3>Soll der Betrag auf das Konto erstattet werden, von dem der Beitrag
gezahlt wurde?</h3>
<div class="choices">
<button
type="button"
class="choice"
:class="{active: sameAccount === true}"
@click="sameAccount = true"
>Ja</button>
<button
type="button"
class="choice"
:class="{active: sameAccount === false}"
@click="sameAccount = false"
>Nein</button>
</div>
<template v-if="sameAccount === false">
<p class="hint">
So können wir den Betrag leider nicht erstatten: Zurück geht er nur auf
das Konto, von dem der Teilnahmebeitrag gezahlt wurde. Wurde er von
einem anderen Konto überwiesen &ndash; etwa dem eines Elternteils
&ndash;, gib bitte dieses an. Hilft das nicht weiter, wende dich bitte
an die Aktionsleitung: {{ props.eventEmail }}
</p>
<button type="button" class="button" @click="reset()">
Zurück zur Auswahl
</button>
</template>
</template>
<!--
Die Kontoangaben -- gleich in beiden Wegen, in denen sie erfragt werden:
nach einem „Ja" auf die Herkunftsfrage und bei einer Zahlungsart ohne
Ursprungskonto. Nur beim bereits bekannten Konto entfallen sie.
-->
<template v-if="showAccountFields">
<div class="field">
<label for="account-owner">Kontoinhaber*in</label>
<input
@@ -312,13 +423,17 @@ async function submit() {
<IbanInput id="account-iban" v-model="form.accountIban" class="form-input" />
<ErrorText :message="errors.accountIban" />
</div>
</template>
<!--
Die Erklärungen, die anschließend auf dem Eigenbeleg stehen. Sie
müssen hier gelesen und angekreuzt werden -- sonst schriebe der Beleg
dem Teili Zusicherungen zu, die er nie abgegeben hat.
In beiden Wegen dieselben: Beim bekannten Konto bestätigt der Teili
Angezeigtes statt Getipptes -- der Beleg bleibt dadurch unverändert.
-->
<template v-if="accountComplete">
<template v-if="showDeclarations">
<div class="declaration">
<input
id="refund-declaration"
@@ -339,7 +454,7 @@ async function submit() {
type="checkbox"
/>
<TextResource
text-name="CONFIRMATION_PARTICIPANT_REFUND_ACCOUNT"
:text-name="accountDeclarationText"
belongs-to="refund-account-declaration"
/>
</div>
@@ -356,7 +471,6 @@ async function submit() {
{{ saving ? 'Wird gespeichert…' : 'Angaben absenden' }}
</button>
</template>
</template>
</template>
</form>
</template>
@@ -429,6 +543,31 @@ h3 {
font-weight: bold;
}
/* Das bekannte Konto -- Anzeige, kein Feld. Bewusst wie ein Beleg gesetzt und nicht wie ein Formular,
damit gar nicht erst der Eindruck entsteht, hier ließe sich etwas ändern. */
.known-account {
display: flex;
flex-direction: column;
gap: 2px;
padding: 12px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
background-color: #f9fafb;
margin-bottom: 4px;
}
.known-account__owner {
font-weight: bold;
color: #374151;
}
.known-account__iban {
font-family: monospace;
font-size: 1.05rem;
letter-spacing: 0.05em;
color: #4b5563;
}
.field {
margin-bottom: 16px;
}