# `UblEx`
[🔗](https://github.com/Octarion/ubl_ex/blob/v0.8.1/lib/ubl_ex.ex#L1)

UBL (Universal Business Language) document generation and parsing for Elixir.

This library provides:
- Peppol BIS Billing 3.0 compliant invoice, credit note, and application response generation
- SBDH (Standard Business Document Header) support for Peppol network transmission
- Fast SAX-based XML parsing using Saxy (1.4-4x faster than xmerl)
- Full round-trip support (parse → generate → parse without data loss)

## Quick Start

### Generating a UBL Invoice

    document_data = %{
      type: :invoice,
      number: "F2024001",
      date: ~D[2024-01-15],
      expires: ~D[2024-02-14],
      supplier: %{
        endpoint_id: "0797948229",
        scheme: "0208",
        name: "Company Name",
        street: "Street 40",
        city: "City",
        zipcode: "2180",
        country: "BE",
        vat: "BE0797948229",
        email: "invoice@company.com"
      },
      customer: %{
        endpoint_id: "0012345625",
        scheme: "0208",
        name: "Customer Name",
        vat: "BE0012345625",
        street: "Customer Street",
        housenumber: "10",
        city: "Brussels",
        zipcode: "1000",
        country: "BE"
      },
      details: [
        %{
          name: "Service",
          quantity: Decimal.new("1.00"),
          price: Decimal.new("100.00"),
          vat: Decimal.new("21.00"),
          discount: Decimal.new("0.00")
        }
      ]
    }

    xml = UblEx.generate(document_data)

### Parsing a UBL Document

    {:ok, parsed} = UblEx.parse(xml_content)

    # Document type is in the data
    case parsed.type do
      :invoice -> handle_invoice(parsed)
      :credit -> handle_credit_note(parsed)
      :application_response -> handle_response(parsed)
    end

## Tax Categories

Line items support all Peppol BIS 3.0 tax categories via the optional `tax_category` field:

| Atom | Peppol Code | Use Case |
|------|-------------|----------|
| `:standard` | S | Standard rated VAT (default for 6/12/21%) |
| `:zero_rated` | Z | Zero rated goods (default for 0% VAT) |
| `:exempt` | E | Exempt from tax |
| `:reverse_charge` | AE | Domestic reverse charge |
| `:intra_community` | K | EU cross-border B2B (intra-community supply) |
| `:export` | G | Export outside EU |
| `:outside_scope` | O | Services outside scope of tax |

If `tax_category` is not specified, it defaults to `:standard` for non-zero VAT
and `:zero_rated` for 0% VAT.

## Tax Exemption Fields

According to Peppol BIS 3.0 validation rules (BR-O-11 through BR-O-14), when using
tax categories `:exempt`, `:export`, `:intra_community`, or `:reverse_charge`, you
**must** provide tax exemption information via these fields:

- `tax_exemption_reason_code` - A VATEX code (e.g., "vatex-eu-ic", "vatex-eu-ae")
- `tax_exemption_reason` - A human-readable explanation (e.g., "Vrijgestelde intracommunautaire levering - Art. 39bis WBTW")

The `tax_exemption_reason_code` follows the format `vatex-{country}-{code}`:
- `vatex-eu-ic` - Intra-community supply
- `vatex-eu-ae` - Reverse charge / Autoliquidation
- `vatex-eu-g` - Export outside EU
- `vatex-be-xxx` - Belgian-specific codes

### Example with Tax Categories and Exemption Fields

    details: [
      %{
        name: "Standard Service",
        quantity: Decimal.new("1.00"),
        price: Decimal.new("100.00"),
        vat: Decimal.new("21.00"),
        discount: Decimal.new("0.00")
        # tax_category defaults to :standard
      },
      %{
        name: "EU Cross-Border Service",
        quantity: Decimal.new("1.00"),
        price: Decimal.new("500.00"),
        vat: Decimal.new("0.00"),
        discount: Decimal.new("0.00"),
        tax_category: :intra_community,
        tax_exemption_reason_code: "vatex-eu-ic",
        tax_exemption_reason: "Vrijgestelde intracommunautaire levering - Art. 39bis WBTW"
      }
    ]

## Usage Pattern

Parse documents and handle them in your own code:

    {:ok, parsed} = UblEx.parse(xml)
    MyApp.save_invoice(parsed)
    MyApp.send_notification(parsed)

# `generate`

```elixir
@spec generate(map()) :: String.t() | {:error, String.t()}
```

Generate a Peppol-compliant UBL document based on the type field.

Routes to the appropriate generator based on `document_data.type`:
- `:invoice` → Invoice generator
- `:credit` → CreditNote generator
- `:application_response` → ApplicationResponse generator

## Examples

    # Parse and regenerate
    {:ok, parsed} = UblEx.parse(xml)
    regenerated_xml = UblEx.generate(parsed)

    # Generate invoice
    document_data = %{type: :invoice, number: "F001", ...}
    xml = UblEx.generate(document_data)

    # Generate application response
    response_data = %{type: :application_response, id: "RESP-001", ...}
    xml = UblEx.generate(response_data)

# `generate_with_sbdh`

```elixir
@spec generate_with_sbdh(map()) :: String.t() | {:error, String.t()}
```

Generate a UBL document wrapped in SBDH (Standard Business Document Header).

SBDH is used in Peppol networks for routing and identification. This function
generates the standard UBL document and wraps it with the SBDH header.

## Example

    document_data = %{type: :invoice, number: "F001", ...}
    sbdh_xml = UblEx.generate_with_sbdh(document_data)

# `parse`

```elixir
@spec parse(String.t()) :: {:ok, map()} | {:error, String.t()}
```

Parse UBL XML with automatic schema detection.

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

The document type is available in `parsed_data.type` (`:invoice`, `:credit`, or `:application_response`).

## Example

    {:ok, parsed} = UblEx.parse(xml_content)
    IO.inspect(parsed.type)  # :invoice, :credit, or :application_response

# `strip_sbdh`

```elixir
@spec strip_sbdh(String.t()) :: String.t()
```

Strip the StandardBusinessDocument/StandardBusinessDocumentHeader wrapper from UBL XML.

Some accounting software cannot process documents with SBDH wrappers. This function
extracts the inner UBL document (Invoice, CreditNote, or ApplicationResponse).

If the XML is not wrapped in an SBDH, it is returned unchanged.

## Example

    {:ok, sbdh_xml} = File.read("invoice_with_sbdh.xml")
    plain_xml = UblEx.strip_sbdh(sbdh_xml)

---

*Consult [api-reference.md](api-reference.md) for complete listing*
