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:
Generated
+17
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 & 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user