Compare commits

...

5 Commits

11 changed files with 153 additions and 35 deletions

View File

@@ -9,7 +9,7 @@ The package can be installed by adding `ukraine_tax_id` to your list of dependen
```elixir
def deps do
[
{:ukraine_tax_id, "~> 0.1.0"}
{:ukraine_tax_id, "~> 0.1.3"}
]
end
```

BIN
assets/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

View File

@@ -1,5 +1,56 @@
defmodule UkraineTaxidEx do
@itin_length UkraineTaxidEx.Itin.length()
@edrpou_length UkraineTaxidEx.Edrpou.length()
@moduledoc """
Documentation for `UkraineTaxidEx`.
"""
@doc """
`determine/1`: Takes a tax ID string and identifies its type based on length
- Returns `{:ok, UkraineTaxidEx.Itin}` for ITIN numbers
- Returns `{:ok, UkraineTaxidEx.Edrpou}` for EDRPOU numbers
- Returns an error if the length is invalid
"""
def determine(tax_id) when is_binary(tax_id) do
case String.length(tax_id) do
@itin_length -> {:ok, UkraineTaxidEx.Itin}
@edrpou_length -> {:ok, UkraineTaxidEx.Edrpou}
_ -> {:error, "Invalid tax ID length"}
end
end
@doc """
`parse/1`: Determines the tax ID type and parses it using the appropriate parser
- Uses `Module.concat` to dynamically find the correct parser module
- Returns a tuple with status, result, and type information
"""
def parse(tax_id) do
case determine(tax_id) do
{:ok, type} ->
parser = Module.concat(type, "Parser")
{status, result} = parser.parse(tax_id)
{status, result, type}
{:error, error} ->
{:error, error}
end
end
@doc """
`validate/1`: Validates a tax ID by attempting to parse it
- Returns `:ok` if valid
- Returns error tuples with details if invalid
"""
def validate(tax_id) do
case parse(tax_id) do
{:ok, _, _} -> :ok
{:error, error, type} -> {:error, error, type}
{:error, error} -> {:error, error}
end
end
end

View File

@@ -11,8 +11,8 @@ defmodule UkraineTaxidEx.Base do
alias UkraineTaxidEx.{Base, Serialize, Commons}
@parse_module (Module.split(__MODULE__) ++ ["Parser"]) |> Module.safe_concat()
# def parse_module(), do: @parse_module
@parse_module (Module.split(__MODULE__) ++ ["Parser"]) |> Module.concat()
def parse_module(), do: @parse_module
@impl Base
@spec to_map(data :: t()) :: map()

View File

