Bankkarte: IBAN pruefen + BIC/Bank offline aus Bundesbank-BLZ ausfuellen

Neuer Button "BIC & Bank aus IBAN abrufen" im Bankkarten-Modal fuellt BIC +
Banknamen automatisch und validiert dabei die IBAN-Pruefziffer (mod-97).
Leeres IBAN-Feld -> OK-Messagebox statt Anfrage.

Datenschutzfreundlich/offline: kein Dritt-Dienst. Nachschlag im eigenen
Backend ueber die Bundesbank-Bankleitzahlendatei (bankdata-germany) +
ibantools fuer die Pruefziffer. Die IBAN verlaesst nie den Server; zurueck
kommen nur oeffentliche Bankverzeichnis-Daten.

Endpoint: POST /api/bank-cards/iban-lookup (nur eingeloggt).
Wartung: bankdata-germany/ibantools ~quartalsweise per npm update ziehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 12:32:53 +02:00
co-authored by Claude Opus 4.8
parent aff3f7e84a
commit 73bd72bd9f
7 changed files with 196 additions and 6 deletions
@@ -2692,12 +2692,54 @@ function BankCardModal({
});
const [formData, setFormData] = useState(getInitialFormData);
const [ibanLookupLoading, setIbanLookupLoading] = useState(false);
const [showIbanRequired, setShowIbanRequired] = useState(false);
// Reset form when bankCard changes
useState(() => {
setFormData(getInitialFormData());
});
// BIC + Bankname aus der IBAN ableiten (offline über Bundesbank-BLZ im
// Backend). Ist das IBAN-Feld leer, kommt eine OK-Messagebox statt einer
// Anfrage. Gleichzeitig prüft der Server die IBAN-Prüfziffer (Tippfehler).
const handleIbanLookup = async () => {
const iban = formData.iban.replace(/\s+/g, '').trim();
if (!iban) {
setShowIbanRequired(true);
return;
}
setIbanLookupLoading(true);
try {
const res = await bankCardApi.ibanLookup(iban);
const d = res.data;
if (!d) {
toast.error('IBAN-Prüfung fehlgeschlagen.');
return;
}
if (!d.valid) {
toast.error('Die IBAN ist ungültig bitte Eingabe/Prüfziffer kontrollieren.');
return;
}
if (d.bic || d.bankName) {
setFormData((prev) => ({
...prev,
bic: d.bic || prev.bic,
bankName: d.bankName || prev.bankName,
}));
toast.success('IBAN gültig BIC und Bank übernommen.');
} else {
toast('IBAN ist gültig, aber es wurden keine Bankdaten gefunden (nur deutsche IBANs werden aufgelöst).', {
icon: '️',
});
}
} catch (err: any) {
toast.error(err?.response?.data?.error || 'Fehler beim IBAN-Nachschlagen.');
} finally {
setIbanLookupLoading(false);
}
};
const createMutation = useMutation({
mutationFn: (data: any) => bankCardApi.create(customerId, data),
onSuccess: () => {
@@ -2738,6 +2780,7 @@ function BankCardModal({
}, [bankCard?.id]);
return (
<>
<Modal isOpen={isOpen} onClose={onClose} title={isEditing ? 'Bankkarte bearbeiten' : 'Bankkarte hinzufügen'}>
<form onSubmit={handleSubmit} className="space-y-4">
<Input
@@ -2747,12 +2790,30 @@ function BankCardModal({
required
/>
<Input
label="IBAN"
value={formData.iban}
onChange={(e) => setFormData({ ...formData, iban: e.target.value })}
required
/>
<div>
<Input
label="IBAN"
value={formData.iban}
onChange={(e) => setFormData({ ...formData, iban: e.target.value })}
required
/>
<div className="flex justify-end mt-1.5">
<button
type="button"
onClick={handleIbanLookup}
disabled={ibanLookupLoading}
className="inline-flex items-center gap-1.5 text-sm font-medium text-blue-600 hover:text-blue-800 disabled:opacity-50 disabled:cursor-not-allowed"
title="Prüft die IBAN und füllt BIC + Bank aus (offline, Bundesbank-Bankleitzahlen)"
>
{ibanLookupLoading ? (
<RefreshCw className="w-4 h-4 animate-spin" />
) : (
<Search className="w-4 h-4" />
)}
BIC &amp; Bank aus IBAN abrufen
</button>
</div>
</div>
<Input
label="BIC"
@@ -2813,6 +2874,20 @@ function BankCardModal({
</div>
</form>
</Modal>
{showIbanRequired && (
<Modal isOpen onClose={() => setShowIbanRequired(false)} title="IBAN erforderlich" size="sm">
<div className="space-y-4">
<p className="text-sm text-gray-700">
Bitte trage zuerst eine IBAN im Feld IBAN ein. BIC und Bank werden daraus ermittelt.
</p>
<div className="flex justify-end">
<Button onClick={() => setShowIbanRequired(false)}>OK</Button>
</div>
</div>
</Modal>
)}
</>
);
}
+15
View File
@@ -377,8 +377,23 @@ export const bankCardApi = {
const res = await api.delete<ApiResponse<void>>(`/bank-cards/${id}`);
return res.data;
},
// IBAN-Prüfung + BIC/Bank-Nachschlag (offline, Bundesbank-BLZ) die IBAN
// verlässt nur unser eigenes Backend, kein Dritt-Dienst.
ibanLookup: async (iban: string) => {
const res = await api.post<ApiResponse<IbanLookupResult>>(`/bank-cards/iban-lookup`, { iban });
return res.data;
},
};
export interface IbanLookupResult {
valid: boolean;
iban: string;
country?: string;
bic?: string | null;
bankName?: string | null;
blz?: string | null;
}
// Identity Documents
export const documentApi = {
getByCustomer: async (customerId: number, showInactive = false) => {