ibankit
Version:
Validation, field extraction and creation of IBAN, BBAN, BIC numbers
949 lines (796 loc) • 33.3 kB
text/typescript
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,
);
}
}
}
}