commit 4353d731b02685d00cf1f3275101ff33f840f8f1
parent 767e006ac586ca437c1ded8897f3be38a92441f2
Author: Antoine A <>
Date: Thu, 13 Mar 2025 17:07:09 +0100
common: improve IBAN logic
Diffstat:
4 files changed, 292 insertions(+), 32 deletions(-)
diff --git a/common/src/main/kotlin/TalerCommon.kt b/common/src/main/kotlin/TalerCommon.kt
@@ -443,7 +443,7 @@ class IbanPayto internal constructor(
}
fun rand(): IbanPayto {
- return parse("payto://iban/SANDBOXX/${IBAN.rand()}").expectIban()
+ return parse("payto://iban/SANDBOXX/${IBAN.rand(Country.DE)}").expectIban()
}
}
}
diff --git a/common/src/main/kotlin/iban.kt b/common/src/main/kotlin/iban.kt
@@ -19,21 +19,6 @@
package tech.libeufin.common
-// Taken from the ISO20022 XSD schema
-private val bicRegex = Regex("^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$")
-
-fun validateBic(bic: String): Boolean {
- return bicRegex.matches(bic)
-}
-
-// Taken from the ISO20022 XSD schema
-private val ibanRegex = Regex("^[A-Z]{2}[0-9]{2}[a-zA-Z0-9]{1,30}$")
-
-fun validateIban(iban: String): Boolean {
- return ibanRegex.matches(iban)
-}
-
-
@JvmInline
value class IBAN private constructor(val value: String) {
override fun toString(): String = value
@@ -44,16 +29,12 @@ value class IBAN private constructor(val value: String) {
fun checksum(iban: String): Int =
(iban.substring(4 until iban.length).asSequence() + iban.substring(0 until 4).asSequence())
.fold(0) { acc, char ->
- var checksum = if (char.isDigit()) {
- acc * 10 + char.code - '0'.code
+ if (char.isDigit()) {
+ (acc * 10 + char.code - '0'.code) % 97
} else {
- acc * 100 + char.code - 'A'.code + 10
- }
- if (checksum > 9_999_999) {
- checksum %= 97
+ (acc * 100 + char.code - 'A'.code + 10) % 97
}
- checksum
- } % 97
+ }
fun parse(raw: String): IBAN {
@@ -61,26 +42,46 @@ value class IBAN private constructor(val value: String) {
if (iban.length < 5) {
throw CommonError.Payto("malformed IBAN, string is too small only ${iban.length} char")
}
- for (c in iban.substring(0 until 2)) {
+ val countryCode = iban.substring(0 until 2)
+ for (c in countryCode) {
if (!c.isLetter()) throw CommonError.Payto("malformed IBAN, malformed country code")
}
for (c in iban.substring(2 until 4)) {
if (!c.isDigit()) throw CommonError.Payto("malformed IBAN, malformed check digit")
}
+ val country = try {
+ Country.valueOf(countryCode)
+ } catch (e: IllegalArgumentException) {
+ throw CommonError.Payto("malformed IBAN, unknown country $countryCode")
+ }
+ if (country == Country.DE && iban.length == 15) {
+ // This is allowed for retrocompatibility with libeufin-bank malformed DE IBAN
+ } else if (!country.bbanRegex.matches(iban.substring(4))) {
+ throw CommonError.Payto("malformed IBAN, invalid char")
+ }
val checksum = checksum(iban)
if (checksum != 1) throw CommonError.Payto("malformed IBAN, modulo is $checksum expected 1")
return IBAN(iban)
}
- fun rand(): IBAN {
- val bban = (0..10).map {
- (0..9).random()
- }.joinToString("") // 10 digits account number
- val checkDigits = 98 - checksum("DE00$bban");
+ fun rand(country: Country): IBAN {
+ val bban = buildString {
+ for ((repetition, c) in country.rules) {
+ val alphabet = when (c) {
+ IbanC.n -> "0123456789"
+ IbanC.a -> "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ IbanC.c -> "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+ }
+ repeat(repetition) {
+ append(alphabet.random())
+ }
+ }
+ }
+ val checkDigits = 98 - checksum("${country.name}00$bban");
return if (checkDigits < 10) {
- IBAN("DE0$checkDigits$bban")
+ IBAN("${country.name}0$checkDigits$bban")
} else {
- IBAN("DE$checkDigits$bban")
+ IBAN("${country.name}$checkDigits$bban")
}
}
}
diff --git a/common/src/main/kotlin/registry.kt b/common/src/main/kotlin/registry.kt
@@ -0,0 +1,215 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2025 Taler Systems S.A.
+
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+package tech.libeufin.common
+
+/** IBAN ASCII characters rules */
+enum class IbanC {
+ /** Digits (0-9) */
+ n,
+ /** Uppercase (A-Z) */
+ a,
+ /** Digits or uppercase (0-9 & A-Z) */
+ c,
+}
+
+enum class Country(val ibanLen: Int, val rules: List<Pair<Int, IbanC>>, val bbanRegex: Regex) {
+ AD(24, listOf(Pair(8, IbanC.n),Pair(12, IbanC.c),), Regex("^[0-9]{8}[0-9A-Z]{12}$")),
+ AE(23, listOf(Pair(19, IbanC.n),), Regex("^[0-9]{19}$")),
+ AL(28, listOf(Pair(8, IbanC.n),Pair(16, IbanC.c),), Regex("^[0-9]{8}[0-9A-Z]{16}$")),
+ AT(20, listOf(Pair(16, IbanC.n),), Regex("^[0-9]{16}$")),
+ AZ(28, listOf(Pair(4, IbanC.a),Pair(20, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{20}$")),
+ BA(20, listOf(Pair(16, IbanC.n),), Regex("^[0-9]{16}$")),
+ BE(16, listOf(Pair(12, IbanC.n),), Regex("^[0-9]{12}$")),
+ BG(22, listOf(Pair(4, IbanC.a),Pair(6, IbanC.n),Pair(8, IbanC.c),), Regex("^[A-Z]{4}[0-9]{6}[0-9A-Z]{8}$")),
+ BH(22, listOf(Pair(4, IbanC.a),Pair(14, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{14}$")),
+ BI(27, listOf(Pair(23, IbanC.n),), Regex("^[0-9]{23}$")),
+ BR(29, listOf(Pair(23, IbanC.n),Pair(1, IbanC.a),Pair(1, IbanC.c),), Regex("^[0-9]{23}[A-Z]{1}[0-9A-Z]{1}$")),
+ BY(28, listOf(Pair(4, IbanC.c),Pair(4, IbanC.n),Pair(16, IbanC.c),), Regex("^[0-9A-Z]{4}[0-9]{4}[0-9A-Z]{16}$")),
+ CH(21, listOf(Pair(5, IbanC.n),Pair(12, IbanC.c),), Regex("^[0-9]{5}[0-9A-Z]{12}$")),
+ CR(22, listOf(Pair(18, IbanC.n),), Regex("^[0-9]{18}$")),
+ CY(28, listOf(Pair(8, IbanC.n),Pair(16, IbanC.c),), Regex("^[0-9]{8}[0-9A-Z]{16}$")),
+ CZ(24, listOf(Pair(20, IbanC.n),), Regex("^[0-9]{20}$")),
+ DE(22, listOf(Pair(18, IbanC.n),), Regex("^[0-9]{18}$")),
+ DJ(27, listOf(Pair(23, IbanC.n),), Regex("^[0-9]{23}$")),
+ DK(18, listOf(Pair(14, IbanC.n),), Regex("^[0-9]{14}$")),
+ DO(28, listOf(Pair(4, IbanC.c),Pair(20, IbanC.n),), Regex("^[0-9A-Z]{4}[0-9]{20}$")),
+ EE(20, listOf(Pair(16, IbanC.n),), Regex("^[0-9]{16}$")),
+ EG(29, listOf(Pair(25, IbanC.n),), Regex("^[0-9]{25}$")),
+ ES(24, listOf(Pair(20, IbanC.n),), Regex("^[0-9]{20}$")),
+ FI(18, listOf(Pair(14, IbanC.n),), Regex("^[0-9]{14}$")),
+ FK(18, listOf(Pair(2, IbanC.a),Pair(12, IbanC.n),), Regex("^[A-Z]{2}[0-9]{12}$")),
+ FO(18, listOf(Pair(14, IbanC.n),), Regex("^[0-9]{14}$")),
+ FR(27, listOf(Pair(10, IbanC.n),Pair(11, IbanC.c),Pair(2, IbanC.n),), Regex("^[0-9]{10}[0-9A-Z]{11}[0-9]{2}$")),
+ GB(22, listOf(Pair(4, IbanC.a),Pair(14, IbanC.n),), Regex("^[A-Z]{4}[0-9]{14}$")),
+ GE(22, listOf(Pair(2, IbanC.a),Pair(16, IbanC.n),), Regex("^[A-Z]{2}[0-9]{16}$")),
+ GI(23, listOf(Pair(4, IbanC.a),Pair(15, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{15}$")),
+ GL(18, listOf(Pair(14, IbanC.n),), Regex("^[0-9]{14}$")),
+ GR(27, listOf(Pair(7, IbanC.n),Pair(16, IbanC.c),), Regex("^[0-9]{7}[0-9A-Z]{16}$")),
+ GT(28, listOf(Pair(24, IbanC.c),), Regex("^[0-9A-Z]{24}$")),
+ HN(28, listOf(Pair(4, IbanC.a),Pair(20, IbanC.n),), Regex("^[A-Z]{4}[0-9]{20}$")),
+ HR(21, listOf(Pair(17, IbanC.n),), Regex("^[0-9]{17}$")),
+ HU(28, listOf(Pair(24, IbanC.n),), Regex("^[0-9]{24}$")),
+ IE(22, listOf(Pair(4, IbanC.a),Pair(14, IbanC.n),), Regex("^[A-Z]{4}[0-9]{14}$")),
+ IL(23, listOf(Pair(19, IbanC.n),), Regex("^[0-9]{19}$")),
+ IQ(23, listOf(Pair(4, IbanC.a),Pair(15, IbanC.n),), Regex("^[A-Z]{4}[0-9]{15}$")),
+ IS(26, listOf(Pair(22, IbanC.n),), Regex("^[0-9]{22}$")),
+ IT(27, listOf(Pair(1, IbanC.a),Pair(10, IbanC.n),Pair(12, IbanC.c),), Regex("^[A-Z]{1}[0-9]{10}[0-9A-Z]{12}$")),
+ JO(30, listOf(Pair(4, IbanC.a),Pair(4, IbanC.n),Pair(18, IbanC.c),), Regex("^[A-Z]{4}[0-9]{4}[0-9A-Z]{18}$")),
+ KW(30, listOf(Pair(4, IbanC.a),Pair(22, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{22}$")),
+ KZ(20, listOf(Pair(3, IbanC.n),Pair(13, IbanC.c),), Regex("^[0-9]{3}[0-9A-Z]{13}$")),
+ LB(28, listOf(Pair(4, IbanC.n),Pair(20, IbanC.c),), Regex("^[0-9]{4}[0-9A-Z]{20}$")),
+ LC(32, listOf(Pair(4, IbanC.a),Pair(24, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{24}$")),
+ LI(21, listOf(Pair(5, IbanC.n),Pair(12, IbanC.c),), Regex("^[0-9]{5}[0-9A-Z]{12}$")),
+ LT(20, listOf(Pair(16, IbanC.n),), Regex("^[0-9]{16}$")),
+ LU(20, listOf(Pair(3, IbanC.n),Pair(13, IbanC.c),), Regex("^[0-9]{3}[0-9A-Z]{13}$")),
+ LV(21, listOf(Pair(4, IbanC.a),Pair(13, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{13}$")),
+ LY(25, listOf(Pair(21, IbanC.n),), Regex("^[0-9]{21}$")),
+ MC(27, listOf(Pair(10, IbanC.n),Pair(11, IbanC.c),Pair(2, IbanC.n),), Regex("^[0-9]{10}[0-9A-Z]{11}[0-9]{2}$")),
+ MD(24, listOf(Pair(20, IbanC.c),), Regex("^[0-9A-Z]{20}$")),
+ ME(22, listOf(Pair(18, IbanC.n),), Regex("^[0-9]{18}$")),
+ MK(19, listOf(Pair(3, IbanC.n),Pair(10, IbanC.c),Pair(2, IbanC.n),), Regex("^[0-9]{3}[0-9A-Z]{10}[0-9]{2}$")),
+ MN(20, listOf(Pair(16, IbanC.n),), Regex("^[0-9]{16}$")),
+ MR(27, listOf(Pair(23, IbanC.n),), Regex("^[0-9]{23}$")),
+ MT(31, listOf(Pair(4, IbanC.a),Pair(5, IbanC.n),Pair(18, IbanC.c),), Regex("^[A-Z]{4}[0-9]{5}[0-9A-Z]{18}$")),
+ MU(30, listOf(Pair(4, IbanC.a),Pair(19, IbanC.n),Pair(3, IbanC.a),), Regex("^[A-Z]{4}[0-9]{19}[A-Z]{3}$")),
+ NI(28, listOf(Pair(4, IbanC.a),Pair(20, IbanC.n),), Regex("^[A-Z]{4}[0-9]{20}$")),
+ NL(18, listOf(Pair(4, IbanC.a),Pair(10, IbanC.n),), Regex("^[A-Z]{4}[0-9]{10}$")),
+ NO(15, listOf(Pair(11, IbanC.n),), Regex("^[0-9]{11}$")),
+ OM(23, listOf(Pair(3, IbanC.n),Pair(16, IbanC.c),), Regex("^[0-9]{3}[0-9A-Z]{16}$")),
+ PK(24, listOf(Pair(4, IbanC.a),Pair(16, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{16}$")),
+ PL(28, listOf(Pair(24, IbanC.n),), Regex("^[0-9]{24}$")),
+ PS(29, listOf(Pair(4, IbanC.a),Pair(21, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{21}$")),
+ PT(25, listOf(Pair(21, IbanC.n),), Regex("^[0-9]{21}$")),
+ QA(29, listOf(Pair(4, IbanC.a),Pair(21, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{21}$")),
+ RO(24, listOf(Pair(4, IbanC.a),Pair(16, IbanC.c),), Regex("^[A-Z]{4}[0-9A-Z]{16}$")),
+ RS(22, listOf(Pair(18, IbanC.n),), Regex("^[0-9]{18}$")),
+ RU(33, listOf(Pair(14, IbanC.n),Pair(15, IbanC.c),), Regex("^[0-9]{14}[0-9A-Z]{15}$")),
+ SA(24, listOf(Pair(2, IbanC.n),Pair(18, IbanC.c),), Regex("^[0-9]{2}[0-9A-Z]{18}$")),
+ SC(31, listOf(Pair(4, IbanC.a),Pair(20, IbanC.n),Pair(3, IbanC.a),), Regex("^[A-Z]{4}[0-9]{20}[A-Z]{3}$")),
+ SD(18, listOf(Pair(14, IbanC.n),), Regex("^[0-9]{14}$")),
+ SE(24, listOf(Pair(20, IbanC.n),), Regex("^[0-9]{20}$")),
+ SI(19, listOf(Pair(15, IbanC.n),), Regex("^[0-9]{15}$")),
+ SK(24, listOf(Pair(20, IbanC.n),), Regex("^[0-9]{20}$")),
+ SM(27, listOf(Pair(1, IbanC.a),Pair(10, IbanC.n),Pair(12, IbanC.c),), Regex("^[A-Z]{1}[0-9]{10}[0-9A-Z]{12}$")),
+ SO(23, listOf(Pair(19, IbanC.n),), Regex("^[0-9]{19}$")),
+ ST(25, listOf(Pair(21, IbanC.n),), Regex("^[0-9]{21}$")),
+ SV(28, listOf(Pair(4, IbanC.a),Pair(20, IbanC.n),), Regex("^[A-Z]{4}[0-9]{20}$")),
+ TL(23, listOf(Pair(19, IbanC.n),), Regex("^[0-9]{19}$")),
+ TN(24, listOf(Pair(20, IbanC.n),), Regex("^[0-9]{20}$")),
+ TR(26, listOf(Pair(6, IbanC.n),Pair(16, IbanC.c),), Regex("^[0-9]{6}[0-9A-Z]{16}$")),
+ UA(29, listOf(Pair(6, IbanC.n),Pair(19, IbanC.c),), Regex("^[0-9]{6}[0-9A-Z]{19}$")),
+ VA(22, listOf(Pair(18, IbanC.n),), Regex("^[0-9]{18}$")),
+ VG(24, listOf(Pair(4, IbanC.a),Pair(16, IbanC.n),), Regex("^[A-Z]{4}[0-9]{16}$")),
+ XK(20, listOf(Pair(16, IbanC.n),), Regex("^[0-9]{16}$")),
+ YE(30, listOf(Pair(4, IbanC.a),Pair(4, IbanC.n),Pair(18, IbanC.c),), Regex("^[A-Z]{4}[0-9]{4}[0-9A-Z]{18}$")),
+;
+ val bbanLen get() = ibanLen - 4
+}
+val VALID_IBAN = listOf(
+ Pair("AD1200012030200359100100", "00012030200359100100"),
+ Pair("AE070331234567890123456", "0331234567890123456"),
+ Pair("AL47212110090000000235698741", "212110090000000235698741"),
+ Pair("AT611904300234573201", "1904300234573201"),
+ Pair("AZ21NABZ00000000137010001944", "NABZ00000000137010001944"),
+ Pair("BA391290079401028494", "1290079401028494"),
+ Pair("BE68539007547034", "539007547034"),
+ Pair("BG80BNBG96611020345678", "BNBG96611020345678"),
+ Pair("BH67BMAG00001299123456", "BMAG00001299123456"),
+ Pair("BI4210000100010000332045181", "10000100010000332045181"),
+ Pair("BR1800360305000010009795493C1", "00360305000010009795493C1"),
+ Pair("BY13NBRB3600900000002Z00AB00", "NBRB 3600900000002Z00AB00"),
+ Pair("CH9300762011623852957", "00762011623852957"),
+ Pair("CR05015202001026284066", "015202001026284066"),
+ Pair("CY17002001280000001200527600", "002001280000001200527600"),
+ Pair("CZ6508000000192000145399", "08000000192000145399"),
+ Pair("DE89370400440532013000", "370400440532013000"),
+ Pair("DJ2100010000000154000100186", "00010000000154000100186"),
+ Pair("DK5000400440116243", "00400440116243"),
+ Pair("DO28BAGR00000001212453611324", "BAGR00000001212453611324"),
+ Pair("EE382200221020145685", "2200221020145685"),
+ Pair("EG380019000500000000263180002", "0019000500000000263180002"),
+ Pair("ES9121000418450200051332", "21000418450200051332"),
+ Pair("FI2112345600000785", null),
+ Pair("FK88SC123456789012", "SC123456789012"),
+ Pair("FO6264600001631634", "64600001631634"),
+ Pair("FR1420041010050500013M02606", "20041010050500013M02606"),
+ Pair("GB29NWBK60161331926819", "NWBK60161331926819"),
+ Pair("GE29NB0000000101904917", "NB0000000101904917"),
+ Pair("GI75NWBK000000007099453", "NWBK000000007099453"),
+ Pair("GL8964710001000206", "64710001000206"),
+ Pair("GR1601101250000000012300695", "01101250000000012300695"),
+ Pair("GT82TRAJ01020000001210029690", "TRAJ01020000001210029690"),
+ Pair("HN88CABF00000000000250005469", "CABF00000000000250005469"),
+ Pair("HR1210010051863000160", "10010051863000160"),
+ Pair("HU42117730161111101800000000", "117730161111101800000000"),
+ Pair("IE29AIBK93115212345678", "AIBK93115212345678"),
+ Pair("IL620108000000099999999", "0108000000099999999"),
+ Pair("IQ98NBIQ850123456789012", "NBIQ850123456789012"),
+ Pair("IS140159260076545510730339", "0159260076545510730339"),
+ Pair("IT60X0542811101000000123456", "X0542811101000000123456"),
+ Pair("JO94CBJO0010000000000131000302", "CBJO0010000000000131000302"),
+ Pair("KW81CBKU0000000000001234560101", "CBKU0000000000001234560101"),
+ Pair("KZ86125KZT5004100100", "125KZT5004100100"),
+ Pair("LB62099900000001001901229114", "0999 0000 0001 0019 0122 9114"),
+ Pair("LC55HEMM000100010012001200023015", "HEMM000100010012001200023015"),
+ Pair("LI21088100002324013AA", "088100002324013AA"),
+ Pair("LT121000011101001000", "1000011101001000"),
+ Pair("LU280019400644750000", "0019400644750000"),
+ Pair("LV80BANK0000435195001", "BANK0000435195001"),
+ Pair("LY83002048000020100120361", "002048000020100120361"),
+ Pair("MC5811222000010123456789030", "11222 00001 01234567890 30"),
+ Pair("MD24AG000225100013104168", "AG000225100013104168"),
+ Pair("ME25505000012345678951", "505000012345678951"),
+ Pair("MK07250120000058984", "250120000058984"),
+ Pair("MN121234123456789123", "1234123456789123"),
+ Pair("MR1300020001010000123456753", "00020001010000123456753"),
+ Pair("MT84MALT011000012345MTLCAST001S", "MALT011000012345MTLCAST001S"),
+ Pair("MU17BOMM0101101030300200000MUR", "BOMM0101101030300200000MUR"),
+ Pair("NI45BAPR00000013000003558124", "BAPR00000013000003558124"),
+ Pair("NL91ABNA0417164300", "ABNA0417164300"),
+ Pair("NO9386011117947", "86011117947"),
+ Pair("OM810180000001299123456", "0180000001299123456"),
+ Pair("PK36SCBL0000001123456702", "SCBL0000001123456702"),
+ Pair("PL61109010140000071219812874", "109010140000071219812874"),
+ Pair("PS92PALS000000000400123456702", "PALS000000000400123456702"),
+ Pair("PT50000201231234567890154", "000201231234567890154"),
+ Pair("QA58DOHB00001234567890ABCDEFG", "DOHB00001234567890ABCDEFG"),
+ Pair("RO49AAAA1B31007593840000", "AAAA1B31007593840000"),
+ Pair("RS35260005601001611379", "260005601001611379"),
+ Pair("RU0304452522540817810538091310419", "044525225 40817 810 5 3809 1310419"),
+ Pair("SA0380000000608010167519", "80000000608010167519"),
+ Pair("SC18SSCB11010000000000001497USD", "SSCB11010000000000001497USD"),
+ Pair("SD2129010501234001", "29010501234001"),
+ Pair("SE4550000000058398257466", "50000000058398257466"),
+ Pair("SI56263300012039086", "263300012039086"),
+ Pair("SK3112000000198742637541", "12000000198742637541"),
+ Pair("SM86U0322509800000000270100", "U0322509800000000270100"),
+ Pair("SO211000001001000100141", "1000001001000100141"),
+ Pair("ST23000100010051845310146", "000100010051845310146"),
+ Pair("SV62CENR00000000000000700025", "CENR00000000000000700025"),
+ Pair("TL380080012345678910157", "0080012345678910157"),
+ Pair("TN5910006035183598478831", "10006035183598478831"),
+ Pair("TR330006100519786457841326", "0006100519786457841326"),
+ Pair("UA213223130000026007233566001", "3223130000026007233566001"),
+ Pair("VA59001123000012345678", "001123000012345678"),
+ Pair("VG96VPVG0000012345678901", "VPVG0000012345678901"),
+ Pair("XK051212012345678906", "1212012345678906"),
+ Pair("YE15CBYE0001018861234567891234", "CBYE0001018861234567891234"),
+);
+\ No newline at end of file
diff --git a/common/src/test/kotlin/IbanTest.kt b/common/src/test/kotlin/IbanTest.kt
@@ -0,0 +1,42 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2025 Taler Systems S.A.
+
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+import org.junit.Test
+import tech.libeufin.common.*
+import kotlin.test.assertEquals
+
+class IbanTest {
+ @Test
+ fun valid() {
+ for ((iban, bban) in VALID_IBAN) {
+ val parsed = IBAN.parse(iban)
+ }
+ }
+
+ @Test
+ fun roundtrip() {
+ for (country in Country.values()) {
+ val gen = IBAN.rand(country)
+ println("$gen")
+ val parsed = IBAN.parse("$gen")
+ println("$gen $parsed")
+ assertEquals(gen, parsed)
+ }
+ }
+}
+\ No newline at end of file