10 Commits

Author SHA1 Message Date
7bce4f8051 Add script to fetch and process IBAN registry data
Include a new Python script that retrieves IBAN registry data from SWIFT and processes it into a structured JSON format,
along with a PDF document for reference.
2025-11-29 12:48:51 -05:00
0ed739b444 Enable Elixir tracking and update .gitignore
- Add .gitattributes for Elixir file tracking. - Extend .gitignore for Elixir-related and other development files. -
Introduce Agents.md with project guidelines and usage rules. - Create CLAUDE.md as a symlink to Agents.md. - Update
mix.exs to specify proper version for elixir_sense.
2025-11-29 12:48:45 -05:00
83ddceec00 Some ToDos added 2024-06-15 12:44:49 -04:00
e847e2c473 BBAN parts: bank_code, account_number, branch_code and national_check supported in parser and validator 2024-05-16 15:01:17 -04:00
a660250af1 0.1.7 2024-05-16 05:07:45 -04:00
709f6c50b5 Partial IBAN parser added 2024-05-16 04:55:21 -04:00
5cfc3f5fa2 Check to existing country module before parsing bban 2024-05-15 00:04:47 -04:00
ce90960649 iban_violates_country_rule? fix 2024-05-14 23:55:32 -04:00
e7e6bbda29 Return blank strings in responses 2024-05-14 23:45:34 -04:00
6ec94020ef Empty strings supported in parse_bban 2024-05-14 23:22:16 -04:00
18 changed files with 845 additions and 131 deletions

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
# Cicada: Enable git function tracking for Elixir
*.ex diff=elixir
*.exs diff=elixir

16
.gitignore vendored
View File

@@ -1,3 +1,19 @@
# Development artefacts
.DS_Store
.vscode
.drone.status
# Elixir LS and Tools
.elixir_ls
.elixir-tools
.lexical
.expert
# AI tools
.claude
.codex
.mcp.json
# The directory Mix will write compiled artifacts to.
/_build/

232
Agents.md Normal file
View File

