From 9901d0951df75c29faf4a8a7be5de19e7e5f5c92 Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Tue, 10 Nov 2026 23:41:33 +0100 Subject: [PATCH 1/3] be: let a bank account carry its country and read or write its iban --- .docs/usage.md | 17 +++- .../insuit/bank/notification/BankAccount.java | 96 +++++++++++++++++-- .../bank/notification/BankAccountTest.java | 84 ++++++++++++++++ 3 files changed, 183 insertions(+), 14 deletions(-) diff --git a/.docs/usage.md b/.docs/usage.md index 0f592ae..6eda1a4 100644 --- a/.docs/usage.md +++ b/.docs/usage.md @@ -95,10 +95,19 @@ stop injection where the text is used: The same holds for the symbols, which are digits only, and the account numbers, which are checked, only less so. -`BankAccount` is `[prefix-]number/bankCode`, without leading zeros, so the same account is equal however it was written, -and it checks the Czech National Bank's modulo 11 rule: an account number misread from an e-mail is refused, not handed -out. `BankAccount.parse("19-2000145399/0800")` reads one, for comparing against yours. `Money` and `PaymentSymbols` have -the same shape as in `moneta-api-java`. +`BankAccount` is a Czech or Slovak account, its `country()` with `[prefix-]number/bankCode`, without leading zeros, so +the same account is equal however it was written: its local form and its IBAN compare equal. It checks the modulo 11 +rule both countries share, and an IBAN's modulo 97 digits, so an account number misread from an e-mail is refused, not +handed out. To compare against yours: + +| You have | Read it with | +|---|---| +| `19-2000145399/0800`, Czech | `BankAccount.parse("19-2000145399/0800")` | +| `4019425381/7500`, Slovak | `BankAccount.parse("4019425381/7500", BankAccount.Country.SK)` | +| `CZ65 0800 0000 1920 0014 5399` | `BankAccount.parseIban("CZ65 0800 0000 1920 0014 5399")`, with or without spaces | + +`iban()` writes the IBAN back, `toString()` the local form. The same digits under a Czech and a Slovak bank code are two +accounts. `Money` and `PaymentSymbols` have the same shape as in `moneta-api-java`. ## Two checks stay with you diff --git a/modules/bank-notifications/src/main/java/cz/insuit/bank/notification/BankAccount.java b/modules/bank-notifications/src/main/java/cz/insuit/bank/notification/BankAccount.java index 3003fc7..5213ca7 100644 --- a/modules/bank-notifications/src/main/java/cz/insuit/bank/notification/BankAccount.java +++ b/modules/bank-notifications/src/main/java/cz/insuit/bank/notification/BankAccount.java @@ -1,18 +1,33 @@ package cz.insuit.bank.notification; +import java.math.BigInteger; +import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.jspecify.annotations.Nullable; /** - * A Czech bank account number, {@code [prefix-]number/bankCode}, checked against the Czech National Bank's modulo 11 - * rule (Vyhláška č. 169/2011 Sb.), so a misread number is refused rather than handed out. + * A Czech or Slovak bank account: the local form {@code [prefix-]number/bankCode} and its IBAN are the same account. + * Both countries number accounts the same way, a prefix of up to 6 digits, a number of up to 10 and a 4-digit bank code, + * each part checked against the same modulo 11 rule (in Czechia Vyhláška č. 169/2011 Sb.), so a misread number is + * refused rather than handed out. An IBAN is checked against its own modulo 97 digits as well. * - * @param prefix the předčíslí without leading zeros, or {@code null} when there is none + *

