lib/qr_nbu/constants.ex

defmodule QRNBU.Constants do
  @moduledoc """
  NBU QR Code specification constants.

  This module centralizes all magic numbers and strings defined in
  NBU Resolution No. 97, August 19, 2025.

  ## Categories

  - Format identifiers
  - Service types (UCT, ICT, XCT)
  - Encoding codes
  - Version numbers
  - Field length limits
  - Currency codes

  ## References

  NBU Resolution No. 97, dated August 19, 2025
  Effective: November 1, 2025
  """

  # Format identifier (BCD = Banking QR Code Data)
  @label "BCD"

  # Service type identifiers
  # Universal Credit Transfer
  @service_uct "UCT"
  # Instant Credit Transfer
  @service_ict "ICT"
  # Extended Credit Transfer (reserved)
  @service_xct "XCT"

  # Character encoding codes as per NBU specification
  @encoding_utf8 1
  @encoding_cp1251 2

  # Supported version numbers
  @version_001 1
  @version_002 2
  @version_003 3

  # Currency code (ISO 4217)
  @currency_uah "UAH"

  # Base URL for V002/V003 QR codes
  @base_url "https://qr.bank.gov.ua/"

  @doc """
  Returns the format label (BCD - Banking QR Code Data).

  This is the first line of all NBU QR codes.
  """
  @spec label() :: String.t()
  def label, do: @label

  @doc """
  Returns all supported service types.

  ## Service Types

  - `UCT` - Universal Credit Transfer (standard payment)
  - `ICT` - Instant Credit Transfer (instant payment, V003 only)
  - `XCT` - Extended Credit Transfer (reserved for future use)
  """
  @spec service_types() :: [String.t()]
  def service_types, do: [@service_uct, @service_ict, @service_xct]

  @doc """
  Returns the service type code for UCT (Universal Credit Transfer).

  This is the default service type for V001 and V002.
  """
  @spec service_uct() :: String.t()
  def service_uct, do: @service_uct

  @doc """
  Returns the service type code for ICT (Instant Credit Transfer).

  Only available in V003 format.
  """
  @spec service_ict() :: String.t()
  def service_ict, do: @service_ict

  @doc """
  Returns the service type code for XCT (Extended Credit Transfer).

  Reserved for future use.
  """
  @spec service_xct() :: String.t()
  def service_xct, do: @service_xct

  @doc """
  Returns the encoding code for the given encoding type.

  ## Encoding Codes

  - `:utf8` → 1
  - `:cp1251` → 2

  ## Examples

      iex> QRNBU.Constants.encoding_code(:utf8)
      1

      iex> QRNBU.Constants.encoding_code(:cp1251)
      2
  """
  @spec encoding_code(:utf8 | :cp1251) :: pos_integer()
  def encoding_code(:utf8), do: @encoding_utf8
  def encoding_code(:cp1251), do: @encoding_cp1251

  @doc """
  Returns the encoding atom for a given encoding code.

  ## Examples

      iex> QRNBU.Constants.encoding_from_code(1)
      :utf8

      iex> QRNBU.Constants.encoding_from_code(2)
      :cp1251
  """
  @spec encoding_from_code(pos_integer()) :: :utf8 | :cp1251 | {:error, String.t()}
  def encoding_from_code(@encoding_utf8), do: :utf8
  def encoding_from_code(@encoding_cp1251), do: :cp1251
  def encoding_from_code(code), do: {:error, "Unknown encoding code: #{code}"}

  @doc """
  Returns all supported version numbers.

  ## Versions

  - `1` - V001 (legacy format, 9 fields)
  - `2` - V002 (URL-based format, 9 fields)
  - `3` - V003 (latest format, 17 fields)
  """
  @spec supported_versions() :: [pos_integer()]
  def supported_versions, do: [@version_001, @version_002, @version_003]

  @doc """
  Returns the maximum recipient name length for a given version.

  ## Limits

  - V001/V002: 70 characters
  - V003: 140 characters

  ## Examples

      iex> QRNBU.Constants.max_recipient_length(1)
      70

      iex> QRNBU.Constants.max_recipient_length(3)
      140
  """
  @spec max_recipient_length(pos_integer()) :: pos_integer()
  def max_recipient_length(version) when version in [@version_001, @version_002], do: 70
  def max_recipient_length(@version_003), do: 140

  @doc """
  Returns the maximum payment purpose length for a given version.

  ## Limits

  - V001/V002: 140 characters
  - V003: 280 characters

  ## Examples

      iex> QRNBU.Constants.max_purpose_length(2)
      140

      iex> QRNBU.Constants.max_purpose_length(3)
      280
  """
  @spec max_purpose_length(pos_integer()) :: pos_integer()
  def max_purpose_length(version) when version in [@version_001, @version_002], do: 140
  def max_purpose_length(@version_003), do: 280

  @doc """
  Returns the IBAN length for Ukraine.

  Ukrainian IBANs are always exactly 29 characters.
  """
  @spec iban_length() :: pos_integer()
  def iban_length, do: 29

  @doc """
  Returns the default currency code (UAH).

  Currently only UAH (Ukrainian Hryvnia) is supported.
  """
  @spec currency() :: String.t()
  def currency, do: @currency_uah

  @doc """
  Returns all supported currency codes.

  Currently only UAH is supported per NBU specification.
  """
  @spec supported_currencies() :: [String.t()]
  def supported_currencies, do: [@currency_uah]

  @doc """
  Returns the base URL for NBU QR codes.

  Used in V002 and V003 formats to generate URLs with Base64URL encoded data.
  """
  @spec base_url() :: String.t()
  def base_url, do: @base_url

  @doc """
  Returns the version format number padded to 3 digits.

  ## Examples

      iex> QRNBU.Constants.format_version(1)
      "001"

      iex> QRNBU.Constants.format_version(2)
      "002"

      iex> QRNBU.Constants.format_version(3)
      "003"
  """
  @spec format_version(pos_integer()) :: String.t()
  def format_version(version) when version in [@version_001, @version_002, @version_003] do
    version |> Integer.to_string() |> String.pad_leading(3, "0")
  end

  @doc """
  Returns the line ending character(s) for a given version.

  ## Line Endings

  - V001/V002: CRLF (\\r\\n) - Windows-style
  - V003: LF (\\n) - Unix-style

  ## Examples

      iex> QRNBU.Constants.line_ending(1)
      "\\r\\n"

      iex> QRNBU.Constants.line_ending(3)
      "\\n"
  """
  @spec line_ending(pos_integer()) :: String.t()
  def line_ending(version) when version in [@version_001, @version_002], do: "\r\n"
  def line_ending(@version_003), do: "\n"

  @doc """
  Validates if a service type is supported for a given version.

  ## Service Type Availability

  - V001/V002: UCT only
  - V003: UCT, ICT, XCT

  ## Examples

      iex> QRNBU.Constants.valid_service_for_version?("UCT", 1)
      true

      iex> QRNBU.Constants.valid_service_for_version?("ICT", 2)
      false

      iex> QRNBU.Constants.valid_service_for_version?("ICT", 3)
      true
  """
  @spec valid_service_for_version?(String.t(), pos_integer()) :: boolean()
  def valid_service_for_version?(service, version)
      when version in [@version_001, @version_002] do
    service == @service_uct
  end

  def valid_service_for_version?(service, @version_003) do
    service in [@service_uct, @service_ict, @service_xct]
  end

  def valid_service_for_version?(_service, _version), do: false
end