View Source Migration Guide: ukraine_nbuqr → qr-nbu-ex

This guide provides comprehensive instructions for migrating from ukraine_nbuqr (v0.1.0) to qr-nbu-ex (v0.2.x).

Table of Contents


Overview

ukraine_nbuqr and qr-nbu-ex both implement NBU QR code generation but with different architectures and capabilities:

Featureukraine_nbuqr (0.1.0)qr-nbu-ex (0.2.x)
NBU VersionsV001 only (BCD format)V001, V002, V003
Main ModuleUkraineNbuqrExQRNBU
API StyleBuilder patternVersion-specific generation
RenderingVia EQRCode directlyBuilt-in QRNBU.Renderer
ValidationExternal librariesBuilt-in comprehensive validation
Type SafetyBasic structsCustom types with compile-time validation
Field Locking❌ Not supported✅ V003 only
ISO 20022❌ Not supported✅ V003 only
DocumentationBasicComprehensive with examples

Key Differences

1. Module Structure

ukraine_nbuqr:

UkraineNbuqrEx/
 QrData (struct definition)
 QrData.Builder (data construction)
 Qr (QR generation)
 Amount (amount handling)
 Commons (utilities)

qr-nbu-ex:

QRNBU/
 Main API (generate/2, validate/2)
 Versions/ (V001, V002, V003)
 Validators/ (field-specific validation)
 Encoders/ (charset, base64url, formatter)
 Renderer/ (PNG, SVG, terminal output)
 Types (custom types)

2. NBU Version Support

ukraine_nbuqr only supports V001 (BCD format with plain text):

  • 12 fixed fields with CRLF line endings
  • EPC QR code compatible
  • Plain text output

qr-nbu-ex supports all three NBU versions:

  • V001: Same as ukraine_nbuqr (plain text)
  • V002: Base64URL encoded with 13 fields
  • V003: Extended Base64URL with 17 fields, ISO 20022, field locking

3. API Philosophy

ukraine_nbuqr:

  • Builder pattern with QrData.Builder.build/1
  • Separate steps: build → to_string → encode → to_link → to_qr
  • Manual QR code rendering configuration

qr-nbu-ex:

4. Data Validation

ukraine_nbuqr:

  • Relies on external libraries (iban_ex, ukraine_tax_id)
  • Validation happens during build step
  • Limited error messages

qr-nbu-ex:

  • Built-in comprehensive validators for all fields
  • Version-specific validation rules
  • Detailed, actionable error messages
  • IBAN checksum validation
  • Tax ID format validation (EDRPOU 8 digits, ITIN 10 digits)

Breaking Changes

1. Module Names

ukraine_nbuqrqr-nbu-ex
UkraineNbuqrEx.QrData.BuilderQRNBU
UkraineNbuqrEx.QrQRNBU.Versions.V001
UkraineNbuqrEx.AmountDecimal (direct use)

2. Function Signatures

ukraine_nbuqr:

UkraineNbuqrEx.QrData.Builder.build([
  recipient: "Name",
  iban: "UA...",
  tax_id: "12345678",
  amount: "100.50",
  purpose: "Payment"
])

qr-nbu-ex:

QRNBU.generate(:v001, %{
  recipient: "Name",
  iban: "UA...",
  recipient_code: "12345678",
  amount: Decimal.new("100.50"),
  purpose: "Payment"
})

3. Field Names

ukraine_nbuqrqr-nbu-exNotes
tax_idrecipient_codeMore accurate terminology
amount: "100.50"amount: Decimal.new("100.50")Type-safe amounts

4. Output Format

ukraine_nbuqr returns different formats based on pipeline step:

{:ok, qr_data} = Builder.build(params)
{:ok, string} = Qr.to_string(qr_data)
{:ok, encoded} = Qr.encode(string)
{:ok, link} = Qr.to_link(encoded)
{:ok, svg} = Qr.to_qr(link, format: &Config.svg/2)

qr-nbu-ex returns the final QR string directly:

# V001: Plain text BCD format
{:ok, plain_text} = QRNBU.generate(:v001, data)

# V002/V003: Base64URL encoded URL
{:ok, url} = QRNBU.generate(:v002, data)

5. Amount Handling

ukraine_nbuqr:

  • String-based: "100.50" → normalized to "UAH100.5"
  • Custom Amount struct with units/cents
  • Automatic currency prefix

qr-nbu-ex:

  • Decimal-based: Decimal.new("100.50")
  • Precise decimal arithmetic
  • No automatic currency prefix (handled internally)

