From 73bd72bd9f833ed84aa0bd91035fdc8eb21d1477 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Wed, 12 Aug 2026 12:32:53 +0200 Subject: [PATCH] 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 --- backend/package-lock.json | 17 ++++ backend/package.json | 2 + .../src/controllers/bankLookup.controller.ts | 54 ++++++++++++ backend/src/routes/bankcard.routes.ts | 5 ++ docs/todo.md | 22 +++++ .../src/pages/customers/CustomerDetail.tsx | 87 +++++++++++++++++-- frontend/src/services/api.ts | 15 ++++ 7 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 backend/src/controllers/bankLookup.controller.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 7b25bbd5..6179030f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,6 +12,7 @@ "@types/cookie-parser": "^1.4.10", "adm-zip": "^0.5.16", "archiver": "^7.0.1", + "bankdata-germany": "^2.2602.0", "bcryptjs": "^2.4.3", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -21,6 +22,7 @@ "express-rate-limit": "^8.4.0", "express-validator": "^7.2.0", "helmet": "^8.1.0", + "ibantools": "^4.5.4", "imapflow": "^1.2.8", "jsonwebtoken": "^9.0.2", "mailparser": "^3.9.3", @@ -984,6 +986,15 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bankdata-germany": { + "version": "2.2602.0", + "resolved": "https://registry.npmjs.org/bankdata-germany/-/bankdata-germany-2.2602.0.tgz", + "integrity": "sha512-3iBD/cIKHZsmAaQIYEMsORhvMjILDTJ4gDQJYNVm+YXCzA5U96mUoB7KBy01rtwpFt/rKrNuCVNc8KZVx+jwpQ==", + "license": "MIT OR MPL-2.0", + "engines": { + "node": ">=20" + } + }, "node_modules/bare-events": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", @@ -1992,6 +2003,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/ibantools": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/ibantools/-/ibantools-4.5.4.tgz", + "integrity": "sha512-6jX1gh4aH6XH+o0ey+wtkMTzkcvsEta7DakIOZSng9voZYpMw3U+gK1+tZChk3aRcPcloEt0NOzksjaRZiqXbw==", + "license": "MIT or MPL-2.0" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/backend/package.json b/backend/package.json index 5e2f0ee1..8c232e48 100644 --- a/backend/package.json +++ b/backend/package.json @@ -24,6 +24,7 @@ "@types/cookie-parser": "^1.4.10", "adm-zip": "^0.5.16", "archiver": "^7.0.1", + "bankdata-germany": "^2.2602.0", "bcryptjs": "^2.4.3", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -33,6 +34,7 @@ "express-rate-limit": "^8.4.0", "express-validator": "^7.2.0", "helmet": "^8.1.0", + "ibantools": "^4.5.4", "imapflow": "^1.2.8", "jsonwebtoken": "^9.0.2", "mailparser": "^3.9.3", diff --git a/backend/src/controllers/bankLookup.controller.ts b/backend/src/controllers/bankLookup.controller.ts new file mode 100644 index 00000000..fd5f9d90 --- /dev/null +++ b/backend/src/controllers/bankLookup.controller.ts @@ -0,0 +1,54 @@ +import { Response } from 'express'; +import { isValidIBAN } from 'ibantools'; +import { bankDataByIBAN } from 'bankdata-germany'; +import { ApiResponse, AuthRequest } from '../types/index.js'; + +/** + * IBAN-Lookup: prüft die Prüfziffer (mod-97, offline über ibantools) und liefert + * – für deutsche IBANs – BIC und Banknamen aus der Bundesbank-Bankleitzahlen- + * datei (offline über bankdata-germany). Die IBAN verlässt NICHT den Server; es + * findet KEIN externer Request statt. Zurückgegeben werden nur öffentliche + * Bankverzeichnis-Daten (BIC/Name), keine kundenbezogenen Informationen. + * + * Antwortformen (immer HTTP 200, sofern eine IBAN übergeben wurde): + * { valid: false } → Prüfziffer falsch (Tippfehler) + * { valid: true, bic, bankName, blz } → gültig + DE-Bank gefunden + * { valid: true, bic: null, bankName: null } → gültig, aber keine DE-Bankdaten + * (Nicht-DE-IBAN oder unbekannte BLZ) + */ +export async function lookupIban(req: AuthRequest, res: Response): Promise { + try { + const raw = typeof req.body?.iban === 'string' ? req.body.iban : ''; + const iban = raw.replace(/\s+/g, '').toUpperCase(); + + if (!iban) { + res.status(400).json({ success: false, error: 'Feld IBAN ist erforderlich.' } as ApiResponse); + return; + } + + if (!isValidIBAN(iban)) { + res.json({ success: true, data: { valid: false, iban } } as ApiResponse); + return; + } + + const country = iban.slice(0, 2); + const bank = country === 'DE' ? bankDataByIBAN(iban) : null; + + res.json({ + success: true, + data: { + valid: true, + iban, + country, + bic: bank?.bic ?? null, + bankName: bank?.bankName ?? null, + blz: bank?.blz ?? null, + }, + } as ApiResponse); + } catch (error) { + res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Fehler beim IBAN-Nachschlagen', + } as ApiResponse); + } +} diff --git a/backend/src/routes/bankcard.routes.ts b/backend/src/routes/bankcard.routes.ts index bfd8d51a..b4cc4e90 100644 --- a/backend/src/routes/bankcard.routes.ts +++ b/backend/src/routes/bankcard.routes.ts @@ -1,9 +1,14 @@ import { Router } from 'express'; import * as customerController from '../controllers/customer.controller.js'; +import * as bankLookupController from '../controllers/bankLookup.controller.js'; import { authenticate, requirePermission } from '../middleware/auth.js'; const router = Router(); +// IBAN-Prüfung + BIC/Bank-Nachschlag (offline, Bundesbank-BLZ). Liefert nur +// öffentliche Bankverzeichnis-Daten – reicht daher ein eingeloggter Nutzer. +router.post('/iban-lookup', authenticate, bankLookupController.lookupIban); + router.put('/:id', authenticate, requirePermission('customers:update'), customerController.updateBankCard); router.delete('/:id', authenticate, requirePermission('customers:delete'), customerController.deleteBankCard); diff --git a/docs/todo.md b/docs/todo.md index 8cde4d89..e12e1fb4 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,28 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🏦 Bankkarte-Modal: IBAN-Prüfung + BIC/Bank-Auto-Ausfüllen** (2026-08-12) + - Button „BIC & Bank aus IBAN abrufen" unter dem IBAN-Feld (Bankkarte + hinzufügen/bearbeiten). Füllt BIC + Banknamen automatisch aus und prüft + dabei die **IBAN-Prüfziffer (mod-97)** → erkennt Tippfehler. + - **Offline / datenschutzfreundlich:** kein Dritt-Dienst. Nachschlag im + eigenen Backend über die **Bundesbank-Bankleitzahlendatei** + (`bankdata-germany`) + `ibantools` für die Prüfziffer. Die IBAN verlässt + nie den Server; zurück kommen nur öffentliche Bankverzeichnis-Daten. + - Endpoint `POST /api/bank-cards/iban-lookup` (nur eingeloggt). Antwort: + `{valid:false}` (Prüfziffer falsch) · `{valid:true, bic, bankName, blz}` + (DE-Bank gefunden) · `{valid:true, bic:null,…}` (gültig, aber Nicht-DE/ + unbekannte BLZ → Info-Toast). + - Leeres IBAN-Feld → **OK-Messagebox** „IBAN erforderlich" statt Anfrage. + - **Wartung:** `bankdata-germany` + `ibantools` müssen ~quartalsweise per + `npm update` aktualisiert werden (Bundesbank-BLZ ändert sich mehrmals/Jahr). + +- [x] **📌 Update-Banner klebt oben fest (sticky)** (2026-08-12) + - „Neue Version verfügbar"-Banner scrollte weg; jetzt `sticky` an der + Viewport-Oberkante. Banner meldet seine Höhe als CSS-Var `--app-banner-h`, + an der die Sticky-Header von ContractDetail/ContractForm einrasten + (`top-[var(--app-banner-h,0px)]`) → keine Überdeckung. + - [x] **📋 Hauptmenü: Gutschriften/Lieferscheine-Gesamtübersicht** (2026-08-12) - Neuer Menüpunkt „Gutschriften" (Sidebar, `show: contracts:read`) → Seite `/credit-notes` mit Tabelle aller Belege (Beleg-Nr., Art, Kunde, Vertrag, diff --git a/frontend/src/pages/customers/CustomerDetail.tsx b/frontend/src/pages/customers/CustomerDetail.tsx index 9e465ad0..866cd880 100644 --- a/frontend/src/pages/customers/CustomerDetail.tsx +++ b/frontend/src/pages/customers/CustomerDetail.tsx @@ -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 ( + <>
- setFormData({ ...formData, iban: e.target.value })} - required - /> +
+ setFormData({ ...formData, iban: e.target.value })} + required + /> +
+ +
+
+ + {showIbanRequired && ( + setShowIbanRequired(false)} title="IBAN erforderlich" size="sm"> +
+

+ Bitte trage zuerst eine IBAN im Feld „IBAN“ ein. BIC und Bank werden daraus ermittelt. +

+
+ +
+
+
+ )} + ); } diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 30651003..37ac5205 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -377,8 +377,23 @@ export const bankCardApi = { const res = await api.delete>(`/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>(`/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) => {