lib/qr_nbu/validators/tax_id.ex

defmodule QRNBU.Validators.TaxID do
  @moduledoc """
  Validates Ukrainian Tax Identification Numbers using the ukraine_tax_id library.

  This validator wraps the ukraine_tax_id library (developed by the same author) to provide
  validation for both EDRPOU (legal entities) and ITIN (individuals) tax IDs.

  ## Tax ID Types

  - **EDRPOU**: 8-digit code for legal entities (companies, organizations)
  - **ITIN**: 10-digit code for individuals (Individual Taxpayer Identification Number)

  ## Validation Rules

  - EDRPOU: Exactly 8 digits with checksum validation
  - ITIN: Exactly 10 digits with checksum validation, includes birth date and gender
  - Only numeric characters allowed
  - Checksum must be valid per Ukrainian tax authority algorithms

  ## Examples

      iex> QRNBU.Validators.TaxID.validate("12345678")
      {:ok, "12345678", :edrpou}

      iex> QRNBU.Validators.TaxID.validate("1234567890")
      {:ok, "1234567890", :itin}

      iex> QRNBU.Validators.TaxID.validate("123")
      {:error, "Invalid tax ID length"}

      iex> QRNBU.Validators.TaxID.validate("1234567X")
      {:error, "Tax ID must contain only digits"}

  ## References

  - Ukrainian State Tax Service regulations
  - Uses ukraine_tax_id library: https://hex.pm/packages/ukraine_tax_id
  """

  @doc """
  Validates a Ukrainian tax identification number (EDRPOU or ITIN).

  Automatically detects the type based on length:
  - 8 digits → EDRPOU (legal entity)
  - 10 digits → ITIN (individual)

  Returns `{:ok, normalized_tax_id, type}` where type is `:edrpou` or `:itin`,
  or `{:error, reason}` if validation fails.
  """
  @spec validate(String.t()) :: {:ok, String.t(), :edrpou | :itin} | {:error, String.t()}
  def validate(tax_id) when is_binary(tax_id) do
    normalized = tax_id |> String.trim() |> String.replace(~r/\s/, "")

    # First check basic format
    with :ok <- validate_format(normalized) do
      case UkraineTaxidEx.parse(normalized) do
        {:ok, _parsed, UkraineTaxidEx.Edrpou} ->
          {:ok, normalized, :edrpou}

        {:ok, _parsed, UkraineTaxidEx.Itin} ->
          {:ok, normalized, :itin}

        {:error, error, UkraineTaxidEx.Edrpou} ->
          {:error, format_edrpou_error(error)}

        {:error, error, UkraineTaxidEx.Itin} ->
          {:error, format_itin_error(error)}

        {:error, "Invalid tax ID length"} ->
          {:error, "Tax ID must be 8 digits (EDRPOU) or 10 digits (ITIN)"}

        {:error, error} ->
          {:error, to_string(error)}
      end
    end
  end

  def validate(_), do: {:error, "Tax ID must be a string"}

  @doc """
  Validates specifically an EDRPOU (8-digit legal entity code).

  Returns `{:ok, edrpou}` or `{:error, reason}`.
  """
  @spec validate_edrpou(String.t()) :: {:ok, String.t()} | {:error, String.t()}
  def validate_edrpou(edrpou) when is_binary(edrpou) do
    normalized = edrpou |> String.trim() |> String.replace(~r/\s/, "")

    case String.length(normalized) do
      8 ->
        case UkraineTaxidEx.Edrpou.Parser.parse(normalized) do
          {:ok, _parsed} -> {:ok, normalized}
          {:error, error} -> {:error, format_edrpou_error(error)}
        end

      _ ->
        {:error, "EDRPOU must be exactly 8 digits"}
    end
  end

  @doc """
  Validates specifically an ITIN (10-digit individual taxpayer code).

  Returns `{:ok, itin}` or `{:error, reason}`.
  """
  @spec validate_itin(String.t()) :: {:ok, String.t()} | {:error, String.t()}
  def validate_itin(itin) when is_binary(itin) do
    normalized = itin |> String.trim() |> String.replace(~r/\s/, "")

    case String.length(normalized) do
      10 ->
        case UkraineTaxidEx.Itin.Parser.parse(normalized) do
          {:ok, _parsed} -> {:ok, normalized}
          {:error, error} -> {:error, format_itin_error(error)}
        end

      _ ->
        {:error, "ITIN must be exactly 10 digits"}
    end
  end

  @doc """
  Parses an ITIN to extract birth date and gender information.

  Returns `{:ok, %{birth_date: Date.t(), gender: :male | :female}}` or `{:error, reason}`.

  ## Examples

      iex> QRNBU.Validators.TaxID.parse_itin("3456789012")
      {:ok, %{birth_date: ~D[2003-12-31], gender: :female}}
  """
  @spec parse_itin(String.t()) :: {:ok, map()} | {:error, String.t()}
  def parse_itin(itin) when is_binary(itin) do
    normalized = itin |> String.trim() |> String.replace(~r/\s/, "")

    case UkraineTaxidEx.Itin.Parser.parse(normalized) do
      {:ok, parsed} ->
        gender = if parsed.gender == 0, do: :male, else: :female

        {:ok,
         %{
           birth_date: parsed.birth_date,
           gender: gender,
           number: parsed.number
         }}

      {:error, error} ->
        {:error, format_itin_error(error)}
    end
  end

  @doc """
  Determines the type of tax ID without full validation.

  Returns `:edrpou`, `:itin`, or `:unknown`.
  """
  @spec determine_type(String.t()) :: :edrpou | :itin | :unknown
  def determine_type(tax_id) when is_binary(tax_id) do
    normalized = tax_id |> String.trim() |> String.replace(~r/\s/, "")

    case String.length(normalized) do
      8 -> :edrpou
      10 -> :itin
      _ -> :unknown
    end
  end

  # Basic format validation (digits only)
  @spec validate_format(String.t()) :: :ok | {:error, String.t()}
  defp validate_format(tax_id) do
    if String.match?(tax_id, ~r/^\d+$/) do
      :ok
    else
      {:error, "Tax ID must contain only digits"}
    end
  end

  # Format EDRPOU-specific errors
  defp format_edrpou_error(:invalid_length), do: "EDRPOU must be exactly 8 digits"
  defp format_edrpou_error(:invalid_format), do: "EDRPOU must contain only digits"
  defp format_edrpou_error(:invalid_checksum), do: "Invalid EDRPOU checksum"
  defp format_edrpou_error(error), do: "EDRPOU validation failed: #{error}"

  # Format ITIN-specific errors
  defp format_itin_error(:invalid_length), do: "ITIN must be exactly 10 digits"
  defp format_itin_error(:invalid_format), do: "ITIN must contain only digits"
  defp format_itin_error(:invalid_checksum), do: "Invalid ITIN checksum"
  defp format_itin_error(:invalid_birth_date), do: "ITIN contains invalid birth date"
  defp format_itin_error(error), do: "ITIN validation failed: #{error}"
end