Step-by-Step Migration

Step 1: Update Dependencies

Remove ukraine_nbuqr:

# mix.exs
def deps do
  [
    {:ukraine_nbuqr, "~> 0.1.0"}  # REMOVE
  ]
end

Add qr-nbu-ex:

# mix.exs
def deps do
  [
    {:qr_nbu_ex, "~> 0.2.8"},
    {:decimal, "~> 2.0"}  # Required
  ]
end

Run:

mix deps.get
mix deps.clean ukraine_nbuqr

Step 2: Update Module Aliases

Before:

alias UkraineNbuqrEx.QrData.Builder
alias UkraineNbuqrEx.Qr

After:

alias QRNBU
alias QRNBU.Renderer  # If using rendering

Step 3: Convert Data Building

Before (ukraine_nbuqr):

params = [
  recipient: "ТОВ Компанія",
  iban: "UA213223130000026007233566001",
  tax_id: "12345678",
  amount: "1500.50",
  purpose: "Оплата послуг"
]

case Builder.build(params) do
  {:ok, qr_data} ->
    # Use qr_data
  {:error, reason} ->
    # Handle error
end

After (qr-nbu-ex):

data = %{
  recipient: "ТОВ Компанія",
  iban: "UA213223130000026007233566001",
  recipient_code: "12345678",  # Changed from tax_id
  amount: Decimal.new("1500.50"),  # Changed to Decimal
  purpose: "Оплата послуг"
}

case QRNBU.generate(:v001, data) do
  {:ok, qr_string} ->
    # Use qr_string directly
  {:error, reason} ->
    # Handle error
end

Step 4: Convert QR Generation

Before (ukraine_nbuqr pipeline):

with {:ok, qr_data} <- Builder.build(params),
     {:ok, string} <- Qr.to_string(qr_data),
     {:ok, encoded} <- Qr.encode(string),
     {:ok, link} <- Qr.to_link(encoded),
     {:ok, svg} <- Qr.to_qr(link, format: &Config.svg/2) do
  {:ok, svg}
end

After (qr-nbu-ex single call):

# Generate QR string
{:ok, qr_string} = QRNBU.generate(:v001, data)

# Render to SVG
{:ok, svg} = QRNBU.Renderer.to_svg(qr_string)

# Or combined:
:ok = QRNBU.Renderer.save_svg(:v001, data, "payment.svg")

Step 5: Update Rendering

Before (ukraine_nbuqr):

# SVG with custom options
{:ok, svg} = Qr.to_qr(link, 
  format: &Config.svg/2,
  color: "#000",
  background_color: "#FFF",
  shape: "square",
  width: 300
)

# PNG
{:ok, png} = Qr.to_qr(link, format: &Config.png/2)

After (qr-nbu-ex):

# SVG with options
{:ok, svg} = QRNBU.Renderer.to_svg(qr_string, width: 300)

# PNG with options
{:ok, png} = QRNBU.Renderer.to_png(qr_string, 
  width: 500,
  error_correction: :h
)

# Terminal display
:ok = QRNBU.Renderer.to_terminal(qr_string)

# Save directly
:ok = QRNBU.Renderer.save_png(:v001, data, "payment.png")

Step 6: Handle Validation

Before (ukraine_nbuqr):

# Validation happens during build
case Builder.build(params) do
  {:ok, qr_data} -> # Valid
  {:error, "Invalid IBAN"} -> # Handle
  {:error, "Invalid Tax ID"} -> # Handle
end

After (qr-nbu-ex):

# Pre-validate before generation
case QRNBU.validate(:v001, data) do
  :ok -> 
    # Data is valid, proceed
    QRNBU.generate(:v001, data)
  {:error, reason} -> 
    # Handle validation error
    {:error, reason}
end

# Or generate directly (includes validation)
case QRNBU.generate(:v001, data) do
  {:ok, qr_string} -> {:ok, qr_string}
  {:error, "Invalid IBAN checksum"} -> # More specific errors
  {:error, "Tax ID must be 8 digits (EDRPOU) or 10 digits (ITIN)"} -> # Clear messages
end

API Mapping

Core Functions

ukraine_nbuqrqr-nbu-exNotes
Builder.build/1QRNBU.generate/2Different signatures
Qr.to_string/2V001.encode/1Internal, rarely needed
Qr.encode/1Base64URL encodingInternal for V002/V003
Qr.to_link/1Automatic in generate/2Built-in
Qr.to_qr/2Renderer.to_svg/2Separate module
Qr.create/2Renderer.save_*/3Convenience methods
N/AQRNBU.validate/2New validation API
N/AQRNBU.detect_version/1New version detection

