3 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
10 changed files with 353 additions and 70 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

@@ -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

@@ -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

View File

@@ -1,5 +1,5 @@
defmodule IbanEx.Country.IS do
# !TODO Iceland IBAN contains identification number (last 10 digits of account number)
# TODO Iceland IBAN contains identification number (last 10 digits of account number)
@moduledoc """
Island IBAN parsing rules

View File

@@ -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

@@ -98,71 +98,17 @@ test "Check Account number format negative cases" do
{"AZ21NABZ0000000013701000944", true},
# invalid characters (leters) in number
{"AT6119043002A4573201", true},
{"BH67BMAG000012991A3456", true}
# {"BE68539007547034", true},
# {"BA391290079401028494", true},
# {"BR1800360305000010009795493C1", true},
# {"BG80BNBG96611020345678", true},
# {"CR05015202001026284066", true},
# {"HR1210010051863000160", true},
# {"CY17002001280000001200527600", true},
# {"CZ6508000000192000145399", true},
# {"DK5000400440116243", true},
# {"DO28BAGR00000001212453611324", true},
# {"EG380019000500000000263180002", true},
# {"SV62CENR00000000000000700025", true},
# {"EE382200221020145685", true},
# {"FO6264600001631634", true},
# {"FI2112345600000785", true},
# {"FR1420041010050500013M02606", true},
# {"GE29NB0000000101904917", true},
# {"DE89370400440532013000", true},
# {"GI75NWBK000000007099453", true},
# {"GR1601101250000000012300695", true},
# {"GL8964710001000206", true},
# {"GT82TRAJ01020000001210029690", true},
# {"HU42117730161111101800000000", true},
# {"IS140159260076545510730339", true},
# {"IE29AIBK93115212345678", true},
# {"IL620108000000099999999", true},
# {"IT60X0542811101000000123456", true},
# {"JO94CBJO0010000000000131000302", true},
# {"KZ86125KZT5004100100", true},
# {"XK051212012345678906", true},
# {"KW81CBKU0000000000001234560101", true},
# {"LV80BANK0000435195001", true},
# {"LB62099900000001001901229114", true},
# {"LI21088100002324013AA", true},
# {"LT121000011101001000", true},
# {"LU280019400644750000", true},
# {"MK07250120000058984", true},
# {"MT84MALT011000012345MTLCAST001S", true},
# {"MR1300020001010000123456753", true},
# {"MC5811222000010123456789030", true},
# {"ME25505000012345678951", true},
# {"NL91ABNA0417164300", true},
# {"NO9386011117947", true},
# {"PK36SCBL0000001123456702", true},
# {"PL61109010140000071219812874", true},
# {"PT50000201231234567890154", true},
# {"QA58DOHB00001234567890ABCDEFG", true},
# {"MD24AG000225100013104168", true},
# {"RO49AAAA1B31007593840000", true},
# {"SM86U0322509800000000270100", true},
# {"SA0380000000608010167519", true},
# {"RS35260005601001611379", true},
# {"SK3112000000198742637541", true},
# {"SI56263300012039086", true},
# {"ES9121000418450200051332", true},
# {"SE4550000000058398257466", true},
# {"CH9300762011623852957", true},
# {"TL380080012345678910157", true},
# {"TR330006100519786457841326", true},
# {"UA213223130000026007233566001", true},
# {"AE070331234567890123456", true},
# {"GB29NWBK60161331926819", true},
# {"VA59001123000012345678", true},
# {"VG96VPVG0000012345678901", 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} ->