/** * IBAN-Pruefung nach ISO 13616 - selbst gerechnet, ohne Paket. * * Warum ueberhaupt pruefen? Weil eine falsch abgetippte IBAN im * Vermoegensverzeichnis beim Gericht landet und weil der Blindindex * (siehe blindindex() in src/lib/crypto/hash.ts) nur dann als "kenne ich * dieses Konto schon?" taugt, wenn die IBAN vorher normalisiert wurde. * Ein Leerzeichen zu viel wuerde sonst ein zweites, scheinbar neues * Konto erzeugen. * * Das Verfahren: die ersten vier Zeichen (Laendercode + Pruefziffer) ans * Ende stellen, jeden Buchstaben durch seine Position im Alphabet + 9 * ersetzen (A=10 ... Z=35) und den entstandenen Riesen-Zahlstring modulo * 97 rechnen. Ergebnis muss 1 sein. */ /** * Laenge der IBAN je Land. Ohne diese Tabelle wuerde eine zu kurze oder * zu lange IBAN durchrutschen, wenn die Pruefziffer zufaellig passt. * Quelle: IBAN-Registry des SWIFT (Stand 2024). */ export const IBAN_LAENGEN: Record = { AD: 24, AE: 23, AL: 28, AT: 20, AZ: 28, BA: 20, BE: 16, BG: 22, BH: 22, BI: 27, BR: 29, BY: 28, CH: 21, CR: 22, CY: 28, CZ: 24, DE: 22, DJ: 27, DK: 18, DO: 28, EE: 20, EG: 29, ES: 24, FI: 18, FO: 18, FR: 27, GB: 22, GE: 22, GI: 23, GL: 18, GR: 27, GT: 28, HR: 21, HU: 28, IE: 22, IL: 23, IQ: 23, IS: 26, IT: 27, JO: 30, KW: 30, KZ: 20, LB: 28, LC: 32, LI: 21, LT: 20, LU: 20, LV: 21, LY: 25, MC: 27, MD: 24, ME: 22, MK: 19, MR: 27, MT: 31, MU: 30, NL: 18, NO: 15, PK: 24, PL: 28, PS: 29, PT: 25, QA: 29, RO: 24, RS: 22, RU: 33, SA: 24, SC: 31, SD: 18, SE: 24, SI: 19, SK: 24, SM: 27, SO: 23, ST: 25, SV: 28, TL: 23, TN: 24, TR: 26, UA: 29, VA: 22, VG: 24, XK: 20, } /** Grossbuchstaben, keine Leerzeichen - so und nicht anders wird gespeichert. */ export function ibanNormalisieren(iban: string | null | undefined): string { if (!iban) return '' return iban.replace(/[\s  -]/g, '').toUpperCase() } /** Vierergruppen, wie sie auf jedem Kontoauszug stehen. */ export function ibanFormatieren(iban: string | null | undefined): string { const roh = ibanNormalisieren(iban) if (roh === '') return '' return roh.replace(/(.{4})/g, '$1 ').trim() } /** * Fuer Listen und Berichte: DE12 **** **** 3456. Die vollstaendige IBAN * steht nur dort, wo sie gebraucht wird - im Vermoegensverzeichnis und * im Kontodetail. */ export function ibanMaskieren(iban: string | null | undefined): string { const roh = ibanNormalisieren(iban) if (roh.length < 8) return roh const anfang = roh.slice(0, 4) const ende = roh.slice(-4) const versteckt = '*'.repeat(Math.max(0, roh.length - 8)) return ibanFormatieren(`${anfang}${versteckt}${ende}`) } /** Modulo 97 ueber den Zahlstring, ziffernweise - kein BigInt noetig. */ function modulo97(zahlstring: string): number { let rest = 0 for (const zeichen of zahlstring) { rest = (rest * 10 + (zeichen.charCodeAt(0) - 48)) % 97 } return rest } export type IbanPruefung = | { gueltig: true; iban: string; land: string; formatiert: string } | { gueltig: false; meldung: string } /** * Prueft eine IBAN vollstaendig: Zeichenvorrat, Laenderlaenge und * Pruefziffer. Die Meldungen sagen, was zu tun ist, nicht was falsch war. */ export function ibanPruefen(eingabe: string | null | undefined): IbanPruefung { const iban = ibanNormalisieren(eingabe) if (iban === '') { return { gueltig: false, meldung: 'Bitte eine IBAN angeben.' } } if (!/^[A-Z0-9]+$/.test(iban)) { return { gueltig: false, meldung: 'Die IBAN darf nur Buchstaben und Ziffern enthalten.', } } if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/.test(iban)) { return { gueltig: false, meldung: 'Eine IBAN beginnt mit zwei Buchstaben für das Land und zwei Prüfziffern, z.B. DE89.', } } const land = iban.slice(0, 2) const erwartet = IBAN_LAENGEN[land] if (erwartet === undefined) { return { gueltig: false, meldung: `Das Länderkürzel "${land}" kenne ich nicht. Bitte die IBAN prüfen.`, } } if (iban.length !== erwartet) { return { gueltig: false, meldung: `Eine ${land}-IBAN hat ${erwartet} Zeichen, diese hat ${iban.length}.`, } } const umgestellt = iban.slice(4) + iban.slice(0, 4) let zahlstring = '' for (const zeichen of umgestellt) { const code = zeichen.charCodeAt(0) zahlstring += code >= 65 ? String(code - 55) : zeichen } if (modulo97(zahlstring) !== 1) { return { gueltig: false, meldung: 'Die Prüfziffer stimmt nicht. Vermutlich ein Zahlendreher – bitte noch einmal vergleichen.', } } return { gueltig: true, iban, land, formatiert: ibanFormatieren(iban) } } /** Kurzform fuer Bedingungen. */ export function ibanGueltig(eingabe: string | null | undefined): boolean { return ibanPruefen(eingabe).gueltig } /** * BIC nach ISO 9362: 8 oder 11 Stellen. Wird nur geprueft, nicht * erzwungen - fuer eine Barkasse oder ein Verwahrgeldkonto im Heim gibt * es keinen BIC. */ export function bicGueltig(eingabe: string | null | undefined): boolean { if (!eingabe || eingabe.trim() === '') return true const bic = eingabe.replace(/\s+/g, '').toUpperCase() return /^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/.test(bic) } export function bicNormalisieren(eingabe: string | null | undefined): string | null { if (!eingabe) return null const bic = eingabe.replace(/\s+/g, '').toUpperCase() return bic === '' ? null : bic }