Commons and EDRPOU basic functionality (parser (but complete only), checksum with test coverage)

This commit is contained in:
2024-12-11 03:02:07 -05:00
parent 9db28e7277
commit a7692124e9
16 changed files with 477 additions and 19 deletions

View File

@@ -2,17 +2,4 @@ defmodule UkraineTaxidEx do
@moduledoc """
Documentation for `UkraineTaxidEx`.
"""
@doc """
Hello world.
## Examples
iex> UkraineTaxidEx.hello()
:world
"""
def hello do
:world
end
end

View File

@@ -0,0 +1,27 @@
defmodule UkraineTaxidEx.Base do
@callback to_map(data :: term) :: map()
@callback to_string(data :: term) :: String.t()
@callback length() :: non_neg_integer()
defmacro __using__(_) do
quote do
@behaviour UkraineTaxidEx.Base
alias UkraineTaxidEx.{Base, Serialize, Commons}
@impl Base
@spec length() :: non_neg_integer()
def length(), do: @length
@impl Base
@spec to_map(data :: t()) :: map()
defdelegate to_map(data), to: Serialize
@impl Base
@spec to_string(data :: t()) :: binary()
defdelegate to_string(data), to: Serialize
defoverridable to_string: 1, to_map: 1, length: 0
end
end
end

View File

@@ -0,0 +1,19 @@
defmodule UkraineTaxidEx.BaseParser do
@type options :: [incomplete: boolean]
@callback parse(string :: String.t(), options :: options()) :: {:ok, term} | {:error, atom}
defmacro __using__(_) do
quote do
@behaviour UkraineTaxidEx.BaseParser
alias UkraineTaxidEx.BaseParser
@impl BaseParser
@spec parse(string :: String.t(), options :: BaseParser.options()) ::
{:ok, term} | {:error, atom}
def parse(data, options \\ [incomplete: false])
defoverridable parse: 2, parse: 1
end
end
end

View File

@@ -0,0 +1,57 @@
defmodule UkraineTaxidEx.Commons do
@moduledoc """
Common functions for UkraineTaxidEx.
"""
@typedoc "A one digit of EDRPOU or ITIN it's non-negative integer from 0 to 9"
@type digit :: non_neg_integer() | nil
@typedoc "List of digits of EDRPOU or ITIN"
@type digits :: [non_neg_integer()] | []
@pad "0"
@doc """
Converts a string or integer to a list of digits.
Takes a value and optional length parameter.
When length is provided, pads the result with leading zeros.
Returns list of digits as integers.
## Examples
iex> UkraineTaxidEx.Commons.digits("123")
[1, 2, 3]
iex> UkraineTaxidEx.Commons.digits(123, 5)
[0, 0, 1, 2, 3]
iex> UkraineTaxidEx.Commons.digits("987", 5)
[0, 0, 9, 8, 7]
"""
@spec digits(value :: String.t() | integer, length :: non_neg_integer()) :: digits
def digits(value, length \\ 0)
def digits(value, length) when is_integer(value), do: digits("#{value}", length)
def digits(value, length) when is_binary(value) do
value
|> clean()
|> String.pad_leading(length, @pad)
|> String.graphemes()
|> Enum.map(&String.to_integer/1)
end
@spec check_digit(digits :: digits) :: digit
def check_digit(digits), do: List.last(digits)
@spec value_digits(digits :: digits) :: digits
def value_digits(digits), do: Enum.take(digits, length(digits) - 1)
@spec value_and_check_digits(digits :: digits) :: {digits, digit}
def value_and_check_digits(digits), do: {value_digits(digits), check_digit(digits)}
@spec clean(string :: String.t()) :: String.t()
defp clean(string), do: String.replace(string, ~r/[^\d]/, "")
def ok(data), do: {:ok, data}
def error(error), do: {:error, error}
end

View File

@@ -0,0 +1,18 @@
defmodule UkraineTaxidEx.Edrpou do
@moduledoc """
Documentation for `UkraineTaxidEx.Edrpou`.
The EDRPOU is a unique 8-digit number that identifies legal entities in Ukraine. It is issued to all companies when they are formed and registered in the official business register. The Unified State Register of Enterprises and Organizations of Ukraine (USREOU/EDRPOU) is an automated system that collects, stores, and processes data on legal entities in Ukraine.
"""
@length 8
use UkraineTaxidEx.Base
@type t :: %__MODULE__{
code: String.t(),
check_digit: C.digit(),
check_sum: C.digit()
}
defstruct code: nil, check_digit: nil, check_sum: nil
end

View File

