Commons and EDRPOU basic functionality (parser (but complete only), checksum with test coverage)
This commit is contained in:
27
lib/ukraine_taxid_ex/base.ex
Normal file
27
lib/ukraine_taxid_ex/base.ex
Normal 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
|
||||
19
lib/ukraine_taxid_ex/base_parser.ex
Normal file
19
lib/ukraine_taxid_ex/base_parser.ex
Normal 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
|
||||
57
lib/ukraine_taxid_ex/commons.ex
Normal file
57
lib/ukraine_taxid_ex/commons.ex
Normal 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
|
||||
18
lib/ukraine_taxid_ex/edrpou.ex
Normal file
18
lib/ukraine_taxid_ex/edrpou.ex
Normal 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
|
||||
68
lib/ukraine_taxid_ex/edrpou/check_sum.ex
Normal file
68
lib/ukraine_taxid_ex/edrpou/check_sum.ex
Normal 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
|
||||
24
lib/ukraine_taxid_ex/edrpou/error.ex
Normal file
24
lib/ukraine_taxid_ex/edrpou/error.ex
Normal 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
|
||||
23
lib/ukraine_taxid_ex/edrpou/parser.ex
Normal file
23
lib/ukraine_taxid_ex/edrpou/parser.ex
Normal 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
|
||||
24
lib/ukraine_taxid_ex/itin.ex
Normal file
24
lib/ukraine_taxid_ex/itin.ex
Normal 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
|
||||
24
lib/ukraine_taxid_ex/itin/error.ex
Normal file
24
lib/ukraine_taxid_ex/itin/error.ex
Normal 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
|
||||
7
lib/ukraine_taxid_ex/serialize.ex
Normal file
7
lib/ukraine_taxid_ex/serialize.ex
Normal 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
|
||||
Reference in New Issue
Block a user