Equal accounts are equal however they were written: leading zeros are dropped, and a local form and an IBAN of the + * same account compare equal. The country is part of the account: the same digits under a Czech and a Slovak bank code + * are two accounts. + * + * @param country the country whose bank keeps the account + * @param prefix the prefix (předčíslí, predčíslie) without leading zeros, or {@code null} when there is none * @param number the account number without leading zeros * @param bankCode the four-digit bank code, e.g. {@code 3030} for Air Bank */ -public record BankAccount(@Nullable String prefix, String number, String bankCode) { +public record BankAccount(Country country, @Nullable String prefix, String number, String bankCode) { + + /** The countries whose accounts this reads; both number accounts the same way. */ + public enum Country { + CZ, + SK + } public BankAccount { if (prefix != null && (!prefix.matches("\\d{1,6}") || !checksumHolds(prefix, PREFIX_WEIGHTS))) { @@ -29,26 +44,67 @@ public record BankAccount(@Nullable String prefix, String number, String bankCod } /** - * Reads {@code 19-2000145399/0800} or {@code 2000145399/0800}. + * Reads a Czech account in its local form, {@code 19-2000145399/0800} or {@code 2000145399/0800}. * * @param text the account as written - * @throws IllegalArgumentException when it is not a valid Czech account number + * @throws IllegalArgumentException when it is not a valid account number */ public static BankAccount parse(String text) { - Matcher m = FORMAT.matcher(text); + return parse(text, Country.CZ); + } + + /** + * Reads an account in the local form of its country, {@code [prefix-]number/bankCode}. + * + * @param text the account as written + * @param country the country whose bank keeps it + * @throws IllegalArgumentException when it is not a valid account number + */ + public static BankAccount parse(String text, Country country) { + Matcher m = LOCAL.matcher(text); if (!m.matches()) { throw new IllegalArgumentException("not in the form [prefix-]number/bankCode: " + text); } - return new BankAccount(m.group(1), m.group(2), m.group(3)); + return new BankAccount(country, m.group(1), m.group(2), m.group(3)); + } + + /** + * Reads a Czech or Slovak IBAN, with or without the spaces between its groups of four: {@code CZ65 0800 0000 1920 + * 0014 5399}. + * + * @param text the IBAN as written + * @throws IllegalArgumentException when it is not a valid IBAN, or not a Czech or Slovak one + */ + public static BankAccount parseIban(String text) { + String iban = text.replace(" ", "").toUpperCase(Locale.ROOT); + Matcher m = IBAN.matcher(iban); + if (!m.matches()) { + throw new IllegalArgumentException("not a Czech or Slovak IBAN: " + text); + } + if (!checksumHolds(iban)) { + throw new IllegalArgumentException("IBAN check digits do not hold: " + text); + } + // In an IBAN the bank code comes first, then the prefix and the number. + return new BankAccount(Country.valueOf(m.group(1)), m.group(3), m.group(4), m.group(2)); } - /** {@code 19-2000145399/0800}, or {@code 2000145399/0800} without a prefix. */ + /** The IBAN, without spaces: {@code CZ6508000000192000145399}. */ + public String iban() { + String bban = bankCode + zeros(prefix == null ? "" : prefix, 6) + zeros(number, 10); + BigInteger value = new BigInteger(digits(bban + country + "00")); + int check = 98 - value.mod(BigInteger.valueOf(97)).intValue(); + return country.name() + (check < 10 ? "0" : "") + check + bban; + } + + /** The local form: {@code 19-2000145399/0800}, or {@code 2000145399/0800} without a prefix. */ @Override public String toString() { return (prefix == null ? "" : prefix + "-") + number + "/" + bankCode; } - private static final Pattern FORMAT = Pattern.compile("(?:(\\d{1,6})-)?(\\d{2,10})/(\\d{4})"); + private static final Pattern LOCAL = Pattern.compile("(?:(\\d{1,6})-)?(\\d{2,10})/(\\d{4})"); + // Country, two check digits, then the bank code, the prefix and the number at their full lengths. + private static final Pattern IBAN = Pattern.compile("(CZ|SK)\\d{2}(\\d{4})(\\d{6})(\\d{10})"); private static final int[] PREFIX_WEIGHTS = {10, 5, 8, 4, 2, 1}; private static final int[] NUMBER_WEIGHTS = {6, 3, 7, 9, 10, 5, 8, 4, 2, 1}; @@ -61,6 +117,26 @@ private static boolean checksumHolds(String digits, int[] weights) { return sum % 11 == 0; } + /** ISO 13616: the first four characters moved to the end, letters as numbers, the whole modulo 97 is 1. */ + private static boolean checksumHolds(String iban) { + String rearranged = iban.substring(4) + iban.substring(0, 4); + return new BigInteger(digits(rearranged)).mod(BigInteger.valueOf(97)).intValue() == 1; + } + + /** Letters as ISO 13616 numbers them, A = 10 to Z = 35; digits as they are. */ + private static String digits(String text) { + StringBuilder out = new StringBuilder(); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + out.append(Character.isDigit(c) ? String.valueOf(c) : String.valueOf(c - 'A' + 10)); + } + return out.toString(); + } + + private static String zeros(String digits, int length) { + return "0".repeat(length - digits.length()) + digits; + } + private static String stripZeros(String digits) { String stripped = digits.replaceFirst("^0+", ""); return stripped.isEmpty() ? "0" : stripped; diff --git a/modules/bank-notifications/src/test/java/cz/insuit/bank/notification/BankAccountTest.java b/modules/bank-notifications/src/test/java/cz/insuit/bank/notification/BankAccountTest.java index a8b0455..1082ef3 100644 --- a/modules/bank-notifications/src/test/java/cz/insuit/bank/notification/BankAccountTest.java +++ b/modules/bank-notifications/src/test/java/cz/insuit/bank/notification/BankAccountTest.java @@ -1,9 +1,11 @@ package cz.insuit.bank.notification; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.math.BigInteger; import org.junit.jupiter.api.Test; class BankAccountTest { @@ -49,4 +51,86 @@ void refusesAnythingElse() { assertThrows(IllegalArgumentException.class, () -> BankAccount.parse("1234567899/30")); assertThrows(IllegalArgumentException.class, () -> BankAccount.parse("CZ6508000000192000145399")); } + + @Test + void readsACzechAccountByDefault() { + assertEquals( + BankAccount.Country.CZ, BankAccount.parse("19-2000145399/0800").country()); + } + + @Test + void readsTheLocalFormOfAnotherCountry() { + BankAccount account = BankAccount.parse("4019425381/7500", BankAccount.Country.SK); + + assertEquals(BankAccount.Country.SK, account.country()); + assertEquals("4019425381/7500", account.toString()); + } + + @Test + void readsAnIbanWithOrWithoutSpaces() { + // The Czech National Bank's sample account, as an IBAN. + BankAccount spaced = BankAccount.parseIban("CZ65 0800 0000 1920 0014 5399"); + + assertEquals(BankAccount.parse("19-2000145399/0800"), spaced); + assertEquals(spaced, BankAccount.parseIban("cz6508000000192000145399")); + } + + @Test + void readsASlovakIban() { + // A real company's account, printed in a public ČSOB SK sample. + BankAccount account = BankAccount.parseIban("SK13 7500 0000 0040 1942 5381"); + + assertEquals(BankAccount.Country.SK, account.country()); + assertNull(account.prefix()); + assertEquals("4019425381", account.number()); + assertEquals("7500", account.bankCode()); + } + + @Test + void writesTheIban() { + assertEquals( + "CZ6508000000192000145399", + BankAccount.parse("19-2000145399/0800").iban()); + assertEquals( + "SK1375000000004019425381", + BankAccount.parse("4019425381/7500", BankAccount.Country.SK).iban()); + } + + @Test + void writesAnIbanThatReadsBackAsTheSameAccount() { + BankAccount account = BankAccount.parse("1234567899/3030"); + + assertEquals(account, BankAccount.parseIban(account.iban())); + } + + @Test + void keepsTheSameDigitsInTwoCountriesApart() { + assertNotEquals( + BankAccount.parse("4019425381/7500", BankAccount.Country.CZ), + BankAccount.parse("4019425381/7500", BankAccount.Country.SK)); + } + + @Test + void refusesAnIbanWhoseCheckDigitsDoNotHold() { + assertThrows(IllegalArgumentException.class, () -> BankAccount.parseIban("CZ66 0800 0000 1920 0014 5399")); + } + + @Test + void refusesAnIbanWhoseAccountFailsTheModulo11Rule() { + // Check digits recomputed for a number that fails modulo 11: the IBAN alone would pass. + String bban = "0800" + "000019" + "2000145398"; + assertThrows(IllegalArgumentException.class, () -> BankAccount.parseIban(validIban("CZ", bban))); + } + + @Test + void refusesAnIbanOfAnotherCountry() { + assertThrows(IllegalArgumentException.class, () -> BankAccount.parseIban("DE89 3704 0044 0532 0130 00")); + } + + private static String validIban(String country, String bban) { + BigInteger value = + new BigInteger(bban + (country.charAt(0) - 'A' + 10) + (country.charAt(1) - 'A' + 10) + "00"); + int check = 98 - value.mod(BigInteger.valueOf(97)).intValue(); + return country + (check < 10 ? "0" : "") + check + bban; + } } From c74bc501e59e2f535f586503228283184f42f87a Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Tue, 10 Nov 2026 23:44:32 +0100 Subject: [PATCH 2/3] be: make every local account name its country, there is no default --- .docs/usage.md | 7 ++-- .../cz/insuit/bank/notification/AirBank.java | 4 +- .../insuit/bank/notification/AirBankTest.java | 14 +++---- .../cz/insuit/bank/notification/Creditas.java | 4 +- .../bank/notification/CreditasTest.java | 4 +- .../cz/insuit/bank/notification/Csob.java | 8 ++-- .../cz/insuit/bank/notification/CsobTest.java | 12 +++--- .../cz/insuit/bank/notification/FioCz.java | 4 +- .../insuit/bank/notification/FioCzTest.java | 14 +++---- .../cz/insuit/bank/notification/Moneta.java | 2 +- .../insuit/bank/notification/MonetaTest.java | 2 +- .../bank/notification/Raiffeisenbank.java | 4 +- .../bank/notification/RaiffeisenbankTest.java | 6 +-- .../insuit/bank/notification/Unicredit.java | 4 +- .../bank/notification/UnicreditTest.java | 4 +- .../insuit/bank/notification/BankAccount.java | 13 +------ .../bank/notification/BankAccountTest.java | 38 ++++++++++--------- .../java/example/ReadsOnSpringBootTest.java | 2 +- 18 files changed, 71 insertions(+), 75 deletions(-) diff --git a/.docs/usage.md b/.docs/usage.md index 6eda1a4..07ec66b 100644 --- a/.docs/usage.md +++ b/.docs/usage.md @@ -102,11 +102,12 @@ handed out. To compare against yours: | You have | Read it with | |---|---| -| `19-2000145399/0800`, Czech | `BankAccount.parse("19-2000145399/0800")` | +| `19-2000145399/0800`, Czech | `BankAccount.parse("19-2000145399/0800", BankAccount.Country.CZ)` | | `4019425381/7500`, Slovak | `BankAccount.parse("4019425381/7500", BankAccount.Country.SK)` | -| `CZ65 0800 0000 1920 0014 5399` | `BankAccount.parseIban("CZ65 0800 0000 1920 0014 5399")`, with or without spaces | +| `CZ65 0800 0000 1920 0014 5399` | `BankAccount.parseIban("CZ65 0800 0000 1920 0014 5399")`, with or without spaces; the IBAN says the country | -`iban()` writes the IBAN back, `toString()` the local form. The same digits under a Czech and a Slovak bank code are two +A local form does not say its country, so `parse` takes it: there is no default. `iban()` writes the IBAN back, +`toString()` the local form. The same digits under a Czech and a Slovak bank code are two accounts. `Money` and `PaymentSymbols` have the same shape as in `moneta-api-java`. ## Two checks stay with you diff --git a/modules/bank-notifications/src/banks/cz/airbank/java/cz/insuit/bank/notification/AirBank.java b/modules/bank-notifications/src/banks/cz/airbank/java/cz/insuit/bank/notification/AirBank.java index 7d31a6c..2f5f9f6 100644 --- a/modules/bank-notifications/src/banks/cz/airbank/java/cz/insuit/bank/notification/AirBank.java +++ b/modules/bank-notifications/src/banks/cz/airbank/java/cz/insuit/bank/notification/AirBank.java @@ -109,8 +109,8 @@ public Transaction parse(@Nullable String subject, String body) { // Both lines this parser reads say "úhrada", a transfer. A card payment or a direct debit, once // seen, is a line of its own. Transaction.Type.TRANSFER, - BankAccount.parse(summary.group(1)), - BankAccount.parse(counterAccount), + BankAccount.parse(summary.group(1), BankAccount.Country.CZ), + BankAccount.parse(counterAccount, BankAccount.Country.CZ), counterName, direction == Direction.INCOMING ? signed : signed.negate(), new PaymentSymbols(variableSymbol, constantSymbol, specificSymbol), diff --git a/modules/bank-notifications/src/banks/cz/airbank/test/cz/insuit/bank/notification/AirBankTest.java b/modules/bank-notifications/src/banks/cz/airbank/test/cz/insuit/bank/notification/AirBankTest.java index af2dde4..0944797 100644 --- a/modules/bank-notifications/src/banks/cz/airbank/test/cz/insuit/bank/notification/AirBankTest.java +++ b/modules/bank-notifications/src/banks/cz/airbank/test/cz/insuit/bank/notification/AirBankTest.java @@ -37,8 +37,8 @@ void readsAnIncomingPayment() throws IOException { "airbank.cz", Direction.INCOMING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/3030"), - BankAccount.parse("9876543211/2010"), + BankAccount.parse("1234567899/3030", BankAccount.Country.CZ), + BankAccount.parse("9876543211/2010", BankAccount.Country.CZ), "Novák, Jan", czk("1.00"), new PaymentSymbols("1337", null, null), @@ -59,8 +59,8 @@ void readsAnOutgoingPaymentAsANegativeAmount() throws IOException { "airbank.cz", Direction.OUTGOING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/3030"), - BankAccount.parse("9876543211/2010"), + BankAccount.parse("1234567899/3030", BankAccount.Country.CZ), + BankAccount.parse("9876543211/2010", BankAccount.Country.CZ), null, czk("-1.00"), new PaymentSymbols("7331", null, null), @@ -81,8 +81,8 @@ void readsTheMessageAndAllThreeSymbolsOfAnIncomingPayment() throws IOException { "airbank.cz", Direction.INCOMING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/3030"), - BankAccount.parse("9876543211/2010"), + BankAccount.parse("1234567899/3030", BankAccount.Country.CZ), + BankAccount.parse("9876543211/2010", BankAccount.Country.CZ), "Novák, Jan", czk("1.00"), new PaymentSymbols("1337", "1234", "123456789"), @@ -183,7 +183,7 @@ void readsTheRecipientsNameOfAnOutgoingPaymentWhenTheBankPrintsIt() throws IOExc Transaction parsed = BankNotifications.parseUnverified(bytes(changed)); assertEquals("Wayne Enterprises s.r.o.", parsed.counterName()); - assertEquals(BankAccount.parse("9876543211/2010"), parsed.counterAccount()); + assertEquals(BankAccount.parse("9876543211/2010", BankAccount.Country.CZ), parsed.counterAccount()); } @Test diff --git a/modules/bank-notifications/src/banks/cz/creditas/java/cz/insuit/bank/notification/Creditas.java b/modules/bank-notifications/src/banks/cz/creditas/java/cz/insuit/bank/notification/Creditas.java index 012f432..4ad0039 100644 --- a/modules/bank-notifications/src/banks/cz/creditas/java/cz/insuit/bank/notification/Creditas.java +++ b/modules/bank-notifications/src/banks/cz/creditas/java/cz/insuit/bank/notification/Creditas.java @@ -77,10 +77,10 @@ public Transaction parse(@Nullable String subject, String body) { DOMAIN, direction, kind.equals("Příchozí úhrada") ? Transaction.Type.TRANSFER : Transaction.Type.GENERAL, - BankAccount.parse(summary.group(1)), + BankAccount.parse(summary.group(1), BankAccount.Country.CZ), counterNumber == null || counterBank == null ? null - : BankAccount.parse(counterNumber + "/" + counterBank), + : BankAccount.parse(counterNumber + "/" + counterBank, BankAccount.Country.CZ), null, direction == Direction.INCOMING ? amount : amount.negate(), new PaymentSymbols( diff --git a/modules/bank-notifications/src/banks/cz/creditas/test/cz/insuit/bank/notification/CreditasTest.java b/modules/bank-notifications/src/banks/cz/creditas/test/cz/insuit/bank/notification/CreditasTest.java index 807691d..d4367c3 100644 --- a/modules/bank-notifications/src/banks/cz/creditas/test/cz/insuit/bank/notification/CreditasTest.java +++ b/modules/bank-notifications/src/banks/cz/creditas/test/cz/insuit/bank/notification/CreditasTest.java @@ -30,8 +30,8 @@ void readsAnIncomingPaymentFromItsSentenceAndList() throws IOException { "creditas.cz", Direction.INCOMING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/2250"), - BankAccount.parse("9876543211/0800"), + BankAccount.parse("1234567899/2250", BankAccount.Country.CZ), + BankAccount.parse("9876543211/0800", BankAccount.Country.CZ), null, czk("1000.00"), new PaymentSymbols("123456", null, null), diff --git a/modules/bank-notifications/src/banks/cz/csob/java/cz/insuit/bank/notification/Csob.java b/modules/bank-notifications/src/banks/cz/csob/java/cz/insuit/bank/notification/Csob.java index 1b031ef..299be27 100644 --- a/modules/bank-notifications/src/banks/cz/csob/java/cz/insuit/bank/notification/Csob.java +++ b/modules/bank-notifications/src/banks/cz/csob/java/cz/insuit/bank/notification/Csob.java @@ -136,8 +136,8 @@ private static Block block(List block) { DOMAIN, direction, kind.equals(CARD) ? Transaction.Type.CARD : Transaction.Type.TRANSFER, - BankAccount.parse(own.group(1) + "/" + BANK_CODE), - counter == null ? null : BankAccount.parse(counter), + BankAccount.parse(own.group(1) + "/" + BANK_CODE, BankAccount.Country.CZ), + counter == null ? null : BankAccount.parse(counter, BankAccount.Country.CZ), fields.get(COUNTER_NAME), amount, new PaymentSymbols( @@ -195,8 +195,8 @@ private static Transaction ownBanking(String body) { DOMAIN, direction, Transaction.Type.TRANSFER, - BankAccount.parse(own.group(1) + "/" + BANK_CODE), - counter == null ? null : BankAccount.parse(counter), + BankAccount.parse(own.group(1) + "/" + BANK_CODE, BankAccount.Country.CZ), + counter == null ? null : BankAccount.parse(counter, BankAccount.Country.CZ), fields.get(COUNTER_NAME), amount, new PaymentSymbols( diff --git a/modules/bank-notifications/src/banks/cz/csob/test/cz/insuit/bank/notification/CsobTest.java b/modules/bank-notifications/src/banks/cz/csob/test/cz/insuit/bank/notification/CsobTest.java index bd7da04..8dcb13d 100644 --- a/modules/bank-notifications/src/banks/cz/csob/test/cz/insuit/bank/notification/CsobTest.java +++ b/modules/bank-notifications/src/banks/cz/csob/test/cz/insuit/bank/notification/CsobTest.java @@ -29,8 +29,8 @@ void readsOneBookedPayment() throws IOException { "csob.cz", Direction.INCOMING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/0300"), - BankAccount.parse("9876543211/0800"), + BankAccount.parse("1234567899/0300", BankAccount.Country.CZ), + BankAccount.parse("9876543211/0800", BankAccount.Country.CZ), "Pennyworth Alfred", czk("1234.56"), new PaymentSymbols("23456789", "3456", null), @@ -59,7 +59,9 @@ void readsEveryTransactionOfAnEMailThatListsSeveral() throws IOException { assertEquals(2, all.size()); assertEquals(czk("1234.56"), all.get(0).amount()); assertEquals(czk("987.65"), all.get(1).amount()); - assertEquals(BankAccount.parse("5555555002/0100"), all.get(1).counterAccount()); + assertEquals( + BankAccount.parse("5555555002/0100", BankAccount.Country.CZ), + all.get(1).counterAccount()); assertEquals(all.get(0).messageId(), all.get(1).messageId()); } @@ -141,8 +143,8 @@ void readsTheOwnBankingNotice() throws IOException { "csob.cz", Direction.INCOMING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/0300"), - BankAccount.parse("9876543211/2700"), + BankAccount.parse("1234567899/0300", BankAccount.Country.CZ), + BankAccount.parse("9876543211/2700", BankAccount.Country.CZ), "WAYNE ENTERPRISES", czk("10000.00"), PaymentSymbols.NONE, diff --git a/modules/bank-notifications/src/banks/cz/fio/java/cz/insuit/bank/notification/FioCz.java b/modules/bank-notifications/src/banks/cz/fio/java/cz/insuit/bank/notification/FioCz.java index 486f277..def170a 100644 --- a/modules/bank-notifications/src/banks/cz/fio/java/cz/insuit/bank/notification/FioCz.java +++ b/modules/bank-notifications/src/banks/cz/fio/java/cz/insuit/bank/notification/FioCz.java @@ -102,7 +102,7 @@ public Transaction parse(@Nullable String subject, String body) { if (counterParty != null) { Matcher counter = COUNTER.matcher(counterParty); if (!counter.matches()) throw new Unparsed("counter-party not in the known format"); - counterAccount = BankAccount.parse(counter.group(1)); + counterAccount = BankAccount.parse(counter.group(1), BankAccount.Country.CZ); counterName = counter.group(2); } String balance = fields.get(BALANCE); @@ -111,7 +111,7 @@ public Transaction parse(@Nullable String subject, String body) { DOMAIN, direction, type(fields.get(TYPE)), - BankAccount.parse(summary.group("account") + "/" + BANK_CODE), + BankAccount.parse(summary.group("account") + "/" + BANK_CODE, BankAccount.Country.CZ), counterAccount, counterName, direction == Direction.INCOMING ? amount : amount.negate(), diff --git a/modules/bank-notifications/src/banks/cz/fio/test/cz/insuit/bank/notification/FioCzTest.java b/modules/bank-notifications/src/banks/cz/fio/test/cz/insuit/bank/notification/FioCzTest.java index d8b53e6..400f3b6 100644 --- a/modules/bank-notifications/src/banks/cz/fio/test/cz/insuit/bank/notification/FioCzTest.java +++ b/modules/bank-notifications/src/banks/cz/fio/test/cz/insuit/bank/notification/FioCzTest.java @@ -35,8 +35,8 @@ void readsAnInstantIncomingPayment() throws IOException { "fio.cz", Direction.INCOMING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/2010"), - BankAccount.parse("5555555002/0800"), + BankAccount.parse("1234567899/2010", BankAccount.Country.CZ), + BankAccount.parse("5555555002/0800", BankAccount.Country.CZ), "pennyworth alfred", czk("1000.00"), new PaymentSymbols(null, "0", null), @@ -65,8 +65,8 @@ void readsAnOutgoingTransferAsANegativeAmount() throws IOException { "fio.cz", Direction.OUTGOING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/2010"), - BankAccount.parse("1939000008/5500"), + BankAccount.parse("1234567899/2010", BankAccount.Country.CZ), + BankAccount.parse("1939000008/5500", BankAccount.Country.CZ), null, czk("-2768.00"), PaymentSymbols.NONE, @@ -97,7 +97,7 @@ void readsADirectDebit() throws IOException { assertEquals(Transaction.Type.DIRECT_DEBIT, parsed.type()); assertEquals(czk("-602.00"), parsed.amount()); assertEquals(new PaymentSymbols("1000000042", "0308", null), parsed.symbols()); - assertEquals(BankAccount.parse("270000000/0300"), parsed.counterAccount()); + assertEquals(BankAccount.parse("270000000/0300", BankAccount.Country.CZ), parsed.counterAccount()); } @Test @@ -211,8 +211,8 @@ void readsTheEnglishTemplate() throws IOException { "fio.cz", Direction.INCOMING, Transaction.Type.GENERAL, - BankAccount.parse("1234567899/2010"), - BankAccount.parse("9876543211/2010"), + BankAccount.parse("1234567899/2010", BankAccount.Country.CZ), + BankAccount.parse("9876543211/2010", BankAccount.Country.CZ), "Pennyworth, Alfred", czk("1.00"), new PaymentSymbols("123456", null, null), diff --git a/modules/bank-notifications/src/banks/cz/moneta/java/cz/insuit/bank/notification/Moneta.java b/modules/bank-notifications/src/banks/cz/moneta/java/cz/insuit/bank/notification/Moneta.java index e405c55..6444261 100644 --- a/modules/bank-notifications/src/banks/cz/moneta/java/cz/insuit/bank/notification/Moneta.java +++ b/modules/bank-notifications/src/banks/cz/moneta/java/cz/insuit/bank/notification/Moneta.java @@ -90,7 +90,7 @@ public Transaction parse(@Nullable String subject, String body) { stated, Transaction.Type.GENERAL, null, - counter == null ? null : BankAccount.parse(counter), + counter == null ? null : BankAccount.parse(counter, BankAccount.Country.CZ), null, stated == Direction.INCOMING ? value : value.negate(), new PaymentSymbols(variableSymbol, null, null), diff --git a/modules/bank-notifications/src/banks/cz/moneta/test/cz/insuit/bank/notification/MonetaTest.java b/modules/bank-notifications/src/banks/cz/moneta/test/cz/insuit/bank/notification/MonetaTest.java index 7e51f06..e070b39 100644 --- a/modules/bank-notifications/src/banks/cz/moneta/test/cz/insuit/bank/notification/MonetaTest.java +++ b/modules/bank-notifications/src/banks/cz/moneta/test/cz/insuit/bank/notification/MonetaTest.java @@ -29,7 +29,7 @@ void readsAnIncomingPaymentWithoutAccountOrTransactionId() throws IOException { Direction.INCOMING, Transaction.Type.GENERAL, null, - BankAccount.parse("9876543211/0300"), + BankAccount.parse("9876543211/0300", BankAccount.Country.CZ), null, czk("35000.00"), new PaymentSymbols("202600009", null, null), diff --git a/modules/bank-notifications/src/banks/cz/raiffeisenbank/java/cz/insuit/bank/notification/Raiffeisenbank.java b/modules/bank-notifications/src/banks/cz/raiffeisenbank/java/cz/insuit/bank/notification/Raiffeisenbank.java index f8ee588..3e71db3 100644 --- a/modules/bank-notifications/src/banks/cz/raiffeisenbank/java/cz/insuit/bank/notification/Raiffeisenbank.java +++ b/modules/bank-notifications/src/banks/cz/raiffeisenbank/java/cz/insuit/bank/notification/Raiffeisenbank.java @@ -82,8 +82,8 @@ public Transaction parse(@Nullable String subject, String body) { DOMAIN, direction, card ? Transaction.Type.CARD : Transaction.Type.TRANSFER, - BankAccount.parse(own.group(1)), - counter == null ? null : BankAccount.parse(counter.group(1)), + BankAccount.parse(own.group(1), BankAccount.Country.CZ), + counter == null ? null : BankAccount.parse(counter.group(1), BankAccount.Country.CZ), counter == null || counter.group(2).isBlank() ? null : counter.group(2).strip(), diff --git a/modules/bank-notifications/src/banks/cz/raiffeisenbank/test/cz/insuit/bank/notification/RaiffeisenbankTest.java b/modules/bank-notifications/src/banks/cz/raiffeisenbank/test/cz/insuit/bank/notification/RaiffeisenbankTest.java index 7ad2177..169a3bb 100644 --- a/modules/bank-notifications/src/banks/cz/raiffeisenbank/test/cz/insuit/bank/notification/RaiffeisenbankTest.java +++ b/modules/bank-notifications/src/banks/cz/raiffeisenbank/test/cz/insuit/bank/notification/RaiffeisenbankTest.java @@ -31,8 +31,8 @@ void readsAnIncomingPaymentFromHtml() throws IOException { "rb.cz", Direction.INCOMING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/5500"), - BankAccount.parse("9876543211/0100"), + BankAccount.parse("1234567899/5500", BankAccount.Country.CZ), + BankAccount.parse("9876543211/0100", BankAccount.Country.CZ), "Pennyworth Alfred", czk("1234.56"), new PaymentSymbols("2606001", "308", null), @@ -53,7 +53,7 @@ void readsACardPaymentWithItsDetailsAsTheMessage() throws IOException { assertEquals(Transaction.Type.CARD, card.type()); assertEquals(Direction.OUTGOING, card.direction()); assertEquals(czk("-1234.56"), card.amount()); - assertEquals(BankAccount.parse("1234567899/5500"), card.account()); + assertEquals(BankAccount.parse("1234567899/5500", BankAccount.Country.CZ), card.account()); assertNull(card.counterAccount()); assertEquals("DEMO SOFTWARE; PRAHA; CZE", card.message()); } diff --git a/modules/bank-notifications/src/banks/cz/unicredit/java/cz/insuit/bank/notification/Unicredit.java b/modules/bank-notifications/src/banks/cz/unicredit/java/cz/insuit/bank/notification/Unicredit.java index e1f2872..39929e6 100644 --- a/modules/bank-notifications/src/banks/cz/unicredit/java/cz/insuit/bank/notification/Unicredit.java +++ b/modules/bank-notifications/src/banks/cz/unicredit/java/cz/insuit/bank/notification/Unicredit.java @@ -87,8 +87,8 @@ public Transaction parse(@Nullable String subject, String body) { DOMAIN, direction, Transaction.Type.GENERAL, - BankAccount.parse(sentence.group(1) + "/" + BANK_CODE), - counter == null ? null : BankAccount.parse(counter), + BankAccount.parse(sentence.group(1) + "/" + BANK_CODE, BankAccount.Country.CZ), + counter == null ? null : BankAccount.parse(counter, BankAccount.Country.CZ), fields.get(COUNTER_NAME), direction == Direction.INCOMING ? value : value.negate(), new PaymentSymbols( diff --git a/modules/bank-notifications/src/banks/cz/unicredit/test/cz/insuit/bank/notification/UnicreditTest.java b/modules/bank-notifications/src/banks/cz/unicredit/test/cz/insuit/bank/notification/UnicreditTest.java index 1b10c46..0e1daef 100644 --- a/modules/bank-notifications/src/banks/cz/unicredit/test/cz/insuit/bank/notification/UnicreditTest.java +++ b/modules/bank-notifications/src/banks/cz/unicredit/test/cz/insuit/bank/notification/UnicreditTest.java @@ -28,8 +28,8 @@ void readsAnIncomingPaymentWithAnEnglishAmount() throws IOException { "unicreditgroup.cz", Direction.INCOMING, Transaction.Type.GENERAL, - BankAccount.parse("1234567899/2700"), - BankAccount.parse("9876543211/0600"), + BankAccount.parse("1234567899/2700", BankAccount.Country.CZ), + BankAccount.parse("9876543211/0600", BankAccount.Country.CZ), "WAYNE ENTERPRISES s.r.o.", czk("20546.00"), new PaymentSymbols("0002026017", null, null), diff --git a/modules/bank-notifications/src/main/java/cz/insuit/bank/notification/BankAccount.java b/modules/bank-notifications/src/main/java/cz/insuit/bank/notification/BankAccount.java index 5213ca7..cb518f2 100644 --- a/modules/bank-notifications/src/main/java/cz/insuit/bank/notification/BankAccount.java +++ b/modules/bank-notifications/src/main/java/cz/insuit/bank/notification/BankAccount.java @@ -44,17 +44,8 @@ public enum Country { } /** - * Reads a Czech account in its local form, {@code 19-2000145399/0800} or {@code 2000145399/0800}. - * - * @param text the account as written - * @throws IllegalArgumentException when it is not a valid account number - */ - public static BankAccount parse(String text) { - return parse(text, Country.CZ); - } - - /** - * Reads an account in the local form of its country, {@code [prefix-]number/bankCode}. + * Reads an account in the local form of its country, {@code [prefix-]number/bankCode}. The form does not say the + * country, so the caller does: the same digits under a Czech and a Slovak bank code are two accounts. * * @param text the account as written * @param country the country whose bank keeps it diff --git a/modules/bank-notifications/src/test/java/cz/insuit/bank/notification/BankAccountTest.java b/modules/bank-notifications/src/test/java/cz/insuit/bank/notification/BankAccountTest.java index 1082ef3..e659a40 100644 --- a/modules/bank-notifications/src/test/java/cz/insuit/bank/notification/BankAccountTest.java +++ b/modules/bank-notifications/src/test/java/cz/insuit/bank/notification/BankAccountTest.java @@ -13,7 +13,7 @@ class BankAccountTest { @Test void readsAnAccountWithAPrefix() { // The Czech National Bank's sample account in its own guidance. - BankAccount account = BankAccount.parse("19-2000145399/0800"); + BankAccount account = BankAccount.parse("19-2000145399/0800", BankAccount.Country.CZ); assertEquals("19", account.prefix()); assertEquals("2000145399", account.number()); @@ -23,7 +23,7 @@ void readsAnAccountWithAPrefix() { @Test void readsAnAccountWithoutAPrefix() { - BankAccount account = BankAccount.parse("1234567899/3030"); + BankAccount account = BankAccount.parse("1234567899/3030", BankAccount.Country.CZ); assertNull(account.prefix()); assertEquals("1234567899/3030", account.toString()); @@ -31,31 +31,33 @@ void readsAnAccountWithoutAPrefix() { @Test void dropsLeadingZerosSoTheSameAccountIsEqual() { - assertEquals(BankAccount.parse("000019-2000145399/0800"), BankAccount.parse("19-2000145399/0800")); - assertEquals(BankAccount.parse("000000-1234567899/3030"), BankAccount.parse("1234567899/3030")); + assertEquals( + BankAccount.parse("000019-2000145399/0800", BankAccount.Country.CZ), + BankAccount.parse("19-2000145399/0800", BankAccount.Country.CZ)); + assertEquals( + BankAccount.parse("000000-1234567899/3030", BankAccount.Country.CZ), + BankAccount.parse("1234567899/3030", BankAccount.Country.CZ)); } @Test void refusesANumberThatFailsTheChecksum() { - assertThrows(IllegalArgumentException.class, () -> BankAccount.parse("1234567890/3030")); + assertThrows( + IllegalArgumentException.class, () -> BankAccount.parse("1234567890/3030", BankAccount.Country.CZ)); } @Test void refusesAPrefixThatFailsTheChecksum() { - assertThrows(IllegalArgumentException.class, () -> BankAccount.parse("18-2000145399/0800")); + assertThrows( + IllegalArgumentException.class, () -> BankAccount.parse("18-2000145399/0800", BankAccount.Country.CZ)); } @Test void refusesAnythingElse() { - assertThrows(IllegalArgumentException.class, () -> BankAccount.parse("1234567899")); - assertThrows(IllegalArgumentException.class, () -> BankAccount.parse("1234567899/30")); - assertThrows(IllegalArgumentException.class, () -> BankAccount.parse("CZ6508000000192000145399")); - } - - @Test - void readsACzechAccountByDefault() { - assertEquals( - BankAccount.Country.CZ, BankAccount.parse("19-2000145399/0800").country()); + assertThrows(IllegalArgumentException.class, () -> BankAccount.parse("1234567899", BankAccount.Country.CZ)); + assertThrows(IllegalArgumentException.class, () -> BankAccount.parse("1234567899/30", BankAccount.Country.CZ)); + assertThrows( + IllegalArgumentException.class, + () -> BankAccount.parse("CZ6508000000192000145399", BankAccount.Country.CZ)); } @Test @@ -71,7 +73,7 @@ void readsAnIbanWithOrWithoutSpaces() { // The Czech National Bank's sample account, as an IBAN. BankAccount spaced = BankAccount.parseIban("CZ65 0800 0000 1920 0014 5399"); - assertEquals(BankAccount.parse("19-2000145399/0800"), spaced); + assertEquals(BankAccount.parse("19-2000145399/0800", BankAccount.Country.CZ), spaced); assertEquals(spaced, BankAccount.parseIban("cz6508000000192000145399")); } @@ -90,7 +92,7 @@ void readsASlovakIban() { void writesTheIban() { assertEquals( "CZ6508000000192000145399", - BankAccount.parse("19-2000145399/0800").iban()); + BankAccount.parse("19-2000145399/0800", BankAccount.Country.CZ).iban()); assertEquals( "SK1375000000004019425381", BankAccount.parse("4019425381/7500", BankAccount.Country.SK).iban()); @@ -98,7 +100,7 @@ void writesTheIban() { @Test void writesAnIbanThatReadsBackAsTheSameAccount() { - BankAccount account = BankAccount.parse("1234567899/3030"); + BankAccount account = BankAccount.parse("1234567899/3030", BankAccount.Country.CZ); assertEquals(account, BankAccount.parseIban(account.iban())); } diff --git a/testing/spring-boot/shared/src/test/java/example/ReadsOnSpringBootTest.java b/testing/spring-boot/shared/src/test/java/example/ReadsOnSpringBootTest.java index fc1bef6..babdd1d 100644 --- a/testing/spring-boot/shared/src/test/java/example/ReadsOnSpringBootTest.java +++ b/testing/spring-boot/shared/src/test/java/example/ReadsOnSpringBootTest.java @@ -31,7 +31,7 @@ void readsAndVerifies() throws Exception { Transaction t = notifications.read(signed(key)); - assertEquals(BankAccount.parse("1234567899/3030"), t.account()); + assertEquals(BankAccount.parse("1234567899/3030", BankAccount.Country.CZ), t.account()); assertEquals(new BigDecimal("1.00"), t.amount().amount()); assertEquals("1337", t.symbols().variable()); } From 53a51bfaaa3174fc5f5c5adf6ee40314aae246c1 Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Tue, 10 Nov 2026 23:47:19 +0100 Subject: [PATCH 3/3] be: make country an enum of its own, not one nested in bank account --- .docs/usage.md | 4 +- .../cz/insuit/bank/notification/AirBank.java | 4 +- .../insuit/bank/notification/AirBankTest.java | 14 +++--- .../cz/insuit/bank/notification/Creditas.java | 4 +- .../bank/notification/CreditasTest.java | 4 +- .../cz/insuit/bank/notification/Csob.java | 8 ++-- .../cz/insuit/bank/notification/CsobTest.java | 11 +++-- .../cz/insuit/bank/notification/FioCz.java | 4 +- .../insuit/bank/notification/FioCzTest.java | 14 +++--- .../cz/insuit/bank/notification/Moneta.java | 2 +- .../insuit/bank/notification/MonetaTest.java | 2 +- .../bank/notification/Raiffeisenbank.java | 4 +- .../bank/notification/RaiffeisenbankTest.java | 6 +-- .../insuit/bank/notification/Unicredit.java | 4 +- .../bank/notification/UnicreditTest.java | 4 +- .../insuit/bank/notification/BankAccount.java | 6 --- .../cz/insuit/bank/notification/Country.java | 10 +++++ .../bank/notification/BankAccountTest.java | 43 ++++++++----------- .../java/example/ReadsOnSpringBootTest.java | 3 +- 19 files changed, 75 insertions(+), 76 deletions(-) create mode 100644 modules/bank-notifications/src/main/java/cz/insuit/bank/notification/Country.java diff --git a/.docs/usage.md b/.docs/usage.md index 07ec66b..ed949d7 100644 --- a/.docs/usage.md +++ b/.docs/usage.md @@ -102,8 +102,8 @@ handed out. To compare against yours: | You have | Read it with | |---|---| -| `19-2000145399/0800`, Czech | `BankAccount.parse("19-2000145399/0800", BankAccount.Country.CZ)` | -| `4019425381/7500`, Slovak | `BankAccount.parse("4019425381/7500", BankAccount.Country.SK)` | +| `19-2000145399/0800`, Czech | `BankAccount.parse("19-2000145399/0800", Country.CZ)` | +| `4019425381/7500`, Slovak | `BankAccount.parse("4019425381/7500", Country.SK)` | | `CZ65 0800 0000 1920 0014 5399` | `BankAccount.parseIban("CZ65 0800 0000 1920 0014 5399")`, with or without spaces; the IBAN says the country | A local form does not say its country, so `parse` takes it: there is no default. `iban()` writes the IBAN back, diff --git a/modules/bank-notifications/src/banks/cz/airbank/java/cz/insuit/bank/notification/AirBank.java b/modules/bank-notifications/src/banks/cz/airbank/java/cz/insuit/bank/notification/AirBank.java index 2f5f9f6..a1597f4 100644 --- a/modules/bank-notifications/src/banks/cz/airbank/java/cz/insuit/bank/notification/AirBank.java +++ b/modules/bank-notifications/src/banks/cz/airbank/java/cz/insuit/bank/notification/AirBank.java @@ -109,8 +109,8 @@ public Transaction parse(@Nullable String subject, String body) { // Both lines this parser reads say "úhrada", a transfer. A card payment or a direct debit, once // seen, is a line of its own. Transaction.Type.TRANSFER, - BankAccount.parse(summary.group(1), BankAccount.Country.CZ), - BankAccount.parse(counterAccount, BankAccount.Country.CZ), + BankAccount.parse(summary.group(1), Country.CZ), + BankAccount.parse(counterAccount, Country.CZ), counterName, direction == Direction.INCOMING ? signed : signed.negate(), new PaymentSymbols(variableSymbol, constantSymbol, specificSymbol), diff --git a/modules/bank-notifications/src/banks/cz/airbank/test/cz/insuit/bank/notification/AirBankTest.java b/modules/bank-notifications/src/banks/cz/airbank/test/cz/insuit/bank/notification/AirBankTest.java index 0944797..67c040e 100644 --- a/modules/bank-notifications/src/banks/cz/airbank/test/cz/insuit/bank/notification/AirBankTest.java +++ b/modules/bank-notifications/src/banks/cz/airbank/test/cz/insuit/bank/notification/AirBankTest.java @@ -37,8 +37,8 @@ void readsAnIncomingPayment() throws IOException { "airbank.cz", Direction.INCOMING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/3030", BankAccount.Country.CZ), - BankAccount.parse("9876543211/2010", BankAccount.Country.CZ), + BankAccount.parse("1234567899/3030", Country.CZ), + BankAccount.parse("9876543211/2010", Country.CZ), "Novák, Jan", czk("1.00"), new PaymentSymbols("1337", null, null), @@ -59,8 +59,8 @@ void readsAnOutgoingPaymentAsANegativeAmount() throws IOException { "airbank.cz", Direction.OUTGOING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/3030", BankAccount.Country.CZ), - BankAccount.parse("9876543211/2010", BankAccount.Country.CZ), + BankAccount.parse("1234567899/3030", Country.CZ), + BankAccount.parse("9876543211/2010", Country.CZ), null, czk("-1.00"), new PaymentSymbols("7331", null, null), @@ -81,8 +81,8 @@ void readsTheMessageAndAllThreeSymbolsOfAnIncomingPayment() throws IOException { "airbank.cz", Direction.INCOMING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/3030", BankAccount.Country.CZ), - BankAccount.parse("9876543211/2010", BankAccount.Country.CZ), + BankAccount.parse("1234567899/3030", Country.CZ), + BankAccount.parse("9876543211/2010", Country.CZ), "Novák, Jan", czk("1.00"), new PaymentSymbols("1337", "1234", "123456789"), @@ -183,7 +183,7 @@ void readsTheRecipientsNameOfAnOutgoingPaymentWhenTheBankPrintsIt() throws IOExc Transaction parsed = BankNotifications.parseUnverified(bytes(changed)); assertEquals("Wayne Enterprises s.r.o.", parsed.counterName()); - assertEquals(BankAccount.parse("9876543211/2010", BankAccount.Country.CZ), parsed.counterAccount()); + assertEquals(BankAccount.parse("9876543211/2010", Country.CZ), parsed.counterAccount()); } @Test diff --git a/modules/bank-notifications/src/banks/cz/creditas/java/cz/insuit/bank/notification/Creditas.java b/modules/bank-notifications/src/banks/cz/creditas/java/cz/insuit/bank/notification/Creditas.java index 4ad0039..6991e52 100644 --- a/modules/bank-notifications/src/banks/cz/creditas/java/cz/insuit/bank/notification/Creditas.java +++ b/modules/bank-notifications/src/banks/cz/creditas/java/cz/insuit/bank/notification/Creditas.java @@ -77,10 +77,10 @@ public Transaction parse(@Nullable String subject, String body) { DOMAIN, direction, kind.equals("Příchozí úhrada") ? Transaction.Type.TRANSFER : Transaction.Type.GENERAL, - BankAccount.parse(summary.group(1), BankAccount.Country.CZ), + BankAccount.parse(summary.group(1), Country.CZ), counterNumber == null || counterBank == null ? null - : BankAccount.parse(counterNumber + "/" + counterBank, BankAccount.Country.CZ), + : BankAccount.parse(counterNumber + "/" + counterBank, Country.CZ), null, direction == Direction.INCOMING ? amount : amount.negate(), new PaymentSymbols( diff --git a/modules/bank-notifications/src/banks/cz/creditas/test/cz/insuit/bank/notification/CreditasTest.java b/modules/bank-notifications/src/banks/cz/creditas/test/cz/insuit/bank/notification/CreditasTest.java index d4367c3..b3e20ce 100644 --- a/modules/bank-notifications/src/banks/cz/creditas/test/cz/insuit/bank/notification/CreditasTest.java +++ b/modules/bank-notifications/src/banks/cz/creditas/test/cz/insuit/bank/notification/CreditasTest.java @@ -30,8 +30,8 @@ void readsAnIncomingPaymentFromItsSentenceAndList() throws IOException { "creditas.cz", Direction.INCOMING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/2250", BankAccount.Country.CZ), - BankAccount.parse("9876543211/0800", BankAccount.Country.CZ), + BankAccount.parse("1234567899/2250", Country.CZ), + BankAccount.parse("9876543211/0800", Country.CZ), null, czk("1000.00"), new PaymentSymbols("123456", null, null), diff --git a/modules/bank-notifications/src/banks/cz/csob/java/cz/insuit/bank/notification/Csob.java b/modules/bank-notifications/src/banks/cz/csob/java/cz/insuit/bank/notification/Csob.java index 299be27..ecb5f27 100644 --- a/modules/bank-notifications/src/banks/cz/csob/java/cz/insuit/bank/notification/Csob.java +++ b/modules/bank-notifications/src/banks/cz/csob/java/cz/insuit/bank/notification/Csob.java @@ -136,8 +136,8 @@ private static Block block(List block) { DOMAIN, direction, kind.equals(CARD) ? Transaction.Type.CARD : Transaction.Type.TRANSFER, - BankAccount.parse(own.group(1) + "/" + BANK_CODE, BankAccount.Country.CZ), - counter == null ? null : BankAccount.parse(counter, BankAccount.Country.CZ), + BankAccount.parse(own.group(1) + "/" + BANK_CODE, Country.CZ), + counter == null ? null : BankAccount.parse(counter, Country.CZ), fields.get(COUNTER_NAME), amount, new PaymentSymbols( @@ -195,8 +195,8 @@ private static Transaction ownBanking(String body) { DOMAIN, direction, Transaction.Type.TRANSFER, - BankAccount.parse(own.group(1) + "/" + BANK_CODE, BankAccount.Country.CZ), - counter == null ? null : BankAccount.parse(counter, BankAccount.Country.CZ), + BankAccount.parse(own.group(1) + "/" + BANK_CODE, Country.CZ), + counter == null ? null : BankAccount.parse(counter, Country.CZ), fields.get(COUNTER_NAME), amount, new PaymentSymbols( diff --git a/modules/bank-notifications/src/banks/cz/csob/test/cz/insuit/bank/notification/CsobTest.java b/modules/bank-notifications/src/banks/cz/csob/test/cz/insuit/bank/notification/CsobTest.java index 8dcb13d..8e820ae 100644 --- a/modules/bank-notifications/src/banks/cz/csob/test/cz/insuit/bank/notification/CsobTest.java +++ b/modules/bank-notifications/src/banks/cz/csob/test/cz/insuit/bank/notification/CsobTest.java @@ -29,8 +29,8 @@ void readsOneBookedPayment() throws IOException { "csob.cz", Direction.INCOMING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/0300", BankAccount.Country.CZ), - BankAccount.parse("9876543211/0800", BankAccount.Country.CZ), + BankAccount.parse("1234567899/0300", Country.CZ), + BankAccount.parse("9876543211/0800", Country.CZ), "Pennyworth Alfred", czk("1234.56"), new PaymentSymbols("23456789", "3456", null), @@ -60,8 +60,7 @@ void readsEveryTransactionOfAnEMailThatListsSeveral() throws IOException { assertEquals(czk("1234.56"), all.get(0).amount()); assertEquals(czk("987.65"), all.get(1).amount()); assertEquals( - BankAccount.parse("5555555002/0100", BankAccount.Country.CZ), - all.get(1).counterAccount()); + BankAccount.parse("5555555002/0100", Country.CZ), all.get(1).counterAccount()); assertEquals(all.get(0).messageId(), all.get(1).messageId()); } @@ -143,8 +142,8 @@ void readsTheOwnBankingNotice() throws IOException { "csob.cz", Direction.INCOMING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/0300", BankAccount.Country.CZ), - BankAccount.parse("9876543211/2700", BankAccount.Country.CZ), + BankAccount.parse("1234567899/0300", Country.CZ), + BankAccount.parse("9876543211/2700", Country.CZ), "WAYNE ENTERPRISES", czk("10000.00"), PaymentSymbols.NONE, diff --git a/modules/bank-notifications/src/banks/cz/fio/java/cz/insuit/bank/notification/FioCz.java b/modules/bank-notifications/src/banks/cz/fio/java/cz/insuit/bank/notification/FioCz.java index def170a..14da867 100644 --- a/modules/bank-notifications/src/banks/cz/fio/java/cz/insuit/bank/notification/FioCz.java +++ b/modules/bank-notifications/src/banks/cz/fio/java/cz/insuit/bank/notification/FioCz.java @@ -102,7 +102,7 @@ public Transaction parse(@Nullable String subject, String body) { if (counterParty != null) { Matcher counter = COUNTER.matcher(counterParty); if (!counter.matches()) throw new Unparsed("counter-party not in the known format"); - counterAccount = BankAccount.parse(counter.group(1), BankAccount.Country.CZ); + counterAccount = BankAccount.parse(counter.group(1), Country.CZ); counterName = counter.group(2); } String balance = fields.get(BALANCE); @@ -111,7 +111,7 @@ public Transaction parse(@Nullable String subject, String body) { DOMAIN, direction, type(fields.get(TYPE)), - BankAccount.parse(summary.group("account") + "/" + BANK_CODE, BankAccount.Country.CZ), + BankAccount.parse(summary.group("account") + "/" + BANK_CODE, Country.CZ), counterAccount, counterName, direction == Direction.INCOMING ? amount : amount.negate(), diff --git a/modules/bank-notifications/src/banks/cz/fio/test/cz/insuit/bank/notification/FioCzTest.java b/modules/bank-notifications/src/banks/cz/fio/test/cz/insuit/bank/notification/FioCzTest.java index 400f3b6..974311f 100644 --- a/modules/bank-notifications/src/banks/cz/fio/test/cz/insuit/bank/notification/FioCzTest.java +++ b/modules/bank-notifications/src/banks/cz/fio/test/cz/insuit/bank/notification/FioCzTest.java @@ -35,8 +35,8 @@ void readsAnInstantIncomingPayment() throws IOException { "fio.cz", Direction.INCOMING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/2010", BankAccount.Country.CZ), - BankAccount.parse("5555555002/0800", BankAccount.Country.CZ), + BankAccount.parse("1234567899/2010", Country.CZ), + BankAccount.parse("5555555002/0800", Country.CZ), "pennyworth alfred", czk("1000.00"), new PaymentSymbols(null, "0", null), @@ -65,8 +65,8 @@ void readsAnOutgoingTransferAsANegativeAmount() throws IOException { "fio.cz", Direction.OUTGOING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/2010", BankAccount.Country.CZ), - BankAccount.parse("1939000008/5500", BankAccount.Country.CZ), + BankAccount.parse("1234567899/2010", Country.CZ), + BankAccount.parse("1939000008/5500", Country.CZ), null, czk("-2768.00"), PaymentSymbols.NONE, @@ -97,7 +97,7 @@ void readsADirectDebit() throws IOException { assertEquals(Transaction.Type.DIRECT_DEBIT, parsed.type()); assertEquals(czk("-602.00"), parsed.amount()); assertEquals(new PaymentSymbols("1000000042", "0308", null), parsed.symbols()); - assertEquals(BankAccount.parse("270000000/0300", BankAccount.Country.CZ), parsed.counterAccount()); + assertEquals(BankAccount.parse("270000000/0300", Country.CZ), parsed.counterAccount()); } @Test @@ -211,8 +211,8 @@ void readsTheEnglishTemplate() throws IOException { "fio.cz", Direction.INCOMING, Transaction.Type.GENERAL, - BankAccount.parse("1234567899/2010", BankAccount.Country.CZ), - BankAccount.parse("9876543211/2010", BankAccount.Country.CZ), + BankAccount.parse("1234567899/2010", Country.CZ), + BankAccount.parse("9876543211/2010", Country.CZ), "Pennyworth, Alfred", czk("1.00"), new PaymentSymbols("123456", null, null), diff --git a/modules/bank-notifications/src/banks/cz/moneta/java/cz/insuit/bank/notification/Moneta.java b/modules/bank-notifications/src/banks/cz/moneta/java/cz/insuit/bank/notification/Moneta.java index 6444261..9673f14 100644 --- a/modules/bank-notifications/src/banks/cz/moneta/java/cz/insuit/bank/notification/Moneta.java +++ b/modules/bank-notifications/src/banks/cz/moneta/java/cz/insuit/bank/notification/Moneta.java @@ -90,7 +90,7 @@ public Transaction parse(@Nullable String subject, String body) { stated, Transaction.Type.GENERAL, null, - counter == null ? null : BankAccount.parse(counter, BankAccount.Country.CZ), + counter == null ? null : BankAccount.parse(counter, Country.CZ), null, stated == Direction.INCOMING ? value : value.negate(), new PaymentSymbols(variableSymbol, null, null), diff --git a/modules/bank-notifications/src/banks/cz/moneta/test/cz/insuit/bank/notification/MonetaTest.java b/modules/bank-notifications/src/banks/cz/moneta/test/cz/insuit/bank/notification/MonetaTest.java index e070b39..e87aff5 100644 --- a/modules/bank-notifications/src/banks/cz/moneta/test/cz/insuit/bank/notification/MonetaTest.java +++ b/modules/bank-notifications/src/banks/cz/moneta/test/cz/insuit/bank/notification/MonetaTest.java @@ -29,7 +29,7 @@ void readsAnIncomingPaymentWithoutAccountOrTransactionId() throws IOException { Direction.INCOMING, Transaction.Type.GENERAL, null, - BankAccount.parse("9876543211/0300", BankAccount.Country.CZ), + BankAccount.parse("9876543211/0300", Country.CZ), null, czk("35000.00"), new PaymentSymbols("202600009", null, null), diff --git a/modules/bank-notifications/src/banks/cz/raiffeisenbank/java/cz/insuit/bank/notification/Raiffeisenbank.java b/modules/bank-notifications/src/banks/cz/raiffeisenbank/java/cz/insuit/bank/notification/Raiffeisenbank.java index 3e71db3..4cca75b 100644 --- a/modules/bank-notifications/src/banks/cz/raiffeisenbank/java/cz/insuit/bank/notification/Raiffeisenbank.java +++ b/modules/bank-notifications/src/banks/cz/raiffeisenbank/java/cz/insuit/bank/notification/Raiffeisenbank.java @@ -82,8 +82,8 @@ public Transaction parse(@Nullable String subject, String body) { DOMAIN, direction, card ? Transaction.Type.CARD : Transaction.Type.TRANSFER, - BankAccount.parse(own.group(1), BankAccount.Country.CZ), - counter == null ? null : BankAccount.parse(counter.group(1), BankAccount.Country.CZ), + BankAccount.parse(own.group(1), Country.CZ), + counter == null ? null : BankAccount.parse(counter.group(1), Country.CZ), counter == null || counter.group(2).isBlank() ? null : counter.group(2).strip(), diff --git a/modules/bank-notifications/src/banks/cz/raiffeisenbank/test/cz/insuit/bank/notification/RaiffeisenbankTest.java b/modules/bank-notifications/src/banks/cz/raiffeisenbank/test/cz/insuit/bank/notification/RaiffeisenbankTest.java index 169a3bb..dbe27e8 100644 --- a/modules/bank-notifications/src/banks/cz/raiffeisenbank/test/cz/insuit/bank/notification/RaiffeisenbankTest.java +++ b/modules/bank-notifications/src/banks/cz/raiffeisenbank/test/cz/insuit/bank/notification/RaiffeisenbankTest.java @@ -31,8 +31,8 @@ void readsAnIncomingPaymentFromHtml() throws IOException { "rb.cz", Direction.INCOMING, Transaction.Type.TRANSFER, - BankAccount.parse("1234567899/5500", BankAccount.Country.CZ), - BankAccount.parse("9876543211/0100", BankAccount.Country.CZ), + BankAccount.parse("1234567899/5500", Country.CZ), + BankAccount.parse("9876543211/0100", Country.CZ), "Pennyworth Alfred", czk("1234.56"), new PaymentSymbols("2606001", "308", null), @@ -53,7 +53,7 @@ void readsACardPaymentWithItsDetailsAsTheMessage() throws IOException { assertEquals(Transaction.Type.CARD, card.type()); assertEquals(Direction.OUTGOING, card.direction()); assertEquals(czk("-1234.56"), card.amount()); - assertEquals(BankAccount.parse("1234567899/5500", BankAccount.Country.CZ), card.account()); + assertEquals(BankAccount.parse("1234567899/5500", Country.CZ), card.account()); assertNull(card.counterAccount()); assertEquals("DEMO SOFTWARE; PRAHA; CZE", card.message()); } diff --git a/modules/bank-notifications/src/banks/cz/unicredit/java/cz/insuit/bank/notification/Unicredit.java b/modules/bank-notifications/src/banks/cz/unicredit/java/cz/insuit/bank/notification/Unicredit.java index 39929e6..ff34bc9 100644 --- a/modules/bank-notifications/src/banks/cz/unicredit/java/cz/insuit/bank/notification/Unicredit.java +++ b/modules/bank-notifications/src/banks/cz/unicredit/java/cz/insuit/bank/notification/Unicredit.java @@ -87,8 +87,8 @@ public Transaction parse(@Nullable String subject, String body) { DOMAIN, direction, Transaction.Type.GENERAL, - BankAccount.parse(sentence.group(1) + "/" + BANK_CODE, BankAccount.Country.CZ), - counter == null ? null : BankAccount.parse(counter, BankAccount.Country.CZ), + BankAccount.parse(sentence.group(1) + "/" + BANK_CODE, Country.CZ), + counter == null ? null : BankAccount.parse(counter, Country.CZ), fields.get(COUNTER_NAME), direction == Direction.INCOMING ? value : value.negate(), new PaymentSymbols( diff --git a/modules/bank-notifications/src/banks/cz/unicredit/test/cz/insuit/bank/notification/UnicreditTest.java b/modules/bank-notifications/src/banks/cz/unicredit/test/cz/insuit/bank/notification/UnicreditTest.java index 0e1daef..f78f8e5 100644 --- a/modules/bank-notifications/src/banks/cz/unicredit/test/cz/insuit/bank/notification/UnicreditTest.java +++ b/modules/bank-notifications/src/banks/cz/unicredit/test/cz/insuit/bank/notification/UnicreditTest.java @@ -28,8 +28,8 @@ void readsAnIncomingPaymentWithAnEnglishAmount() throws IOException { "unicreditgroup.cz", Direction.INCOMING, Transaction.Type.GENERAL, - BankAccount.parse("1234567899/2700", BankAccount.Country.CZ), - BankAccount.parse("9876543211/0600", BankAccount.Country.CZ), + BankAccount.parse("1234567899/2700", Country.CZ), + BankAccount.parse("9876543211/0600", Country.CZ), "WAYNE ENTERPRISES s.r.o.", czk("20546.00"), new PaymentSymbols("0002026017", null, null), diff --git a/modules/bank-notifications/src/main/java/cz/insuit/bank/notification/BankAccount.java b/modules/bank-notifications/src/main/java/cz/insuit/bank/notification/BankAccount.java index cb518f2..cf2e657 100644 --- a/modules/bank-notifications/src/main/java/cz/insuit/bank/notification/BankAccount.java +++ b/modules/bank-notifications/src/main/java/cz/insuit/bank/notification/BankAccount.java @@ -23,12 +23,6 @@ */ public record BankAccount(Country country, @Nullable String prefix, String number, String bankCode) { - /** The countries whose accounts this reads; both number accounts the same way. */ - public enum Country { - CZ, - SK - } - public BankAccount { if (prefix != null && (!prefix.matches("\\d{1,6}") || !checksumHolds(prefix, PREFIX_WEIGHTS))) { throw new IllegalArgumentException("not a valid account prefix: " + prefix); diff --git a/modules/bank-notifications/src/main/java/cz/insuit/bank/notification/Country.java b/modules/bank-notifications/src/main/java/cz/insuit/bank/notification/Country.java new file mode 100644 index 0000000..3ea059f --- /dev/null +++ b/modules/bank-notifications/src/main/java/cz/insuit/bank/notification/Country.java @@ -0,0 +1,10 @@ +package cz.insuit.bank.notification; + +/** + * A country whose bank accounts {@link BankAccount} reads. Czechia and Slovakia number accounts the same way, a prefix, + * a number and a four-digit bank code under one modulo 11 rule; another country is another constant. + */ +public enum Country { + CZ, + SK +} diff --git a/modules/bank-notifications/src/test/java/cz/insuit/bank/notification/BankAccountTest.java b/modules/bank-notifications/src/test/java/cz/insuit/bank/notification/BankAccountTest.java index e659a40..919a1fb 100644 --- a/modules/bank-notifications/src/test/java/cz/insuit/bank/notification/BankAccountTest.java +++ b/modules/bank-notifications/src/test/java/cz/insuit/bank/notification/BankAccountTest.java @@ -13,7 +13,7 @@ class BankAccountTest { @Test void readsAnAccountWithAPrefix() { // The Czech National Bank's sample account in its own guidance. - BankAccount account = BankAccount.parse("19-2000145399/0800", BankAccount.Country.CZ); + BankAccount account = BankAccount.parse("19-2000145399/0800", Country.CZ); assertEquals("19", account.prefix()); assertEquals("2000145399", account.number()); @@ -23,7 +23,7 @@ void readsAnAccountWithAPrefix() { @Test void readsAnAccountWithoutAPrefix() { - BankAccount account = BankAccount.parse("1234567899/3030", BankAccount.Country.CZ); + BankAccount account = BankAccount.parse("1234567899/3030", Country.CZ); assertNull(account.prefix()); assertEquals("1234567899/3030", account.toString()); @@ -32,39 +32,35 @@ void readsAnAccountWithoutAPrefix() { @Test void dropsLeadingZerosSoTheSameAccountIsEqual() { assertEquals( - BankAccount.parse("000019-2000145399/0800", BankAccount.Country.CZ), - BankAccount.parse("19-2000145399/0800", BankAccount.Country.CZ)); + BankAccount.parse("000019-2000145399/0800", Country.CZ), + BankAccount.parse("19-2000145399/0800", Country.CZ)); assertEquals( - BankAccount.parse("000000-1234567899/3030", BankAccount.Country.CZ), - BankAccount.parse("1234567899/3030", BankAccount.Country.CZ)); + BankAccount.parse("000000-1234567899/3030", Country.CZ), + BankAccount.parse("1234567899/3030", Country.CZ)); } @Test void refusesANumberThatFailsTheChecksum() { - assertThrows( - IllegalArgumentException.class, () -> BankAccount.parse("1234567890/3030", BankAccount.Country.CZ)); + assertThrows(IllegalArgumentException.class, () -> BankAccount.parse("1234567890/3030", Country.CZ)); } @Test void refusesAPrefixThatFailsTheChecksum() { - assertThrows( - IllegalArgumentException.class, () -> BankAccount.parse("18-2000145399/0800", BankAccount.Country.CZ)); + assertThrows(IllegalArgumentException.class, () -> BankAccount.parse("18-2000145399/0800", Country.CZ)); } @Test void refusesAnythingElse() { - assertThrows(IllegalArgumentException.class, () -> BankAccount.parse("1234567899", BankAccount.Country.CZ)); - assertThrows(IllegalArgumentException.class, () -> BankAccount.parse("1234567899/30", BankAccount.Country.CZ)); - assertThrows( - IllegalArgumentException.class, - () -> BankAccount.parse("CZ6508000000192000145399", BankAccount.Country.CZ)); + assertThrows(IllegalArgumentException.class, () -> BankAccount.parse("1234567899", Country.CZ)); + assertThrows(IllegalArgumentException.class, () -> BankAccount.parse("1234567899/30", Country.CZ)); + assertThrows(IllegalArgumentException.class, () -> BankAccount.parse("CZ6508000000192000145399", Country.CZ)); } @Test void readsTheLocalFormOfAnotherCountry() { - BankAccount account = BankAccount.parse("4019425381/7500", BankAccount.Country.SK); + BankAccount account = BankAccount.parse("4019425381/7500", Country.SK); - assertEquals(BankAccount.Country.SK, account.country()); + assertEquals(Country.SK, account.country()); assertEquals("4019425381/7500", account.toString()); } @@ -73,7 +69,7 @@ void readsAnIbanWithOrWithoutSpaces() { // The Czech National Bank's sample account, as an IBAN. BankAccount spaced = BankAccount.parseIban("CZ65 0800 0000 1920 0014 5399"); - assertEquals(BankAccount.parse("19-2000145399/0800", BankAccount.Country.CZ), spaced); + assertEquals(BankAccount.parse("19-2000145399/0800", Country.CZ), spaced); assertEquals(spaced, BankAccount.parseIban("cz6508000000192000145399")); } @@ -82,7 +78,7 @@ void readsASlovakIban() { // A real company's account, printed in a public ČSOB SK sample. BankAccount account = BankAccount.parseIban("SK13 7500 0000 0040 1942 5381"); - assertEquals(BankAccount.Country.SK, account.country()); + assertEquals(Country.SK, account.country()); assertNull(account.prefix()); assertEquals("4019425381", account.number()); assertEquals("7500", account.bankCode()); @@ -92,15 +88,15 @@ void readsASlovakIban() { void writesTheIban() { assertEquals( "CZ6508000000192000145399", - BankAccount.parse("19-2000145399/0800", BankAccount.Country.CZ).iban()); + BankAccount.parse("19-2000145399/0800", Country.CZ).iban()); assertEquals( "SK1375000000004019425381", - BankAccount.parse("4019425381/7500", BankAccount.Country.SK).iban()); + BankAccount.parse("4019425381/7500", Country.SK).iban()); } @Test void writesAnIbanThatReadsBackAsTheSameAccount() { - BankAccount account = BankAccount.parse("1234567899/3030", BankAccount.Country.CZ); + BankAccount account = BankAccount.parse("1234567899/3030", Country.CZ); assertEquals(account, BankAccount.parseIban(account.iban())); } @@ -108,8 +104,7 @@ void writesAnIbanThatReadsBackAsTheSameAccount() { @Test void keepsTheSameDigitsInTwoCountriesApart() { assertNotEquals( - BankAccount.parse("4019425381/7500", BankAccount.Country.CZ), - BankAccount.parse("4019425381/7500", BankAccount.Country.SK)); + BankAccount.parse("4019425381/7500", Country.CZ), BankAccount.parse("4019425381/7500", Country.SK)); } @Test diff --git a/testing/spring-boot/shared/src/test/java/example/ReadsOnSpringBootTest.java b/testing/spring-boot/shared/src/test/java/example/ReadsOnSpringBootTest.java index babdd1d..2d91ec5 100644 --- a/testing/spring-boot/shared/src/test/java/example/ReadsOnSpringBootTest.java +++ b/testing/spring-boot/shared/src/test/java/example/ReadsOnSpringBootTest.java @@ -6,6 +6,7 @@ import cz.insuit.bank.notification.BankAccount; import cz.insuit.bank.notification.BankNotificationException; import cz.insuit.bank.notification.BankNotifications; +import cz.insuit.bank.notification.Country; import cz.insuit.bank.notification.Transaction; import java.io.ByteArrayInputStream; import java.math.BigDecimal; @@ -31,7 +32,7 @@ void readsAndVerifies() throws Exception { Transaction t = notifications.read(signed(key)); - assertEquals(BankAccount.parse("1234567899/3030", BankAccount.Country.CZ), t.account()); + assertEquals(BankAccount.parse("1234567899/3030", Country.CZ), t.account()); assertEquals(new BigDecimal("1.00"), t.amount().amount()); assertEquals("1337", t.symbols().variable()); }