README.md

# UblEx

Parse and generate UBL (Universal Business Language) documents in Elixir with full round-trip support.

**Peppol BIS Billing 3.0 compliant** • **UBL 2.1** • **EN16931**

## Features

- **Parse** UBL Invoice, CreditNote, and ApplicationResponse XML documents
- **Generate** Peppol-compliant UBL XML
- **Round-trip support** - parse → generate → parse without data loss
- **Type-safe** - proper Elixir types (Date, Decimal, atoms)
- **Attachment support** - embed PDF files and other documents
- **Auto-detection** - automatically identify document types
- **Simple API** - no complex behaviours or callbacks

## Installation

Add `ubl_ex` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:ubl_ex, "~> 0.1.0"}
  ]
end
```

## Quick Start

### Parsing a UBL Document

```elixir
# Simple parse with automatic schema detection
{:ok, parsed} = UblEx.parse(xml_content)

# Or be explicit about the schema
{:ok, parsed} = UblEx.parse_xml(xml_content, :ubl_peppol)

# Access the data
IO.puts("Document type: #{parsed.type}")        # :invoice, :credit, or :application_response
IO.puts("Invoice number: #{parsed.number}")
IO.puts("Supplier: #{parsed.supplier.name}")
IO.puts("Customer: #{parsed.customer.name}")
IO.puts("Total: #{length(parsed.details)} line items")
```

### Generating a UBL Invoice

```elixir
document_data = %{
  type: :invoice,
  number: "F2024001",
  date: ~D[2024-01-15],
  expires: ~D[2024-02-14],
  reverse_charge: false,

  supplier: %{
    endpoint_id: "0797948229",
    scheme: "0208",
    name: "My Company",
    street: "Main Street 123",
    city: "Brussels",
    zipcode: "1000",
    country: "BE",
    vat: "BE0797948229",
    email: "invoice@mycompany.com"
  },

  customer: %{
    endpoint_id: "0012345625",
    scheme: "0208",
    name: "Customer Corp",
    vat: "BE0012345625",
    street: "Customer Street",
    housenumber: "45",
    city: "Antwerp",
    zipcode: "2000",
    country: "BE"
  },

  details: [
    %{
      name: "Consulting Services - January 2024",
      quantity: Decimal.new("40.0"),
      price: Decimal.new("75.00"),
      vat: Decimal.new("21.00"),
      discount: Decimal.new("0.00")
    }
  ]
}

# Generate the XML
xml = UblEx.generate(document_data)
```

### Generating a Credit Note

```elixir
credit_note_data = %{
  type: :credit,
  number: "C2024001",
  date: ~D[2024-01-20],
  reverse_charge: false,

  # Reference original invoices
  billing_references: ["F2024001", "F2024002"],

  supplier: %{...},
  customer: %{...},
  details: [...]
}

xml = UblEx.generate(credit_note_data)
```

### Generating an Application Response

Application responses are used to acknowledge receipt and processing status of invoices:

```elixir
response_data = %{
  type: :application_response,
  id: "RESPONSE-001",
  date: ~D[2025-06-02],
  response_code: "AB",  # AB = Acknowledged, RE = Rejected
  document_reference: "INV-123",
  sender: %{
    endpoint_id: "0797948229",
    scheme: "0208",
    name: "My Company"
  },
  receiver: %{
    endpoint_id: "0844125969",
    scheme: "0208",
    name: "Supplier Inc"
  },
  status_reason: "Invoice approved",  # Optional
  note: "Payment scheduled"  # Optional
}

xml = UblEx.generate(response_data)
```

### Working with Attachments

```elixir
# Include PDF attachments (e.g., signed invoice)
document_data = %{
  type: :invoice,
  number: "F2024001",
  # ... other fields ...

  attachments: [
    %{
      filename: "F2024001.pdf",
      mime_type: "application/pdf",
      data: Base.encode64(pdf_binary)
    },
    %{
      filename: "terms.pdf",
      mime_type: "application/pdf",
      data: Base.encode64(terms_pdf)
    }
  ]
}

xml = UblEx.generate(document_data)

# Parse documents with attachments
{:ok, parsed} = UblEx.parse(xml)
parsed.attachments
# => [%{filename: "F2024001.pdf", mime_type: "application/pdf", data: "base64..."}]
```

### Generating SBDH-Wrapped Documents

For Peppol network transmission, wrap documents in a Standard Business Document Header (SBDH):

```elixir
# Same document data as before
document_data = %{
  type: :invoice,
  number: "F2024001",
  date: ~D[2024-01-15],
  # ... all other fields ...
}

# Generate with SBDH wrapper for Peppol network
sbdh_xml = UblEx.generate_with_sbdh(document_data)

# The SBDH includes routing information automatically derived from:
# - Supplier endpoint_id and scheme -> SBDH Sender
# - Customer VAT (if no endpoint_id) -> SBDH Receiver
# - Document type and customization -> SBDH Business Scope

# Parse SBDH-wrapped documents (automatically unwraps)
{:ok, parsed} = UblEx.parse(sbdh_xml)
# Returns the same data structure as parsing unwrapped UBL
```

## Document Structure

### Invoice and Credit Note Structure

```elixir
%{
  # Document metadata
  type: :invoice | :credit,
  number: "F2024001",
  date: ~D[2024-01-15],
  expires: ~D[2024-02-14],           # Invoices only
  reverse_charge: false,              # EU intra-community reverse charge
  order_reference: "PO-12345",
  billing_references: ["F001"],       # Credit notes only
  payment_id: "+++000/2024/00186+++", # Optional: Belgian structured payment reference

  # Supplier information
  supplier: %{
    endpoint_id: "0797948229",
    scheme: "0208",                   # Peppol scheme ID
    name: "Company Name",
    street: "Street 123",
    city: "City",
    zipcode: "1000",
    country: "BE",
    vat: "BE0797948229",
    email: "invoice@company.com",
    iban: "BE68539007547034"           # Required for payment means
  },

  # Customer information
  customer: %{
    endpoint_id: "0012345625",
    scheme: "0208",
    name: "Customer Name",
    vat: "BE0012345625",
    street: "Customer Street",
    housenumber: "45",
    city: "City",
    zipcode: "2000",
    country: "BE"
  },

  # Line items
  details: [
    %{
      name: "Service or product description",
      quantity: Decimal.new("1.00"),
      price: Decimal.new("100.00"),
      vat: Decimal.new("21.00"),      # VAT percentage
      discount: Decimal.new("0.00")   # Discount percentage
    }
  ],

  # Optional attachments
  attachments: [
    %{
      filename: "invoice.pdf",
      mime_type: "application/pdf",
      data: "base64encoded..."
    }
  ]
}
```

### Application Response Structure

```elixir
%{
  # Document metadata
  type: :application_response,
  id: "RESPONSE-001",
  date: ~D[2025-06-02],
  response_code: "AB",                # AB = Acknowledged, RE = Rejected, AP = Accepted with errors, CA = Conditionally accepted
  document_reference: "INV-123",      # The invoice/credit note being acknowledged
  status_reason: "Optional reason",
  note: "Optional note",

  # Sender (the party sending the response)
  sender: %{
    endpoint_id: "0797948229",
    scheme: "0208",
    name: "Company Name"
  },

  # Receiver (the party receiving the response)
  receiver: %{
    endpoint_id: "0844125969",
    scheme: "0208",
    name: "Supplier Name"
  }
}
```

## API Reference

### Parsing

#### `UblEx.parse(xml_content)`

Parse UBL XML with automatic schema detection. This is the recommended way to parse documents.

Returns `{:ok, parsed_data}` or `{:error, reason}`.

#### `UblEx.parse_xml(xml_content, schema_id)`

Parse XML with a specific schema (`:ubl_peppol`). Use this when you know the schema in advance.

Returns `{:ok, parsed_data}` or `{:error, reason}`.

### Generation

#### `UblEx.generate(document_data)`

Generate XML based on the `:type` field in the data (`:invoice`, `:credit`, or `:application_response`).

#### `UblEx.generate_with_sbdh(document_data)`

Generate XML wrapped in SBDH (Standard Business Document Header) for Peppol network transmission.

## EU Reverse Charge (Intra-Community Transactions)

For B2B transactions between EU countries where the customer is liable for VAT:

```elixir
document_data = %{
  # ...
  reverse_charge: true,  # Triggers tax category "K" in UBL
  # ...
}
```

This generates the correct UBL tax category for intra-community reverse charge transactions according to EU VAT regulations.

## Real-World Usage

### Basic Invoice Processing

```elixir
defmodule MyApp.Invoices do
  def import_ubl_invoice(xml_file_path) do
    with {:ok, xml} <- File.read(xml_file_path),
         {:ok, parsed} <- UblEx.parse(xml) do

      # Save to your database
      %Invoice{}
      |> Invoice.changeset(%{
        number: parsed.number,
        date: parsed.date,
        supplier_name: parsed.supplier.name,
        customer_name: parsed.customer.name,
        total: calculate_total(parsed.details)
      })
      |> Repo.insert()
    end
  end

  defp calculate_total(details) do
    Enum.reduce(details, Decimal.new(0), fn item, acc ->
      line_total = Decimal.mult(item.quantity, item.price)
      Decimal.add(acc, line_total)
    end)
  end
end
```

### Generate Invoice from Database

```elixir
defmodule MyApp.Invoices do
  def generate_ubl_xml(invoice_id) do
    invoice = Repo.get!(Invoice, invoice_id) |> Repo.preload([:customer, :supplier, :line_items])

    document_data = %{
      type: :invoice,
      number: invoice.number,
      date: invoice.date,
      expires: invoice.due_date,
      reverse_charge: invoice.reverse_charge?,

      supplier: %{
        endpoint_id: invoice.supplier.endpoint_id,
        scheme: invoice.supplier.scheme,
        name: invoice.supplier.name,
        street: invoice.supplier.street,
        city: invoice.supplier.city,
        zipcode: invoice.supplier.zipcode,
        country: invoice.supplier.country,
        vat: invoice.supplier.vat,
        email: invoice.supplier.email
      },

      customer: %{
        endpoint_id: invoice.customer.endpoint_id,
        scheme: invoice.customer.scheme,
        name: invoice.customer.name,
        vat: invoice.customer.vat,
        street: invoice.customer.street,
        housenumber: invoice.customer.housenumber,
        city: invoice.customer.city,
        zipcode: invoice.customer.zipcode,
        country: invoice.customer.country
      },

      details: Enum.map(invoice.line_items, fn item ->
        %{
          name: item.description,
          quantity: item.quantity,
          price: item.unit_price,
          vat: item.vat_rate,
          discount: item.discount_percentage
        }
      end)
    }

    UblEx.generate(document_data)
  end
end
```

## Compliance

This library generates UBL documents compliant with:

- **UBL 2.1** - Universal Business Language version 2.1
- **Peppol BIS Billing 3.0** - Pan-European Public Procurement Online
- **EN16931** - European standard for electronic invoicing

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for version history.

## License

MIT License - see [LICENSE](LICENSE) for details.

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.