Compare commits
8 Commits
0.1.7.rc2
...
7bce4f8051
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bce4f8051 | |||
| 0ed739b444 | |||
| 83ddceec00 | |||
| e847e2c473 | |||
| a660250af1 | |||
| 709f6c50b5 | |||
| 5cfc3f5fa2 | |||
| ce90960649 |
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Cicada: Enable git function tracking for Elixir
|
||||||
|
*.ex diff=elixir
|
||||||
|
*.exs diff=elixir
|
||||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -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.
|
# The directory Mix will write compiled artifacts to.
|
||||||
/_build/
|
/_build/
|
||||||
|
|
||||||
|
|||||||
232
Agents.md
Normal file
232
Agents.md
Normal 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>
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ The package can be installed by adding `iban_ex` to your list of dependencies in
|
|||||||
```elixir
|
```elixir
|
||||||
def deps do
|
def deps do
|
||||||
[
|
[
|
||||||
{:iban_ex, "~> 0.1.6"}
|
{:iban_ex, "~> 0.1.8"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|||||||
86
docs/international_wide_ibans/get_iban_registry.py
Normal file
86
docs/international_wide_ibans/get_iban_registry.py
Normal 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)
|
||||||
BIN
docs/international_wide_ibans/iban-registry-100.pdf
Normal file
BIN
docs/international_wide_ibans/iban-registry-100.pdf
Normal file
Binary file not shown.
@@ -41,6 +41,7 @@ defmodule IbanEx.Country do
|
|||||||
"IE" => IbanEx.Country.IE,
|
"IE" => IbanEx.Country.IE,
|
||||||
"IL" => IbanEx.Country.IL,
|
"IL" => IbanEx.Country.IL,
|
||||||
"IT" => IbanEx.Country.IT,
|
"IT" => IbanEx.Country.IT,
|
||||||
|
"IS" => IbanEx.Country.IS,
|
||||||
"JO" => IbanEx.Country.JO,
|
"JO" => IbanEx.Country.JO,
|
||||||
"KZ" => IbanEx.Country.KZ,
|
"KZ" => IbanEx.Country.KZ,
|
||||||
"KW" => IbanEx.Country.KW,
|
"KW" => IbanEx.Country.KW,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
defmodule IbanEx.Country.BG do
|
defmodule IbanEx.Country.BG do
|
||||||
|
# TODO Bulgaria IBAN contains account type (first 2 digits of account number)
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Bulgaria IBAN parsing rules
|
Bulgaria IBAN parsing rules
|
||||||
|
|
||||||
|
|||||||
45
lib/iban_ex/country/is.ex
Normal file
45
lib/iban_ex/country/is.ex
Normal 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
|
||||||
@@ -9,7 +9,10 @@ defmodule IbanEx.Country.Template do
|
|||||||
|
|
||||||
@callback size() :: size()
|
@callback size() :: size()
|
||||||
@callback rule() :: rule()
|
@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(), joiner()) :: String.t()
|
||||||
@callback to_string(Iban.t()) :: String.t()
|
@callback to_string(Iban.t()) :: String.t()
|
||||||
|
|
||||||
@@ -47,26 +50,50 @@ def size(), do: @size
|
|||||||
@spec rule() :: Regex.t()
|
@spec rule() :: Regex.t()
|
||||||
def rule(), do: @rule
|
def rule(), do: @rule
|
||||||
|
|
||||||
@doc """
|
|
||||||
Return Regex without trailing “$” for parsing incomplete BBAN (part of IBAN string) (for partial suggestions)
|
|
||||||
"""
|
|
||||||
@impl IbanEx.Country.Template
|
@impl IbanEx.Country.Template
|
||||||
@spec incomplete_rule() :: Regex.t()
|
@spec bban_size() :: integer()
|
||||||
def incomplete_rule() do
|
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 =
|
source =
|
||||||
@rule
|
@rule
|
||||||
|> Regex.source()
|
|> Regex.source()
|
||||||
|> String.slice(0..-2//1)
|
|
||||||
|> String.replace("{", "{0,")
|
|
||||||
|
|
||||||
opts =
|
{list, bban_length} =
|
||||||
@rule
|
Regex.scan(scanner, source)
|
||||||
|> Regex.opts()
|
|> 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
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ defmodule IbanEx.Error do
|
|||||||
| :can_not_parse_map
|
| :can_not_parse_map
|
||||||
| :length_to_long
|
| :length_to_long
|
||||||
| :length_to_short
|
| :length_to_short
|
||||||
|
| :invalid_bank_code
|
||||||
|
| :invalid_account_number
|
||||||
|
| :invalid_branch_code
|
||||||
|
| :invalid_national_check
|
||||||
| atom()
|
| atom()
|
||||||
@type errors() :: [error()]
|
@type errors() :: [error()]
|
||||||
@errors [
|
@errors [
|
||||||
@@ -18,7 +22,11 @@ defmodule IbanEx.Error do
|
|||||||
:invalid_checksum,
|
:invalid_checksum,
|
||||||
:can_not_parse_map,
|
:can_not_parse_map,
|
||||||
:length_to_long,
|
:length_to_long,
|
||||||
:length_to_short
|
:length_to_short,
|
||||||
|
:invalid_bank_code,
|
||||||
|
:invalid_account_number,
|
||||||
|
:invalid_branch_code,
|
||||||
|
:invalid_national_check
|
||||||
]
|
]
|
||||||
|
|
||||||
@messages [
|
@messages [
|
||||||
@@ -28,7 +36,11 @@ defmodule IbanEx.Error do
|
|||||||
invalid_checksum: "IBAN's checksum is invalid",
|
invalid_checksum: "IBAN's checksum is invalid",
|
||||||
can_not_parse_map: "Can't parse map to IBAN struct",
|
can_not_parse_map: "Can't parse map to IBAN struct",
|
||||||
length_to_long: "IBAN longer then required length",
|
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()
|
@spec message(error()) :: String.t()
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ defmodule IbanEx.Parser do
|
|||||||
@spec parse({:ok, binary()}) :: iban_or_error()
|
@spec parse({:ok, binary()}) :: iban_or_error()
|
||||||
def parse({:ok, iban_string}), do: parse(iban_string)
|
def parse({:ok, iban_string}), do: parse(iban_string)
|
||||||
|
|
||||||
@spec parse(binary()) :: iban_or_error()
|
def parse(iban_string, options \\ [incomplete: false])
|
||||||
def parse(iban_string) do
|
|
||||||
|
def parse(iban_string, incomplete: false) do
|
||||||
case Validator.validate(iban_string) do
|
case Validator.validate(iban_string) do
|
||||||
{:ok, valid_iban} ->
|
{:ok, valid_iban} ->
|
||||||
iban_map = %{
|
iban_map = %{
|
||||||
@@ -41,20 +42,59 @@ def parse(iban_string) do
|
|||||||
end
|
end
|
||||||
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()
|
@spec parse_bban(binary(), <<_::16>>) :: map()
|
||||||
def parse_bban(bban_string, country_code, options \\ [incomplete: false])
|
def parse_bban(bban_string, country_code, options \\ [incomplete: false])
|
||||||
|
|
||||||
def parse_bban(bban_string, country_code, incomplete: true) do
|
def parse_bban(bban_string, country_code, incomplete: true) do
|
||||||
Country.country_module(country_code).incomplete_rule()
|
case Country.is_country_code_supported?(country_code) do
|
||||||
|> parse_bban_by_regex(bban_string)
|
true ->
|
||||||
|
country_code
|
||||||
|
|> Country.country_module()
|
||||||
|
|> parse_bban_by_country_rules(bban_string)
|
||||||
|
false ->
|
||||||
|
%{}
|
||||||
|
end
|
||||||
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()
|
Country.country_module(country_code).rule()
|
||||||
|> parse_bban_by_regex(bban_string)
|
|> 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
|
end
|
||||||
|
|
||||||
defp parse_bban_by_regex(_regex, nil), do: %{}
|
defp parse_bban_by_regex(_regex, nil), do: %{}
|
||||||
|
|
||||||
defp parse_bban_by_regex(regex, bban_string) do
|
defp parse_bban_by_regex(regex, bban_string) do
|
||||||
case Regex.named_captures(regex, bban_string) do
|
case Regex.named_captures(regex, bban_string) do
|
||||||
map when is_map(map) ->
|
map when is_map(map) ->
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ defmodule IbanEx.Validator do
|
|||||||
|
|
||||||
alias IbanEx.{Country, Parser}
|
alias IbanEx.{Country, Parser}
|
||||||
alias IbanEx.Validator.Replacements
|
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_message)
|
||||||
defp error_accumulator(acc, {:error, error}), do: [error | acc]
|
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 error_accumulator(acc, _), do: acc
|
||||||
|
|
||||||
defp violation_functions(),
|
defp violation_functions(),
|
||||||
@@ -15,12 +16,25 @@ defp violation_functions(),
|
|||||||
{&__MODULE__.iban_unsupported_country?/1, {:error, :unsupported_country_code}},
|
{&__MODULE__.iban_unsupported_country?/1, {:error, :unsupported_country_code}},
|
||||||
{&__MODULE__.iban_violates_length?/1, {:error, :invalid_length}},
|
{&__MODULE__.iban_violates_length?/1, {:error, :invalid_length}},
|
||||||
{&__MODULE__.iban_violates_country_rule?/1, {:error, :invalid_format_for_country}},
|
{&__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 """
|
@doc """
|
||||||
Accumulate check results in the list of errors
|
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()]
|
@spec violations(String.t()) :: [] | [atom()]
|
||||||
def violations(iban) do
|
def violations(iban) do
|
||||||
@@ -36,8 +50,11 @@ def violations(iban) do
|
|||||||
iban_unsupported_country?,
|
iban_unsupported_country?,
|
||||||
iban_violates_length?,
|
iban_violates_length?,
|
||||||
iban_violates_country_rule?,
|
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() :: binary()
|
||||||
@type iban_or_error() ::
|
@type iban_or_error() ::
|
||||||
@@ -46,6 +63,10 @@ def violations(iban) do
|
|||||||
| {:invalid_format, binary()}
|
| {:invalid_format, binary()}
|
||||||
| {:invalid_length, binary()}
|
| {:invalid_length, binary()}
|
||||||
| {:unsupported_country_code, 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()}
|
@spec validate(String.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||||
|
|
||||||
def validate(iban) do
|
def validate(iban) do
|
||||||
@@ -54,6 +75,10 @@ def validate(iban) do
|
|||||||
iban_unsupported_country?(iban) -> {:error, :unsupported_country_code}
|
iban_unsupported_country?(iban) -> {:error, :unsupported_country_code}
|
||||||
iban_violates_length?(iban) -> {:error, :invalid_length}
|
iban_violates_length?(iban) -> {:error, :invalid_length}
|
||||||
iban_violates_country_rule?(iban) -> {:error, :invalid_format_for_country}
|
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}
|
iban_violates_checksum?(iban) -> {:error, :invalid_checksum}
|
||||||
true -> {:ok, normalize(iban)}
|
true -> {:ok, normalize(iban)}
|
||||||
end
|
end
|
||||||
@@ -71,6 +96,34 @@ defp size(iban) do
|
|||||||
def iban_violates_format?(iban),
|
def iban_violates_format?(iban),
|
||||||
do: Regex.match?(~r/[^A-Z0-9]/i, normalize(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.
|
# - Check whether a given IBAN violates the supported countries.
|
||||||
@spec iban_unsupported_country?(String.t()) :: boolean
|
@spec iban_unsupported_country?(String.t()) :: boolean
|
||||||
def iban_unsupported_country?(iban) do
|
def iban_unsupported_country?(iban) do
|
||||||
@@ -123,7 +176,7 @@ def iban_violates_country_rule?(iban) do
|
|||||||
rule <- country_module.rule() do
|
rule <- country_module.rule() do
|
||||||
!Regex.match?(rule, bban)
|
!Regex.match?(rule, bban)
|
||||||
else
|
else
|
||||||
{:error, _error} -> true
|
_ -> true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
7
mix.exs
7
mix.exs
@@ -2,7 +2,7 @@ defmodule IbanEx.MixProject do
|
|||||||
use Mix.Project
|
use Mix.Project
|
||||||
|
|
||||||
@source_url "https://g.tulz.dev/opensource/iban-ex"
|
@source_url "https://g.tulz.dev/opensource/iban-ex"
|
||||||
@version "0.1.6"
|
@version "0.1.8"
|
||||||
|
|
||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
@@ -69,10 +69,7 @@ defp deps do
|
|||||||
{:sobelow, ">= 0.0.0", only: ~w(dev test)a, runtime: false},
|
{:sobelow, ">= 0.0.0", only: ~w(dev test)a, runtime: false},
|
||||||
{:mix_audit, ">= 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},
|
{:observer_cli, "~> 1.7.4", only: :dev, runtime: false},
|
||||||
{:elixir_sense, github: "elixir-lsp/elixir_sense", only: ~w(dev)a}
|
{:elixir_sense, "~> 1.0.0", only: :dev}
|
||||||
|
|
||||||
# {:dep_from_hexpm, "~> 0.3.0"},
|
|
||||||
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
172
test/iban_ex_parser_test.exs
Normal file
172
test/iban_ex_parser_test.exs
Normal 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
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
defmodule IbanExTest do
|
defmodule IbanExTest do
|
||||||
alias IbanEx.{Country, Iban, Parser}
|
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
doctest_file "README.md"
|
doctest_file "README.md"
|
||||||
doctest IbanEx.Country.AD
|
doctest IbanEx.Country.AD
|
||||||
doctest IbanEx.Country.AE
|
doctest IbanEx.Country.AE
|
||||||
@@ -37,6 +37,7 @@ defmodule IbanExTest do
|
|||||||
doctest IbanEx.Country.IE
|
doctest IbanEx.Country.IE
|
||||||
doctest IbanEx.Country.IL
|
doctest IbanEx.Country.IL
|
||||||
doctest IbanEx.Country.IT
|
doctest IbanEx.Country.IT
|
||||||
|
doctest IbanEx.Country.IS
|
||||||
doctest IbanEx.Country.KZ
|
doctest IbanEx.Country.KZ
|
||||||
doctest IbanEx.Country.KW
|
doctest IbanEx.Country.KW
|
||||||
doctest IbanEx.Country.LB
|
doctest IbanEx.Country.LB
|
||||||
@@ -70,93 +71,4 @@ defmodule IbanExTest do
|
|||||||
doctest IbanEx.Country.VA
|
doctest IbanEx.Country.VA
|
||||||
doctest IbanEx.Country.VG
|
doctest IbanEx.Country.VG
|
||||||
doctest IbanEx.Country.XK
|
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
|
end
|
||||||
|
|||||||
@@ -2,7 +2,121 @@ defmodule IbanExValidatorTest do
|
|||||||
alias IbanEx.{Validator}
|
alias IbanEx.{Validator}
|
||||||
use ExUnit.Case, async: true
|
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 = [
|
cases = [
|
||||||
{"FG2112345CC6000007", {:error, :unsupported_country_code}},
|
{"FG2112345CC6000007", {:error, :unsupported_country_code}},
|
||||||
{"UK2112345CC6000007", {:error, :unsupported_country_code}},
|
{"UK2112345CC6000007", {:error, :unsupported_country_code}},
|
||||||
|
|||||||
Reference in New Issue
Block a user