diff --git a/lib/ukraine_taxid_ex/base.ex b/lib/ukraine_taxid_ex/base.ex index 5e80364..bd1a48a 100644 --- a/lib/ukraine_taxid_ex/base.ex +++ b/lib/ukraine_taxid_ex/base.ex @@ -1,9 +1,9 @@ defmodule UkraineTaxidEx.Base do - @callback length() :: non_neg_integer() @callback parse(data :: {:ok, String.t()} | String.t(), options :: Keyword.t()) :: {:ok, term} | {:error, atom()} @callback to_map(data :: term) :: map() @callback to_string(data :: term) :: String.t() + @callback length() :: non_neg_integer() defmacro __using__(_) do quote do @@ -11,9 +11,8 @@ defmodule UkraineTaxidEx.Base do alias UkraineTaxidEx.{Base, Serialize, Commons} - @impl Base - @spec length() :: non_neg_integer() - def length(), do: @length + @parse_module (Module.split(__MODULE__) ++ ["Parser"]) |> Module.safe_concat() + # def parse_module(), do: @parse_module @impl Base @spec to_map(data :: t()) :: map() @@ -23,7 +22,19 @@ defmodule UkraineTaxidEx.Base do @spec to_string(data :: t()) :: binary() defdelegate to_string(data), to: Serialize - defoverridable to_string: 1, to_map: 1, length: 0 + @doc """ + Returns the length of the code. + """ + @impl Base + @spec length() :: non_neg_integer() + def length(), do: @length + + @impl Base + @spec parse(data :: {:ok, String.t()} | String.t(), options :: Keyword.t()) :: + {:ok, t()} | {:error, atom()} + defdelegate parse(data, options \\ [normalize?: false, clean?: false]), to: @parse_module + + defoverridable to_string: 1, to_map: 1, length: 0, parse: 2 end end end diff --git a/lib/ukraine_taxid_ex/base_parser.ex b/lib/ukraine_taxid_ex/base_parser.ex index 0a15945..92cad60 100644 --- a/lib/ukraine_taxid_ex/base_parser.ex +++ b/lib/ukraine_taxid_ex/base_parser.ex @@ -7,6 +7,33 @@ defmodule UkraineTaxidEx.BaseParser do @behaviour UkraineTaxidEx.BaseParser alias UkraineTaxidEx.BaseParser + + @type string_or_ok() :: String.t() | {:ok, String.t()} + @type struct_or_error() :: {:ok, term} | {:error, atom()} + + @struct_module Module.split(__MODULE__) |> Enum.slice(0..-2//1) |> Module.safe_concat() + # def struct_module(), do: @struct_module + + defp to_struct(map), do: struct(@struct_module, map) + + @impl BaseParser + @spec parse(data :: string_or_ok, options :: BaseParser.options()) :: struct_or_error() + def parse(data, options \\ [normalize?: false, clean?: false]) + def parse({:ok, string}, options), do: parse(string, options) + def parse({:error, error}, _options), do: {:error, error} + + def parse(string, options) do + length = (Keyword.get(options, :normalize?, false) && length()) || 0 + clean? = Keyword.get(options, :clean?, false) + + string + |> digits(length, clean?) + |> undigits() + |> validate() + |> generate() + end + + defp generate({:error, error}), do: {:error, error} end end end diff --git a/lib/ukraine_taxid_ex/base_validator.ex b/lib/ukraine_taxid_ex/base_validator.ex new file mode 100644 index 0000000..93fe047 --- /dev/null +++ b/lib/ukraine_taxid_ex/base_validator.ex @@ -0,0 +1,65 @@ +defmodule UkraineTaxidEx.BaseValidator do + @callback validate(String.t()) :: + {:ok, String.t()} + | {:error, + :length_too_short | :length_too_long | :invalid_length | :invalid_checksum} + @callback violates_length?(String.t()) :: boolean + @callback violates_length_too_short?(String.t()) :: boolean + @callback violates_length_too_long?(String.t()) :: boolean + @callback violates_checksum?(String.t()) :: boolean + + defmacro __using__(_) do + quote do + alias UkraineTaxidEx.BaseValidator + + @behaviour UkraineTaxidEx.BaseValidator + import UkraineTaxidEx.Commons, only: [digits: 1, digits_and_check_digit: 1, error: 1, ok: 1] + + @impl BaseValidator + @spec validate(String.t()) :: + {:ok, String.t()} + | {:error, + :length_too_short | :length_too_long | :invalid_length | :invalid_checksum} + def validate(string) do + cond do + violates_length_too_short?(string) -> error(:length_too_short) + violates_length_too_long?(string) -> error(:length_too_long) + violates_checksum?(string) -> error(:invalid_checksum) + true -> ok(string) + end + end + + @doc "Check whether a given EDRPOU violates the required length" + @impl BaseValidator + @spec violates_length?(String.t()) :: boolean + def violates_length?(string), + do: String.length(string) != length() + + @doc "Check whether a given EDRPOU too short" + @impl BaseValidator + @spec violates_length_too_short?(String.t()) :: boolean + def violates_length_too_short?(string), + do: String.length(string) < length() + + @doc "Check whether a given EDRPOU too long" + @impl BaseValidator + @spec violates_length_too_long?(String.t()) :: boolean + def violates_length_too_long?(string), + do: String.length(string) > length() + + @doc "Check whether a given EDRPOU has correct checksum" + @impl BaseValidator + @spec violates_checksum?(String.t()) :: boolean + def violates_checksum?(string) do + {digits, check_digit} = + string + |> digits() + |> digits_and_check_digit() + + check_sum = check_sum(digits) + + check_sum != check_digit + end + end + end +end diff --git a/lib/ukraine_taxid_ex/edrpou.ex b/lib/ukraine_taxid_ex/edrpou.ex index 2706c0b..a2ef4a7 100644 --- a/lib/ukraine_taxid_ex/edrpou.ex +++ b/lib/ukraine_taxid_ex/edrpou.ex @@ -6,8 +6,6 @@ defmodule UkraineTaxidEx.Edrpou do """ @length 8 - alias UkraineTaxidEx.Edrpou.Parser - use UkraineTaxidEx.Base @type t :: %__MODULE__{ code: String.t(), @@ -17,8 +15,5 @@ defmodule UkraineTaxidEx.Edrpou do defstruct code: nil, check_digit: nil, check_sum: nil - @impl Base - @spec parse(data :: {:ok, String.t()} | String.t(), options :: Keyword.t()) :: - {:ok, t()} | {:error, atom()} - defdelegate parse(data, options \\ [normalize?: false, clean?: false]), to: Parser + use UkraineTaxidEx.Base end diff --git a/lib/ukraine_taxid_ex/edrpou/parser.ex b/lib/ukraine_taxid_ex/edrpou/parser.ex index d597a5c..544b68b 100644 --- a/lib/ukraine_taxid_ex/edrpou/parser.ex +++ b/lib/ukraine_taxid_ex/edrpou/parser.ex @@ -2,31 +2,8 @@ defmodule UkraineTaxidEx.Edrpou.Parser do @moduledoc """ Parser module for EDRPOU (Unified State Register of Ukrainian Enterprises and Organizations) codes. Handles validation and structure creation for EDRPOU codes with additional options for normalization and cleaning. - """ - alias UkraineTaxidEx.Edrpou - - import UkraineTaxidEx.Edrpou, only: [length: 0] - import UkraineTaxidEx.Edrpou.CheckSum, only: [check_sum: 1] - import UkraineTaxidEx.Edrpou.Validator, only: [validate: 1] - import UkraineTaxidEx.Commons, only: [check_digit: 1, digits: 1, digits: 3, undigits: 1, ok: 1] - - use UkraineTaxidEx.BaseParser - - @type edrpou_string() :: String.t() - @type edrpou_string_or_ok() :: edrpou_string() | {:ok, edrpou_string()} - @type edrpou() :: Edrpou.t() - @type edrpou_or_error() :: - {:ok, Edrpou.t()} - | {:error, - :length_too_short - | :length_too_long - | :invalid_checksum} - - @impl BaseParser - - @doc """ - Parses an EDRPOU code string into a structured format (clean and normalize, validate and decompose). + Parses an EDRPOU code string into a structured format (clean and normalize, validate and decompo). Options: - normalize?: When true, pads string to full EDRPOU length. Defaults to false. - clean?: When true, removes non-digit characters before processing. Defaults to false. @@ -34,6 +11,7 @@ defmodule UkraineTaxidEx.Edrpou.Parser do ## Examples + ```elixir iex> UkraineTaxidEx.Edrpou.Parser.parse("00032112") {:ok, %UkraineTaxidEx.Edrpou{code: "00032112", check_digit: 2, check_sum: 2}} @@ -54,33 +32,34 @@ defmodule UkraineTaxidEx.Edrpou.Parser do iex> UkraineTaxidEx.Edrpou.Parser.parse("123", normalize?: true) {:error, :invalid_checksum} + ``` """ - @spec parse(data :: edrpou_string_or_ok, options :: BaseParser.options()) :: - edrpou_or_error() - def parse(data, options \\ [normalize?: false, clean?: false]) - def parse({:ok, edrpou_string}, options), do: parse(edrpou_string, options) - def parse({:error, error}, _options), do: {:error, error} - def parse(edrpou_string, options) do - length = (Keyword.get(options, :normalize?, false) && length()) || 0 - clean? = Keyword.get(options, :clean?, false) + alias UkraineTaxidEx.Edrpou - edrpou_string - |> digits(length, clean?) - |> undigits() - |> validate() - |> generate_edrpou() - end + import UkraineTaxidEx.Edrpou, only: [length: 0] + import UkraineTaxidEx.Edrpou.CheckSum, only: [check_sum: 1] + import UkraineTaxidEx.Edrpou.Validator, only: [validate: 1] + import UkraineTaxidEx.Commons, only: [check_digit: 1, digits: 1, digits: 3, undigits: 1, ok: 1] - defp generate_edrpou({:error, error}), do: {:error, error} + use UkraineTaxidEx.BaseParser - defp generate_edrpou({:ok, edrpou_string}) do - digits = digits(edrpou_string) + @type edrpou_string() :: String.t() + @type edrpou() :: Edrpou.t() + @type edrpou_or_error() :: + {:ok, Edrpou.t()} + | {:error, + :length_too_short + | :length_too_long + | :invalid_checksum} - %{code: edrpou_string, check_sum: check_sum(digits), check_digit: check_digit(digits)} - |> create_struct() + defp generate({:error, error}), do: {:error, error} + + defp generate({:ok, string}) do + digits = digits(string) + + %{code: string, check_sum: check_sum(digits), check_digit: check_digit(digits)} + |> to_struct() |> ok() end - - defp create_struct(map), do: struct(Edrpou, map) end diff --git a/lib/ukraine_taxid_ex/edrpou/validator.ex b/lib/ukraine_taxid_ex/edrpou/validator.ex index 5394647..9b72655 100644 --- a/lib/ukraine_taxid_ex/edrpou/validator.ex +++ b/lib/ukraine_taxid_ex/edrpou/validator.ex @@ -3,13 +3,7 @@ defmodule UkraineTaxidEx.Edrpou.Validator do Functions for validating EDRPOU number format and checksum. This module provides validation functions to verify if an EDRPOU number meets the standard requirements including length and checksum validation. - """ - import UkraineTaxidEx.Commons, only: [digits: 1, digits_and_check_digit: 1, error: 1, ok: 1] - import UkraineTaxidEx.Edrpou, only: [length: 0] - import UkraineTaxidEx.Edrpou.CheckSum, only: [check_sum: 1] - - @doc """ Validates an EDRPOU number to check if it meets length requirements and has a valid checksum. Returns: @@ -17,44 +11,25 @@ defmodule UkraineTaxidEx.Edrpou.Validator do * `{:error, :length_too_short}` if shorter than required length * `{:error, :length_too_long}` if longer than required length * `{:error, :invalid_checksum}` if checksum is invalid + + ## Examples + + ```elixir + iex> UkraineTaxidEx.Edrpou.Validator.validate("00032112") + {:ok, "00032112"} + + iex> UkraineTaxidEx.Edrpou.Validator.validate("0003211") + {:error, :length_too_short} + + iex> UkraineTaxidEx.Edrpou.Validator.validate("000321122") + {:error, :length_too_long} + + iex> UkraineTaxidEx.Edrpou.Validator.validate("00032113") + {:error, :invalid_checksum} + ``` """ - @spec validate(String.t()) :: - {:ok, String.t()} - | {:error, :length_too_short | :length_too_long | :invalid_length | :invalid_checksum} - def validate(edrpou) do - cond do - violates_length_too_short?(edrpou) -> error(:length_too_short) - violates_length_too_long?(edrpou) -> error(:length_too_long) - violates_checksum?(edrpou) -> error(:invalid_checksum) - true -> ok(edrpou) - end - end + import UkraineTaxidEx.Edrpou, only: [length: 0] + import UkraineTaxidEx.Edrpou.CheckSum, only: [check_sum: 1] - @doc "Check whether a given EDRPOU violates the required length" - @spec violates_length?(String.t()) :: boolean - def violates_length?(edrpou), - do: String.length(edrpou) != length() - - @doc "Check whether a given EDRPOU too short" - @spec violates_length_too_short?(String.t()) :: boolean - def violates_length_too_short?(edrpou), - do: String.length(edrpou) < length() - - @doc "Check whether a given EDRPOU too long" - @spec violates_length_too_long?(String.t()) :: boolean - def violates_length_too_long?(edrpou), - do: String.length(edrpou) > length() - - @doc "Check whether a given EDRPOU has correct checksum" - @spec violates_checksum?(String.t()) :: boolean - def violates_checksum?(edrpou) do - {digits, check_digit} = - edrpou - |> digits() - |> digits_and_check_digit() - - check_sum = check_sum(digits) - - check_sum != check_digit - end + use UkraineTaxidEx.BaseValidator end diff --git a/lib/ukraine_taxid_ex/itin.ex b/lib/ukraine_taxid_ex/itin.ex index 2fc092f..8f1cae7 100644 --- a/lib/ukraine_taxid_ex/itin.ex +++ b/lib/ukraine_taxid_ex/itin.ex @@ -5,20 +5,19 @@ defmodule UkraineTaxidEx.Itin do The ITIN is a unique 10-digit number that identifies individuals in Ukraine. It is issued to all individuals when they are registered in the official tax register. The Individual Taxpayer Identification Number (ITIN) is an automated system that collects, stores, and processes data on individuals in Ukraine. """ - @base_date Date.new!(1899, 12, 31) @length 10 - use UkraineTaxidEx.Base @type gender :: 0 | 1 @type t :: %__MODULE__{ code: String.t(), birth_date: Date.t(), + number: integer(), gender: gender, check_digit: C.digit(), check_sum: C.digit() } - defstruct code: nil, birth_date: nil, gender: nil, check_digit: nil, check_sum: nil + defstruct code: nil, birth_date: nil, number: nil, gender: nil, check_digit: nil, check_sum: nil - def base_date(), do: @base_date + use UkraineTaxidEx.Base end diff --git a/lib/ukraine_taxid_ex/itin/check_sum.ex b/lib/ukraine_taxid_ex/itin/check_sum.ex new file mode 100644 index 0000000..67de985 --- /dev/null +++ b/lib/ukraine_taxid_ex/itin/check_sum.ex @@ -0,0 +1,42 @@ +defmodule UkraineTaxidEx.Itin.CheckSum do + @moduledoc """ + Module for calculating the checksum of Ukrainian Individual Tax Identification Numbers (ITIN). + Provides functions for checksum calculation based on weighted digits and helper functions + for working with ITIN weights and dividers. Uses specified numerical weights to multiply + each digit of the ITIN and validate its authenticity. + """ + + alias UkraineTaxidEx.Commons, as: C + + import UkraineTaxidEx.Commons, only: [value_digits: 1] + + @weights [-1, 5, 7, 9, 4, 6, 10, 5, 7] + + @doc """ + Returns the list of numerical weights used to calculate the ITIN checksum. + Each digit in the ITIN is multiplied by its corresponding weight. + """ + @spec weights() :: C.digits() + def weights(), do: @weights + + @spec divider() :: non_neg_integer() + defp divider(), do: 11 + + @doc """ + Calculate checksum for ITIN number. + The checksum for ITIN is calculated in several steps: + 1. Multiply each digit by its corresponding weight + 2. Sum the products + 3. Take mod 11 of the sum + 4. If mod 11 is greater or equal than 10, repeat steps 2-4 with weights +2 + """ + @spec check_sum(digits :: C.digits(), weights :: C.digits()) :: integer() + def check_sum(digits, weights \\ @weights) do + digits + |> value_digits() + |> Enum.zip(weights) + |> Enum.reduce(0, fn {digit, weight}, acc -> digit * weight + acc end) + |> rem(divider()) + |> rem(10) + end +end diff --git a/lib/ukraine_taxid_ex/itin/error.ex b/lib/ukraine_taxid_ex/itin/error.ex index 0026181..4f5b49c 100644 --- a/lib/ukraine_taxid_ex/itin/error.ex +++ b/lib/ukraine_taxid_ex/itin/error.ex @@ -12,10 +12,10 @@ defmodule UkraineTaxidEx.Itin.Error do :length_to_short ] @messages [ - invalid_length: "EDRPOU violates the required length", - invalid_checksum: "EDRPOU checksum is invalid", - length_to_long: "EDRPOU longer then required length", - length_to_short: "EDRPOU shorter then required length" + invalid_length: "ITIN violates the required length", + invalid_checksum: "ITIN checksum is invalid", + length_to_long: "ITIN longer then required length", + length_to_short: "ITIN shorter then required length" ] @spec message(error()) :: String.t() diff --git a/lib/ukraine_taxid_ex/itin/parser.ex b/lib/ukraine_taxid_ex/itin/parser.ex new file mode 100644 index 0000000..25058c4 --- /dev/null +++ b/lib/ukraine_taxid_ex/itin/parser.ex @@ -0,0 +1,93 @@ +defmodule UkraineTaxidEx.Itin.Parser do + @moduledoc """ + This module provides parsing functionality for Ukrainian Individual Taxpayer Identification Numbers (ITIN). + + ITINs (also known as РНОКПП/ІПН) are unique identifiers assigned to individuals in Ukraine for tax purposes. + The parser validates the number format and extracts meaningful components like the checksum according to + official requirements. + + Key features: + - Validates ITIN format and length + - Calculates and verifies checksum + - Parses components into a structured format + - Handles both raw strings and pre-validated input + + Examples of successful ITIN parsing: + + ```elixir + iex> UkraineTaxidEx.Itin.Parser.parse("2222222222") + {:ok, %UkraineTaxidEx.Itin{code: "2222222222", birth_date: ~D[1960-12-17], number: 2222, gender: 0, check_sum: 2, check_digit: 2}} + + iex> UkraineTaxidEx.Itin.Parser.parse("3333333333") + {:ok, %UkraineTaxidEx.Itin{code: "3333333333", birth_date: ~D[1991-03-05], number: 3333, gender: 1, check_sum: 3, check_digit: 3}} + ``` + + Examples of unsuccessful ITIN parsing: + + ```elixir + iex> UkraineTaxidEx.Itin.Parser.parse("123456") + {:error, :length_too_short} + + iex> UkraineTaxidEx.Itin.Parser.parse("12345678901") + {:error, :length_too_long} + + iex> UkraineTaxidEx.Itin.Parser.parse("1234567890") + {:error, :invalid_checksum} + ``` + """ + alias UkraineTaxidEx.Itin + alias UkraineTaxidEx.Commons, as: C + + import UkraineTaxidEx.Itin, only: [length: 0] + import UkraineTaxidEx.Itin.CheckSum, only: [check_sum: 1] + import UkraineTaxidEx.Itin.Validator, only: [validate: 1] + import UkraineTaxidEx.Commons, only: [check_digit: 1, digits: 1, digits: 3, undigits: 1, ok: 1] + + require Integer + + @type itin_string() :: String.t() + @type itin() :: Itin.t() + @type itin_or_error() :: + {:ok, Itin.t()} + | {:error, + :length_too_short + | :length_too_long + | :invalid_checksum} + + @base_date Date.new!(1899, 12, 31) + def base_date(), do: @base_date + + use UkraineTaxidEx.BaseParser + + defp generate({:ok, string}) do + digits = digits(string) + + %{ + code: string, + birth_date: birth_date(digits), + number: number(digits), + gender: gender(digits), + check_sum: check_sum(digits), + check_digit: check_digit(digits) + } + |> to_struct() + |> ok() + end + + @spec slice(digits :: C.digits(), Range.t()) :: Date.t() + defp slice(digits, range) do + digits + |> Enum.slice(range) + |> undigits() + |> String.to_integer() + end + + @spec birth_date(digits :: C.digits()) :: Date.t() + def birth_date(digits), do: Date.add(base_date(), slice(digits, 0..4)) + + @spec number(digits :: C.digits()) :: integer() + def number(digits), do: slice(digits, 5..8) + + @spec gender(digits :: C.digits()) :: Itin.gender() + def gender(digits), do: (Integer.is_odd(slice(digits, -2..-2//1)) && 1) || 0 +end diff --git a/lib/ukraine_taxid_ex/itin/validator.ex b/lib/ukraine_taxid_ex/itin/validator.ex new file mode 100644 index 0000000..b036a09 --- /dev/null +++ b/lib/ukraine_taxid_ex/itin/validator.ex @@ -0,0 +1,35 @@ +defmodule UkraineTaxidEx.Itin.Validator do + @moduledoc """ + Validator module for Ukrainian Individual Taxpayer Identification Number (ITIN/IPN). + Handles validation of ITIN numbers according to Ukrainian tax authority requirements. + + Validates an ITIN number to check if it meets length requirements and has a valid checksum. + + Returns: + * `{:ok, itin}` if validation successful + * `{:error, :length_too_short}` if shorter than required length + * `{:error, :length_too_long}` if longer than required length + * `{:error, :invalid_checksum}` if checksum is invalid + + Examples: + + ```elixir + iex> UkraineTaxidEx.Itin.Validator.validate("3184710691") + {:ok, "3184710691"} + + iex> UkraineTaxidEx.Itin.Validator.validate("123456") + {:error, :length_too_short} + + iex> UkraineTaxidEx.Itin.Validator.validate("12345678901") + {:error, :length_too_long} + + iex> UkraineTaxidEx.Itin.Validator.validate("3184710692") + {:error, :invalid_checksum} + ``` + """ + + import UkraineTaxidEx.Itin, only: [length: 0] + import UkraineTaxidEx.Itin.CheckSum, only: [check_sum: 1] + + use UkraineTaxidEx.BaseValidator +end diff --git a/test/ukraine_taxid_ex/edrpou_test.exs b/test/ukraine_taxid_ex/edrpou_test.exs index 9abbe55..1ea9b94 100644 --- a/test/ukraine_taxid_ex/edrpou_test.exs +++ b/test/ukraine_taxid_ex/edrpou_test.exs @@ -1,3 +1,5 @@ defmodule UkraineTaxidEx.EdrpouTest do use ExUnit.Case + doctest UkraineTaxidEx.Edrpou.Validator + doctest UkraineTaxidEx.Itin.Validator end diff --git a/test/ukraine_taxid_ex/itin/check_sum_test.exs b/test/ukraine_taxid_ex/itin/check_sum_test.exs new file mode 100644 index 0000000..ff28373 --- /dev/null +++ b/test/ukraine_taxid_ex/itin/check_sum_test.exs @@ -0,0 +1,95 @@ +defmodule UkraineTaxidEx.Itin.CheckSumTest do + use ExUnit.Case, async: true + alias UkraineTaxidEx.Itin.CheckSum + + describe "weights/0" do + test "returns the correct list of weights" do + expected_weights = [-1, 5, 7, 9, 4, 6, 10, 5, 7] + assert CheckSum.weights() == expected_weights + end + + test "returns a list of 9 weights" do + assert length(CheckSum.weights()) == 9 + end + end + + describe "check_sum/2" do + test "calculates correct checksum for valid ITIN (3334513284)" do + # Test case with known ITIN and its checksum + digits = [3, 3, 3, 4, 5, 1, 3, 2, 8, 4] + assert CheckSum.check_sum(digits) == 4 + end + + test "calculates correct checksum for valid ITIN (2038817850)" do + # Test case with known ITIN and its checksum + digits = [2, 0, 3, 8, 8, 1, 7, 8, 5, 0] + assert CheckSum.check_sum(digits) == 0 + end + + test "calculates correct checksum for valid ITIN (3486110380)" do + # Test case with known ITIN and its checksum + digits = [3, 4, 8, 6, 1, 1, 0, 3, 8, 0] + assert CheckSum.check_sum(digits) == 0 + end + + test "calculates correct checksum for valid ITIN (3402109517)" do + # Test case with known ITIN and its checksum + digits = [3, 4, 0, 2, 1, 0, 9, 5, 1, 7] + assert CheckSum.check_sum(digits) == 7 + end + + test "calculates correct checksum for valid ITIN (2598917292)" do + # Test case with known ITIN and its checksum + digits = [2, 5, 9, 8, 9, 1, 7, 2, 9, 2] + assert CheckSum.check_sum(digits) == 2 + end + + test "calculates correct checksum for valid ITIN (3505804094)" do + # Test case with known ITIN and its checksum + digits = [3, 5, 0, 5, 8, 0, 4, 0, 9, 4] + assert CheckSum.check_sum(digits) == 4 + end + + test "calculates correct checksum for valid ITIN (2971306745)" do + # Test case with known ITIN and its checksum + digits = [2, 9, 7, 1, 3, 0, 6, 7, 4, 5] + assert CheckSum.check_sum(digits) == 5 + end + + test "calculates correct checksum for ITIN with invalid check digit (2598917291)" do + # Test case with known ITIN and its checksum + digits = [2, 5, 9, 8, 9, 1, 7, 2, 9, 1] + assert CheckSum.check_sum(digits) == 2 + end + + test "calculates correct checksum for ITIN with invalid check digit (3505804099)" do + # Test case with known ITIN and its checksum + digits = [3, 5, 0, 5, 8, 0, 4, 0, 9, 9] + assert CheckSum.check_sum(digits) == 4 + end + + test "calculates correct checksum for ITIN with invalid check digit (2971306747)" do + # Test case with known ITIN and its checksum + digits = [2, 9, 7, 1, 3, 0, 6, 7, 4, 7] + assert CheckSum.check_sum(digits) == 5 + end + + test "calculates checksum with custom weights" do + digits = [1, 2, 3] + weights = [1, 2, 3] + assert CheckSum.check_sum(digits, weights) == 5 + end + + test "handles zero digits" do + digits = [0, 0, 0, 0, 0, 0, 0, 0, 0] + assert CheckSum.check_sum(digits) == 0 + end + + test "handles single digit input" do + digits = [5] + weights = [2] + # (5*2) = 10 % 11 % 10 = 0 + assert CheckSum.check_sum(digits, weights) == 0 + end + end +end diff --git a/test/ukraine_taxid_ex/itin/parser_test.exs b/test/ukraine_taxid_ex/itin/parser_test.exs new file mode 100644 index 0000000..4d82f4d --- /dev/null +++ b/test/ukraine_taxid_ex/itin/parser_test.exs @@ -0,0 +1,87 @@ +defmodule UkraineTaxidEx.Itin.ParserTest do + use ExUnit.Case, async: true + alias UkraineTaxidEx.Itin + alias UkraineTaxidEx.Itin.Parser + + describe "parse/1" do + test "successfully parses valid ITIN" do + assert {:ok, %Itin{} = itin} = Parser.parse("2847211760") + assert itin.code == "2847211760" + assert itin.birth_date == ~D[1977-12-14] + assert itin.number == 1176 + assert itin.gender == 0 + assert itin.check_sum == 0 + assert itin.check_digit == 0 + + assert {:ok, %Itin{} = itin} = Parser.parse("3176422893") + assert itin.code == "3176422893" + assert itin.birth_date == ~D[1986-12-19] + assert itin.number == 2289 + assert itin.gender == 1 + assert itin.check_sum == 3 + assert itin.check_digit == 3 + end + + test "returns error for string shorter than required length" do + assert {:error, :length_too_short} = Parser.parse("123456") + end + + test "returns error for string longer than required length" do + assert {:error, :length_too_long} = Parser.parse("12345678901") + end + + test "returns error for invalid checksum" do + assert {:error, :invalid_checksum} = Parser.parse("1234567890") + end + end + + describe "birth_date/1" do + test "calculates correct birth date from digits" do + digits = [3, 7, 5, 8, 0, 0, 5, 3, 6, 4] + assert Parser.birth_date(digits) == ~D[2002-11-21] + + digits = [3, 0, 4, 3, 2, 1, 6, 8, 2, 4] + assert Parser.birth_date(digits) == ~D[1983-04-27] + + digits = [2, 8, 8, 1, 2, 2, 0, 4, 5, 2] + assert Parser.birth_date(digits) == ~D[1978-11-19] + end + end + + describe "number/1" do + test "extracts correct number from digits" do + digits = [2, 8, 8, 1, 2, 2, 0, 4, 5, 2] + assert Parser.number(digits) == 2045 + + digits = [3, 5, 8, 9, 4, 1, 2, 3, 7, 1] + assert Parser.number(digits) == 1237 + + digits = [3, 2, 1, 5, 6, 0, 5, 0, 5, 3] + assert Parser.number(digits) == 505 + end + end + + describe "gender/1" do + test "determines correct gender from digits" do + # Even number in the second-to-last position (gender 1) + digits = [2, 5, 8, 5, 1, 1, 9, 1, 1, 5] + assert Parser.gender(digits) == 1 + + digits = [2, 6, 0, 3, 8, 2, 1, 5, 3, 1] + assert Parser.gender(digits) == 1 + + # Odd number in the second-to-last position (gender 0) + digits = [2, 2, 0, 7, 3, 1, 5, 4, 2, 8] + assert Parser.gender(digits) == 0 + + digits = [3, 1, 2, 6, 7, 0, 7, 0, 2, 6] + assert Parser.gender(digits) == 0 + end + end + + describe "base_date/0" do + test "returns the correct base date" do + assert Parser.base_date() == ~D[1899-12-31] + end + end +end