@@ -0,0 +1,68 @@
defmodule UkraineTaxidEx.Edrpou.CheckSum do
alias UkraineTaxidEx.Commons, as: C
import UkraineTaxidEx.Commons, only: [value_digits: 1]
@typedoc """
Coefficients (weights) for digits to calculate EDRPOU check sum may be two types:
base ([1, 2, 3, 4, 5, 6, 7] for EDRPOU < 30M or EDRPOU > 60M)
or alternative ([7, 1, 2, 3, 4, 5, 6] if EDRPOU between 30M and 60M)
"""
@type weights_type :: :base | :alternative
@doc """
Calculate checksum for EDRPOU number.
The checksum for EDRPOU is calculated in several steps:
1. Define the type of weights (base or alternative) as described in `weights_type/1`
2. Multiply each digit by its corresponding weight
3. Sum the products
4. Take mod 11 of the sum
5. If mod 11 is greater or equal than 10, repeat steps 2-4 with doubled weights
"""
@spec check_sum(digits :: C.digits()) :: integer()
def check_sum(digits) do
type =
digits
|> Integer.undigits()
|> weights_type()
value_digits = value_digits(digits)
case calculate_check_sum(value_digits, weights(type, false)) do
s when s >= 10 -> calculate_check_sum(value_digits, weights(type, true))
s -> s
end
end
defguardp is_base_weights(value) when value < 30_000_000 or value > 60_000_000
@spec weights(type :: weights_type, double_added? :: boolean()) :: C.digits()
defp weights(type \\ :base, double_added \\ false)
defp weights(:base, false), do: Enum.to_list(1..7)
defp weights(:alternative, false) do
base = weights()
[List.last(base) | Enum.take(base, length(base) - 1)]
end
defp weights(type, true) do
type
|> weights(false)
|> Enum.map(&(&1 + 2))
end
@spec divider() :: non_neg_integer()
defp divider(), do: 11
@spec weights_type(value :: pos_integer()) :: C.digits()
defp weights_type(value) when is_base_weights(value), do: :base
defp weights_type(_value), do: :alternative
@spec calculate_check_sum(digits :: C.digits(), weights :: C.digits()) :: non_neg_integer()
defp calculate_check_sum(digits, weights) do
digits
|> Enum.zip(weights)
|> Enum.reduce(0, fn {digit, weight}, acc -> digit * weight + acc end)
|> rem(divider())
end
end

View File

@@ -0,0 +1,24 @@
defmodule UkraineTaxidEx.Edrpou.Error do
@type error() ::
:invalid_length
| :invalid_checksum
| :length_to_long
| :length_to_short
@type errors() :: [error()]
@errors [
:invalid_length,
:invalid_checksum,
:length_to_long,
: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"
]
@spec message(error()) :: String.t()
def message(error) when error in @errors, do: @messages[error]
def message(_error), do: "Undefined error"
end

View File

@@ -0,0 +1,23 @@
defmodule UkraineTaxidEx.Edrpou.Parser do
alias UkraineTaxidEx.Edrpou
import UkraineTaxidEx.Edrpou, only: [length: 0]
import UkraineTaxidEx.Edrpou.CheckSum, only: [check_sum: 1]
import UkraineTaxidEx.Commons, only: [check_digit: 1, digits: 2, ok: 1]
use UkraineTaxidEx.BaseParser
def parse(edrpou_string, incomplete: false) do
digits = digits(edrpou_string, length())
%{
code: edrpou_string,
check_sum: check_sum(digits),
check_digit: check_digit(digits)
}
|> create_struct()
|> ok()
end
defp create_struct(map), do: struct(Edrpou, map)
end

View File

@@ -0,0 +1,24 @@
defmodule UkraineTaxidEx.Itin do
@moduledoc """
Documentation for `UkraineTaxidEx.Itin`.
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(),
gender: gender,
check_digit: C.digit(),
check_sum: C.digit()
}
defstruct code: nil, birth_date: nil, gender: nil, check_digit: nil, check_sum: nil
def base_date(), do: @base_date
end

View File

@@ -0,0 +1,24 @@
defmodule UkraineTaxidEx.Itin.Error do
@type error() ::
:invalid_length
| :invalid_checksum
| :length_to_long
| :length_to_short
@type errors() :: [error()]
@errors [
:invalid_length,
:invalid_checksum,
:length_to_long,
: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"
]
@spec message(error()) :: String.t()
def message(error) when error in @errors, do: @messages[error]
def message(_error), do: "Undefined error"
end

View File

@@ -0,0 +1,7 @@
defmodule UkraineTaxidEx.Serialize do
@spec to_string(Edrpou.t() | Itin.t()) :: String.t()
def to_string(data), do: data.code
@spec to_map(Edrpou.t() | Itin.t()) :: map()
def to_map(data), do: Map.from_struct(data)
end