@@ -0,0 +1,232 @@
This is a web application written using the Phoenix web framework.
## Project guidelines
- Use `mix check` alias when you are done with all changes and fix any pending issues
- Use the already included and available `:req` (`Req`) library for HTTP requests, **avoid** `:httpoison`, `:tesla`, and `:httpc`. Req is included by default and is the preferred HTTP client for Banker
- Use the already included and available Elixir native JSON module to encode and decode JSON, **avoid** `:jason`, `:poison`, and other. JSON is a part of Elixir standard library and is the preferred JSON parser and generator for Banker
<!-- elixir-toolkit-commands--execution-start -->
## Command Execution
**🔴 CRITICAL: ALWAYS prefix mix, iex commands with environment variables:**
**For Development:**
```bash
env $(cat .dev.env | xargs) mix <COMMAND>
env $(cat .dev.env | xargs) iex -S mix
```
**For Testing:**
```bash
env $(cat .test.env | xargs) mix test
env $(cat .test.env | xargs) iex -S mix
```
**Examples:**
```bash
# Development
env $(cat .dev.env | xargs) mix test
env $(cat .dev.env | xargs) mix compile
env $(cat .dev.env | xargs) mix phx.server
# Testing
env $(cat .test.env | xargs) mix test
env $(cat .test.env | xargs) mix test test/banker/assistants/invoice_data_assistant_test.exs
env $(cat .test.env | xargs) iex -S mix
```
**NEVER run mix commands without the appropriate ENV file prefix** - will fail with missing ENV variable errors.
- Use `.dev.env` for development commands
- Use `.test.env` for test commands
<!-- elixir-toolkit-commands--execution-end -->
<!-- tool-usage-start -->
## Tool Usage Guidelines
<!-- tool-usage:code-start -->
### Code base analysys and semantic code search
Register project to make Tree Sitter tool available for analyzing the Banker codebase and get next posibilities:
- **Search codebase**: Find files, functions, or patterns
- **Understand architecture**: Explore modules, domains, resources
- **Code navigation**: Jump to definitions, find usages
- **Quality analysis**: Detect complexity, duplication, dependencies
- **Strategic exploration**: Understand domain structure and relationships
**Always use tree_sitter_banker tool** for:
1. **Code Navigation**: Extract functions, classes, modules. Find where symbols are used. Search with regex patterns. Read file contents efficiently. Get abstract syntax trees
2. **Analysis Tools**: Measure cyclomatic complexity. Find imports and dependencies. Detect code duplication. Execute tree-sitter queries
3. **Project Understanding**: Get file lists by pattern or extension. Analyze project structure. Get file metadata and line counts. Navigate dependencies.
<!-- tool-usage:code-end -->
<!-- tool-usage:documentation-start -->
### Documentation
- **Always use HEXDocs tool** to get and analyze **actual documentation** for Elixir, Elixir libraries and Phoenix framework
<!-- tool-usage:documentation-end -->
<!-- tool-usage:database-start -->
### Database Access
- **Use only** postgres_banker_dev tool to comunicate with development database
- **Use only** postgres_banker_test tool to comunicate with test database
- **Only in case when** postgres_banker_dev or postgres_banker_test tools is unavaliable you can get data from databases directly:
- Development database: `psql banker_dev -c "<query>"`
- Test database: `psql banker_test -c "<query>"`
- **NEVER** try do anything with production database. You are **FORBIDDEN** from execute any queries on prod environmemnt
<!-- tool-usage:database-end -->
<!-- tool-usage-end -->
<!-- guidelines-start -->
<!-- guidelines:elixir-start -->
## Elixir Core Usage Rules
### Pattern Matching
- Use pattern matching over conditional logic when possible
- Prefer to match on function heads instead of using `if`/`else` or `case` in function bodies
- `%{}` matches ANY map, not just empty maps. Use `map_size(map) == 0` guard to check for truly empty maps
### Error Handling
- Use `{:ok, result}` and `{:error, reason}` tuples for operations that can fail
- Avoid raising exceptions for control flow
- Use `with` for chaining operations that return `{:ok, _}` or `{:error, _}`
### Common Mistakes to Avoid
- Elixir has no `return` statement, nor early returns. The last expression in a block is always returned.
- Don't use `Enum` functions on large collections when `Stream` is more appropriate
- Avoid nested `case` statements - refactor to a single `case`, `with` or separate functions
- Don't use `String.to_atom/1` on user input (memory leak risk)
- Lists and enumerables cannot be indexed with brackets. Use pattern matching or `Enum` functions
- Prefer `Enum` functions like `Enum.reduce` over recursion
- When recursion is necessary, prefer to use pattern matching in function heads for base case detection
- Using the process dictionary is typically a sign of unidiomatic code
- Only use macros if explicitly requested
- There are many useful standard library functions, prefer to use them where possible
- **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors
### Function Design
- Use guard clauses: `when is_binary(name) and byte_size(name) > 0`
- Prefer multiple function clauses over complex conditional logic
- Name functions descriptively: `calculate_total_price/2` not `calc/2`
- Predicate function names should not start with `is` and should end in a question mark.
- Names like `is_thing` should be reserved for guards
### Data Structures
- Use structs over maps when the shape is known: `defstruct [:name, :age]`
- Use maps for dynamic key-value data
- **Never** use map access syntax (`changeset[:field]`) on structs as they do not implement the Access behaviour by default. For regular structs, you **must** access the fields directly, such as `my_struct.field` or use higher level APIs that are available on the struct if they exist, `Ecto.Changeset.get_field/2` for changesets
- Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common `Time`, `Date`, `DateTime`, and `Calendar` interfaces by accessing their documentation as necessary. **Never** install additional dependencies unless asked or for date/time parsing (which you can use the `date_time_parser` package)
- Don't use `String.to_atom/1` on user input (memory leak risk)
- Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards
- Elixir's builtin OTP primitives like `DynamicSupervisor` and `Registry`, require names in the child spec, such as `{DynamicSupervisor, name: Banker.MyDynamicSup}`, then you can use `DynamicSupervisor.start_child(Banker.MyDynamicSup, child_spec)`
- Use `Task.async_stream(collection, callback, options)` for concurrent enumeration with back-pressure. The majority of times you will want to pass `timeout: :infinity` as option
- Elixir variables are immutable, but can be rebound, so for block expressions like `if`, `case`, `cond`, etc
you *must* bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, ie:
# INVALID: we are rebinding inside the `if` and the result never gets assigned
if connected?(socket) do
socket = assign(socket, :val, val)
end
# VALID: we rebind the result of the `if` to a new variable
socket =
if connected?(socket) do
assign(socket, :val, val)
end
- Prefer keyword lists for options: `[timeout: 5000, retries: 3]`
- Prefer to prepend to lists `[new | list]` not `list ++ [new]`
- Elixir lists **do not support index based access via the access syntax**
**Never do this (invalid)**:
i = 0
mylist = ["blue", "green"]
mylist[i]
Instead, **always** use `Enum.at`, pattern matching, or `List` for index based list access, ie:
i = 0
mylist = ["blue", "green"]
Enum.at(mylist, i)
### Mix Tasks
- Use `mix help` to list available mix tasks
- Use `mix help task_name` to get docs for an individual task
- Read the docs and options before using tasks (by using `mix help task_name`)
- To debug test failures, run tests in a specific file with `mix test test/my_test.exs` or run all previously failed tests with `mix test --failed`
- `mix deps.clean --all` is **almost never needed**. **Avoid** using it unless you have good reason
### Testing
- Run tests in a specific file with `mix test test/my_test.exs` and a specific test with the line number `mix test path/to/test.exs:123`
- Limit the number of failed tests with `mix test --max-failures n`
- Use `@tag` to tag specific tests, and `mix test --only tag` to run only those tests
- Use `assert_raise` for testing expected exceptions: `assert_raise ArgumentError, fn -> invalid_function() end`
- Use `mix help test` to for full documentation on running tests
### Debugging
- Use `dbg/1` to print values while debugging. This will display the formatted value and other relevant information in the console.
<!-- guidelines:elixir-end -->
<!-- guidelines:otp-start -->
## OTP Usage Rules
### GenServer Best Practices
- Keep state simple and serializable
- Handle all expected messages explicitly
- Use `handle_continue/2` for post-init work
- Implement proper cleanup in `terminate/2` when necessary
### Process Communication
- Use `GenServer.call/3` for synchronous requests expecting replies
- Use `GenServer.cast/2` for fire-and-forget messages.
- When in doubt, use `call` over `cast`, to ensure back-pressure
- Set appropriate timeouts for `call/3` operations
### Fault Tolerance
- Set up processes such that they can handle crashing and being restarted by supervisors
- Use `:max_restarts` and `:max_seconds` to prevent restart loops
### Task and Async
- Use `Task.Supervisor` for better fault tolerance
- Handle task failures with `Task.yield/2` or `Task.shutdown/2`
- Set appropriate task timeouts
- Use `Task.async_stream/3` for concurrent enumeration with back-pressure
<!-- guidelines:otp-end -->
<!-- guidelines-end -->
<cicada>
**ALWAYS use cicada-mcp tools for Elixir and Python code searches. NEVER use Grep/Find for these tasks.**
### Use cicada tools for:
- YOUR PRIMARY TOOL - Start here for ALL code exploration and discovery. `mcp__cicada__query`
- DEEP-DIVE TOOL: View a module's complete API and dependencies after discovering it with query. `mcp__cicada__search_module`
- DEEP-DIVE TOOL: Find function definitions and call sites after discovering with query. `mcp__cicada__search_function`
- UNIFIED HISTORY TOOL: One tool for all git history queries - replaces get_blame, get_commit_history, find_pr_for_line, and get_file_pr_history. `mcp__cicada__git_history`
- ANALYSIS TOOL: Find potentially unused public functions with confidence levels. `mcp__cicada__find_dead_code`
- DRILL-DOWN TOOL: Expand a query result to see complete details. `mcp__cicada__expand_result`
- ADVANCED: Execute jq queries directly against the Cicada index for custom analysis and data exploration. `mcp__cicada__query_jq`
### DO NOT use Grep for:
- ❌ Searching for module structure
- ❌ Searching for function definitions
- ❌ Searching for module imports/usage
### You can still use Grep for:
- ✓ Non-code files (markdown, JSON, config)
- ✓ String literal searches
- ✓ Pattern matching in single line comments
</cicada>

