iban-ex/lib/iban_ex/validator/validator.ex

203 lines
7.4 KiB
Elixir
Raw Normal View History

2024-03-05 11:02:58 +00:00
defmodule IbanEx.Validator do
@moduledoc false
2024-03-05 11:02:58 +00:00
alias IbanEx.{Country, Parser}
alias IbanEx.Validator.Replacements
import IbanEx.Commons, only: [normalize: 1, normalize_and_slice: 2]
2024-03-05 11:02:58 +00:00
2024-05-10 21:56:29 +00:00
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
2024-05-10 21:56:29 +00:00
defp error_accumulator(acc, _), do: acc
defp violation_functions(),
do: [
{&__MODULE__.iban_violates_format?/1, {:error, :invalid_format}},
{&__MODULE__.iban_unsupported_country?/1, {:error, :unsupported_country_code}},
{&__MODULE__.iban_violates_length?/1, {:error, :invalid_length}},
2024-05-14 23:15:55 +00:00
{&__MODULE__.iban_violates_country_rule?/1, {:error, :invalid_format_for_country}},
{&__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}},
2024-05-10 21:56:29 +00:00
]
@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_bank_code_format?,
iban_violates_account_number_format?
iban_violates_branch_code_format?,
iban_violates_national_check_format?,
iban_violates_checksum?,
2024-05-10 21:56:29 +00:00
"""
@spec violations(String.t()) :: [] | [atom()]
def violations(iban) do
violation_functions()
|> Enum.reduce([], fn {fun, value}, acc -> error_accumulator(acc, !fun.(iban) or value) end)
|> Enum.reverse()
end
2024-03-05 11:02:58 +00:00
2024-05-10 21:56:29 +00:00
@doc """
Make checks in this order step-by-step before first error ->
2024-03-05 11:02:58 +00:00
2024-05-10 21:56:29 +00:00
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?,
2024-05-10 21:56:29 +00:00
"""
2024-05-14 23:15:55 +00:00
@type iban() :: binary()
@type iban_or_error() ::
{:ok, iban()}
| {:invalid_checksum, binary()}
| {: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()}
2024-05-14 23:15:55 +00:00
@spec validate(String.t()) :: {:ok, String.t()} | {:error, atom()}
2024-05-10 21:56:29 +00:00
def validate(iban) do
cond do
iban_violates_format?(iban) -> {:error, :invalid_format}
iban_unsupported_country?(iban) -> {:error, :unsupported_country_code}
iban_violates_length?(iban) -> {:error, :invalid_length}
2024-05-14 23:15:55 +00:00
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}
2024-05-10 21:56:29 +00:00
iban_violates_checksum?(iban) -> {:error, :invalid_checksum}
true -> {:ok, normalize(iban)}
2024-03-05 11:02:58 +00:00
end
end
@spec size(String.t()) :: non_neg_integer()
defp size(iban) do
iban
|> normalize()
|> String.length()
end
# - Check whether a given IBAN violates the required format.
@spec iban_violates_format?(String.t()) :: boolean
2024-05-10 21:56:29 +00:00
def iban_violates_format?(iban),
2024-03-05 11:02:58 +00:00
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
2024-03-05 11:02:58 +00:00
# - Check whether a given IBAN violates the supported countries.
@spec iban_unsupported_country?(String.t()) :: boolean
2024-05-10 21:56:29 +00:00
def iban_unsupported_country?(iban) do
2024-03-05 11:02:58 +00:00
supported? =
iban
|> Parser.country_code()
|> Country.is_country_code_supported?()
!supported?
end
2024-05-10 21:56:29 +00:00
@doc "Check whether a given IBAN violates the required length."
2024-03-05 11:02:58 +00:00
@spec iban_violates_length?(String.t()) :: boolean
2024-05-10 21:56:29 +00:00
def iban_violates_length?(iban) do
2024-03-05 11:02:58 +00:00
with country_code <- Parser.country_code(iban),
2024-05-10 21:56:29 +00:00
country_module when is_atom(country_module) <- Country.country_module(country_code) do
2024-03-05 11:02:58 +00:00
size(iban) != country_module.size()
else
2024-05-10 21:56:29 +00:00
{:error, _error} -> true
end
end
@doc "Check length of IBAN"
2024-05-14 23:15:55 +00:00
@spec check_iban_length(String.t()) :: {:error, :length_to_short | :length_to_long} | :ok
2024-05-10 21:56:29 +00:00
def check_iban_length(iban) do
2024-05-14 23:15:55 +00:00
case iban_unsupported_country?(iban) do
true ->
{:error, :unsupported_country_code}
false ->
country_module =
iban
|> Parser.country_code()
|> Country.country_module()
case country_module.size() - size(iban) do
diff when diff > 0 -> {:error, :length_to_short}
diff when diff < 0 -> {:error, :length_to_long}
0 -> :ok
end
2024-03-05 11:02:58 +00:00
end
end
2024-05-10 21:56:29 +00:00
@doc "Check whether a given IBAN violates the country rules"
2024-03-05 11:02:58 +00:00
@spec iban_violates_country_rule?(String.t()) :: boolean
2024-05-10 21:56:29 +00:00
def iban_violates_country_rule?(iban) do
2024-03-05 11:02:58 +00:00
with country_code <- Parser.country_code(iban),
bban <- Parser.bban(iban),
2024-05-10 21:56:29 +00:00
country_module when is_atom(country_module) <- Country.country_module(country_code),
2024-03-05 11:02:58 +00:00
rule <- country_module.rule() do
!Regex.match?(rule, bban)
else
2024-05-15 03:55:32 +00:00
_ -> true
2024-03-05 11:02:58 +00:00
end
end
2024-05-10 21:56:29 +00:00
@doc "Check whether a given IBAN violates the required checksum."
2024-03-05 11:02:58 +00:00
@spec iban_violates_checksum?(String.t()) :: boolean
2024-05-10 21:56:29 +00:00
def iban_violates_checksum?(iban) do
2024-03-05 11:02:58 +00:00
check_sum_base = Parser.bban(iban) <> Parser.country_code(iban) <> "00"
replacements = Replacements.replacements()
remainder =
for(<<c <- check_sum_base>>, into: "", do: replacements[<<c>>] || <<c>>)
|> String.to_integer()
|> rem(97)
checksum =
(98 - remainder)
|> Integer.to_string()
|> String.pad_leading(2, "0")
checksum !== Parser.check_digits(iban)
end
end