UNPKG

ibankit

Version:

Validation, field extraction and creation of IBAN, BBAN, BIC numbers

949 lines (796 loc) 33.3 kB
import { CharacterType, BbanStructurePart, PartType } from "./structurePart"; import { CountryCode } from "./country"; import { FormatException, FormatViolation, RequiredPartTypeMissing } from "./exceptions"; /** * MOD11 check digit computation */ function mod11(value: string, weights: number[]) { return ( (11 - (value.split("").reduce((acc, s, idx) => acc + parseInt(s, 10) * weights[idx % weights.length], 0) % 11)) % 11 ); } function nationalES(bban: string, structure: BbanStructure) { const weights = [1, 2, 4, 8, 5, 10, 9, 7, 3, 6]; const combined = [PartType.BANK_CODE, PartType.BRANCH_CODE].map((p) => structure.extractValueMust(bban, p)).join(""); function to11(v: number) { if (v === 10) { return 1; } else if (v === 11) { return 0; } return v; } const d1 = to11(mod11(`00${combined}`, weights)); const d2 = to11(mod11(structure.extractValueMust(bban, PartType.ACCOUNT_NUMBER), weights)); return `${d1}${d2}`; } /** * France Checksum (shared) */ function nationalFR(bban: string, structure: BbanStructure) { const replaceChars = { ["[AJ]"]: "1", ["[BKS]"]: "2", ["[CLT]"]: "3", ["[DMU]"]: "4", ["[ENV]"]: "5", ["[FOW]"]: "6", ["[GPX]"]: "7", ["[HQY]"]: "8", ["[IRZ]"]: "9", }; let combined = [PartType.BANK_CODE, PartType.BRANCH_CODE, PartType.ACCOUNT_NUMBER] .map((p) => String(structure.extractValue(bban, p))) .join("") + "00"; Object.entries(replaceChars).map(([k, v]) => (combined = combined.replace(new RegExp(k, "g"), v))); // Number is bigger than max integer, take the mod%97 by hand const expected = 97 - combined.split("").reduce((acc, v) => (acc * 10 + parseInt(v)) % 97, 0); return String(expected).padStart(2, "0"); } function nationalIT(bban: string, structure: BbanStructure) { const even = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]; const odd = [1, 0, 5, 7, 9, 13, 15, 17, 19, 21, 2, 4, 18, 20, 11, 3, 6, 8, 12, 14, 16, 10, 22, 25, 24, 23]; const V0 = "0".charCodeAt(0); const V9 = "9".charCodeAt(0); const VA = "A".charCodeAt(0); const value = [PartType.BANK_CODE, PartType.BRANCH_CODE, PartType.ACCOUNT_NUMBER] .map((p) => structure.extractValueMust(bban, p)) .join("") .split("") .map((v) => v.toUpperCase().charCodeAt(0)) .map((v) => v - (V0 <= v && v <= V9 ? V0 : VA)) .reduce((acc, v, idx) => acc + (idx % 2 === 0 ? odd[v] : even[v]), 0) % 26; return String.fromCharCode(VA + value); } function nationalNO(bban: string, structure: BbanStructure) { const value = [PartType.BANK_CODE, PartType.ACCOUNT_NUMBER].map((p) => structure.extractValueMust(bban, p)).join(""); return String(mod11(value, [5, 4, 3, 2, 7, 6, 5, 4, 3, 2]) % 10); } // ISO 7064 MOD 10 function nationalPT(bban: string, structure: BbanStructure) { const V0 = "0".charCodeAt(0); const weights = [73, 17, 89, 38, 62, 45, 53, 15, 50, 5, 49, 34, 81, 76, 27, 90, 9, 30, 3]; const remainder = [PartType.BANK_CODE, PartType.BRANCH_CODE, PartType.ACCOUNT_NUMBER] .map((p) => structure.extractValueMust(bban, p)) .join("") .split("") .map((v) => v.charCodeAt(0)) .reduce((acc, v, idx) => (acc + (v - V0) * weights[idx]) % 97, 0); return String(98 - remainder).padStart(2, "0"); } /** * Class which represents bban structure * * Useful references -- * https://www.mobilefish.com/services/bban_iban/bban_iban.php */ export class BbanStructure { private static bbanFR = new BbanStructure( BbanStructurePart.bankCode(5, CharacterType.n), BbanStructurePart.branchCode(5, CharacterType.n), BbanStructurePart.accountNumber(11, CharacterType.c), BbanStructurePart.nationalCheckDigit(2, CharacterType.n, nationalFR), ); static structures: { [key in CountryCode]?: BbanStructure } = { [CountryCode.AD]: new BbanStructure( // AD2!n4!n4!n12!c BbanStructurePart.bankCode(4, CharacterType.n), BbanStructurePart.branchCode(4, CharacterType.n), BbanStructurePart.accountNumber(12, CharacterType.c), ), [CountryCode.AE]: new BbanStructure( // AE2!n3!n16!n BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.accountNumber(16, CharacterType.c), ), [CountryCode.AL]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.branchCode(4, CharacterType.n), BbanStructurePart.nationalCheckDigit(1, CharacterType.n), BbanStructurePart.accountNumber(16, CharacterType.c), ), // Provisional [CountryCode.AO]: new BbanStructure(BbanStructurePart.accountNumber(21, CharacterType.n)), [CountryCode.AT]: new BbanStructure( BbanStructurePart.bankCode(5, CharacterType.n), BbanStructurePart.accountNumber(11, CharacterType.n), ), [CountryCode.AZ]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.accountNumber(20, CharacterType.c), ), [CountryCode.BA]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.branchCode(3, CharacterType.n), BbanStructurePart.accountNumber(8, CharacterType.n), BbanStructurePart.nationalCheckDigit(2, CharacterType.n), ), [CountryCode.BE]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.accountNumber(7, CharacterType.n), BbanStructurePart.nationalCheckDigit(2, CharacterType.n, (bban: string, structure: BbanStructure) => { const accountNumber = structure.extractValue(bban, PartType.ACCOUNT_NUMBER); const bankCode = structure.extractValue(bban, PartType.BANK_CODE); if (accountNumber === null || bankCode === null) { throw new FormatException(FormatViolation.NOT_EMPTY, "account number or bank code missing"); } const value = parseInt(`${bankCode}${accountNumber}`, 10); const remainder = Math.floor(value / 97); let expected = value - remainder * 97; if (expected === 0) { expected = 97; } return String(expected).padStart(2, "0"); }), ), // Provisional [CountryCode.BF]: new BbanStructure(BbanStructurePart.accountNumber(23, CharacterType.n)), [CountryCode.BG]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.branchCode(4, CharacterType.n), BbanStructurePart.accountType(2, CharacterType.n), BbanStructurePart.accountNumber(8, CharacterType.c), ), [CountryCode.BH]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.accountNumber(14, CharacterType.c), ), // Provisional [CountryCode.BI]: new BbanStructure( // BI2!n5!n5!n11!n2!n // Changed on October 21 (from 12!n) BbanStructurePart.bankCode(5, CharacterType.n), BbanStructurePart.branchCode(5, CharacterType.n), BbanStructurePart.accountNumber(11, CharacterType.n), BbanStructurePart.nationalCheckDigit(2, CharacterType.n), // BbanStructurePart.accountNumber(12, CharacterType.n), ), // Provisional [CountryCode.BJ]: new BbanStructure( BbanStructurePart.bankCode(5, CharacterType.c), BbanStructurePart.branchCode(5, CharacterType.n), BbanStructurePart.accountNumber(12, CharacterType.n), BbanStructurePart.nationalCheckDigit(2, CharacterType.n, nationalFR), ), [CountryCode.BR]: new BbanStructure( BbanStructurePart.bankCode(8, CharacterType.n), BbanStructurePart.branchCode(5, CharacterType.n), BbanStructurePart.accountNumber(10, CharacterType.n), BbanStructurePart.accountType(1, CharacterType.a), BbanStructurePart.ownerAccountNumber(1, CharacterType.c), ), // https://www.nbrb.by/payment/ibanbic/ais-pbi_v2-7.pdf // 4c - symbolic code of the bank from the BIC directory (SI029); // 4n - balance sheet account according to the Chart of accounts of // accounting in banks and non-bank financial institutions of the // Republic of Belarus and according to the Chart of accounts of // accounting in the National Bank. Corresponds to the directory of // balance sheet accounts of RB banks (SI002) and the directory of // balance sheet accounts of the National Bank (SI001) [CountryCode.BY]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.c), BbanStructurePart.accountType(4, CharacterType.n), BbanStructurePart.accountNumber(16, CharacterType.c), ), // Provisional [CountryCode.CF]: new BbanStructure( BbanStructurePart.accountNumber(23, CharacterType.n), // @TODO is this france? ), // Provisional [CountryCode.CG]: new BbanStructure( BbanStructurePart.accountNumber(23, CharacterType.n), // @TODO is this france? ), [CountryCode.CH]: new BbanStructure( BbanStructurePart.bankCode(5, CharacterType.n), BbanStructurePart.accountNumber(12, CharacterType.c), ), // Provisional [CountryCode.CI]: new BbanStructure( BbanStructurePart.bankCode(2, CharacterType.c), BbanStructurePart.accountNumber(22, CharacterType.n), ), // Provisional [CountryCode.CM]: new BbanStructure(BbanStructurePart.accountNumber(23, CharacterType.n)), [CountryCode.CR]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.n), BbanStructurePart.accountNumber(14, CharacterType.n), ), // Provisional [CountryCode.CV]: new BbanStructure(BbanStructurePart.accountNumber(21, CharacterType.n)), [CountryCode.CY]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.branchCode(5, CharacterType.n), BbanStructurePart.accountNumber(16, CharacterType.c), ), // Registry defines this as 4!n6!n10!n -- but does not discuss branch information // This is improved with info from // https://www.cnb.cz/en/payments/iban/iban-international-bank-account-number-basic-information/ [CountryCode.CZ]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.n), BbanStructurePart.branchCode(6, CharacterType.n), BbanStructurePart.accountNumber(10, CharacterType.n), ), [CountryCode.DE]: new BbanStructure( BbanStructurePart.bankCode(8, CharacterType.n), BbanStructurePart.accountNumber(10, CharacterType.n), ), // Provisional [CountryCode.DJ]: new BbanStructure( // BI2!n5!n5!n11!n2!n // Changed on May 22 (from France's standard) BbanStructurePart.bankCode(5, CharacterType.n), BbanStructurePart.branchCode(5, CharacterType.n), BbanStructurePart.accountNumber(11, CharacterType.n), BbanStructurePart.nationalCheckDigit(2, CharacterType.n), ), // Registry defines 4!n9!n1!n -- however no information on // nationalCheckDigit exist and all documentation discusses // that the account number is "10 digits" // // This mentions checksum // https://www.finanssiala.fi/maksujenvalitys/dokumentit/IBAN_in_payments.pdf [CountryCode.DK]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.n), BbanStructurePart.accountNumber(10, CharacterType.n), ), [CountryCode.DO]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.c), BbanStructurePart.accountNumber(20, CharacterType.n), ), // Provisional [CountryCode.DZ]: new BbanStructure(BbanStructurePart.accountNumber(20, CharacterType.n)), [CountryCode.EE]: new BbanStructure( BbanStructurePart.bankCode(2, CharacterType.n), BbanStructurePart.branchCode(2, CharacterType.n), BbanStructurePart.accountNumber(11, CharacterType.n), BbanStructurePart.nationalCheckDigit(1, CharacterType.n), ), [CountryCode.EG]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.n), BbanStructurePart.branchCode(4, CharacterType.n), BbanStructurePart.accountNumber(17, CharacterType.n), ), // Spain is 4!n4!n1!n1!n10!n -- but the check digit is 2 digits? [CountryCode.ES]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.n), BbanStructurePart.branchCode(4, CharacterType.n), BbanStructurePart.nationalCheckDigit(2, CharacterType.n, nationalES), BbanStructurePart.accountNumber(10, CharacterType.n), ), // Additional details: // https://www.finanssiala.fi/maksujenvalitys/dokumentit/IBAN_in_payments.pdf [CountryCode.FI]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.accountNumber(11, CharacterType.n), ), [CountryCode.FK]: new BbanStructure( // Added July 23 BbanStructurePart.bankCode(2, CharacterType.a), BbanStructurePart.accountNumber(12, CharacterType.n), ), [CountryCode.FO]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.n), BbanStructurePart.accountNumber(9, CharacterType.n), BbanStructurePart.nationalCheckDigit(1, CharacterType.n), ), // FR IBAN covers: // GF, GP, MQ, RE, PF, TF, YT, NC, BL, MF, PM, WF [CountryCode.FR]: BbanStructure.bbanFR, // Provisional [CountryCode.GA]: BbanStructure.bbanFR, // GB IBAN covers: // IM, JE, GG [CountryCode.GB]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.branchCode(6, CharacterType.n), BbanStructurePart.accountNumber(8, CharacterType.n), ), [CountryCode.GE]: new BbanStructure( // Added Apr 23 BbanStructurePart.bankCode(2, CharacterType.a), BbanStructurePart.accountNumber(16, CharacterType.n), ), [CountryCode.GI]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.accountNumber(15, CharacterType.c), ), // Same as DK (same issues) [CountryCode.GL]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.n), BbanStructurePart.accountNumber(10, CharacterType.n), ), // Provisional [CountryCode.GQ]: BbanStructure.bbanFR, [CountryCode.GR]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.branchCode(4, CharacterType.n), BbanStructurePart.accountNumber(16, CharacterType.c), ), [CountryCode.GT]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.c), BbanStructurePart.currencyType(2, CharacterType.n), BbanStructurePart.accountType(2, CharacterType.n), BbanStructurePart.accountNumber(16, CharacterType.c), ), [CountryCode.HR]: new BbanStructure( BbanStructurePart.bankCode(7, CharacterType.n), BbanStructurePart.accountNumber(10, CharacterType.n), ), // Provisional [CountryCode.HN]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.accountNumber(20, CharacterType.n), ), // Spec says account number is 1!n15!n // no information on 1!n exists -- most likely a bank/branch check digit // https://stackoverflow.com/questions/40282199/hungarian-bban-validation [CountryCode.HU]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.branchCode(4, CharacterType.n), BbanStructurePart.branchCheckDigit(1, CharacterType.n), BbanStructurePart.accountNumber(15, CharacterType.n), BbanStructurePart.nationalCheckDigit(1, CharacterType.n), ), [CountryCode.IE]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.branchCode(6, CharacterType.n), BbanStructurePart.accountNumber(8, CharacterType.n), ), [CountryCode.IL]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.branchCode(3, CharacterType.n), BbanStructurePart.accountNumber(13, CharacterType.n), ), [CountryCode.IQ]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.branchCode(3, CharacterType.n), BbanStructurePart.accountNumber(12, CharacterType.n), ), // Provisional [CountryCode.IR]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.accountNumber(19, CharacterType.n), ), [CountryCode.IS]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.n), BbanStructurePart.branchCode(2, CharacterType.n), BbanStructurePart.accountNumber(6, CharacterType.n), BbanStructurePart.identificationNumber(10, CharacterType.n), ), [CountryCode.IT]: new BbanStructure( BbanStructurePart.nationalCheckDigit(1, CharacterType.a, nationalIT), BbanStructurePart.bankCode(5, CharacterType.n), BbanStructurePart.branchCode(5, CharacterType.n), BbanStructurePart.accountNumber(12, CharacterType.c), ), [CountryCode.JO]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.branchCode(4, CharacterType.n), BbanStructurePart.accountNumber(18, CharacterType.c), ), // Provisional [CountryCode.KM]: new BbanStructure(BbanStructurePart.accountNumber(23, CharacterType.n)), [CountryCode.KW]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.accountNumber(22, CharacterType.c), ), [CountryCode.KZ]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.accountNumber(13, CharacterType.c), ), [CountryCode.LB]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.n), BbanStructurePart.accountNumber(20, CharacterType.c), ), [CountryCode.LC]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.accountNumber(24, CharacterType.n), ), [CountryCode.LI]: new BbanStructure( BbanStructurePart.bankCode(5, CharacterType.n), BbanStructurePart.accountNumber(12, CharacterType.c), ), [CountryCode.LT]: new BbanStructure( BbanStructurePart.bankCode(5, CharacterType.n), BbanStructurePart.accountNumber(11, CharacterType.n), ), [CountryCode.LU]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.accountNumber(13, CharacterType.c), ), [CountryCode.LV]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.accountNumber(13, CharacterType.c), ), [CountryCode.LY]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.branchCode(3, CharacterType.n), BbanStructurePart.accountNumber(15, CharacterType.n), ), // Provisional [CountryCode.MA]: new BbanStructure(BbanStructurePart.accountNumber(24, CharacterType.n)), [CountryCode.MC]: new BbanStructure( BbanStructurePart.bankCode(5, CharacterType.n), BbanStructurePart.branchCode(5, CharacterType.n), BbanStructurePart.accountNumber(11, CharacterType.c), BbanStructurePart.nationalCheckDigit(2, CharacterType.n, nationalFR), ), [CountryCode.MD]: new BbanStructure( BbanStructurePart.bankCode(2, CharacterType.c), BbanStructurePart.accountNumber(18, CharacterType.c), ), [CountryCode.ME]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.accountNumber(13, CharacterType.n), BbanStructurePart.nationalCheckDigit(2, CharacterType.n), // @TODO checkdigit ), // Provisional [CountryCode.MG]: new BbanStructure( BbanStructurePart.bankCode(5, CharacterType.n), BbanStructurePart.branchCode(5, CharacterType.n), BbanStructurePart.accountNumber(11, CharacterType.c), BbanStructurePart.nationalCheckDigit(2, CharacterType.n), ), [CountryCode.MK]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.accountNumber(10, CharacterType.c), BbanStructurePart.nationalCheckDigit(2, CharacterType.n), // @TODO checkdigit ), // Provisional [CountryCode.ML]: new BbanStructure( BbanStructurePart.bankCode(1, CharacterType.a), BbanStructurePart.accountNumber(25, CharacterType.n), ), [CountryCode.MN]: new BbanStructure( // MN2!n4!n12!n // Added April 2023 BbanStructurePart.bankCode(4, CharacterType.n), BbanStructurePart.accountNumber(12, CharacterType.n), ), [CountryCode.MR]: new BbanStructure( BbanStructurePart.bankCode(5, CharacterType.n), BbanStructurePart.branchCode(5, CharacterType.n), BbanStructurePart.accountNumber(11, CharacterType.n), BbanStructurePart.nationalCheckDigit(2, CharacterType.n), ), [CountryCode.MT]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.branchCode(5, CharacterType.n), BbanStructurePart.accountNumber(18, CharacterType.c), ), // Spec: 4!a2!n2!n12!n3!n3!a // No docs on the last 3!n -- assuming account type // all found IBANs have '000' [CountryCode.MU]: new BbanStructure( BbanStructurePart.bankCode(6, CharacterType.c), // 4!a2!n BbanStructurePart.branchCode(2, CharacterType.n), BbanStructurePart.accountNumber(12, CharacterType.c), BbanStructurePart.accountType(3, CharacterType.n), BbanStructurePart.currencyType(3, CharacterType.a), ), // Provisional [CountryCode.MZ]: new BbanStructure(BbanStructurePart.accountNumber(21, CharacterType.n)), // Provisional [CountryCode.NE]: new BbanStructure( BbanStructurePart.bankCode(2, CharacterType.a), BbanStructurePart.accountNumber(22, CharacterType.n), ), [CountryCode.NI]: new BbanStructure( // NI2!n4!a20!n // Added April 2023 BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.accountNumber(20, CharacterType.n), ), [CountryCode.NL]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.accountNumber(10, CharacterType.n), ), [CountryCode.NO]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.n), BbanStructurePart.accountNumber(6, CharacterType.n), BbanStructurePart.nationalCheckDigit(1, CharacterType.n, nationalNO), ), /** * According to the SWIFT IBAN registry, the Account Number length for Oman is specified as 16!c. * However, the Central Bank of Oman specifies it as 16!n. * * References: * - SWIFT IBAN Registry (Release 99 – Dec 2024): * https://www.swift.com/swift-resource/22851/download * - Central Bank of Oman IBAN Checker: * https://cbo.gov.om/Pages/InternationalIBANChecker.aspx */ [CountryCode.OM]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.accountNumber(16, CharacterType.n), ), [CountryCode.PK]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.c), BbanStructurePart.accountNumber(16, CharacterType.c), ), // 8!n16!n [CountryCode.PL]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.branchCode(4, CharacterType.n), BbanStructurePart.nationalCheckDigit(1, CharacterType.n), BbanStructurePart.accountNumber(16, CharacterType.n), ), [CountryCode.PS]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.accountNumber(21, CharacterType.c), ), [CountryCode.PT]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.n), BbanStructurePart.branchCode(4, CharacterType.n), BbanStructurePart.accountNumber(11, CharacterType.n), BbanStructurePart.nationalCheckDigit(2, CharacterType.n, nationalPT), ), [CountryCode.QA]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.accountNumber(21, CharacterType.c), ), [CountryCode.RO]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.accountNumber(16, CharacterType.c), ), [CountryCode.RS]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.accountNumber(13, CharacterType.n), BbanStructurePart.nationalCheckDigit(2, CharacterType.n), ), [CountryCode.RU]: new BbanStructure( // RU2!n9!n5!n15!c // Added May 2022 BbanStructurePart.bankCode(9, CharacterType.n), BbanStructurePart.branchCode(5, CharacterType.n), BbanStructurePart.accountNumber(15, CharacterType.c), ), [CountryCode.SA]: new BbanStructure( BbanStructurePart.bankCode(2, CharacterType.n), BbanStructurePart.accountNumber(18, CharacterType.c), ), [CountryCode.SC]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.branchCode(2, CharacterType.n), BbanStructurePart.branchCheckDigit(2, CharacterType.n), BbanStructurePart.accountNumber(16, CharacterType.n), BbanStructurePart.currencyType(3, CharacterType.a), ), [CountryCode.SD]: new BbanStructure( // SD2!n2!n12!n // Added October 2021 BbanStructurePart.bankCode(2, CharacterType.n), BbanStructurePart.accountNumber(12, CharacterType.n), ), [CountryCode.SE]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.accountNumber(16, CharacterType.n), BbanStructurePart.nationalCheckDigit(1, CharacterType.n), ), [CountryCode.SI]: new BbanStructure( BbanStructurePart.bankCode(2, CharacterType.n), BbanStructurePart.branchCode(3, CharacterType.n), BbanStructurePart.accountNumber(8, CharacterType.n), BbanStructurePart.nationalCheckDigit(2, CharacterType.n), ), [CountryCode.SK]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.n), BbanStructurePart.accountNumber(16, CharacterType.n), ), [CountryCode.SM]: new BbanStructure( BbanStructurePart.nationalCheckDigit(1, CharacterType.a, nationalIT), BbanStructurePart.bankCode(5, CharacterType.n), BbanStructurePart.branchCode(5, CharacterType.n), BbanStructurePart.accountNumber(12, CharacterType.c), ), // Provisional [CountryCode.SN]: new BbanStructure( BbanStructurePart.bankCode(5, CharacterType.c), BbanStructurePart.branchCode(5, CharacterType.n), BbanStructurePart.accountNumber(14, CharacterType.n), ), [CountryCode.SO]: new BbanStructure( // SO2!n4!n3!n12!n // Added Feb 2023 BbanStructurePart.bankCode(4, CharacterType.n), BbanStructurePart.branchCode(3, CharacterType.n), BbanStructurePart.accountNumber(12, CharacterType.n), ), [CountryCode.ST]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.n), BbanStructurePart.branchCode(4, CharacterType.n), BbanStructurePart.accountNumber(13, CharacterType.n), ), [CountryCode.SV]: new BbanStructure( // SV2!n4!a20!n // Added March 2021 BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.branchCode(4, CharacterType.n), BbanStructurePart.accountNumber(16, CharacterType.n), ), // Provisional [CountryCode.TG]: new BbanStructure( BbanStructurePart.bankCode(2, CharacterType.a), BbanStructurePart.accountNumber(22, CharacterType.n), ), // Provisional [CountryCode.TD]: new BbanStructure( BbanStructurePart.accountNumber(23, CharacterType.n), // @TODO is this france? ), [CountryCode.TL]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.n), BbanStructurePart.accountNumber(14, CharacterType.n), BbanStructurePart.nationalCheckDigit(2, CharacterType.n), ), [CountryCode.TN]: new BbanStructure( BbanStructurePart.bankCode(2, CharacterType.n), BbanStructurePart.branchCode(3, CharacterType.n), BbanStructurePart.accountNumber(13, CharacterType.c), BbanStructurePart.nationalCheckDigit(2, CharacterType.c), ), [CountryCode.TR]: new BbanStructure( BbanStructurePart.bankCode(5, CharacterType.n), BbanStructurePart.nationalCheckDigit(1, CharacterType.c), BbanStructurePart.accountNumber(16, CharacterType.c), ), [CountryCode.UA]: new BbanStructure( BbanStructurePart.bankCode(6, CharacterType.n), BbanStructurePart.accountNumber(19, CharacterType.n), ), [CountryCode.VA]: new BbanStructure( BbanStructurePart.bankCode(3, CharacterType.c), BbanStructurePart.accountNumber(15, CharacterType.n), ), [CountryCode.VG]: new BbanStructure( BbanStructurePart.bankCode(4, CharacterType.a), BbanStructurePart.accountNumber(16, CharacterType.n), ), [CountryCode.XK]: new BbanStructure( BbanStructurePart.bankCode(2, CharacterType.n), BbanStructurePart.branchCode(2, CharacterType.n), BbanStructurePart.accountNumber(10, CharacterType.n), BbanStructurePart.nationalCheckDigit(2, CharacterType.n), ), }; private entries: BbanStructurePart[]; private constructor(...entries: BbanStructurePart[]) { this.entries = entries; } getParts(): BbanStructurePart[] { return this.entries; } validate(bban: string): void { this.validateBbanLength(bban); this.validateBbanEntries(bban); } extractValue(bban: string, partType: PartType): string | null { let bbanPartOffset = 0; let result = null; for (const part of this.getParts()) { const partLength = part.getLength(); const partValue = bban.substring(bbanPartOffset, bbanPartOffset + partLength); bbanPartOffset = bbanPartOffset + partLength; if (part.getPartType() == partType) { result = (result || "") + partValue; } } return result; } /** * Return part type or fail */ extractValueMust(bban: string, partType: PartType): string { const value = this.extractValue(bban, partType); if (value === null) { throw new RequiredPartTypeMissing(`Required part type [${partType}] missing`); } return value; } /** * @param countryCode the country code. * @return BbanStructure for specified country or null if country is not supported. */ static forCountry(countryCode: CountryCode | string | undefined): BbanStructure | null { if (!countryCode) { return null; } return this.structures[countryCode as CountryCode] || null; } static getEntries(): BbanStructure[] { return Object.values(this.structures) as BbanStructure[]; } static supportedCountries(): CountryCode[] { return Object.keys(this.structures) as CountryCode[]; } /** * Returns the length of bban. * * @return int length */ public getBbanLength(): number { return this.entries.reduce((acc, e) => acc + e.getLength(), 0); } private validateBbanLength(bban: string) { const expectedBbanLength = this.getBbanLength(); const bbanLength = bban.length; if (expectedBbanLength != bbanLength) { throw new FormatException( FormatViolation.BBAN_LENGTH, `[${bban}] length is ${bbanLength}, expected BBAN length is: ${expectedBbanLength}`, String(bbanLength), String(expectedBbanLength), ); } } private validateBbanEntries(bban: string) { let offset = 0; for (const part of this.getParts()) { const partLength = part.getLength(); const entryValue = bban.substring(offset, offset + partLength); offset = offset + partLength; // validate character type this.validateBbanEntryCharacterType(bban, part, entryValue); } } private validateBbanEntryCharacterType(bban: string, part: BbanStructurePart, entryValue: string) { if (!part.validate(entryValue)) { switch (part.getCharacterType()) { case CharacterType.a: throw new FormatException( FormatViolation.BBAN_ONLY_UPPER_CASE_LETTERS, `[${entryValue}] must contain only upper case letters.`, entryValue, ); case CharacterType.c: throw new FormatException( FormatViolation.BBAN_ONLY_DIGITS_OR_LETTERS, `[${entryValue}] must contain only digits or letters.`, entryValue, ); case CharacterType.n: throw new FormatException( FormatViolation.BBAN_ONLY_DIGITS, `[${entryValue}] must contain only digits.`, entryValue, ); } } if (part.getPartType() === PartType.NATIONAL_CHECK_DIGIT && part.hasGenerator) { const expected = part.generate(bban, this); if (entryValue !== expected) { throw new FormatException( FormatViolation.NATIONAL_CHECK_DIGIT, `national check digit(s) don't match expect=[${expected}] actual=[${entryValue}]`, expected, entryValue, ); } } } }