1
CLAUDE.md Symbolic link
View File

@@ -0,0 +1 @@
./Agents.md

View File

@@ -71,7 +71,7 @@ The package can be installed by adding `iban_ex` to your list of dependencies in
```elixir
def deps do
[
{:iban_ex, "~> 0.1.6"}
{:iban_ex, "~> 0.1.8"}
]
end
```

View File

@@ -0,0 +1,86 @@
#!/usr/bin/env python
import json
import re
from urllib.parse import urljoin
import requests
from bs4 import BeautifulSoup
COUNTRY_CODE_PATTERN = r"[A-Z]{2}"
EMPTY_RANGE = (0, 0)
URL = "https://www.swift.com/standards/data-standards/iban"
def get_raw():
soup = BeautifulSoup(requests.get(URL).content, "html.parser")
link = soup.find("a", attrs={"data-tracking-title": "IBAN Registry (TXT)"})
return requests.get(urljoin(URL, link["href"])).content.decode(encoding="latin1")
def parse_int(raw):
return int(re.search(r"\d+", raw).group())
def parse_range(raw):
pattern = r".*?(?P<from>\d+)\s*-\s*(?P<to>\d+)"
match = re.search(pattern, raw)
if not match:
return EMPTY_RANGE
return (int(match["from"]) - 1, int(match["to"]))
def parse(raw):
columns = {}
for line in raw.split("\r\n"):
header, *rows = line.split("\t")
if header == "IBAN prefix country code (ISO 3166)":
columns["country"] = [re.search(COUNTRY_CODE_PATTERN, item).group() for item in rows]
elif header == "Country code includes other countries/territories":
columns["other_countries"] = [re.findall(COUNTRY_CODE_PATTERN, item) for item in rows]
elif header == "BBAN structure":
columns["bban_spec"] = rows
elif header == "BBAN length":
columns["bban_length"] = [parse_int(item) for item in rows]
elif header == "Bank identifier position within the BBAN":
columns["bank_code_position"] = [parse_range(item) for item in rows]
elif header == "Branch identifier position within the BBAN":
columns["branch_code_position"] = [parse_range(item) for item in rows]
elif header == "IBAN structure":
columns["iban_spec"] = rows
elif header == "IBAN length":
columns["iban_length"] = [parse_int(item) for item in rows]
return [dict(zip(columns.keys(), row)) for row in zip(*columns.values())]
def process(records):
registry = {}
for record in records:
country_codes = [record["country"]]
country_codes.extend(record["other_countries"])
for code in country_codes:
registry[code] = {
"bban_spec": record["bban_spec"],
"iban_spec": record["iban_spec"],
"bban_length": record["bban_length"],
"iban_length": record["iban_length"],
"positions": process_positions(record),
}
return registry
def process_positions(record):
bank_code = record["bank_code_position"]
branch_code = record["branch_code_position"]
if branch_code == EMPTY_RANGE:
branch_code = (bank_code[1], bank_code[1])
return {
"account_code": (max(bank_code[1], branch_code[1]), record["bban_length"]),
"bank_code": bank_code,
"branch_code": branch_code,
}
if __name__ == "__main__":
with open("schwifty/iban_registry/generated.json", "w+") as fp:
json.dump(process(parse(get_raw())), fp, indent=2)

Binary file not shown.

View File

@@ -41,6 +41,7 @@ defmodule IbanEx.Country do
"IE" => IbanEx.Country.IE,
"IL" => IbanEx.Country.IL,
"IT" => IbanEx.Country.IT,
"IS" => IbanEx.Country.IS,
"JO" => IbanEx.Country.JO,
"KZ" => IbanEx.Country.KZ,
"KW" => IbanEx.Country.KW,

View File

