defmodule IbanEx.Validator do @moduledoc false alias IbanEx.{Country, Parser} alias IbanEx.Validator.Replacements 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(), 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}}, {&__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}}, ] @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?, """ @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 @doc """ Make checks in this order step-by-step before first error -> 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?, """ @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()} @spec validate(String.t()) :: {:ok, String.t()} | {:error, atom()} 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} 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 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 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 supported? = iban |> Parser.country_code() |> Country.is_country_code_supported?() !supported? end @doc "Check whether a given IBAN violates the required length." @spec iban_violates_length?(String.t()) :: boolean def iban_violates_length?(iban) do with country_code <- Parser.country_code(iban), country_module when is_atom(country_module) <- Country.country_module(country_code) do size(iban) != country_module.size() else {:error, _error} -> true end end @doc "Check length of IBAN" @spec check_iban_length(String.t()) :: {:error, :length_to_short | :length_to_long} | :ok def check_iban_length(iban) do 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 end end @doc "Check whether a given IBAN violates the country rules" @spec iban_violates_country_rule?(String.t()) :: boolean def iban_violates_country_rule?(iban) do with country_code <- Parser.country_code(iban), bban <- Parser.bban(iban), country_module when is_atom(country_module) <- Country.country_module(country_code), rule <- country_module.rule() do !Regex.match?(rule, bban) else _ -> true end end @doc "Check whether a given IBAN violates the required checksum." @spec iban_violates_checksum?(String.t()) :: boolean def iban_violates_checksum?(iban) do check_sum_base = Parser.bban(iban) <> Parser.country_code(iban) <> "00" replacements = Replacements.replacements() remainder = for(<>, into: "", do: replacements[<>] || <>) |> String.to_integer() |> rem(97) checksum = (98 - remainder) |> Integer.to_string() |> String.pad_leading(2, "0") checksum !== Parser.check_digits(iban) end end