defmodule QRNBU.Versions.V001 do
@moduledoc """
NBU QR Code Version 001 structure with validation and encoding.
V001 is the plain text format with CRLF line endings as per EPC QR code specification.
## Fields
### Required Fields
- `:recipient` - Name of the recipient (validated by RecipientValidator)
- `:iban` - Bank account IBAN (validated by IBANValidator)
- `:recipient_code` - Recipient's identification code (validated by RecipientCodeValidator)
- `:purpose` - Payment purpose description (validated by PurposeValidator)
### Optional Fields
- `:amount` - Payment amount as Decimal (validated by AmountValidator)
- `:function` - Payment function code: **only `:uct` supported** (default: `:uct`)
- `:encoding` - Character encoding: **only `:utf8` supported** (default: `:utf8`)
## NBU Specification Restrictions
V001 has strict limitations per NBU specification:
- **Function code**: Only `:uct` (Unattended Customer Transfer) is allowed
- **Encoding**: Only `:utf8` is allowed
- For `:ict` or `:xct` function codes, use V003
- For `:cp1251` encoding, use V002 or V003
## Examples
iex> QRNBU.Versions.V001.new(%{
...> recipient: "ТОВ Компанія",
...> iban: "UA213223130000026007233566001",
...> recipient_code: "12345678",
...> purpose: "Оплата товарів"
...> })
{:ok, %QRNBU.Versions.V001{...}}
iex> QRNBU.Versions.V001.new(%{
...> recipient: "ТОВ Компанія",
...> iban: "UA213223130000026007233566001",
...> amount: Decimal.new("100.50"),
...> recipient_code: "12345678",
...> purpose: "Оплата товарів",
...> function: :uct,
...> encoding: :utf8
...> })
{:ok, %QRNBU.Versions.V001{...}}
"""
alias QRNBU.Validators.{
Recipient,
IBAN,
RecipientCode,
Purpose,
Amount,
FunctionCode
}
alias QRNBU.Encoders.Formatter
@type t :: %__MODULE__{
recipient: String.t(),
iban: String.t(),
recipient_code: String.t(),
purpose: String.t(),
amount: Decimal.t() | nil,
function: :uct | :ict | :xct,
encoding: :utf8 | :cp1251
}
@enforce_keys [:recipient, :iban, :recipient_code, :purpose]
defstruct [
:recipient,
:iban,
:recipient_code,
:purpose,
:amount,
function: :uct,
encoding: :utf8
]
@doc """
Creates a new V001 QR code structure with validation.
## Parameters
- `attrs` - Map with required and optional fields
## Returns
- `{:ok, %QRNBU.Versions.V001{}}` - Valid V001 structure
- `{:error, String.t()}` - Validation error message
## Examples
iex> QRNBU.Versions.V001.new(%{
...> recipient: "ТОВ Компанія",
...> iban: "UA213223130000026007233566001",
...> recipient_code: "12345678",
...> purpose: "Оплата товарів"
...> })
{:ok, %QRNBU.Versions.V001{}}
iex> QRNBU.Versions.V001.new(%{
...> recipient: "",
...> iban: "invalid",
...> recipient_code: "123",
...> purpose: ""
...> })
{:error, "Recipient name is required"}
"""
@spec new(map()) :: {:ok, t()} | {:error, String.t()}
def new(attrs) when is_map(attrs) do
with :ok <- validate_required_fields(attrs),
{:ok, validated_attrs} <- validate_fields(attrs) do
struct = struct(__MODULE__, validated_attrs)
{:ok, struct}
end
end
@doc """
Encodes a V001 structure into the NBU QR code data string.
Returns the plain text format with CRLF line endings as per EPC specification.
## Parameters
- `v001` - V001 structure to encode
## Returns
- `{:ok, String.t()}` - Encoded QR data string
- `{:error, String.t()}` - Encoding error
## Examples
iex> {:ok, v001} = QRNBU.Versions.V001.new(%{...})
iex> QRNBU.Versions.V001.encode(v001)
{:ok, " \\r\\nBCD\\r\\n001\\r\\n1\\r\\nUCT\\r\\n..."}
"""
@spec encode(t()) :: {:ok, String.t()} | {:error, String.t()}
def encode(%__MODULE__{} = v001) do
data = %{
recipient: v001.recipient,
iban: v001.iban,
recipient_code: v001.recipient_code,
purpose: v001.purpose,
amount: v001.amount,
function: v001.function
}
opts = [encoding: v001.encoding]
Formatter.format_v001(data, opts)
end
# Private Functions
defp validate_required_fields(attrs) do
required = [:recipient, :iban, :recipient_code, :purpose]
missing =
Enum.filter(required, fn key ->
not Map.has_key?(attrs, key) or is_nil(Map.get(attrs, key))
end)
case missing do
[] -> :ok
[field | _] -> {:error, "Missing required field: #{field}"}
end
end
defp validate_fields(attrs) do
encoding = Map.get(attrs, :encoding, :utf8)
with {:ok, recipient} <- Recipient.validate(attrs.recipient, encoding: encoding),
{:ok, iban} <- IBAN.validate(attrs.iban),
{:ok, recipient_code} <- RecipientCode.validate(attrs.recipient_code),
{:ok, purpose} <- Purpose.validate(attrs.purpose, encoding: encoding),
{:ok, amount} <- validate_optional_amount(Map.get(attrs, :amount)),
{:ok, function} <- validate_function(Map.get(attrs, :function, :uct)),
{:ok, encoding} <- validate_encoding(encoding) do
validated = %{
recipient: recipient,
iban: iban,
recipient_code: recipient_code,
purpose: purpose,
amount: amount,
function: function,
encoding: encoding
}
{:ok, validated}
end
end
defp validate_optional_amount(nil), do: {:ok, nil}
defp validate_optional_amount(amount) do
Amount.validate(amount)
end
defp validate_function(function) do
# V001 only supports UCT function code per NBU spec
case function do
:uct ->
FunctionCode.validate(function)
other when other in [:ict, :xct] ->
{:error, "V001 only supports :uct function code. Use V003 for :ict or :xct"}
_ ->
FunctionCode.validate(function)
end
end
defp validate_encoding(encoding) do
# V001 only supports UTF-8 encoding per NBU spec
case encoding do
:utf8 ->
{:ok, encoding}
:cp1251 ->
{:error, "V001 only supports :utf8 encoding. Use V002 or V003 for :cp1251"}
other ->
{:error, "Invalid encoding: #{inspect(other)}. V001 only supports :utf8"}
end
end
end