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