@@ -1,6 +1,12 @@
defmodule UkraineTaxidEx.BaseParser do
@typedoc """
Options for parsing:
- `:normalize?` - if true, pad the string to the right length (8 for EDRPOU, 10 for ITIN)
- `:clean?` - if true, remove non-digit characters
"""
@type options :: [normalize?: boolean, clean?: boolean]
@callback parse(string :: String.t(), options :: options()) :: {:ok, term} | {:error, atom}
@callback parse(code :: String.t(), options :: options()) :: {:ok, term} | {:error, atom}
defmacro __using__(_) do
quote do
@@ -11,14 +17,14 @@ defmodule UkraineTaxidEx.BaseParser do
@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()
@struct_module Module.split(__MODULE__) |> Enum.slice(0..-2//1) |> Module.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(data, options \\ [normalize?: false, clean?: true])
def parse({:ok, string}, options), do: parse(string, options)
def parse({:error, error}, _options), do: {:error, error}
@@ -34,6 +40,7 @@ defmodule UkraineTaxidEx.BaseParser do
end
defp generate({:error, error}), do: {:error, error}
defp generate({:ok, string}), do: generate(string)
end
end
end

View File

@@ -1,12 +1,12 @@
defmodule UkraineTaxidEx.BaseValidator do
@callback validate(String.t()) ::
{:ok, String.t()}
@callback validate(code :: String.t()) ::
{:ok, code :: 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
@callback violates_length?(code :: String.t()) :: boolean
@callback violates_length_too_short?(code :: String.t()) :: boolean
@callback violates_length_too_long?(code :: String.t()) :: boolean
@callback violates_checksum?(code :: String.t()) :: boolean
defmacro __using__(_) do
quote do
@@ -31,28 +31,25 @@ defmodule UkraineTaxidEx.BaseValidator do
@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()
@spec violates_length?(code :: String.t()) :: boolean
def violates_length?(code), do: String.length(code) != 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()
@spec violates_length_too_short?(code :: String.t()) :: boolean
def violates_length_too_short?(code), do: String.length(code) < 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()
@spec violates_length_too_long?(code :: String.t()) :: boolean
def violates_length_too_long?(code), do: String.length(code) > length()
@doc "Check whether a given EDRPOU has correct checksum"
@impl BaseValidator
@spec violates_checksum?(String.t()) :: boolean
def violates_checksum?(string) do
@spec violates_checksum?(code :: String.t()) :: boolean
def violates_checksum?(code) do
{digits, check_digit} =
string
code
|> digits()
|> digits_and_check_digit()

View File

@@ -53,9 +53,7 @@ defmodule UkraineTaxidEx.Edrpou.Parser do
| :length_too_long
| :invalid_checksum}
defp generate({:error, error}), do: {:error, error}
defp generate({:ok, string}) do
defp generate(string) do
digits = digits(string)
%{code: string, check_sum: check_sum(digits), check_digit: check_digit(digits)}

View File

@@ -2,20 +2,20 @@ defmodule UkraineTaxidEx.Itin.Error do
@type error() ::
:invalid_length
| :invalid_checksum
| :length_to_long
| :length_to_short
| :length_too_long
| :length_too_short
@type errors() :: [error()]
@errors [
:invalid_length,
:invalid_checksum,
:length_to_long,
:length_to_short
:length_too_long,
:length_too_short
]
@messages [
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"
length_too_long: "ITIN longer then required length",
length_too_short: "ITIN shorter then required length"
]
@spec message(error()) :: String.t()

View File

@@ -59,7 +59,7 @@ defmodule UkraineTaxidEx.Itin.Parser do
use UkraineTaxidEx.BaseParser
defp generate({:ok, string}) do
defp generate(string) do
digits = digits(string)
%{

View File

@@ -3,9 +3,11 @@ defmodule UkraineTaxidEx.MixProject do
@app :ukraine_tax_id
@module UkraineTaxidEx
@author "Danylo Negrienko"
@authors [@author]
@source_url "https://g.tulz.dev/opensource/ukraine-taxid-ex"
@docs_url "https://hexdocs.pm/#{@app}"
@version "0.1.0"
@version "0.1.3"
def project do
[
@@ -50,6 +52,9 @@ defmodule UkraineTaxidEx.MixProject do
defp docs() do
[
authors: @authors,
cover: "assets/cover.png",
# logo: "assets/logo.svg",
main: "readme",
name: "#{@module}",
source_ref: "v#{@version}",

View File

@@ -1,4 +1,64 @@
defmodule UkraineTaxidExTest do
use ExUnit.Case, async: true
doctest UkraineTaxidEx
describe "determine/1" do
test "correctly identifies ITIN tax ID" do
assert {:ok, UkraineTaxidEx.Itin} = UkraineTaxidEx.determine("1234567890")
end
test "correctly identifies EDRPOU tax ID" do
assert {:ok, UkraineTaxidEx.Edrpou} = UkraineTaxidEx.determine("12345678")
end
test "returns error for invalid length" do
assert {:error, "Invalid tax ID length"} = UkraineTaxidEx.determine("123")
assert {:error, "Invalid tax ID length"} = UkraineTaxidEx.determine("1234")
assert {:error, "Invalid tax ID length"} = UkraineTaxidEx.determine("12345")
assert {:error, "Invalid tax ID length"} = UkraineTaxidEx.determine("123456")
assert {:error, "Invalid tax ID length"} = UkraineTaxidEx.determine("1234567")
assert {:error, "Invalid tax ID length"} = UkraineTaxidEx.determine("123456789")
assert {:error, "Invalid tax ID length"} = UkraineTaxidEx.determine("12345678901")
end
end
describe "parse/1" do
test "successfully parses valid ITIN" do
assert {:ok, %{code: "1759013776"}, UkraineTaxidEx.Itin} =
UkraineTaxidEx.parse("1759013776")
end
test "successfully parses valid EDRPOU" do
assert {:ok, %{code: "30283027"}, UkraineTaxidEx.Edrpou} =
UkraineTaxidEx.parse("30283027")
end
test "returns error for invalid tax ID" do
assert {:error, "Invalid tax ID length"} = UkraineTaxidEx.parse("123")
end
test "returns :length_too_short error for invalid format tax id (valid length but invalid after clean)" do
assert {:error, :length_too_short, UkraineTaxidEx.Itin} =
UkraineTaxidEx.parse("abcdefghij")
assert {:error, :length_too_short, UkraineTaxidEx.Edrpou} =
UkraineTaxidEx.parse("abcdefgh")
end
end
describe "validate/1" do
test "returns :invalid_checksum for ITIN with invalid checksum" do
assert {:error, :invalid_checksum, UkraineTaxidEx.Itin} =
UkraineTaxidEx.validate("1234567890")
end
test "returns :invalid_checksum for EDRPOU with invalid checksum" do
assert {:error, :invalid_checksum, UkraineTaxidEx.Edrpou} =
UkraineTaxidEx.validate("12345679")
end
test "returns error for invalid tax ID" do
assert {:error, "Invalid tax ID length"} = UkraineTaxidEx.validate("123")
end
end
end