@@ -1,4 +1,6 @@
defmodule IbanEx.Country.BG do
# TODO Bulgaria IBAN contains account type (first 2 digits of account number)
@moduledoc """
Bulgaria IBAN parsing rules

45
lib/iban_ex/country/is.ex Normal file
View File

@@ -0,0 +1,45 @@
defmodule IbanEx.Country.IS do
# TODO Iceland IBAN contains identification number (last 10 digits of account number)
@moduledoc """
Island IBAN parsing rules
## Examples
```elixir
iex> %IbanEx.Iban{
...> country_code: "IS",
...> check_digits: "14",
...> bank_code: "0159",
...> branch_code: "26",
...> national_check: nil,
...> account_number: "0076545510730339"
...> }
...> |> IbanEx.Country.IS.to_string()
"IS 14 0159 26 0076545510730339"
```
"""
@size 26
@rule ~r/^(?<bank_code>[0-9]{4})(?<branch_code>[0-9]{2})(?<account_number>[0-9]{16})$/i
use IbanEx.Country.Template
@impl IbanEx.Country.Template
@spec to_string(Iban.t()) :: binary()
@spec to_string(Iban.t(), binary()) :: binary()
def to_string(
%Iban{
country_code: country_code,
check_digits: check_digits,
bank_code: bank_code,
branch_code: branch_code,
national_check: _national_check,
account_number: account_number
} = _iban,
joiner \\ " "
) do
[country_code, check_digits, bank_code, branch_code, account_number]
|> Enum.join(joiner)
end
end

View File

@@ -9,7 +9,10 @@ defmodule IbanEx.Country.Template do
@callback size() :: size()
@callback rule() :: rule()
@callback incomplete_rule() :: rule()
@callback rules() :: []
@callback rules_map() :: %{}
@callback bban_fields() :: [atom()]
@callback bban_size() :: non_neg_integer()
@callback to_string(Iban.t(), joiner()) :: String.t()
@callback to_string(Iban.t()) :: String.t()
@@ -47,26 +50,50 @@ def size(), do: @size
@spec rule() :: Regex.t()
def rule(), do: @rule
@doc """
Return Regex without trailing $ for parsing incomplete BBAN (part of IBAN string) (for partial suggestions)
"""
@impl IbanEx.Country.Template
@spec incomplete_rule() :: Regex.t()
def incomplete_rule() do
@spec bban_size() :: integer()
def bban_size() do
{_rules, bban_size} = calculate_rules()
bban_size
end
@impl IbanEx.Country.Template
@spec bban_fields() :: []
def bban_fields(), do: rules_map() |> Map.keys()
@impl IbanEx.Country.Template
@spec rules_map() :: %{}
def rules_map(), do: rules() |> Map.new()
@impl IbanEx.Country.Template
@spec rules() :: []
def rules() do
{rules, _bban_size} = calculate_rules()
rules
end
defp calculate_rules() do
scanner = ~r/\(\?\<([\w_]+)\>(([^{]+)\{(\d+)\})\)/i
source =
@rule
|> Regex.source()
|> String.slice(0..-2//1)
|> String.replace("{", "{0,")
opts =
@rule
|> Regex.opts()
{list, bban_length} =
Regex.scan(scanner, source)
|> Enum.reduce({[], 0}, fn [_part, k, r, _syms, l], {list, position} = acc ->
key = String.to_atom(k)
{:ok, regex} = Regex.compile(r, "i")
length = String.to_integer(l)
left = position
right = left + length - 1
{[{key, %{regex: regex, range: left..right}} | list], right + 1}
end)
Regex.compile!(source, opts)
{Enum.reverse(list), bban_length}
end
defoverridable to_string: 1, to_string: 2, size: 0, rule: 0, incomplete_rule: 0
defoverridable to_string: 1, to_string: 2, size: 0, rule: 0
end
end
end

View File

@@ -9,6 +9,10 @@ defmodule IbanEx.Error do
| :can_not_parse_map
| :length_to_long
| :length_to_short
| :invalid_bank_code
| :invalid_account_number
| :invalid_branch_code
| :invalid_national_check
| atom()
@type errors() :: [error()]
@errors [
@@ -18,7 +22,11 @@ defmodule IbanEx.Error do
:invalid_checksum,
:can_not_parse_map,
:length_to_long,
:length_to_short
:length_to_short,
:invalid_bank_code,
:invalid_account_number,
:invalid_branch_code,
:invalid_national_check
]
@messages [
@@ -28,7 +36,11 @@ defmodule IbanEx.Error do
invalid_checksum: "IBAN's checksum is invalid",
can_not_parse_map: "Can't parse map to IBAN struct",
length_to_long: "IBAN longer then required length",
length_to_short: "IBAN shorter then required length"
length_to_short: "IBAN shorter then required length",
invalid_bank_code: "Bank code violates required format",
invalid_account_number: "Account number violates required format",
invalid_branch_code: "Branch code violates required format",
invalid_national_check: "National check symbols violates required format",
]
@spec message(error()) :: String.t()

View File

@@ -2,7 +2,7 @@ defmodule IbanEx.Parser do
@moduledoc false
alias IbanEx.{Country, Iban, Validator}
import IbanEx.Commons, only: [normalize_and_slice: 2, blank: 1]
import IbanEx.Commons, only: [normalize_and_slice: 2]
@type iban_string() :: String.t()
@type country_code_string() :: <<_::16>>
@@ -20,8 +20,9 @@ defmodule IbanEx.Parser do
@spec parse({:ok, binary()}) :: iban_or_error()
def parse({:ok, iban_string}), do: parse(iban_string)
@spec parse(binary()) :: iban_or_error()
def parse(iban_string) do
def parse(iban_string, options \\ [incomplete: false])
def parse(iban_string, incomplete: false) do
case Validator.validate(iban_string) do
{:ok, valid_iban} ->
iban_map = %{
@@ -41,25 +42,65 @@ def parse(iban_string) do
end
end
def parse(iban_string, incomplete: true) do
iban_map = %{
country_code: country_code(iban_string),
check_digits: check_digits(iban_string)
}
bban = bban(iban_string)
case Country.is_country_code_supported?(iban_map.country_code) do
true ->
result =
parse_bban(bban, iban_map.country_code, incomplete: true)
|> Map.merge(iban_map)
{:ok, struct(Iban, result)}
false ->
{:error, :unsupported_country_code}
end
end
@spec parse_bban(binary(), <<_::16>>) :: map()
def parse_bban(bban_string, country_code, options \\ [incomplete: false])
def parse_bban(bban_string, country_code, incomplete: true) do
Country.country_module(country_code).incomplete_rule()
|> parse_bban_by_regex(bban_string)
case Country.is_country_code_supported?(country_code) do
true ->
country_code
|> Country.country_module()
|> parse_bban_by_country_rules(bban_string)
false ->
%{}
end
end
def parse_bban(bban_string, country_code, _options) do
def parse_bban(bban_string, country_code, incomplete: false) do
case Country.is_country_code_supported?(country_code) do
true ->
Country.country_module(country_code).rule()
|> parse_bban_by_regex(bban_string)
false ->
%{}
end
end
defp parse_bban_by_country_rules(country_module, bban_string) do
for {field, rule} <- country_module.rules,
into: %{},
do: {field, normalize_and_slice(bban_string, rule.range)}
end
defp parse_bban_by_regex(_regex, nil), do: %{}
defp parse_bban_by_regex(regex, bban_string) do
case Regex.named_captures(regex, bban_string) do
map when is_map(map) ->
for {key, val} <- map,
into: %{},
do: {String.to_atom(key), blank(val)}
do: {String.to_atom(key), val}
nil ->
%{}
@@ -67,11 +108,11 @@ defp parse_bban_by_regex(regex, bban_string) do
end
@spec country_code(iban_string()) :: country_code_string()
def country_code(iban_string), do: normalize_and_slice(iban_string, 0..1) |> blank()
def country_code(iban_string), do: normalize_and_slice(iban_string, 0..1)
@spec check_digits(binary()) :: check_digits_string()
def check_digits(iban_string), do: normalize_and_slice(iban_string, 2..3) |> blank()
def check_digits(iban_string), do: normalize_and_slice(iban_string, 2..3)
@spec bban(binary()) :: binary()
def bban(iban_string), do: normalize_and_slice(iban_string, 4..-1//1) |> blank()
def bban(iban_string), do: normalize_and_slice(iban_string, 4..-1//1)
end

View File

@@ -3,10 +3,11 @@ defmodule IbanEx.Validator do
alias IbanEx.{Country, Parser}
alias IbanEx.Validator.Replacements
import IbanEx.Commons, only: [normalize: 1]
import IbanEx.Commons, only: [normalize: 1, normalize_and_slice: 2]
defp error_accumulator(acc, error_message)
defp error_accumulator(acc, {:error, error}), do: [error | acc]
# defp error_accumulator(acc, list) when is_list(list), do: list ++ acc
defp error_accumulator(acc, _), do: acc
defp violation_functions(),
@@ -15,12 +16,25 @@ defp violation_functions(),
{&__MODULE__.iban_unsupported_country?/1, {:error, :unsupported_country_code}},
{&__MODULE__.iban_violates_length?/1, {:error, :invalid_length}},
{&__MODULE__.iban_violates_country_rule?/1, {:error, :invalid_format_for_country}},
{&__MODULE__.iban_violates_checksum?/1, {:error, :invalid_checksum}}
{&__MODULE__.iban_violates_bank_code_format?/1, {:error, :invalid_bank_code}},
{&__MODULE__.iban_violates_account_number_format?/1, {:error, :invalid_account_number}},
{&__MODULE__.iban_violates_branch_code_format?/1, {:error, :invalid_branch_code}},
{&__MODULE__.iban_violates_national_check_format?/1, {:error, :invalid_national_check}},
{&__MODULE__.iban_violates_checksum?/1, {:error, :invalid_checksum}},
]
@doc """
Accumulate check results in the list of errors
Check iban_violates_format?, iban_unsupported_country?, iban_violates_length?, iban_violates_country_rule?, iban_violates_checksum?
Check
iban_violates_format?,
iban_unsupported_country?,
iban_violates_length?,
iban_violates_country_rule?,
iban_violates_bank_code_format?,
iban_violates_account_number_format?
iban_violates_branch_code_format?,
iban_violates_national_check_format?,
iban_violates_checksum?,
"""
@spec violations(String.t()) :: [] | [atom()]
def violations(iban) do
@@ -36,8 +50,11 @@ def violations(iban) do
iban_unsupported_country?,
iban_violates_length?,
iban_violates_country_rule?,
iban_violates_checksum?
iban_violates_bank_code_format?,
iban_violates_account_number_format?,
iban_violates_branch_code_format?,
iban_violates_national_check_format?,
iban_violates_checksum?,
"""
@type iban() :: binary()
@type iban_or_error() ::
@@ -46,6 +63,10 @@ def violations(iban) do
| {:invalid_format, binary()}
| {:invalid_length, binary()}
| {:unsupported_country_code, binary()}
| {:invalid_bank_code, binary()}
| {:invalid_account_number, binary()}
| {:invalid_branch_code, binary()}
| {:invalid_national_check, binary()}
@spec validate(String.t()) :: {:ok, String.t()} | {:error, atom()}
def validate(iban) do
@@ -54,6 +75,10 @@ def validate(iban) do
iban_unsupported_country?(iban) -> {:error, :unsupported_country_code}
iban_violates_length?(iban) -> {:error, :invalid_length}
iban_violates_country_rule?(iban) -> {:error, :invalid_format_for_country}
iban_violates_bank_code_format?(iban) -> {:error, :invalid_bank_code}
iban_violates_account_number_format?(iban) -> {:error, :invalid_account_number}
iban_violates_branch_code_format?(iban) -> {:error, :invalid_branch_code}
iban_violates_national_check_format?(iban) -> {:error, :invalid_national_check}
iban_violates_checksum?(iban) -> {:error, :invalid_checksum}
true -> {:ok, normalize(iban)}
end
@@ -71,6 +96,34 @@ defp size(iban) do
def iban_violates_format?(iban),
do: Regex.match?(~r/[^A-Z0-9]/i, normalize(iban))
# - Check whether a given IBAN violates the required format in bank_code.
@spec iban_violates_bank_code_format?(binary()) :: boolean
def iban_violates_bank_code_format?(iban), do: iban_violates_bban_part_format?(iban, :bank_code)
# - Check whether a given IBAN violates the required format in branch_code.
@spec iban_violates_branch_code_format?(binary()) :: boolean
def iban_violates_branch_code_format?(iban), do: iban_violates_bban_part_format?(iban, :branch_code)
# - Check whether a given IBAN violates the required format in account_number.
@spec iban_violates_account_number_format?(binary()) :: boolean
def iban_violates_account_number_format?(iban), do: iban_violates_bban_part_format?(iban, :account_number)
# - Check whether a given IBAN violates the required format in national_check.
@spec iban_violates_national_check_format?(binary()) :: boolean
def iban_violates_national_check_format?(iban), do: iban_violates_bban_part_format?(iban, :national_check)
defp iban_violates_bban_part_format?(iban, part) do
with country <- Parser.country_code(iban),
bban <- Parser.bban(iban),
true <- Country.is_country_code_supported?(country),
country_module <- Country.country_module(country),
{:ok, rule} <- Map.fetch(country_module.rules_map(), part) do
!Regex.match?(rule.regex, normalize_and_slice(bban, rule.range))
else
_ -> false
end
end
# - Check whether a given IBAN violates the supported countries.
@spec iban_unsupported_country?(String.t()) :: boolean
def iban_unsupported_country?(iban) do
@@ -123,7 +176,7 @@ def iban_violates_country_rule?(iban) do
rule <- country_module.rule() do
!Regex.match?(rule, bban)
else
{:error, _error} -> true
_ -> true
end
end

View File

@@ -2,7 +2,7 @@ defmodule IbanEx.MixProject do
use Mix.Project
@source_url "https://g.tulz.dev/opensource/iban-ex"
@version "0.1.6"
@version "0.1.8"
def project do
[
@@ -69,10 +69,7 @@ defp deps do
{:sobelow, ">= 0.0.0", only: ~w(dev test)a, runtime: false},
{:mix_audit, ">= 0.0.0", only: ~w(dev test)a, runtime: false},
{:observer_cli, "~> 1.7.4", only: :dev, runtime: false},
{:elixir_sense, github: "elixir-lsp/elixir_sense", only: ~w(dev)a}
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
{:elixir_sense, "~> 1.0.0", only: :dev}
]
end
end

View File

@@ -0,0 +1,172 @@
defmodule IbanExParserTest do
alias IbanEx.{Country, Iban, Parser}
use ExUnit.Case, async: true
@ibans [
"AL47212110090000000235698741",
"AD1200012030200359100100",
"AT611904300234573201",
"AZ21NABZ00000000137010001944",
"BH67BMAG00001299123456",
"BE68539007547034",
"BA391290079401028494",
"BR1800360305000010009795493C1",
"BG80BNBG96611020345678",
"CR05015202001026284066",
"HR1210010051863000160",
"CY17002001280000001200527600",
"CZ6508000000192000145399",
"DK5000400440116243",
"DO28BAGR00000001212453611324",
"EG380019000500000000263180002",
"SV62CENR00000000000000700025",
"EE382200221020145685",
"FO6264600001631634",
"FI2112345600000785",
"FR1420041010050500013M02606",
"GE29NB0000000101904917",
"DE89370400440532013000",
"GI75NWBK000000007099453",
"GR1601101250000000012300695",
"GL8964710001000206",
"GT82TRAJ01020000001210029690",
"HU42117730161111101800000000",
"IS140159260076545510730339",
"IE29AIBK93115212345678",
"IL620108000000099999999",
"IT60X0542811101000000123456",
"JO94CBJO0010000000000131000302",
"KZ86125KZT5004100100",
"XK051212012345678906",
"KW81CBKU0000000000001234560101",
"LV80BANK0000435195001",
"LB62099900000001001901229114",
"LI21088100002324013AA",
"LT121000011101001000",
"LU280019400644750000",
"MK07250120000058984",
"MT84MALT011000012345MTLCAST001S",
"MR1300020001010000123456753",
"MC5811222000010123456789030",
"ME25505000012345678951",
"NL91ABNA0417164300",
"NO9386011117947",
"PK36SCBL0000001123456702",
"PL61109010140000071219812874",
"PT50000201231234567890154",
"QA58DOHB00001234567890ABCDEFG",
"MD24AG000225100013104168",
"RO49AAAA1B31007593840000",
"SM86U0322509800000000270100",
"SA0380000000608010167519",
"RS35260005601001611379",
"SK3112000000198742637541",
"SI56263300012039086",
"ES9121000418450200051332",
"SE4550000000058398257466",
"CH9300762011623852957",
"TL380080012345678910157",
"TR330006100519786457841326",
"UA213223130000026007233566001",
"AE070331234567890123456",
"GB29NWBK60161331926819",
"VA59001123000012345678",
"VG96VPVG0000012345678901"
]
test "parsing valid IBANs from available countries returns {:ok, %IbanEx.Iban{}}" do
Enum.all?(@ibans, fn iban ->
iban_country =
iban
|> String.upcase()
|> String.slice(0..1)
result =
case {Country.is_country_code_supported?(iban_country), Parser.parse(iban)} do
{true, {:ok, %Iban{}}} ->
true
_ ->
false
end
assert(result, iban)
end)
end
test "parsing invalid IBANs from unavailable countries returns {:error, :unsupported_country_code}" do
invalid_ibans =
[
# Fake country codes
"SD3112000000198742637541",
"SU56263300012039086",
"ZZ9121000418450200051332",
"FU4550000000058398257466",
"GF9300762011623852957",
"FX380080012345678910157",
"RT330006100519786457841326",
"UL213223130000026007233566001",
"AP070331234567890123456",
"FF29NWBK60161331926819",
"VV59001123000012345678",
"GV96VPVG0000012345678901",
# Unsupported now by library
"AA0096VPVG0000012345",
"AO213223130000026",
"AX00213223130000026007",
"BF3112000000198742637541375",
"BI31120000001987",
"BJ31120000001987426375413750",
"BL3112000000198742637541375",
"BY31120000001987426375413754",
"CF3112000000198742637541375",
"CG3112000000198742637541375",
"CI31120000001987426375413750",
"CM3112000000198742637541375",
"CV31120000001987426375413",
"DJ3112000000198742637541375",
"DZ3112000000198742637541",
"GA3112000000198742637541375",
"GF3112000000198742637541375",
"GP3112000000198742637541375",
"GQ3112000000198742637541375",
"GW31120000001987426375413",
"HN31120000001987426375413759",
"IQ311200000019874263754",
"IR311200000019874263754137",
"KM3112000000198742637541375",
"LC311200000019874263754",
"MA31120000001987426375413750",
"MF3112000000198742637541375",
"MG3112000000198742637541375",
"ML31120000001987426375413750",
"MQ3112000000198742637541375",
"MU3112000000198742637541375000",
"MZ31120000001987426375413",
"NC3112000000198742637541375",
"NE31120000001987426375413750",
"NI311200000019874263754137500000",
"PF3112000000198742637541375",
"PM3112000000198742637541375",
"PS311200000019874263754137500",
"RE3112000000198742637541375",
"SC311200000019874263754137500000",
"SN31120000001987426375413750",
"ST31120000001987426375413",
"TD3112000000198742637541375",
"TF3112000000198742637541375",
"TG31120000001987426375413750",
"TN3112000000198742637541",
"WF3112000000198742637541375",
"YT3112000000198742637541375"
]
Enum.all?(
invalid_ibans,
&assert(
match?({:error, :unsupported_country_code}, Parser.parse(&1)),
"expected #{&1} to match {:error, :unsupported_country_code}"
)
)
end
end

View File

@@ -1,6 +1,6 @@
defmodule IbanExTest do
alias IbanEx.{Country, Iban, Parser}
use ExUnit.Case, async: true
doctest_file "README.md"
doctest IbanEx.Country.AD
doctest IbanEx.Country.AE
@@ -37,6 +37,7 @@ defmodule IbanExTest do
doctest IbanEx.Country.IE
doctest IbanEx.Country.IL
doctest IbanEx.Country.IT
doctest IbanEx.Country.IS
doctest IbanEx.Country.KZ
doctest IbanEx.Country.KW
doctest IbanEx.Country.LB
@@ -70,93 +71,4 @@ defmodule IbanExTest do
doctest IbanEx.Country.VA
doctest IbanEx.Country.VG
doctest IbanEx.Country.XK
@ibans [
"AL47212110090000000235698741",
"AD1200012030200359100100",
"AT611904300234573201",
"AZ21NABZ00000000137010001944",
"BH67BMAG00001299123456",
"BE68539007547034",
"BA391290079401028494",
"BR1800360305000010009795493C1",
"BG80BNBG96611020345678",
"CR05015202001026284066",
"HR1210010051863000160",
"CY17002001280000001200527600",
"CZ6508000000192000145399",
"DK5000400440116243",
"DO28BAGR00000001212453611324",
"EG380019000500000000263180002",
"SV62CENR00000000000000700025",
"EE382200221020145685",
"FO6264600001631634",
"FI2112345600000785",
"FR1420041010050500013M02606",
"GE29NB0000000101904917",
"DE89370400440532013000",
"GI75NWBK000000007099453",
"GR1601101250000000012300695",
"GL8964710001000206",
"GT82TRAJ01020000001210029690",
"HU42117730161111101800000000",
"IS140159260076545510730339",
"IE29AIBK93115212345678",
"IL620108000000099999999",
"IT60X0542811101000000123456",
"JO94CBJO0010000000000131000302",
"KZ86125KZT5004100100",
"XK051212012345678906",
"KW81CBKU0000000000001234560101",
"LV80BANK0000435195001",
"LB62099900000001001901229114",
"LI21088100002324013AA",
"LT121000011101001000",
"LU280019400644750000",
"MK07250120000058984",
"MT84MALT011000012345MTLCAST001S",
"MR1300020001010000123456753",
"MC5811222000010123456789030",
"ME25505000012345678951",
"NL91ABNA0417164300",
"NO9386011117947",
"PK36SCBL0000001123456702",
"PL61109010140000071219812874",
"PT50000201231234567890154",
"QA58DOHB00001234567890ABCDEFG",
"MD24AG000225100013104168",
"RO49AAAA1B31007593840000",
"SM86U0322509800000000270100",
"SA0380000000608010167519",
"RS35260005601001611379",
"SK3112000000198742637541",
"SI56263300012039086",
"ES9121000418450200051332",
"SE4550000000058398257466",
"CH9300762011623852957",
"TL380080012345678910157",
"TR330006100519786457841326",
"UA213223130000026007233566001",
"AE070331234567890123456",
"GB29NWBK60161331926819",
"VA59001123000012345678",
"VG96VPVG0000012345678901"
]
test "parsing valid IBANs from available countries returns {:ok, %IbanEx.Iban{}}" do
assert Enum.all?(@ibans, fn iban ->
iban_country = iban |> String.upcase() |> String.slice(0..1)
case {Country.is_country_code_supported?(iban_country), Parser.parse(iban)} do
{true, {:ok, %Iban{}}} ->
true
{false, {:error, :unsupported_country_code}} ->
true
_ ->
false
end
end)
end
end