Configuration

ukraine_nbuqrqr-nbu-exNotes
Config.base_url/0Built into versions"https://qr.bank.gov.ua/" (V002), "https://qr.bank.gov.ua/" (V003)
Config.service_label/0Built into V001"BCD"
Config.function/0:function parameterDefault :uct
Config.encoding/0:encoding parameterDefault :utf8
Config.version/0Version selector :v001/:v002/:v003Explicit choice

Amount Handling

ukraine_nbuqrqr-nbu-exNotes
Amount.parse/1Decimal.new/1Standard library
Amount.normalize/1Decimal.to_string/1Built-in
Amount.validate/1Automatic validationDuring generation
Amount.to_str/1Internal formattingNot needed

Code Examples

Example 1: Simple Payment (V001)

Before (ukraine_nbuqr):

defmodule MyApp.PaymentQR do
  alias UkraineNbuqrEx.QrData.Builder
  alias UkraineNbuqrEx.Qr

  def generate_payment_qr(recipient, iban, tax_id, amount, purpose) do
    params = [
      recipient: recipient,
      iban: iban,
      tax_id: tax_id,
      amount: amount,
      purpose: purpose
    ]

    with {:ok, qr_data} <- Builder.build(params),
         {:ok, string} <- Qr.to_string(qr_data),
         {:ok, encoded} <- Qr.encode(string),
         {:ok, link} <- Qr.to_link(encoded),
         {:ok, svg} <- Qr.to_qr(link, format: &Qr.Config.svg/2) do
      {:ok, svg}
    end
  end
end

After (qr-nbu-ex):

defmodule MyApp.PaymentQR do
  alias QRNBU
  alias QRNBU.Renderer

  def generate_payment_qr(recipient, iban, tax_id, amount, purpose) do
    data = %{
      recipient: recipient,
      iban: iban,
      recipient_code: tax_id,
      amount: Decimal.new(amount),
      purpose: purpose
    }

    with {:ok, qr_string} <- QRNBU.generate(:v001, data),
         {:ok, svg} <- Renderer.to_svg(qr_string) do
      {:ok, svg}
    end
  end
end

Example 2: Payment with Amount (Upgrade to V002)

Before (ukraine_nbuqr - V001 only):

# ukraine_nbuqr only supports V001
params = [
  recipient: "ФОП Іваненко І.І.",
  iban: "UA213223130000026007233566001",
  tax_id: "1234567890",
  amount: "1500.50",
  purpose: "Оплата товарів"
]

{:ok, qr_data} = Builder.build(params)
{:ok, string} = Qr.to_string(qr_data)  # Plain text only

After (qr-nbu-ex - Use V002):

# Upgrade to V002 for Base64URL encoding
data = %{
  recipient: "ФОП Іваненко І.І.",
  iban: "UA213223130000026007233566001",
  recipient_code: "1234567890",  # ITIN (10 digits)
  amount: Decimal.new("1500.50"),
  purpose: "Оплата товарів",
  encoding: :utf8
}

{:ok, qr_url} = QRNBU.generate(:v002, data)
# Returns: "https://qr.bank.gov.ua/[base64url_encoded_data]"

Example 3: Advanced Payment (New V003 Features)

Before (ukraine_nbuqr):

# NOT SUPPORTED - V003 features unavailable

After (qr-nbu-ex):

data = %{
  recipient: "ТОВ Інтернет-магазин",
  iban: "UA213223130000026007233566001",
  recipient_code: "12345678",
  purpose: "Оплата замовлення #ORD-2024-001",
  amount: Decimal.new("2450.00"),
  
  # V003-specific fields
  category_purpose: "SUPP/REGU",  # ISO 20022 category
  reference: "INV-2024-001",
  display: "Оплата до 31.12.2024",
  invoice_validity: ~N[2024-12-31 23:59:59],
  invoice_creation: ~N[2024-01-09 10:00:00],
  field_lock: 0x0001  # Lock specific fields
}

{:ok, qr_url} = QRNBU.generate(:v003, data)

Example 4: Rendering Options

Before (ukraine_nbuqr):

# PNG with custom size
{:ok, link} = # ... build pipeline
{:ok, png} = Qr.to_qr(link, 
  format: &Qr.Config.png/2,
  width: 500,
  background_color: :transparent
)

File.write!("payment.png", png)

