Commons and EDRPOU basic functionality (parser (but complete only), checksum with test coverage)
This commit is contained in:
parent
9db28e7277
commit
a7692124e9
2
.iex.exs
2
.iex.exs
|
@ -21,4 +21,4 @@ IEx.configure(
|
|||
|> IO.chardata_to_string()
|
||||
)
|
||||
|
||||
alias UkraineTaxidEx.{Itin, Edrpou}
|
||||
alias UkraineTaxidEx.{Commons, Edrpou, Itin}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,92 @@
|
|||
defmodule UkraineTaxidEx.CommonsTest do
|
||||
use ExUnit.Case
|
||||
alias UkraineTaxidEx.Commons
|
||||
doctest UkraineTaxidEx.Commons
|
||||
|
||||
describe "digits/2" do
|
||||
test "converts string to list of digits" do
|
||||
assert Commons.digits("123") == [1, 2, 3]
|
||||
assert Commons.digits("456") == [4, 5, 6]
|
||||
assert Commons.digits("789") == [7, 8, 9]
|
||||
end
|
||||
|
||||
test "converts integer to list of digits" do
|
||||
assert Commons.digits(123) == [1, 2, 3]
|
||||
assert Commons.digits(456) == [4, 5, 6]
|
||||
assert Commons.digits(789) == [7, 8, 9]
|
||||
end
|
||||
|
||||
test "pads with zeros when length is specified" do
|
||||
assert Commons.digits("123", 5) == [0, 0, 1, 2, 3]
|
||||
assert Commons.digits(123, 5) == [0, 0, 1, 2, 3]
|
||||
assert Commons.digits("45", 4) == [0, 0, 4, 5]
|
||||
end
|
||||
|
||||
test "handles strings with non-digit characters" do
|
||||
assert Commons.digits("1-2-3") == [1, 2, 3]
|
||||
assert Commons.digits("A1B2C3") == [1, 2, 3]
|
||||
assert Commons.digits("12.34") == [1, 2, 3, 4]
|
||||
end
|
||||
|
||||
test "handles empty string" do
|
||||
assert Commons.digits("") == []
|
||||
assert Commons.digits("", 3) == [0, 0, 0]
|
||||
end
|
||||
end
|
||||
|
||||
describe "check_digit/1" do
|
||||
test "returns the last digit from the list" do
|
||||
assert Commons.check_digit([1, 2, 3, 4, 5]) == 5
|
||||
assert Commons.check_digit([9, 8, 7]) == 7
|
||||
end
|
||||
|
||||
test "returns nil for empty list" do
|
||||
assert Commons.check_digit([]) == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "value_digits/1" do
|
||||
test "returns all digits except the last one" do
|
||||
assert Commons.value_digits([1, 2, 3, 4, 5]) == [1, 2, 3, 4]
|
||||
assert Commons.value_digits([9, 8, 7]) == [9, 8]
|
||||
end
|
||||
|
||||
test "returns empty list for empty input" do
|
||||
assert Commons.value_digits([]) == []
|
||||
end
|
||||
|
||||
test "returns empty list for single digit input" do
|
||||
assert Commons.value_digits([1]) == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "value_and_check_digits/1" do
|
||||
test "returns tuple with value digits and check digit" do
|
||||
assert Commons.value_and_check_digits([1, 2, 3, 4, 5]) == {[1, 2, 3, 4], 5}
|
||||
assert Commons.value_and_check_digits([9, 8, 7]) == {[9, 8], 7}
|
||||
end
|
||||
|
||||
test "handles empty list" do
|
||||
assert Commons.value_and_check_digits([]) == {[], nil}
|
||||
end
|
||||
|
||||
test "handles single digit" do
|
||||
assert Commons.value_and_check_digits([1]) == {[], 1}
|
||||
end
|
||||
end
|
||||
|
||||
describe "ok/1" do
|
||||
test "wraps data in ok tuple" do
|
||||
assert Commons.ok(123) == {:ok, 123}
|
||||
assert Commons.ok([1, 2, 3]) == {:ok, [1, 2, 3]}
|
||||
assert Commons.ok("test") == {:ok, "test"}
|
||||
end
|
||||
end
|
||||
|
||||
describe "error/1" do
|
||||
test "wraps error in error tuple" do
|
||||
assert Commons.error("invalid") == {:error, "invalid"}
|
||||
assert Commons.error(:invalid_format) == {:error, :invalid_format}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,66 @@
|
|||
defmodule UkraineTaxidEx.Edrpou.CheckSumTest do
|
||||
use ExUnit.Case
|
||||
alias UkraineTaxidEx.Edrpou.CheckSum
|
||||
|
||||
describe "check_sum/1" do
|
||||
test "calculates correct checksum for EDRPOU with base weights (< 30M) short EDRPOU with leading zeros" do
|
||||
# Example EDRPOU: 0003212[9] (where 2 is the check digit)
|
||||
digits = [0, 0, 0, 3, 2, 1, 2, 9]
|
||||
assert CheckSum.check_sum(digits) == 9
|
||||
end
|
||||
|
||||
test "calculates correct checksum for EDRPOU with base weights (< 30M)" do
|
||||
# Example EDRPOU: 1436050[6] (where 2 is the check digit)
|
||||
digits = [1, 4, 3, 6, 0, 5, 0, 6]
|
||||
assert CheckSum.check_sum(digits) == 6
|
||||
end
|
||||
|
||||
test "calculates correct checksum for EDRPOU with base weights (> 60M)" do
|
||||
# Example EDRPOU: 6543217[6]
|
||||
digits = [6, 5, 4, 3, 2, 1, 7, 6]
|
||||
assert CheckSum.check_sum(digits) == 6
|
||||
end
|
||||
|
||||
test "calculates correct checksum for EDRPOU with alternative weights (30M-60M)" do
|
||||
# Example EDRPOU: 3145193[2]
|
||||
digits = [3, 1, 4, 5, 1, 9, 3, 2]
|
||||
assert CheckSum.check_sum(digits) == 2
|
||||
end
|
||||
|
||||
test "calculates correct checksum for EDRPOU with alternative weights (30M-60M) when first calculation >= 10" do
|
||||
# Example EDRPOU: 3702668[4]
|
||||
digits = [3, 7, 0, 2, 6, 6, 8, 4]
|
||||
assert CheckSum.check_sum(digits) == 4
|
||||
end
|
||||
|
||||
test "calculates correct checksum for EDRPOU with base weights when first calculation >= 10" do
|
||||
# Example EDRPOU: 2113335[2]
|
||||
digits = [2, 1, 1, 3, 3, 3, 5, 2]
|
||||
assert CheckSum.check_sum(digits) == 2
|
||||
end
|
||||
|
||||
test "handles edge case at 30M boundary" do
|
||||
# Just below 30M
|
||||
digits = [2, 9, 9, 9, 9, 9, 9]
|
||||
result_below = CheckSum.check_sum(digits)
|
||||
|
||||
# Just at 30M
|
||||
digits = [3, 0, 0, 0, 0, 0, 0]
|
||||
result_at = CheckSum.check_sum(digits)
|
||||
|
||||
assert result_below != result_at
|
||||
end
|
||||
|
||||
test "handles edge case at 60M boundary" do
|
||||
# Just below 60M
|
||||
digits = [5, 9, 9, 9, 9, 9, 9]
|
||||
result_below = CheckSum.check_sum(digits)
|
||||
|
||||
# Just at 60M
|
||||
digits = [6, 0, 0, 0, 0, 0, 0]
|
||||
result_at = CheckSum.check_sum(digits)
|
||||
|
||||
assert result_below != result_at
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,26 @@
|
|||
defmodule UkraineTaxidEx.EdrpouTest do
|
||||
use ExUnit.Case
|
||||
# alias UkraineTaxidEx.Edrpou
|
||||
|
||||
# describe "weights/2" do
|
||||
# test "returns base weights by default" do
|
||||
# assert Edrpou.weights() == [1, 2, 3, 4, 5, 6, 7]
|
||||
# end
|
||||
|
||||
# test "returns base weights when not doubled" do
|
||||
# assert Edrpou.weights(:base, false) == [1, 2, 3, 4, 5, 6, 7]
|
||||
# end
|
||||
|
||||
# test "returns alternative weights when not doubled" do
|
||||
# assert Edrpou.weights(:alternative, false) == [7, 1, 2, 3, 4, 5, 6]
|
||||
# end
|
||||
|
||||
# test "returns doubled base weights" do
|
||||
# assert Edrpou.weights(:base, true) == [2, 4, 6, 8, 10, 12, 14]
|
||||
# end
|
||||
|
||||
# test "returns doubled alternative weights" do
|
||||
# assert Edrpou.weights(:alternative, true) == [14, 2, 4, 6, 8, 10, 12]
|
||||
# end
|
||||
# end
|
||||
end
|
|
@ -1,8 +1,4 @@
|
|||
defmodule UkraineTaxidExTest do
|
||||
use ExUnit.Case
|
||||
use ExUnit.Case, async: true
|
||||
doctest UkraineTaxidEx
|
||||
|
||||
test "greets the world" do
|
||||
assert UkraineTaxidEx.hello() == :world
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue