Itin parser and validator added

This commit is contained in:
2024-12-12 21:17:30 -05:00
parent 0edc3c3139
commit 695ffc31c5
14 changed files with 513 additions and 108 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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