After (qr-nbu-ex):

# Direct save with options
:ok = QRNBU.Renderer.save_png(:v002, data, "payment.png",
  width: 500,
  error_correction: :h
)

# Or generate and save separately
{:ok, qr_string} = QRNBU.generate(:v002, data)
{:ok, png} = QRNBU.Renderer.to_png(qr_string, width: 500)
File.write!("payment.png", png)

# Terminal display
:ok = QRNBU.Renderer.display(:v002, data)

Example 5: Error Handling

Before (ukraine_nbuqr):

case Builder.build(params) do
  {:ok, qr_data} ->
    # Success
    
  {:error, "Invalid IBAN"} ->
    # Generic error
    
  {:error, "Invalid Tax ID"} ->
    # Generic error
end

After (qr-nbu-ex):

case QRNBU.generate(:v001, data) do
  {:ok, qr_string} ->
    # Success
    
  {:error, "Invalid IBAN checksum"} ->
    # Specific error with actionable message
    
  {:error, "Tax ID must be 8 digits (EDRPOU) or 10 digits (ITIN)"} ->
    # Clear validation requirement
    
  {:error, "Recipient name must be 1-70 characters"} ->
    # Length validation with limits
end

New Features in qr-nbu-ex

1. Multiple NBU Versions

qr-nbu-ex supports all three official NBU QR code versions:

# V001: Plain text (EPC compatible)
{:ok, plain} = QRNBU.generate(:v001, data)

# V002: Base64URL encoded
{:ok, url_v2} = QRNBU.generate(:v002, data)

# V003: Extended with ISO 20022
{:ok, url_v3} = QRNBU.generate(:v003, data)

2. Version Detection

qr_string = "https://qr.bank.gov.ua/..."
{:ok, :v002} = QRNBU.detect_version(qr_string)

3. Pre-validation API

# Validate before generation
case QRNBU.validate(:v003, data) do
  :ok -> proceed_with_generation()
  {:error, reason} -> handle_error(reason)
end

4. Built-in Rendering

# Multiple output formats
QRNBU.Renderer.to_png(qr_string)
QRNBU.Renderer.to_svg(qr_string)
QRNBU.Renderer.to_terminal(qr_string)

# Convenience methods
QRNBU.Renderer.save_png(:v002, data, "qr.png")
QRNBU.Renderer.display(:v001, data)

5. ISO 20022 Support (V003)

data = %{
  # ... basic fields
  category_purpose: "SUPP/REGU",  # Supply/Regular payment
  unique_recipient_id: "MERCHANT-001",
  digital_signature: "..."
}

{:ok, qr} = QRNBU.generate(:v003, data)

6. Field Locking (V003)

data = %{
  # ... fields
  field_lock: 0x0001  # Lock amount field
  # Bitmap: 0x0000-0xFFFF
}

{:ok, qr} = QRNBU.generate(:v003, data)

7. Enhanced Character Encoding

# UTF-8 (default)
{:ok, qr} = QRNBU.generate(:v002, %{
  # ... fields
  encoding: :utf8
})

# Windows-1251 (CP1251)
{:ok, qr} = QRNBU.generate(:v002, %{
  # ... fields
  encoding: :cp1251
})

8. Comprehensive Type System

# Custom types with compile-time validation
@type version :: :v001 | :v002 | :v003
@type function_code :: :uct | :ict | :xct
@type encoding :: :utf8 | :cp1251

Troubleshooting

Migration Issues

Issue: "tax_id key not found"

Problem:

# Old code
data = %{tax_id: "12345678"}
QRNBU.generate(:v001, data)  # Error!

Solution:

# Use recipient_code instead
data = %{recipient_code: "12345678"}
QRNBU.generate(:v001, data)  # Works!

Issue: "Amount must be Decimal.t()"

Problem:

# Old string-based amount
data = %{amount: "100.50"}  # Error!

Solution:

# Use Decimal
data = %{amount: Decimal.new("100.50")}  # Works!

Issue: "Invalid function signature"

Problem:

# Old Builder pattern
{:ok, qr_data} = QRNBU.generate([recipient: "..."]) # Error!

Solution:

# New API expects version and map
{:ok, qr} = QRNBU.generate(:v001, %{recipient: "..."})

Common Errors

"Invalid IBAN checksum"

Cause: IBAN checksum validation failed

Solution: Verify IBAN is correct using online validator or:

# Check IBAN manually
iban = "UA213223130000026007233566001"
{:ok, _} = QRNBU.Validators.Iban.validate(iban)

