lib/qr_nbu/versions/v003.ex

defmodule QRNBU.Versions.V003 do
  @moduledoc """
  NBU QR Code Version 003 structure with validation and encoding.

  V003 is the extended Base64URL format with 17 fields including category purpose,
  field lock, and invoice timestamps.

  ## 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)
  - `:reference` - Payment reference number (validated by ReferenceValidator)
  - `:unique_recipient_id` - Unique recipient identifier (validated by UniqueRecipientIDValidator)
  - `:category_purpose` - Categorized purpose of payment (validated by CategoryPurposeValidator)
  - `:display` - Display text for the QR code (validated by DisplayValidator)
  - `:field_lock` - Field edit control bitmap as integer 0x0000-0xFFFF (validated by FieldLockValidator)
  - `:invoice_validity` - Invoice expiration datetime (validated by InvoiceValidityValidator)
  - `:invoice_creation` - Invoice creation datetime (validated by InvoiceCreationValidator)
  - `:digital_signature` - Digital signature string (validated by DigitalSignatureValidator)
  - `:function` - Payment function code: `:uct`, `:ict`, or `:xct` (default: `:uct`)
  - `:encoding` - Character encoding: `:utf8` or `:cp1251` (default: `:utf8`)

  ## Examples

      iex> QRNBU.Versions.V003.new(%{
      ...>   recipient: "ТОВ Компанія",
      ...>   iban: "UA213223130000026007233566001",
      ...>   recipient_code: "12345678",
      ...>   purpose: "Оплата товарів"
      ...> })
      {:ok, %QRNBU.Versions.V003{...}}

      iex> QRNBU.Versions.V003.new(%{
      ...>   recipient: "ТОВ Компанія",
      ...>   iban: "UA213223130000026007233566001",
      ...>   amount: Decimal.new("100.50"),
      ...>   recipient_code: "12345678",
      ...>   purpose: "Оплата товарів",
      ...>   category_purpose: "Товари",
      ...>   field_lock: 0x1234,
      ...>   invoice_validity: ~N[2025-12-31 23:59:59],
      ...>   invoice_creation: ~N[2025-01-08 10:30:00]
      ...> })
      {:ok, %QRNBU.Versions.V003{...}}

  """

  alias QRNBU.Validators.{
    Recipient,
    IBAN,
    RecipientCode,
    Purpose,
    Amount,
    Reference,
    UniqueRecipientID,
    CategoryPurpose,
    Display,
    FieldLock,
    InvoiceValidity,
    InvoiceCreation,
    DigitalSignature,
    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,
          reference: String.t() | nil,
          unique_recipient_id: String.t() | nil,
          category_purpose: String.t() | nil,
          display: String.t() | nil,
          field_lock: non_neg_integer() | nil,
          invoice_validity: NaiveDateTime.t() | nil,
          invoice_creation: NaiveDateTime.t() | nil,
          digital_signature: String.t() | nil,
          function: :uct | :ict | :xct,
          encoding: :utf8 | :cp1251
        }

  @enforce_keys [:recipient, :iban, :recipient_code, :purpose]
  defstruct [
    :recipient,
    :iban,
    :recipient_code,
    :purpose,
    :amount,
    :reference,
    :unique_recipient_id,
    :category_purpose,
    :display,
    :field_lock,
    :invoice_validity,
    :invoice_creation,
    :digital_signature,
    function: :uct,
    encoding: :utf8
  ]

  @doc """
  Creates a new V003 QR code structure with validation.

  ## Parameters

  - `attrs` - Map with required and optional fields

  ## Returns

  - `{:ok, %QRNBU.Versions.V003{}}` - Valid V003 structure
  - `{:error, String.t()}` - Validation error message

  ## Examples

      iex> QRNBU.Versions.V003.new(%{
      ...>   recipient: "ТОВ Компанія",
      ...>   iban: "UA213223130000026007233566001",
      ...>   recipient_code: "12345678",
      ...>   purpose: "Оплата товарів"
      ...> })
      {:ok, %QRNBU.Versions.V003{}}

      iex> QRNBU.Versions.V003.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 V003 structure into the NBU QR code data string.

  Returns the Base64URL encoded format with https://qr.bank.gov.ua/ prefix.

  ## Parameters

  - `v003` - V003 structure to encode

  ## Returns

  - `{:ok, String.t()}` - Encoded QR data string
  - `{:error, String.t()}` - Encoding error

  ## Examples

      iex> {:ok, v003} = QRNBU.Versions.V003.new(%{...})
      iex> QRNBU.Versions.V003.encode(v003)
      {:ok, "https://qr.bank.gov.ua/MQpVQ1QKVUFICgo..."}

  """
  @spec encode(t()) :: {:ok, String.t()} | {:error, String.t()}
  def encode(%__MODULE__{} = v003) do
    data = %{
      recipient: v003.recipient,
      iban: v003.iban,
      recipient_code: v003.recipient_code,
      purpose: v003.purpose,
      amount: v003.amount,
      reference: v003.reference,
      unique_recipient_id: v003.unique_recipient_id,
      category_purpose: v003.category_purpose,
      display: v003.display,
      field_lock: v003.field_lock,
      invoice_validity: v003.invoice_validity,
      invoice_creation: v003.invoice_creation,
      digital_signature: v003.digital_signature,
      function: v003.function
    }

    opts = [encoding: v003.encoding]
    Formatter.format_v003(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, reference} <- validate_optional_reference(Map.get(attrs, :reference)),
         {:ok, unique_recipient_id} <-
           validate_optional_unique_recipient_id(Map.get(attrs, :unique_recipient_id)),
         {:ok, category_purpose} <-
           validate_optional_category_purpose(Map.get(attrs, :category_purpose), encoding),
         {:ok, display} <- validate_optional_display(Map.get(attrs, :display), encoding),
         {:ok, field_lock} <- validate_optional_field_lock(Map.get(attrs, :field_lock)),
         {:ok, invoice_validity} <-
           validate_optional_invoice_validity(Map.get(attrs, :invoice_validity)),
         {:ok, invoice_creation} <-
           validate_optional_invoice_creation(Map.get(attrs, :invoice_creation)),
         {:ok, digital_signature} <-
           validate_optional_digital_signature(Map.get(attrs, :digital_signature)),
         {: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,
        reference: reference,
        unique_recipient_id: unique_recipient_id,
        category_purpose: category_purpose,
        display: display,
        field_lock: field_lock,
        invoice_validity: invoice_validity,
        invoice_creation: invoice_creation,
        digital_signature: digital_signature,
        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)

  defp validate_optional_reference(nil), do: {:ok, nil}
  defp validate_optional_reference(reference), do: Reference.validate(reference)

  defp validate_optional_unique_recipient_id(nil), do: {:ok, nil}

  defp validate_optional_unique_recipient_id(id),
    do: UniqueRecipientID.validate(id)

  defp validate_optional_category_purpose(nil, _encoding), do: {:ok, nil}

  defp validate_optional_category_purpose(category, encoding),
    do: CategoryPurpose.validate(category, encoding: encoding)

  defp validate_optional_display(nil, _encoding), do: {:ok, nil}

  defp validate_optional_display(display, encoding),
    do: Display.validate(display, encoding: encoding)

  defp validate_optional_field_lock(nil), do: {:ok, nil}
  defp validate_optional_field_lock(lock), do: FieldLock.validate(lock)

  defp validate_optional_invoice_validity(nil), do: {:ok, nil}

  defp validate_optional_invoice_validity(datetime),
    do: InvoiceValidity.validate(datetime)

  defp validate_optional_invoice_creation(nil), do: {:ok, nil}

  defp validate_optional_invoice_creation(datetime),
    do: InvoiceCreation.validate(datetime)

  defp validate_optional_digital_signature(nil), do: {:ok, nil}

  defp validate_optional_digital_signature(signature),
    do: DigitalSignature.validate(signature)

  defp validate_function(function), do: FunctionCode.validate(function)

  defp validate_encoding(encoding) when encoding in [:utf8, :cp1251] do
    {:ok, encoding}
  end

  defp validate_encoding(encoding) do
    {:error, "Invalid encoding: #{inspect(encoding)}. Must be :utf8 or :cp1251"}
  end
end