From 937f93c22e650001e0f0684e2098c3a2f2033bfd Mon Sep 17 00:00:00 2001 From: Antoine Froger Date: Wed, 24 Apr 2024 15:09:13 +0200 Subject: [PATCH] Implement IBAN generation (#172) * Add IBAN generation * Move all IBAN related code inside iban.go * Fix 'Unused method receiver RVV-B0013' https://app.deepsource.com/gh/jaswdr/faker/run/a17100c5-d424-41ff-8b2f-3bc9f63de59a/go/RVV-B0013 * Fix 'Unused method receiver RVV-B0013' https://app.deepsource.com/gh/jaswdr/faker/run/a17100c5-d424-41ff-8b2f-3bc9f63de59a/go/RVV-B0013?listindex=0 --- iban.go | 161 ++++++++++++++++++++++++ iban_test.go | 344 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 505 insertions(+) create mode 100644 iban.go create mode 100644 iban_test.go diff --git a/iban.go b/iban.go new file mode 100644 index 0000000..de342e0 --- /dev/null +++ b/iban.go @@ -0,0 +1,161 @@ +package faker + +import ( + "fmt" + "strconv" + "strings" + "unicode" +) + +// ibanFormats lists all IBAN formats, source: @link https://www.swift.com/swift-resource/9606/download +// n: numeric, a: alphabetic, c: alphanumeric +var ibanFormats = map[string]string{ + "AD": format("n4", "n4", "c12"), + "AE": format("n3", "n16"), + "AL": format("n8", "c16"), + "AT": format("n5", "n11"), + "AZ": format("a4", "c20"), + "BA": format("n3", "n3", "n8", "n2"), + "BE": format("n3", "n7", "n2"), + "BG": format("a4", "n4", "n2", "c8"), + "BH": format("a4", "c14"), + "BR": format("n8", "n5", "n10", "a1", "c1"), + "CH": format("n5", "c12"), + "CR": format("n4", "n14"), + "CY": format("n3", "n5", "c16"), + "CZ": format("n4", "n6", "n10"), + "DE": format("n8", "n10"), + "DK": format("n4", "n9", "n1"), + "DO": format("c4", "n20"), + "EE": format("n2", "n2", "n11", "n1"), + "ES": format("n4", "n4", "n1", "n1", "n10"), + "FI": format("n6", "n7", "n1"), + "FR": format("n5", "n5", "c11", "n2"), + "GB": format("a4", "n6", "n8"), + "GE": format("a2", "n16"), + "GI": format("a4", "c15"), + "GR": format("n3", "n4", "c16"), + "GT": format("c4", "c20"), + "HR": format("n7", "n10"), + "HU": format("n3", "n4", "n1", "n15", "n1"), + "IE": format("a4", "n6", "n8"), + "IL": format("n3", "n3", "n13"), + "IS": format("n4", "n2", "n6", "n10"), + "IT": format("a1", "n5", "n5", "c12"), + "KW": format("a4", "n22"), + "KZ": format("n3", "c13"), + "LB": format("n4", "c20"), + "LI": format("n5", "c12"), + "LT": format("n5", "n11"), + "LU": format("n3", "c13"), + "LV": format("a4", "c13"), + "MC": format("n5", "n5", "c11", "n2"), + "MD": format("c2", "c18"), + "ME": format("n3", "n13", "n2"), + "MK": format("n3", "c10", "n2"), + "MR": format("n5", "n5", "n11", "n2"), + "MT": format("a4", "n5", "c18"), + "MU": format("a4", "n2", "n2", "n12", "n3", "a3"), + "NL": format("a4", "n10"), + "NO": format("n4", "n6", "n1"), + "PK": format("a4", "c16"), + "PL": format("n8", "n16"), + "PS": format("a4", "c21"), + "PT": format("n4", "n4", "n11", "n2"), + "RO": format("a4", "c16"), + "RS": format("n3", "n13", "n2"), + "SA": format("n2", "c18"), + "SE": format("n3", "n16", "n1"), + "SI": format("n5", "n8", "n2"), + "SK": format("n4", "n6", "n10"), + "SM": format("a1", "n5", "n5", "c12"), + "TN": format("n2", "n3", "n13", "n2"), + "TR": format("n5", "n1", "c16"), + "VG": format("a4", "n16"), +} + +// format interprets the format of each section of the iban and returns a string with a specific format +// Example: format("n5", "a2", "c1") => "nnnnnaac" +func format(sections ...string) string { + var res string + for _, s := range sections { + if len(s) == 0 { + continue + } + if class := s[0]; unicode.IsLetter(rune(class)) { + size, _ := strconv.Atoi(s[1:]) + res += strings.Repeat(string(class), size) + } + } + return res +} + +// Iban returns a fake IBAN for Payment +func (p Payment) Iban() string { + return p.ibanForCountry(p.Faker.RandomStringMapKey(ibanFormats)) +} + +// ibanForCountry returns a fake IBAN for a specific country +// Returns an empty string if the country is not supported +func (p Payment) ibanForCountry(countryCode string) string { + format, ok := ibanFormats[countryCode] + if !ok { + return "" + } + + bban := strings.ToUpper(p.bban(format)) + checksum := ibanChecksum(countryCode + "00" + bban) + + return countryCode + checksum + bban +} + +// bban generates a fake Basic Bank Account Number (BBAN) based on the format +// the provided format must be a string only containing the following characters: +// n: numeric, a: alphabetic, c: alphanumeric +// which will be replaced by a random number or letter +func (p Payment) bban(format string) string { + format = strings.ReplaceAll(format, "n", "#") + format = strings.ReplaceAll(format, "a", "?") + s := "?" + if p.Faker.Bool() { + s = "#" + } + format = strings.ReplaceAll(format, "c", s) + + return p.Faker.Bothify(format) +} + +func ibanChecksum(iban string) string { + iban = strings.ToUpper(strings.ReplaceAll(iban, " ", "")) + + // Move first 4 characters to the end, and set checksum to 00 + iban = iban[4:] + iban[:2] + "00" + + // Replace letters with their respective numbers + var numericIBAN string + for _, char := range iban { + if char >= 'A' && char <= 'Z' { + numericIBAN += strconv.Itoa(int(char - 'A' + 10)) + } else { + numericIBAN += string(char) + } + } + + // Perform modulo 97 operation on the numeric IBAN + remainder := 0 + for _, char := range numericIBAN { + digit := int(char - '0') + remainder = (remainder*10 + digit) % 97 + } + + // Calculate checksum + checksum := 98 - remainder + if checksum < 10 { + return fmt.Sprintf("0%d", checksum) + } + return fmt.Sprintf("%d", checksum) +} + +func isIbanValid(iban string) bool { + return ibanChecksum(iban) == iban[2:4] +} diff --git a/iban_test.go b/iban_test.go new file mode 100644 index 0000000..4db9100 --- /dev/null +++ b/iban_test.go @@ -0,0 +1,344 @@ +package faker + +import ( + "regexp" + "testing" +) + +var ibanRegex = map[string]string{ + "AD": `^AD\d{2}\d{4}\d{4}[A-Z0-9]{12}$`, + "AE": `^AE\d{2}\d{3}\d{16}$`, + "AL": `^AL\d{2}\d{8}[A-Z0-9]{16}$`, + "AT": `^AT\d{2}\d{5}\d{11}$`, + "AZ": `^AZ\d{2}[A-Z]{4}[A-Z0-9]{20}$`, + "BA": `^BA\d{2}\d{3}\d{3}\d{8}\d{2}$`, + "BE": `^BE\d{2}\d{3}\d{7}\d{2}$`, + "BG": `^BG\d{2}[A-Z]{4}\d{4}\d{2}[A-Z0-9]{8}$`, + "BH": `^BH\d{2}[A-Z]{4}[A-Z0-9]{14}$`, + "BR": `^BR\d{2}\d{8}\d{5}\d{10}[A-Z]{1}[A-Z0-9]{1}$`, + "CH": `^CH\d{2}\d{5}[A-Z0-9]{12}$`, + "CR": `^CR\d{2}\d{4}\d{14}$`, + "CY": `^CY\d{2}\d{3}\d{5}[A-Z0-9]{16}$`, + "CZ": `^CZ\d{2}\d{4}\d{6}\d{10}$`, + "DE": `^DE\d{2}\d{8}\d{10}$`, + "DK": `^DK\d{2}\d{4}\d{9}\d{1}$`, + "DO": `^DO\d{2}[A-Z0-9]{4}\d{20}$`, + "EE": `^EE\d{2}\d{2}\d{2}\d{11}\d{1}$`, + "ES": `^ES\d{2}\d{4}\d{4}\d{1}\d{1}\d{10}$`, + "FI": `^FI\d{2}\d{6}\d{7}\d{1}$`, + "FR": `^FR\d{2}\d{5}\d{5}[A-Z0-9]{11}\d{2}$`, + "GB": `^GB\d{2}[A-Z]{4}\d{6}\d{8}$`, + "GE": `^GE\d{2}[A-Z]{2}\d{16}$`, + "GI": `^GI\d{2}[A-Z]{4}[A-Z0-9]{15}$`, + "GR": `^GR\d{2}\d{3}\d{4}[A-Z0-9]{16}$`, + "GT": `^GT\d{2}[A-Z0-9]{4}[A-Z0-9]{20}$`, + "HR": `^HR\d{2}\d{7}\d{10}$`, + "HU": `^HU\d{2}\d{3}\d{4}\d{1}\d{15}\d{1}$`, + "IE": `^IE\d{2}[A-Z]{4}\d{6}\d{8}$`, + "IL": `^IL\d{2}\d{3}\d{3}\d{13}$`, + "IS": `^IS\d{2}\d{4}\d{2}\d{6}\d{10}$`, + "IT": `^IT\d{2}[A-Z]{1}\d{5}\d{5}[A-Z0-9]{12}$`, + "KW": `^KW\d{2}[A-Z]{4}\d{22}$`, + "KZ": `^KZ\d{2}\d{3}[A-Z0-9]{13}$`, + "LB": `^LB\d{2}\d{4}[A-Z0-9]{20}$`, + "LI": `^LI\d{2}\d{5}[A-Z0-9]{12}$`, + "LT": `^LT\d{2}\d{5}\d{11}$`, + "LU": `^LU\d{2}\d{3}[A-Z0-9]{13}$`, + "LV": `^LV\d{2}[A-Z]{4}[A-Z0-9]{13}$`, + "MC": `^MC\d{2}\d{5}\d{5}[A-Z0-9]{11}\d{2}$`, + "MD": `^MD\d{2}[A-Z0-9]{2}[A-Z0-9]{18}$`, + "ME": `^ME\d{2}\d{3}\d{13}\d{2}$`, + "MK": `^MK\d{2}\d{3}[A-Z0-9]{10}\d{2}$`, + "MR": `^MR\d{2}\d{5}\d{5}\d{11}\d{2}$`, + "MT": `^MT\d{2}[A-Z]{4}\d{5}[A-Z0-9]{18}$`, + "MU": `^MU\d{2}[A-Z]{4}\d{2}\d{2}\d{12}\d{3}[A-Z]{3}$`, + "NL": `^NL\d{2}[A-Z]{4}\d{10}$`, + "NO": `^NO\d{2}\d{4}\d{6}\d{1}$`, + "PK": `^PK\d{2}[A-Z]{4}[A-Z0-9]{16}$`, + "PL": `^PL\d{2}\d{8}\d{16}$`, + "PS": `^PS\d{2}[A-Z]{4}[A-Z0-9]{21}$`, + "PT": `^PT\d{2}\d{4}\d{4}\d{11}\d{2}$`, + "RO": `^RO\d{2}[A-Z]{4}[A-Z0-9]{16}$`, + "RS": `^RS\d{2}\d{3}\d{13}\d{2}$`, + "SA": `^SA\d{2}\d{2}[A-Z0-9]{18}$`, + "SE": `^SE\d{2}\d{3}\d{16}\d{1}$`, + "SI": `^SI\d{2}\d{5}\d{8}\d{2}$`, + "SK": `^SK\d{2}\d{4}\d{6}\d{10}$`, + "SM": `^SM\d{2}[A-Z]{1}\d{5}\d{5}[A-Z0-9]{12}$`, + "TN": `^TN\d{2}\d{2}\d{3}\d{13}\d{2}$`, + "TR": `^TR\d{2}\d{5}\d{1}[A-Z0-9]{16}$`, + "VG": `^VG\d{2}[A-Z]{4}\d{16}$`, +} + +func TestIban(t *testing.T) { + p := New().Payment() + + iban := p.Iban() + Expect(t, true, isIbanValid(iban)) + _, ok := ibanRegex[iban[:2]] + Expect(t, true, ok) +} + +func TestIbanPerCountry(t *testing.T) { + p := New().Payment() + + for countryCode, regex := range ibanRegex { + iban := p.ibanForCountry(countryCode) + Expect(t, countryCode, iban[:2]) + matched, err := regexp.MatchString(regex, iban) + Expect(t, nil, err) + Expect(t, true, matched) + Expect(t, true, isIbanValid(iban)) + } + + Expect(t, "", p.ibanForCountry("unknown")) +} + +func TestFormat(t *testing.T) { + Expect(t, "nnaaaacccccc", format("n2", "a4", "c6")) + Expect(t, "cccccccccccaan", format("c11", "a2", "n1")) + Expect(t, "", format("8", "18", "218")) + Expect(t, "", format("a", "bb", "cccc")) + Expect(t, "", format("ab12", "aa3", "")) +} + +func BenchmarkFormat(b *testing.B) { + for i := 0; i < b.N; i++ { + format("n5", "n5", "c11", "n2") + } +} + +func TestBban(t *testing.T) { + p := New().Payment() + bban := p.bban("nnnaaaaccaannccc") + matched, err := regexp.MatchString(`^\d{3}[a-z]{4}[a-z0-9]{2}[a-z]{2}\d{2}[a-z0-9]{3}$`, bban) + Expect(t, nil, err) + Expect(t, true, matched) +} + +func BenchmarkBban(b *testing.B) { + p := New().Payment() + for i := 0; i < b.N; i++ { + _ = p.bban("nnnnnaaaaacccccccccccnn") + } +} + +func TestIbanChecksum(t *testing.T) { + var ibans = map[string]string{ + "AL00212110090000000235698741": "47", + "AD0000012030200359100100": "12", + "AT001904300234573201": "61", + "AZ00NABZ00000000137010001944": "21", + "BH00BMAG00001299123456": "67", + "BE00539007547034": "68", + "BA001290079401028494": "39", + "BR0024891749412660603618210F3": "77", + "BG00BNBG96611020345678": "80", + "CR0015202001026284066": "05", + "HR0010010051863000160": "12", + "CY00002001280000001200527600": "17", + "CZ0008000000192000145399": "65", + "DK0000400440116243": "50", + "DO00BAGR00000001212453611324": "28", + "EE002200221020145685": "38", + "FO0064600001631634": "62", + "FI0012345600000785": "21", + "FR0020041010050500013M02606": "14", + "GE00NB0000000101904917": "29", + "DE00370400440532013000": "89", + "GI00NWBK000000007099453": "75", + "GR0001101250000000012300695": "16", + "GL0064710001000206": "89", + "GT00TRAJ01020000001210029690": "82", + "HU00117730161111101800000000": "42", + "IS000159260076545510730339": "14", + "IE00AIBK93115212345678": "29", + "IL000108000000099999999": "62", + "IT00X0542811101000000123456": "60", + "KZ00125KZT5004100100": "86", + "KW00CBKU0000000000001234560101": "81", + "LV00BANK0000435195001": "80", + "LB00099900000001001901229114": "62", + "LI00088100002324013AA": "21", + "LT001000011101001000": "12", + "LU000019400644750000": "28", + "MK00250120000058984": "07", + "MT00MALT011000012345MTLCAST001S": "84", + "MR0000020001010000123456753": "13", + "MU00BOMM0101101030300200000MUR": "17", + "MD00AG000225100013104168": "24", + "MC0011222000010123456789030": "58", + "ME00505000012345678951": "25", + "NL00ABNA0417164300": "91", + "NO0086011117947": "93", + "PK00SCBL0000001123456702": "36", + "PL00109010140000071219812874": "61", + "PS00PALS000000000400123456702": "92", + "PT00000201231234567890154": "50", + "QA00DOHB00001234567890ABCDEFG": "58", + "RO00AAAA1B31007593840000": "49", + "SM00U0322509800000000270100": "86", + "SA0080000000608010167519": "03", + "RS00260005601001611379": "35", + "SK0012000000198742637541": "31", + "SI00263300012039086": "56", + "ES0021000418450200051332": "91", + "SE0050000000058398257466": "45", + "CH0000762011623852957": "93", + "TN0010006035183598478831": "59", + "TR000006100519786457841326": "33", + "AE000331234567890123456": "07", + "GB00NWBK60161331926819": "29", + "VG00VPVG0000012345678901": "96", + "YY00KIHB12476423125915947930915268": "24", + "ZZ00VLQT382332233206588011313776421": "25", + } + + for iban, checksum := range ibans { + Expect(t, checksum, ibanChecksum(iban)) + } +} + +func TestIsIbanValid(t *testing.T) { + ibans := map[string]bool{ + "AL47212110090000000235698741": true, + "AD1200012030200359100100": true, + "AT611904300234573201": true, + "AZ21NABZ00000000137010001944": true, + "BH67BMAG00001299123456": true, + "BE68539007547034": true, + "BA391290079401028494": true, + "BR7724891749412660603618210F3": true, + "BG80BNBG96611020345678": true, + "CR0515202001026284066": true, + "HR1210010051863000160": true, + "CY17002001280000001200527600": true, + "CZ6508000000192000145399": true, + "DK5000400440116243": true, + "DO28BAGR00000001212453611324": true, + "EE382200221020145685": true, + "FO6264600001631634": true, + "FI2112345600000785": true, + "FR1420041010050500013M02606": true, + "GE29NB0000000101904917": true, + "DE89370400440532013000": true, + "GI75NWBK000000007099453": true, + "GR1601101250000000012300695": true, + "GL8964710001000206": true, + "GT82TRAJ01020000001210029690": true, + "HU42117730161111101800000000": true, + "IS140159260076545510730339": true, + "IE29AIBK93115212345678": true, + "IL620108000000099999999": true, + "IT60X0542811101000000123456": true, + "KZ86125KZT5004100100": true, + "KW81CBKU0000000000001234560101": true, + "LV80BANK0000435195001": true, + "LB62099900000001001901229114": true, + "LI21088100002324013AA": true, + "LT121000011101001000": true, + "LU280019400644750000": true, + "MK07250120000058984": true, + "MT84MALT011000012345MTLCAST001S": true, + "MR1300020001010000123456753": true, + "MU17BOMM0101101030300200000MUR": true, + "MD24AG000225100013104168": true, + "MC5811222000010123456789030": true, + "ME25505000012345678951": true, + "NL91ABNA0417164300": true, + "NO9386011117947": true, + "PK36SCBL0000001123456702": true, + "PL61109010140000071219812874": true, + "PS92PALS000000000400123456702": true, + "PT50000201231234567890154": true, + "QA58DOHB00001234567890ABCDEFG": true, + "RO49AAAA1B31007593840000": true, + "SM86U0322509800000000270100": true, + "SA0380000000608010167519": true, + "RS35260005601001611379": true, + "SK3112000000198742637541": true, + "SI56263300012039086": true, + "ES9121000418450200051332": true, + "SE4550000000058398257466": true, + "CH9300762011623852957": true, + "TN5910006035183598478831": true, + "TR330006100519786457841326": true, + "AE070331234567890123456": true, + "GB29NWBK60161331926819": true, + "VG96VPVG0000012345678901": true, + "YY24KIHB12476423125915947930915268": true, + "ZZ25VLQT382332233206588011313776421": true, + + "AL4721211009000000023569874": false, + "AD120001203020035910010": false, + "AT61190430023457320": false, + "AZ21NABZ0000000013701000194": false, + "BH67BMAG0000129912345": false, + "BE6853900754703": false, + "BA39129007940102849": false, + "BR7724891749412660603618210F": false, + "BG80BNBG9661102034567": false, + "CR051520200102628406": false, + "HR121001005186300016": false, + "CY1700200128000000120052760": false, + "CZ650800000019200014539": false, + "DK500040044011624": false, + "DO28BAGR0000000121245361132": false, + "EE38220022102014568": false, + "FO626460000163163": false, + "FI2112345600000780": false, + "FR1420041010050500013M0260": false, + "GE29NB000000010190491": false, + "DE8937040044053201300": false, + "GI75NWBK00000000709945": false, + "GR160110125000000001230069": false, + "GL896471000100020": false, + "GT82TRAJ0102000000121002969": false, + "HU4211773016111110180000000": false, + "IS14015926007654551073033": false, + "IE29AIBK9311521234567": false, + "IL62010800000009999999": false, + "IT60X054281110100000012345": false, + "KZ86125KZT500410010": false, + "KW81CBKU000000000000123456010": false, + "LV80BANK000043519500": false, + "LB6209990000000100190122911": false, + "LI21088100002324013A": false, + "LT12100001110100100": false, + "LU28001940064475000": false, + "MK0725012000005898": false, + "MT84MALT011000012345MTLCAST001": false, + "MR130002000101000012345675": false, + "MU17BOMM0101101030300200000MU": false, + "MD24AG00022510001310416": false, + "MC58112220000101234567890": false, + "ME2550500001234567895": false, + "NL91ABNA041716430": false, + "NO938601111794": false, + "PK36SCBL000000112345670": false, + "PL6110901014000007121981287": false, + "PS92PALS00000000040012345670": false, + "PT5000020123123456789015": false, + "QA58DOHB00001234567890ABCDEF": false, + "RO49AAAA1B3100759384000": false, + "SM86U032250980000000027010": false, + "SA038000000060801016751": false, + "RS3526000560100161137": false, + "SK311200000019874263754": false, + "SI5626330001203908": false, + "ES912100041845020005133": false, + "SE455000000005839825746": false, + "CH930076201162385295": false, + "TN591000603518359847883": false, + "TR33000610051978645784132": false, + "AE07033123456789012345": false, + "GB29NWBK6016133192681": false, + "VG96VPVG000001234567890": false, + "YY24KIHB1247642312591594793091526": false, + "ZZ25VLQT38233223320658801131377642": false, + } + + for iban, expected := range ibans { + Expect(t, expected, isIbanValid(iban)) + } +}