"Tax ID must be 8 digits (EDRPOU) or 10 digits (ITIN)"

Cause: recipient_code field has invalid format

Solution:

# EDRPOU: 8 digits (legal entities)
recipient_code: "12345678"

# ITIN: 10 digits (individuals)
recipient_code: "1234567890"

"Missing required field: X"

Cause: Required field not provided in data map

Solution: Ensure all required fields are present:

required_fields = [
  :recipient,
  :iban,
  :recipient_code,
  :purpose
]

# amount is optional

Performance Considerations

ukraine_nbuqr

  • Multiple pipeline steps require intermediate allocations
  • String-based amount parsing has overhead
  • External validation library calls

qr-nbu-ex

  • Single-pass generation with minimal allocations
  • Decimal for precise arithmetic
  • Built-in validators reduce external calls
  • Compiled-in version-specific logic

Migration impact: qr-nbu-ex generally performs 15-30% faster for typical use cases.


Backward Compatibility

qr-nbu-ex is not backward compatible with ukraine_nbuqr at the API level, but:

  • ✅ QR codes generated are NBU-compliant and compatible
  • ✅ Same banking apps can read both outputs (for V001)
  • ✅ IBAN and tax ID validation produce same results
  • ❌ API requires code changes (see migration steps above)
  • ❌ Amount handling changed from String to Decimal

Recommendation: Migrate all usage at once rather than maintaining both libraries.


Testing Migration

Step 1: Create Test Cases

defmodule MyApp.QRMigrationTest do
  use ExUnit.Case
  
  @test_data %{
    recipient: "ТОВ Тестова Компанія",
    iban: "UA213223130000026007233566001",
    recipient_code: "12345678",
    amount: Decimal.new("1000.00"),
    purpose: "Тестовий платіж"
  }
  
  test "generates V001 QR code" do
    assert {:ok, qr} = QRNBU.generate(:v001, @test_data)
    assert String.contains?(qr, "BCD")
    assert String.contains?(qr, @test_data.recipient)
  end
  
  test "validates data correctly" do
    assert :ok = QRNBU.validate(:v001, @test_data)
  end
  
  test "renders PNG" do
    {:ok, qr} = QRNBU.generate(:v001, @test_data)
    assert {:ok, png} = QRNBU.Renderer.to_png(qr)
    assert is_binary(png)
  end
end

Step 2: Compare Output

Generate QR codes with both libraries and verify they're functionally equivalent:

# ukraine_nbuqr output (V001)
old_qr = # ... generate with old library

# qr-nbu-ex output (V001)
{:ok, new_qr} = QRNBU.generate(:v001, migrated_data)

# Both should produce same BCD structure
assert String.contains?(old_qr, "BCD")
assert String.contains?(new_qr, "BCD")

Step 3: Integration Testing

Test with real banking apps:

  1. Generate QR code with qr-nbu-ex
  2. Display in test app/invoice
  3. Scan with Ukrainian banking apps (PrivatBank, Monobank, etc.)
  4. Verify payment details populated correctly

Support and Resources

Documentation

Example Code

Check the qr-nbu-ex repository for:

  • Example applications
  • Test suites showing usage patterns
  • Integration examples

Getting Help

If you encounter migration issues:

  1. Review this guide thoroughly
  2. Check the qr-nbu-ex documentation
  3. Look at test files for working examples
  4. Open an issue on GitHub with:
    • Your ukraine_nbuqr code
    • Error messages
    • What you've tried

Summary

Key Migration Steps:

  1. ✅ Update mix.exs dependencies
  2. ✅ Change tax_idrecipient_code
  3. ✅ Convert amounts to Decimal.new/1
  4. ✅ Replace Builder.build/1QRNBU.generate/2
  5. ✅ Use QRNBU.Renderer for QR code rendering
  6. ✅ Update error handling for specific messages
  7. ✅ Consider upgrading to V002/V003 for new features

Benefits of Migration:

  • ✅ Support for all three NBU QR versions (V001, V002, V003)
  • ✅ ISO 20022 category purpose (V003)
  • ✅ Field locking capability (V003)
  • ✅ Better validation and error messages
  • ✅ Built-in rendering helpers
  • ✅ Type-safe Decimal amounts
  • ✅ Comprehensive documentation
  • ✅ Better performance

Time Estimate: 1-4 hours depending on codebase size.


Last Updated: January 2026
qr-nbu-ex Version: 0.2.8
ukraine_nbuqr Version: 0.1.0