View File

@@ -2,7 +2,121 @@ defmodule IbanExValidatorTest do
alias IbanEx.{Validator}
use ExUnit.Case, async: true
test "check IBANs length" do
@ibans [
"AL47212110090000000235698741",
"AD1200012030200359100100",
"AT611904300234573201",
"AZ21NABZ00000000137010001944",
"BH67BMAG00001299123456",
"BE68539007547034",
"BA391290079401028494",
"BR1800360305000010009795493C1",
"BG80BNBG96611020345678",
"CR05015202001026284066",
"HR1210010051863000160",
"CY17002001280000001200527600",
"CZ6508000000192000145399",
"DK5000400440116243",
"DO28BAGR00000001212453611324",
"EG380019000500000000263180002",
"SV62CENR00000000000000700025",
"EE382200221020145685",
"FO6264600001631634",
"FI2112345600000785",
"FR1420041010050500013M02606",
"GE29NB0000000101904917",
"DE89370400440532013000",
"GI75NWBK000000007099453",
"GR1601101250000000012300695",
"GL8964710001000206",
"GT82TRAJ01020000001210029690",
"HU42117730161111101800000000",
"IS140159260076545510730339",
"IE29AIBK93115212345678",
"IL620108000000099999999",
"IT60X0542811101000000123456",
"JO94CBJO0010000000000131000302",
"KZ86125KZT5004100100",
"XK051212012345678906",
"KW81CBKU0000000000001234560101",
"LV80BANK0000435195001",
"LB62099900000001001901229114",
"LI21088100002324013AA",
"LT121000011101001000",
"LU280019400644750000",
"MK07250120000058984",
"MT84MALT011000012345MTLCAST001S",
"MR1300020001010000123456753",
"MC5811222000010123456789030",
"ME25505000012345678951",
"NL91ABNA0417164300",
"NO9386011117947",
"PK36SCBL0000001123456702",
"PL61109010140000071219812874",
"PT50000201231234567890154",
"QA58DOHB00001234567890ABCDEFG",
"MD24AG000225100013104168",
"RO49AAAA1B31007593840000",
"SM86U0322509800000000270100",
"SA0380000000608010167519",
"RS35260005601001611379",
"SK3112000000198742637541",
"SI56263300012039086",
"ES9121000418450200051332",
"SE4550000000058398257466",
"CH9300762011623852957",
"TL380080012345678910157",
"TR330006100519786457841326",
"UA213223130000026007233566001",
"AE070331234567890123456",
"GB29NWBK60161331926819",
"VA59001123000012345678",
"VG96VPVG0000012345678901"
]
test "Check Account number format positive cases" do
Enum.all?(@ibans, &assert(!Validator.iban_violates_account_number_format?(&1), &1))
end
test "Check National check format positive cases" do
Enum.all?(@ibans, &assert(!Validator.iban_violates_national_check_format?(&1), &1))
end
test "Check Branch code format positive cases" do
Enum.all?(@ibans, &assert(!Validator.iban_violates_branch_code_format?(&1), &1))
end
test "Check Bank code format positive cases" do
Enum.all?(@ibans, &assert(!Validator.iban_violates_bank_code_format?(&1), &1))
end
test "Check Account number format negative cases" do
cases = [
# shorter then need
{"AL4721211009000000023568741", true},
{"AD120001203020035900100", true},
{"AZ21NABZ0000000013701000944", true},
# invalid characters (leters) in number
{"AT6119043002A4573201", true},
{"BH67BMAG000012991A3456", true},
{"BE685390075X7034", true},
{"BA391290079401S28494", true},
{"BR180036030500001000979549CC1", true},
{"HR12100100518630001", true},
# shorter then need and has
# invalid characters (leters) in number
{"BR18003603050000100097CC1", true},
{"CR050152020010262806Ї", true},
# FIXME it is invalid IBAN for Bulgaria — need to change a rules function in Country Template module
# {"BG80BNBG9661102034567Ї", true},
]
Enum.all?(cases, fn {iban, result} ->
assert(Validator.iban_violates_account_number_format?(iban) == result, iban)
end)
end
test "Check IBANs length" do
cases = [
{"FG2112345CC6000007", {:error, :unsupported_country_code}},
{"UK2112345CC6000007", {:error, :unsupported_country_code}},